Merge branch 'develop' into fix-read-receipts
This commit is contained in:
commit
2f2c5c9081
175 changed files with 9502 additions and 5453 deletions
|
@ -45,7 +45,7 @@ class FilePanel extends React.Component {
|
|||
};
|
||||
|
||||
onRoomTimeline = (ev, room, toStartOfTimeline, removed, data) => {
|
||||
if (room.roomId !== this.props.roomId) return;
|
||||
if (room?.roomId !== this.props?.roomId) return;
|
||||
if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return;
|
||||
|
||||
if (ev.isBeingDecrypted()) {
|
||||
|
|
56
src/components/structures/HostSignupAction.tsx
Normal file
56
src/components/structures/HostSignupAction.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import {
|
||||
IconizedContextMenuOption,
|
||||
IconizedContextMenuOptionList,
|
||||
} from "../views/context_menus/IconizedContextMenu";
|
||||
import { _t } from "../../languageHandler";
|
||||
import { HostSignupStore } from "../../stores/HostSignupStore";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
|
||||
interface IProps {}
|
||||
|
||||
interface IState {}
|
||||
|
||||
export default class HostSignupAction extends React.PureComponent<IProps, IState> {
|
||||
private openDialog = async () => {
|
||||
await HostSignupStore.instance.setHostSignupActive(true);
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const hostSignupConfig = SdkConfig.get().hostSignup;
|
||||
if (!hostSignupConfig?.brand) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconHosting"
|
||||
label={_t(
|
||||
"Upgrade to %(hostSignupBrand)s",
|
||||
{
|
||||
hostSignupBrand: hostSignupConfig.brand,
|
||||
},
|
||||
)}
|
||||
onClick={this.openDialog}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -177,7 +177,14 @@ export default class InteractiveAuthComponent extends React.Component {
|
|||
stageState: stageState,
|
||||
errorText: stageState.error,
|
||||
}, () => {
|
||||
if (oldStage != stageType) this._setFocus();
|
||||
if (oldStage !== stageType) {
|
||||
this._setFocus();
|
||||
} else if (
|
||||
!stageState.error && this._stageComponent.current &&
|
||||
this._stageComponent.current.attemptFailed
|
||||
) {
|
||||
this._stageComponent.current.attemptFailed();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ const LeftPanelWidget: React.FC<IProps> = ({ onResize }) => {
|
|||
|
||||
const [height, setHeight] = useLocalStorageState("left-panel-widget-height", INITIAL_HEIGHT);
|
||||
const [expanded, setExpanded] = useLocalStorageState("left-panel-widget-expanded", true);
|
||||
useEffect(onResize, [expanded]);
|
||||
useEffect(onResize, [expanded, onResize]);
|
||||
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex();
|
||||
const tabIndex = isActive ? 0 : -1;
|
||||
|
|
|
@ -54,6 +54,7 @@ import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPa
|
|||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||
import Modal from "../../Modal";
|
||||
import { ICollapseConfig } from "../../resizer/distributors/collapse";
|
||||
import HostSignupContainer from '../views/host_signup/HostSignupContainer';
|
||||
|
||||
// We need to fetch each pinned message individually (if we don't already have it)
|
||||
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
||||
|
@ -215,10 +216,12 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
|
||||
_createResizer() {
|
||||
let size;
|
||||
let collapsed;
|
||||
const collapseConfig: ICollapseConfig = {
|
||||
toggleSize: 260 - 50,
|
||||
onCollapsed: (collapsed) => {
|
||||
if (collapsed) {
|
||||
onCollapsed: (_collapsed) => {
|
||||
collapsed = _collapsed;
|
||||
if (_collapsed) {
|
||||
dis.dispatch({action: "hide_left_panel"}, true);
|
||||
window.localStorage.setItem("mx_lhs_size", '0');
|
||||
} else {
|
||||
|
@ -233,7 +236,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
this.props.resizeNotifier.startResizing();
|
||||
},
|
||||
onResizeStop: () => {
|
||||
window.localStorage.setItem("mx_lhs_size", '' + size);
|
||||
if (!collapsed) window.localStorage.setItem("mx_lhs_size", '' + size);
|
||||
this.props.resizeNotifier.stopResizing();
|
||||
},
|
||||
};
|
||||
|
@ -425,6 +428,14 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
handled = true;
|
||||
}
|
||||
break;
|
||||
case Key.F:
|
||||
if (ctrlCmdOnly && SettingsStore.getValue("ctrlFForSearch")) {
|
||||
dis.dispatch({
|
||||
action: 'focus_search',
|
||||
});
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
case Key.BACKTICK:
|
||||
// Ideally this would be CTRL+P for "Profile", but that's
|
||||
// taken by the print dialog. CTRL+I for "Information"
|
||||
|
@ -638,6 +649,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
</div>
|
||||
<CallContainer />
|
||||
<NonUrgentToastContainer />
|
||||
<HostSignupContainer />
|
||||
</MatrixClientContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -81,6 +81,7 @@ import ThreepidInviteStore, { IThreepidInvite, IThreepidInviteWireFormat } from
|
|||
import {UIFeature} from "../../settings/UIFeature";
|
||||
import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
|
||||
import DialPadModal from "../views/voip/DialPadModal";
|
||||
import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast';
|
||||
|
||||
/** constants for MatrixChat.state.view */
|
||||
export enum Views {
|
||||
|
@ -218,6 +219,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
private screenAfterLogin?: IScreen;
|
||||
private windowWidth: number;
|
||||
private pageChanging: boolean;
|
||||
private tokenLogin?: boolean;
|
||||
private accountPassword?: string;
|
||||
private accountPasswordTimer?: NodeJS.Timeout;
|
||||
private focusComposer: boolean;
|
||||
|
@ -323,13 +325,21 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
Lifecycle.attemptTokenLogin(
|
||||
this.props.realQueryParams,
|
||||
this.props.defaultDeviceDisplayName,
|
||||
).then((loggedIn) => {
|
||||
if (loggedIn) {
|
||||
this.getFragmentAfterLogin(),
|
||||
).then(async (loggedIn) => {
|
||||
if (this.props.realQueryParams?.loginToken) {
|
||||
// remove the loginToken from the URL regardless
|
||||
this.props.onTokenLoginCompleted();
|
||||
}
|
||||
|
||||
// don't do anything else until the page reloads - just stay in
|
||||
// the 'loading' state.
|
||||
return;
|
||||
if (loggedIn) {
|
||||
this.tokenLogin = true;
|
||||
|
||||
// Create and start the client
|
||||
await Lifecycle.restoreFromLocalStorage({
|
||||
ignoreGuest: true,
|
||||
});
|
||||
return this.postLoginSetup();
|
||||
}
|
||||
|
||||
// if the user has followed a login or register link, don't reanimate
|
||||
|
@ -353,6 +363,42 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
CountlyAnalytics.instance.enable(/* anonymous = */ true);
|
||||
}
|
||||
|
||||
private async postLoginSetup() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const cryptoEnabled = cli.isCryptoEnabled();
|
||||
if (!cryptoEnabled) {
|
||||
this.onLoggedIn();
|
||||
}
|
||||
|
||||
const promisesList = [this.firstSyncPromise.promise];
|
||||
if (cryptoEnabled) {
|
||||
// wait for the client to finish downloading cross-signing keys for us so we
|
||||
// know whether or not we have keys set up on this account
|
||||
promisesList.push(cli.downloadKeys([cli.getUserId()]));
|
||||
}
|
||||
|
||||
// Now update the state to say we're waiting for the first sync to complete rather
|
||||
// than for the login to finish.
|
||||
this.setState({ pendingInitialSync: true });
|
||||
|
||||
await Promise.all(promisesList);
|
||||
|
||||
if (!cryptoEnabled) {
|
||||
this.setState({ pendingInitialSync: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const crossSigningIsSetUp = cli.getStoredCrossSigningForUser(cli.getUserId());
|
||||
if (crossSigningIsSetUp) {
|
||||
this.setStateForNewView({ view: Views.COMPLETE_SECURITY });
|
||||
} else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) {
|
||||
this.setStateForNewView({ view: Views.E2E_SETUP });
|
||||
} else {
|
||||
this.onLoggedIn();
|
||||
}
|
||||
this.setState({ pendingInitialSync: false });
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle stage
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillUpdate(props, state) {
|
||||
|
@ -709,6 +755,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
break;
|
||||
case 'on_logged_in':
|
||||
if (
|
||||
// Skip this handling for token login as that always calls onLoggedIn itself
|
||||
!this.tokenLogin &&
|
||||
!Lifecycle.isSoftLogout() &&
|
||||
this.state.view !== Views.LOGIN &&
|
||||
this.state.view !== Views.REGISTER &&
|
||||
|
@ -1186,6 +1234,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
) {
|
||||
showAnalyticsToast(this.props.config.piwik?.policyUrl);
|
||||
}
|
||||
if (SdkConfig.get().mobileGuideToast) {
|
||||
// The toast contains further logic to detect mobile platforms,
|
||||
// check if it has been dismissed before, etc.
|
||||
showMobileGuideToast();
|
||||
}
|
||||
}
|
||||
|
||||
private showScreenAfterLogin() {
|
||||
|
@ -1322,6 +1375,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
cli.on('Session.logged_out', function(errObj) {
|
||||
if (Lifecycle.isLoggingOut()) return;
|
||||
|
||||
// A modal might have been open when we were logged out by the server
|
||||
Modal.closeCurrentModal('Session.logged_out');
|
||||
|
||||
if (errObj.httpStatus === 401 && errObj.data && errObj.data['soft_logout']) {
|
||||
console.warn("Soft logout issued by server - avoiding data deletion");
|
||||
Lifecycle.softLogout();
|
||||
|
@ -1332,6 +1388,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
title: _t('Signed Out'),
|
||||
description: _t('For security, this session has been signed out. Please sign in again.'),
|
||||
});
|
||||
|
||||
dis.dispatch({
|
||||
action: 'logout',
|
||||
});
|
||||
|
@ -1601,10 +1658,16 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
// TODO: Handle encoded room/event IDs: https://github.com/vector-im/element-web/issues/9149
|
||||
|
||||
let threepidInvite: IThreepidInvite;
|
||||
// if we landed here from a 3PID invite, persist it
|
||||
if (params.signurl && params.email) {
|
||||
threepidInvite = ThreepidInviteStore.instance
|
||||
.storeInvite(roomString, params as IThreepidInviteWireFormat);
|
||||
}
|
||||
// otherwise check that this room doesn't already have a known invite
|
||||
if (!threepidInvite) {
|
||||
const invites = ThreepidInviteStore.instance.getInvites();
|
||||
threepidInvite = invites.find(invite => invite.roomId === roomString);
|
||||
}
|
||||
|
||||
// on our URLs there might be a ?via=matrix.org or similar to help
|
||||
// joins to the room succeed. We'll pass these through as an array
|
||||
|
@ -1833,40 +1896,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
|
||||
// Create and start the client
|
||||
await Lifecycle.setLoggedIn(credentials);
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
const cryptoEnabled = cli.isCryptoEnabled();
|
||||
if (!cryptoEnabled) {
|
||||
this.onLoggedIn();
|
||||
}
|
||||
|
||||
const promisesList = [this.firstSyncPromise.promise];
|
||||
if (cryptoEnabled) {
|
||||
// wait for the client to finish downloading cross-signing keys for us so we
|
||||
// know whether or not we have keys set up on this account
|
||||
promisesList.push(cli.downloadKeys([cli.getUserId()]));
|
||||
}
|
||||
|
||||
// Now update the state to say we're waiting for the first sync to complete rather
|
||||
// than for the login to finish.
|
||||
this.setState({ pendingInitialSync: true });
|
||||
|
||||
await Promise.all(promisesList);
|
||||
|
||||
if (!cryptoEnabled) {
|
||||
this.setState({ pendingInitialSync: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const crossSigningIsSetUp = cli.getStoredCrossSigningForUser(cli.getUserId());
|
||||
if (crossSigningIsSetUp) {
|
||||
this.setStateForNewView({ view: Views.COMPLETE_SECURITY });
|
||||
} else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) {
|
||||
this.setStateForNewView({ view: Views.E2E_SETUP });
|
||||
} else {
|
||||
this.onLoggedIn();
|
||||
}
|
||||
this.setState({ pendingInitialSync: false });
|
||||
await this.postLoginSetup();
|
||||
};
|
||||
|
||||
// complete security / e2e setup has finished
|
||||
|
@ -1910,6 +1940,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
<E2eSetup
|
||||
onFinished={this.onCompleteSecurityE2eSetupFinished}
|
||||
accountPassword={this.accountPassword}
|
||||
tokenLogin={!!this.tokenLogin}
|
||||
/>
|
||||
);
|
||||
} else if (this.state.view === Views.LOGGED_IN) {
|
||||
|
|
|
@ -23,9 +23,11 @@ import classNames from 'classnames';
|
|||
import shouldHideEvent from '../../shouldHideEvent';
|
||||
import {wantsDateSeparator} from '../../DateUtils';
|
||||
import * as sdk from '../../index';
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
|
||||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import SettingsStore from '../../settings/SettingsStore';
|
||||
import {Layout, LayoutPropType} from "../../settings/Layout";
|
||||
import {_t} from "../../languageHandler";
|
||||
import {haveTileForEvent} from "../views/rooms/EventTile";
|
||||
import {textForEvent} from "../../TextForEvent";
|
||||
|
@ -135,14 +137,13 @@ export default class MessagePanel extends React.Component {
|
|||
// whether to show reactions for an event
|
||||
showReactions: PropTypes.bool,
|
||||
|
||||
// whether to use the irc layout
|
||||
useIRCLayout: PropTypes.bool,
|
||||
// which layout to use
|
||||
layout: LayoutPropType,
|
||||
|
||||
// whether or not to show flair at all
|
||||
enableFlair: PropTypes.bool,
|
||||
};
|
||||
|
||||
// Force props to be loaded for useIRCLayout
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
@ -207,11 +208,13 @@ export default class MessagePanel extends React.Component {
|
|||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef);
|
||||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
|
@ -224,6 +227,14 @@ export default class MessagePanel extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
onAction = (payload) => {
|
||||
switch (payload.action) {
|
||||
case "scroll_to_bottom":
|
||||
this.scrollToBottom();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onShowTypingNotificationsChange = () => {
|
||||
this.setState({
|
||||
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
|
||||
|
@ -612,7 +623,7 @@ export default class MessagePanel extends React.Component {
|
|||
isSelectedEvent={highlight}
|
||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||
showReactions={this.props.showReactions}
|
||||
useIRCLayout={this.props.useIRCLayout}
|
||||
layout={this.props.layout}
|
||||
enableFlair={this.props.enableFlair}
|
||||
/>
|
||||
</TileErrorBoundary>
|
||||
|
@ -810,7 +821,7 @@ export default class MessagePanel extends React.Component {
|
|||
}
|
||||
|
||||
let ircResizer = null;
|
||||
if (this.props.useIRCLayout) {
|
||||
if (this.props.layout == Layout.IRC) {
|
||||
ircResizer = <IRCTimelineProfileResizer
|
||||
minWidth={20}
|
||||
maxWidth={600}
|
||||
|
|
|
@ -30,7 +30,6 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
|
|||
import {Action} from "../../dispatcher/actions";
|
||||
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
|
||||
import WidgetCard from "../views/right_panel/WidgetCard";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
|
||||
export default class RightPanel extends React.Component {
|
||||
static get propTypes() {
|
||||
|
@ -186,7 +185,7 @@ export default class RightPanel extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
onCloseUserInfo = () => {
|
||||
onClose = () => {
|
||||
// XXX: There are three different ways of 'closing' this panel depending on what state
|
||||
// things are in... this knows far more than it should do about the state of the rest
|
||||
// of the app and is generally a bit silly.
|
||||
|
@ -198,31 +197,21 @@ export default class RightPanel extends React.Component {
|
|||
dis.dispatch({
|
||||
action: "view_home_page",
|
||||
});
|
||||
} else if (this.state.phase === RightPanelPhases.EncryptionPanel &&
|
||||
} else if (
|
||||
this.state.phase === RightPanelPhases.EncryptionPanel &&
|
||||
this.state.verificationRequest && this.state.verificationRequest.pending
|
||||
) {
|
||||
// When the user clicks close on the encryption panel cancel the pending request first if any
|
||||
this.state.verificationRequest.cancel();
|
||||
} else {
|
||||
// Otherwise we have got our user from RoomViewStore which means we're being shown
|
||||
// within a room/group, so go back to the member panel if we were in the encryption panel,
|
||||
// or the member list if we were in the member panel... phew.
|
||||
const isEncryptionPhase = this.state.phase === RightPanelPhases.EncryptionPanel;
|
||||
// the RightPanelStore has no way of knowing which mode room/group it is in, so we handle closing here
|
||||
dis.dispatch({
|
||||
action: Action.ViewUser,
|
||||
member: isEncryptionPhase ? this.state.member : null,
|
||||
action: Action.ToggleRightPanel,
|
||||
type: this.props.groupId ? "group" : "room",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onClose = () => {
|
||||
// the RightPanelStore has no way of knowing which mode room/group it is in, so we handle closing here
|
||||
defaultDispatcher.dispatch({
|
||||
action: Action.ToggleRightPanel,
|
||||
type: this.props.groupId ? "group" : "room",
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const MemberList = sdk.getComponent('rooms.MemberList');
|
||||
const UserInfo = sdk.getComponent('right_panel.UserInfo');
|
||||
|
@ -260,7 +249,7 @@ export default class RightPanel extends React.Component {
|
|||
user={this.state.member}
|
||||
room={this.props.room}
|
||||
key={roomId || this.state.member.userId}
|
||||
onClose={this.onCloseUserInfo}
|
||||
onClose={this.onClose}
|
||||
phase={this.state.phase}
|
||||
verificationRequest={this.state.verificationRequest}
|
||||
verificationRequestPromise={this.state.verificationRequestPromise}
|
||||
|
@ -276,7 +265,7 @@ export default class RightPanel extends React.Component {
|
|||
user={this.state.member}
|
||||
groupId={this.props.groupId}
|
||||
key={this.state.member.userId}
|
||||
onClose={this.onCloseUserInfo} />;
|
||||
onClose={this.onClose} />;
|
||||
break;
|
||||
|
||||
case RightPanelPhases.GroupRoomInfo:
|
||||
|
|
|
@ -477,7 +477,7 @@ export default class RoomDirectory extends React.Component {
|
|||
dis.dispatch(payload);
|
||||
}
|
||||
|
||||
getRow(room) {
|
||||
createRoomCells(room) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const clientRoom = client.getRoom(room.room_id);
|
||||
const hasJoinedRoom = clientRoom && clientRoom.getMyMembership() === "join";
|
||||
|
@ -523,31 +523,56 @@ export default class RoomDirectory extends React.Component {
|
|||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
room.avatar_url, 32, 32, "crop",
|
||||
);
|
||||
return (
|
||||
<tr key={ room.room_id }
|
||||
return [
|
||||
<div key={ `${room.room_id}_avatar` }
|
||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
className="mx_RoomDirectory_roomAvatar"
|
||||
>
|
||||
<td className="mx_RoomDirectory_roomAvatar">
|
||||
<BaseAvatar width={32} height={32} resizeMethod='crop'
|
||||
name={ name } idName={ name }
|
||||
url={ avatarUrl } />
|
||||
</td>
|
||||
<td className="mx_RoomDirectory_roomDescription">
|
||||
<div className="mx_RoomDirectory_name">{ name }</div>
|
||||
<div className="mx_RoomDirectory_topic"
|
||||
onClick={ (ev) => { ev.stopPropagation(); } }
|
||||
dangerouslySetInnerHTML={{ __html: topic }} />
|
||||
<div className="mx_RoomDirectory_alias">{ get_display_alias_for_room(room) }</div>
|
||||
</td>
|
||||
<td className="mx_RoomDirectory_roomMemberCount">
|
||||
{ room.num_joined_members }
|
||||
</td>
|
||||
<td className="mx_RoomDirectory_preview">{previewButton}</td>
|
||||
<td className="mx_RoomDirectory_join">{joinOrViewButton}</td>
|
||||
</tr>
|
||||
);
|
||||
<BaseAvatar width={32} height={32} resizeMethod='crop'
|
||||
name={ name } idName={ name }
|
||||
url={ avatarUrl }
|
||||
/>
|
||||
</div>,
|
||||
<div key={ `${room.room_id}_description` }
|
||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
className="mx_RoomDirectory_roomDescription"
|
||||
>
|
||||
<div className="mx_RoomDirectory_name">{ name }</div>
|
||||
<div className="mx_RoomDirectory_topic"
|
||||
onClick={ (ev) => { ev.stopPropagation(); } }
|
||||
dangerouslySetInnerHTML={{ __html: topic }}
|
||||
/>
|
||||
<div className="mx_RoomDirectory_alias">{ get_display_alias_for_room(room) }</div>
|
||||
</div>,
|
||||
<div key={ `${room.room_id}_memberCount` }
|
||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
className="mx_RoomDirectory_roomMemberCount"
|
||||
>
|
||||
{ room.num_joined_members }
|
||||
</div>,
|
||||
<div key={ `${room.room_id}_preview` }
|
||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
className="mx_RoomDirectory_preview"
|
||||
>
|
||||
{previewButton}
|
||||
</div>,
|
||||
<div key={ `${room.room_id}_join` }
|
||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
className="mx_RoomDirectory_join"
|
||||
>
|
||||
{joinOrViewButton}
|
||||
</div>,
|
||||
];
|
||||
}
|
||||
|
||||
collectScrollPanel = (element) => {
|
||||
|
@ -606,7 +631,8 @@ export default class RoomDirectory extends React.Component {
|
|||
} else if (this.state.protocolsLoading) {
|
||||
content = <Loader />;
|
||||
} else {
|
||||
const rows = (this.state.publicRooms || []).map(room => this.getRow(room));
|
||||
const cells = (this.state.publicRooms || [])
|
||||
.reduce((cells, room) => cells.concat(this.createRoomCells(room)), [],);
|
||||
// we still show the scrollpanel, at least for now, because
|
||||
// otherwise we don't fetch more because we don't get a fill
|
||||
// request from the scrollpanel because there isn't one
|
||||
|
@ -617,14 +643,12 @@ export default class RoomDirectory extends React.Component {
|
|||
}
|
||||
|
||||
let scrollpanel_content;
|
||||
if (rows.length === 0 && !this.state.loading) {
|
||||
if (cells.length === 0 && !this.state.loading) {
|
||||
scrollpanel_content = <i>{ _t('No rooms to show') }</i>;
|
||||
} else {
|
||||
scrollpanel_content = <table className="mx_RoomDirectory_table">
|
||||
<tbody>
|
||||
{ rows }
|
||||
</tbody>
|
||||
</table>;
|
||||
scrollpanel_content = <div className="mx_RoomDirectory_table">
|
||||
{ cells }
|
||||
</div>;
|
||||
}
|
||||
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||
content = <ScrollPanel ref={this.collectScrollPanel}
|
||||
|
|
|
@ -48,6 +48,7 @@ import RoomViewStore from '../../stores/RoomViewStore';
|
|||
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
||||
import WidgetEchoStore from '../../stores/WidgetEchoStore';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import {Layout} from "../../settings/Layout";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import { haveTileForEvent } from "../views/rooms/EventTile";
|
||||
|
@ -181,7 +182,7 @@ export interface IState {
|
|||
};
|
||||
canReact: boolean;
|
||||
canReply: boolean;
|
||||
useIRCLayout: boolean;
|
||||
layout: Layout;
|
||||
matrixClientIsReady: boolean;
|
||||
showUrlPreview?: boolean;
|
||||
e2eStatus?: E2EStatus;
|
||||
|
@ -236,7 +237,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
statusBarVisible: false,
|
||||
canReact: false,
|
||||
canReply: false,
|
||||
useIRCLayout: SettingsStore.getValue("useIRCLayout"),
|
||||
layout: SettingsStore.getValue("layout"),
|
||||
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
||||
};
|
||||
|
||||
|
@ -264,13 +265,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
|
||||
this.showReadReceiptsWatchRef = SettingsStore.watchSetting("showReadReceipts", null,
|
||||
this.onReadReceiptsChange);
|
||||
this.layoutWatcherRef = SettingsStore.watchSetting("useIRCLayout", null, this.onLayoutChange);
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Move into constructor
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillMount() {
|
||||
this.onRoomViewStoreUpdate(true);
|
||||
this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, this.onLayoutChange);
|
||||
}
|
||||
|
||||
private onWidgetStoreUpdate = () => {
|
||||
|
@ -512,6 +507,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.onRoomViewStoreUpdate(true);
|
||||
|
||||
const call = this.getCallForRoom();
|
||||
const callState = call ? call.state : null;
|
||||
this.setState({
|
||||
|
@ -642,7 +639,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
|
||||
private onLayoutChange = () => {
|
||||
this.setState({
|
||||
useIRCLayout: SettingsStore.getValue("useIRCLayout"),
|
||||
layout: SettingsStore.getValue("layout"),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -766,6 +763,9 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
});
|
||||
}
|
||||
break;
|
||||
case 'focus_search':
|
||||
this.onSearchClick();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1946,8 +1946,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
const messagePanelClassNames = classNames(
|
||||
"mx_RoomView_messagePanel",
|
||||
{
|
||||
"mx_IRCLayout": this.state.useIRCLayout,
|
||||
"mx_GroupLayout": !this.state.useIRCLayout,
|
||||
"mx_IRCLayout": this.state.layout == Layout.IRC,
|
||||
"mx_GroupLayout": this.state.layout == Layout.Group,
|
||||
});
|
||||
|
||||
// console.info("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
|
||||
|
@ -1970,7 +1970,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
permalinkCreator={this.getPermalinkCreatorForRoom(this.state.room)}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
showReactions={true}
|
||||
useIRCLayout={this.state.useIRCLayout}
|
||||
layout={this.state.layout}
|
||||
/>);
|
||||
|
||||
let topUnreadMessagesBar = null;
|
||||
|
|
|
@ -20,7 +20,6 @@ import * as React from "react";
|
|||
import {_t} from '../../languageHandler';
|
||||
import * as sdk from "../../index";
|
||||
import AutoHideScrollbar from './AutoHideScrollbar';
|
||||
import { ReactNode } from "react";
|
||||
|
||||
/**
|
||||
* Represents a tab for the TabbedView.
|
||||
|
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import {LayoutPropType} from "../../settings/Layout";
|
||||
import React, {createRef} from 'react';
|
||||
import ReactDOM from "react-dom";
|
||||
import PropTypes from 'prop-types';
|
||||
|
@ -111,8 +112,8 @@ class TimelinePanel extends React.Component {
|
|||
// whether to show reactions for an event
|
||||
showReactions: PropTypes.bool,
|
||||
|
||||
// whether to use the irc layout
|
||||
useIRCLayout: PropTypes.bool,
|
||||
// which layout to use
|
||||
layout: LayoutPropType,
|
||||
}
|
||||
|
||||
// a map from room id to read marker event timestamp
|
||||
|
@ -1442,7 +1443,7 @@ class TimelinePanel extends React.Component {
|
|||
getRelationsForEvent={this.getRelationsForEvent}
|
||||
editState={this.state.editState}
|
||||
showReactions={this.props.showReactions}
|
||||
useIRCLayout={this.props.useIRCLayout}
|
||||
layout={this.props.layout}
|
||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -28,7 +28,6 @@ import Modal from "../../Modal";
|
|||
import LogoutDialog from "../views/dialogs/LogoutDialog";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import {getCustomTheme} from "../../theme";
|
||||
import {getHostingLink} from "../../utils/HostingLink";
|
||||
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import {getHomePageUrl} from "../../utils/pages";
|
||||
|
@ -51,6 +50,8 @@ import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
|||
import ErrorDialog from "../views/dialogs/ErrorDialog";
|
||||
import EditCommunityPrototypeDialog from "../views/dialogs/EditCommunityPrototypeDialog";
|
||||
import {UIFeature} from "../../settings/UIFeature";
|
||||
import HostSignupAction from "./HostSignupAction";
|
||||
import {IHostSignupConfig} from "../views/dialogs/HostSignupDialogTypes";
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
|
@ -272,7 +273,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
const prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||
|
||||
let topSection;
|
||||
const signupLink = getHostingLink("user-context-menu");
|
||||
const hostSignupConfig: IHostSignupConfig = SdkConfig.get().hostSignup;
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
topSection = (
|
||||
<div className="mx_UserMenu_contextMenu_header mx_UserMenu_contextMenu_guestPrompts">
|
||||
|
@ -292,24 +293,19 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
})}
|
||||
</div>
|
||||
)
|
||||
} else if (signupLink) {
|
||||
topSection = (
|
||||
<div className="mx_UserMenu_contextMenu_header mx_UserMenu_contextMenu_hostingLink">
|
||||
{_t(
|
||||
"<a>Upgrade</a> to your own domain", {},
|
||||
{
|
||||
a: sub => (
|
||||
<a
|
||||
href={signupLink}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
tabIndex={-1}
|
||||
>{sub}</a>
|
||||
),
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else if (hostSignupConfig) {
|
||||
if (hostSignupConfig && hostSignupConfig.url) {
|
||||
// If hostSignup.domains is set to a non-empty array, only show
|
||||
// dialog if the user is on the domain or a subdomain.
|
||||
const hostSignupDomains = hostSignupConfig.domains || [];
|
||||
const mxDomain = MatrixClientPeg.get().getDomain();
|
||||
const validDomains = hostSignupDomains.filter(d => (d === mxDomain || mxDomain.endsWith(`.${d}`)));
|
||||
if (!hostSignupConfig.domains || validDomains.length > 0) {
|
||||
topSection = <div onClick={this.onCloseMenu}>
|
||||
<HostSignupAction />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let homeButton = null;
|
||||
|
|
|
@ -24,6 +24,7 @@ export default class E2eSetup extends React.Component {
|
|||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
accountPassword: PropTypes.string,
|
||||
tokenLogin: PropTypes.bool,
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -33,6 +34,7 @@ export default class E2eSetup extends React.Component {
|
|||
<CreateCrossSigningDialog
|
||||
onFinished={this.props.onFinished}
|
||||
accountPassword={this.props.accountPassword}
|
||||
tokenLogin={this.props.tokenLogin}
|
||||
/>
|
||||
</CompleteSecurityBody>
|
||||
</AuthPage>
|
||||
|
|
|
@ -340,8 +340,8 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
|||
};
|
||||
|
||||
onTryRegisterClick = ev => {
|
||||
const hasPasswordFlow = this.state.flows.find(flow => flow.type === "m.login.password");
|
||||
const ssoFlow = this.state.flows.find(flow => flow.type === "m.login.sso" || flow.type === "m.login.cas");
|
||||
const hasPasswordFlow = this.state.flows?.find(flow => flow.type === "m.login.password");
|
||||
const ssoFlow = this.state.flows?.find(flow => flow.type === "m.login.sso" || flow.type === "m.login.cas");
|
||||
// If has no password flow but an SSO flow guess that the user wants to register with SSO.
|
||||
// TODO: instead hide the Register button if registration is disabled by checking with the server,
|
||||
// has no specific errCode currently and uses M_FORBIDDEN.
|
||||
|
|
|
@ -609,8 +609,12 @@ export class SSOAuthEntry extends React.Component {
|
|||
this.props.authSessionId,
|
||||
);
|
||||
|
||||
this._popupWindow = null;
|
||||
window.addEventListener("message", this._onReceiveMessage);
|
||||
|
||||
this.state = {
|
||||
phase: SSOAuthEntry.PHASE_PREAUTH,
|
||||
attemptFailed: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -618,12 +622,35 @@ export class SSOAuthEntry extends React.Component {
|
|||
this.props.onPhaseChange(SSOAuthEntry.PHASE_PREAUTH);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("message", this._onReceiveMessage);
|
||||
if (this._popupWindow) {
|
||||
this._popupWindow.close();
|
||||
this._popupWindow = null;
|
||||
}
|
||||
}
|
||||
|
||||
attemptFailed = () => {
|
||||
this.setState({
|
||||
attemptFailed: true,
|
||||
});
|
||||
};
|
||||
|
||||
_onReceiveMessage = event => {
|
||||
if (event.data === "authDone" && event.origin === this.props.matrixClient.getHomeserverUrl()) {
|
||||
if (this._popupWindow) {
|
||||
this._popupWindow.close();
|
||||
this._popupWindow = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onStartAuthClick = () => {
|
||||
// Note: We don't use PlatformPeg's startSsoAuth functions because we almost
|
||||
// certainly will need to open the thing in a new tab to avoid losing application
|
||||
// context.
|
||||
|
||||
window.open(this._ssoUrl, '_blank');
|
||||
this._popupWindow = window.open(this._ssoUrl, "_blank");
|
||||
this.setState({phase: SSOAuthEntry.PHASE_POSTAUTH});
|
||||
this.props.onPhaseChange(SSOAuthEntry.PHASE_POSTAUTH);
|
||||
};
|
||||
|
@ -656,10 +683,28 @@ export class SSOAuthEntry extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
return <div className='mx_InteractiveAuthEntryComponents_sso_buttons'>
|
||||
{cancelButton}
|
||||
{continueButton}
|
||||
</div>;
|
||||
let errorSection;
|
||||
if (this.props.errorText) {
|
||||
errorSection = (
|
||||
<div className="error" role="alert">
|
||||
{ this.props.errorText }
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.attemptFailed) {
|
||||
errorSection = (
|
||||
<div className="error" role="alert">
|
||||
{ _t("Something went wrong in confirming your identity. Cancel and try again.") }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <React.Fragment>
|
||||
{ errorSection }
|
||||
<div className="mx_InteractiveAuthEntryComponents_sso_buttons">
|
||||
{cancelButton}
|
||||
{continueButton}
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -710,8 +755,7 @@ export class FallbackAuthEntry extends React.Component {
|
|||
this.props.loginType,
|
||||
this.props.authSessionId,
|
||||
);
|
||||
this._popupWindow = window.open(url);
|
||||
this._popupWindow.opener = null;
|
||||
this._popupWindow = window.open(url, "_blank");
|
||||
};
|
||||
|
||||
_onReceiveMessage = event => {
|
||||
|
|
|
@ -196,7 +196,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
|||
|
||||
// Validation and state updates are async, so we need to wait for them to complete
|
||||
// first. Queue a `setState` callback and wait for it to resolve.
|
||||
await new Promise(resolve => this.setState({}, resolve));
|
||||
await new Promise<void>(resolve => this.setState({}, resolve));
|
||||
|
||||
if (this.allFieldsValid()) {
|
||||
return true;
|
||||
|
|
|
@ -194,7 +194,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
|||
|
||||
// Validation and state updates are async, so we need to wait for them to complete
|
||||
// first. Queue a `setState` callback and wait for it to resolve.
|
||||
await new Promise(resolve => this.setState({}, resolve));
|
||||
await new Promise<void>(resolve => this.setState({}, resolve));
|
||||
|
||||
if (this.allFieldsValid()) {
|
||||
return true;
|
||||
|
|
|
@ -50,6 +50,10 @@ export default class ErrorDialog extends React.Component {
|
|||
button: null,
|
||||
};
|
||||
|
||||
onClick = () => {
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
|
@ -64,7 +68,7 @@ export default class ErrorDialog extends React.Component {
|
|||
{ this.props.description || _t('An error has occurred.') }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={this.props.focus}>
|
||||
<button className="mx_Dialog_primary" onClick={this.onClick} autoFocus={this.props.focus}>
|
||||
{ this.props.button || _t('OK') }
|
||||
</button>
|
||||
</div>
|
||||
|
|
291
src/components/views/dialogs/HostSignupDialog.tsx
Normal file
291
src/components/views/dialogs/HostSignupDialog.tsx
Normal file
|
@ -0,0 +1,291 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Modal from "../../../Modal";
|
||||
import PersistedElement from "../elements/PersistedElement";
|
||||
import QuestionDialog from './QuestionDialog';
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import classNames from "classnames";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { HostSignupStore } from "../../../stores/HostSignupStore";
|
||||
import { OwnProfileStore } from "../../../stores/OwnProfileStore";
|
||||
import {
|
||||
IHostSignupConfig,
|
||||
IPostmessage,
|
||||
IPostmessageResponseData,
|
||||
PostmessageAction,
|
||||
} from "./HostSignupDialogTypes";
|
||||
|
||||
const HOST_SIGNUP_KEY = "host_signup";
|
||||
|
||||
interface IProps {}
|
||||
|
||||
interface IState {
|
||||
completed: boolean;
|
||||
error: string;
|
||||
minimized: boolean;
|
||||
}
|
||||
|
||||
export default class HostSignupDialog extends React.PureComponent<IProps, IState> {
|
||||
private iframeRef: React.RefObject<HTMLIFrameElement> = React.createRef();
|
||||
private readonly config: IHostSignupConfig;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
completed: false,
|
||||
error: null,
|
||||
minimized: false,
|
||||
};
|
||||
|
||||
this.config = SdkConfig.get().hostSignup;
|
||||
}
|
||||
|
||||
private messageHandler = async (message: IPostmessage) => {
|
||||
if (!this.config.url.startsWith(message.origin)) {
|
||||
return;
|
||||
}
|
||||
switch (message.data.action) {
|
||||
case PostmessageAction.HostSignupAccountDetailsRequest:
|
||||
this.onAccountDetailsRequest();
|
||||
break;
|
||||
case PostmessageAction.Maximize:
|
||||
this.setState({
|
||||
minimized: false,
|
||||
});
|
||||
break;
|
||||
case PostmessageAction.Minimize:
|
||||
this.setState({
|
||||
minimized: true,
|
||||
});
|
||||
break;
|
||||
case PostmessageAction.SetupComplete:
|
||||
this.setState({
|
||||
completed: true,
|
||||
});
|
||||
break;
|
||||
case PostmessageAction.CloseDialog:
|
||||
return this.closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
private maximizeDialog = () => {
|
||||
this.setState({
|
||||
minimized: false,
|
||||
});
|
||||
// Send this action to the iframe so it can act accordingly
|
||||
this.sendMessage({
|
||||
action: PostmessageAction.Maximize,
|
||||
});
|
||||
}
|
||||
|
||||
private minimizeDialog = () => {
|
||||
this.setState({
|
||||
minimized: true,
|
||||
});
|
||||
// Send this action to the iframe so it can act accordingly
|
||||
this.sendMessage({
|
||||
action: PostmessageAction.Minimize,
|
||||
});
|
||||
}
|
||||
|
||||
private closeDialog = async () => {
|
||||
window.removeEventListener("message", this.messageHandler);
|
||||
// Ensure we destroy the host signup persisted element
|
||||
PersistedElement.destroyElement("host_signup");
|
||||
// Finally clear the flag in
|
||||
return HostSignupStore.instance.setHostSignupActive(false);
|
||||
}
|
||||
|
||||
private onCloseClick = async () => {
|
||||
if (this.state.completed) {
|
||||
// We're done, close
|
||||
return this.closeDialog();
|
||||
} else {
|
||||
Modal.createDialog(
|
||||
QuestionDialog,
|
||||
{
|
||||
title: _t("Confirm abort of host creation"),
|
||||
description: _t(
|
||||
"Are you sure you wish to abort creation of the host? The process cannot be continued.",
|
||||
),
|
||||
button: _t("Abort"),
|
||||
onFinished: result => {
|
||||
if (result) {
|
||||
return this.closeDialog();
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private sendMessage = (message: IPostmessageResponseData) => {
|
||||
this.iframeRef.current.contentWindow.postMessage(message, this.config.url);
|
||||
}
|
||||
|
||||
private async sendAccountDetails() {
|
||||
const openIdToken = await MatrixClientPeg.get().getOpenIdToken();
|
||||
if (!openIdToken || !openIdToken.access_token) {
|
||||
console.warn("Failed to connect to homeserver for OpenID token.")
|
||||
this.setState({
|
||||
completed: true,
|
||||
error: _t("Failed to connect to your homeserver. Please close this dialog and try again."),
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.sendMessage({
|
||||
action: PostmessageAction.HostSignupAccountDetails,
|
||||
account: {
|
||||
accessToken: await MatrixClientPeg.get().getAccessToken(),
|
||||
name: OwnProfileStore.instance.displayName,
|
||||
openIdToken: openIdToken.access_token,
|
||||
serverName: await MatrixClientPeg.get().getDomain(),
|
||||
userLocalpart: await MatrixClientPeg.get().getUserIdLocalpart(),
|
||||
termsAccepted: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private onAccountDetailsDialogFinished = async (result) => {
|
||||
if (result) {
|
||||
return this.sendAccountDetails();
|
||||
}
|
||||
return this.closeDialog();
|
||||
}
|
||||
|
||||
private onAccountDetailsRequest = () => {
|
||||
const textComponent = (
|
||||
<>
|
||||
<p>
|
||||
{_t("Continuing temporarily allows the %(hostSignupBrand)s setup process to access your " +
|
||||
"account to fetch verified email addresses. This data is not stored.", {
|
||||
hostSignupBrand: this.config.brand,
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{_t("Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.",
|
||||
{},
|
||||
{
|
||||
cookiePolicyLink: () => (
|
||||
<a href={this.config.cookiePolicyUrl} target="_blank" rel="noreferrer noopener">
|
||||
{_t("Cookie Policy")}
|
||||
</a>
|
||||
),
|
||||
privacyPolicyLink: () => (
|
||||
<a href={this.config.privacyPolicyUrl} target="_blank" rel="noreferrer noopener">
|
||||
{_t("Privacy Policy")}
|
||||
</a>
|
||||
),
|
||||
termsOfServiceLink: () => (
|
||||
<a href={this.config.termsOfServiceUrl} target="_blank" rel="noreferrer noopener">
|
||||
{_t("Terms of Service")}
|
||||
</a>
|
||||
),
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
Modal.createDialog(
|
||||
QuestionDialog,
|
||||
{
|
||||
title: _t("You should know"),
|
||||
description: textComponent,
|
||||
button: _t("Continue"),
|
||||
onFinished: this.onAccountDetailsDialogFinished,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
window.addEventListener("message", this.messageHandler);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (HostSignupStore.instance.isHostSignupActive) {
|
||||
// Run the close dialog actions if we're still active, otherwise good to go
|
||||
return this.closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<div className="mx_HostSignup_persisted">
|
||||
<PersistedElement key={HOST_SIGNUP_KEY} persistKey={HOST_SIGNUP_KEY}>
|
||||
<div className={classNames({ "mx_Dialog_wrapper": !this.state.minimized })}>
|
||||
<div
|
||||
className={classNames("mx_Dialog",
|
||||
{
|
||||
"mx_HostSignupDialog_minimized": this.state.minimized,
|
||||
"mx_HostSignupDialog": !this.state.minimized,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{this.state.minimized &&
|
||||
<div className="mx_Dialog_header mx_Dialog_headerWithButton">
|
||||
<div className="mx_Dialog_title">
|
||||
{_t("%(hostSignupBrand)s Setup", {
|
||||
hostSignupBrand: this.config.brand,
|
||||
})}
|
||||
</div>
|
||||
<AccessibleButton
|
||||
className="mx_HostSignup_maximize_button"
|
||||
onClick={this.maximizeDialog}
|
||||
aria-label={_t("Maximize dialog")}
|
||||
title={_t("Maximize dialog")}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{!this.state.minimized &&
|
||||
<div className="mx_Dialog_header mx_Dialog_headerWithCancel">
|
||||
<AccessibleButton
|
||||
onClick={this.minimizeDialog}
|
||||
className="mx_HostSignup_minimize_button"
|
||||
aria-label={_t("Minimize dialog")}
|
||||
title={_t("Minimize dialog")}
|
||||
/>
|
||||
<AccessibleButton
|
||||
onClick={this.onCloseClick}
|
||||
className="mx_Dialog_cancelButton"
|
||||
aria-label={_t("Close dialog")}
|
||||
title={_t("Close dialog")}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{this.state.error &&
|
||||
<div>
|
||||
{this.state.error}
|
||||
</div>
|
||||
}
|
||||
{!this.state.error &&
|
||||
<iframe
|
||||
src={this.config.url}
|
||||
ref={this.iframeRef}
|
||||
sandbox="allow-forms allow-scripts allow-same-origin allow-popups"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</PersistedElement>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
56
src/components/views/dialogs/HostSignupDialogTypes.ts
Normal file
56
src/components/views/dialogs/HostSignupDialogTypes.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export enum PostmessageAction {
|
||||
CloseDialog = "close_dialog",
|
||||
HostSignupAccountDetails = "host_signup_account_details",
|
||||
HostSignupAccountDetailsRequest = "host_signup_account_details_request",
|
||||
Minimize = "host_signup_minimize",
|
||||
Maximize = "host_signup_maximize",
|
||||
SetupComplete = "setup_complete",
|
||||
}
|
||||
|
||||
interface IAccountData {
|
||||
accessToken: string;
|
||||
name: string;
|
||||
openIdToken: string;
|
||||
serverName: string;
|
||||
userLocalpart: string;
|
||||
termsAccepted: boolean;
|
||||
}
|
||||
|
||||
export interface IPostmessageRequestData {
|
||||
action: PostmessageAction;
|
||||
}
|
||||
|
||||
export interface IPostmessageResponseData {
|
||||
action: PostmessageAction;
|
||||
account?: IAccountData;
|
||||
}
|
||||
|
||||
export interface IPostmessage {
|
||||
data: IPostmessageRequestData;
|
||||
origin: string;
|
||||
}
|
||||
|
||||
export interface IHostSignupConfig {
|
||||
brand: string;
|
||||
cookiePolicyUrl: string;
|
||||
domains: Array<string>;
|
||||
privacyPolicyUrl: string;
|
||||
termsOfServiceUrl: string;
|
||||
url: string;
|
||||
}
|
|
@ -35,13 +35,13 @@ import {
|
|||
} from "matrix-widget-api";
|
||||
import {StopGapWidgetDriver} from "../../../stores/widgets/StopGapWidgetDriver";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||
import {OwnProfileStore} from "../../../stores/OwnProfileStore";
|
||||
import { arrayFastClone } from "../../../utils/arrays";
|
||||
import { ElementWidget } from "../../../stores/widgets/StopGapWidget";
|
||||
|
||||
interface IProps {
|
||||
widgetDefinition: IModalWidgetOpenRequestData;
|
||||
widgetRoomId?: string;
|
||||
sourceWidgetId: string;
|
||||
onFinished(success: boolean, data?: IModalWidgetReturnData): void;
|
||||
}
|
||||
|
@ -123,7 +123,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
|||
|
||||
public render() {
|
||||
const templated = this.widget.getCompleteUrl({
|
||||
currentRoomId: RoomViewStore.getRoomId(),
|
||||
widgetRoomId: this.props.widgetRoomId,
|
||||
currentUserId: MatrixClientPeg.get().getUserId(),
|
||||
userDisplayName: OwnProfileStore.instance.displayName,
|
||||
userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(),
|
||||
|
|
|
@ -44,7 +44,8 @@ const RegistrationEmailPromptDialog: React.FC<IProps> = ({onFinished}) => {
|
|||
const [email, setEmail] = useState("");
|
||||
const fieldRef = useRef<Field>();
|
||||
|
||||
const onSubmit = async () => {
|
||||
const onSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (email) {
|
||||
const valid = await fieldRef.current.validate({ allowEmpty: false });
|
||||
|
||||
|
@ -73,6 +74,7 @@ const RegistrationEmailPromptDialog: React.FC<IProps> = ({onFinished}) => {
|
|||
<form onSubmit={onSubmit}>
|
||||
<Field
|
||||
ref={fieldRef}
|
||||
autoFocus={true}
|
||||
type="text"
|
||||
label={_t("Email (optional)")}
|
||||
value={email}
|
||||
|
|
|
@ -18,6 +18,7 @@ import React, {createRef} from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import Field from "../elements/Field";
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
|
||||
export default class TextInputDialog extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -29,6 +30,7 @@ export default class TextInputDialog extends React.Component {
|
|||
value: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
button: PropTypes.string,
|
||||
busyMessage: PropTypes.string, // pass _td string
|
||||
focus: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
hasCancel: PropTypes.bool,
|
||||
|
@ -40,6 +42,7 @@ export default class TextInputDialog extends React.Component {
|
|||
title: "",
|
||||
value: "",
|
||||
description: "",
|
||||
busyMessage: _td("Loading..."),
|
||||
focus: true,
|
||||
hasCancel: true,
|
||||
};
|
||||
|
@ -51,6 +54,7 @@ export default class TextInputDialog extends React.Component {
|
|||
|
||||
this.state = {
|
||||
value: this.props.value,
|
||||
busy: false,
|
||||
valid: false,
|
||||
};
|
||||
}
|
||||
|
@ -66,11 +70,13 @@ export default class TextInputDialog extends React.Component {
|
|||
onOk = async ev => {
|
||||
ev.preventDefault();
|
||||
if (this.props.validator) {
|
||||
this.setState({ busy: true });
|
||||
await this._field.current.validate({ allowEmpty: false });
|
||||
|
||||
if (!this._field.current.state.valid) {
|
||||
this._field.current.focus();
|
||||
this._field.current.validate({ allowEmpty: false, focused: true });
|
||||
this.setState({ busy: false });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -125,7 +131,8 @@ export default class TextInputDialog extends React.Component {
|
|||
</div>
|
||||
</form>
|
||||
<DialogButtons
|
||||
primaryButton={this.props.button}
|
||||
primaryButton={this.state.busy ? _t(this.props.busyMessage) : this.props.button}
|
||||
disabled={this.state.busy}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
onCancel={this.onCancel}
|
||||
hasCancel={this.props.hasCancel}
|
||||
|
|
|
@ -70,26 +70,26 @@ export default class WidgetOpenIDPermissionsDialog extends React.Component {
|
|||
return (
|
||||
<BaseDialog className='mx_WidgetOpenIDPermissionsDialog' hasCancel={true}
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("A widget would like to verify your identity")}>
|
||||
title={_t("Allow this widget to verify your identity")}>
|
||||
<div className='mx_WidgetOpenIDPermissionsDialog_content'>
|
||||
<p>
|
||||
{_t(
|
||||
"A widget located at %(widgetUrl)s would like to verify your identity. " +
|
||||
"By allowing this, the widget will be able to verify your user ID, but not " +
|
||||
"perform actions as you.", {
|
||||
widgetUrl: this.props.widget.templateUrl.split("?")[0],
|
||||
},
|
||||
)}
|
||||
{_t("The widget will verify your user ID, but won't be able to perform actions for you:")}
|
||||
</p>
|
||||
<p className="text-muted">
|
||||
{/* cheap trim to just get the path */}
|
||||
{this.props.widget.templateUrl.split("?")[0].split("#")[0]}
|
||||
</p>
|
||||
<LabelledToggleSwitch value={this.state.rememberSelection} toggleInFront={true}
|
||||
onChange={this._onRememberSelectionChange}
|
||||
label={_t("Remember my selection for this widget")} />
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Allow")}
|
||||
primaryButton={_t("Continue")}
|
||||
onPrimaryButtonClick={this._onAllow}
|
||||
cancelButton={_t("Deny")}
|
||||
onCancel={this._onDeny}
|
||||
additive={
|
||||
<LabelledToggleSwitch
|
||||
value={this.state.rememberSelection}
|
||||
toggleInFront={true}
|
||||
onChange={this._onRememberSelectionChange}
|
||||
label={_t("Remember this")} />}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
|
|
|
@ -34,6 +34,7 @@ import InteractiveAuthDialog from '../InteractiveAuthDialog';
|
|||
export default class CreateCrossSigningDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
accountPassword: PropTypes.string,
|
||||
tokenLogin: PropTypes.bool,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
@ -96,6 +97,9 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
|
|||
user: MatrixClientPeg.get().getUserId(),
|
||||
password: this.state.accountPassword,
|
||||
});
|
||||
} else if (this.props.tokenLogin) {
|
||||
// We are hoping the grace period is active
|
||||
await makeRequest({});
|
||||
} else {
|
||||
const dialogAesthetics = {
|
||||
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||
|
@ -144,6 +148,12 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
|
|||
});
|
||||
this.props.onFinished(true);
|
||||
} catch (e) {
|
||||
if (this.props.tokenLogin) {
|
||||
// ignore any failures, we are relying on grace period here
|
||||
this.props.onFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ error: e });
|
||||
console.error("Error bootstrapping cross-signing", e);
|
||||
}
|
||||
|
|
173
src/components/views/elements/DesktopCapturerSourcePicker.tsx
Normal file
173
src/components/views/elements/DesktopCapturerSourcePicker.tsx
Normal file
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import BaseDialog from "..//dialogs/BaseDialog"
|
||||
import AccessibleButton from './AccessibleButton';
|
||||
import {getDesktopCapturerSources} from "matrix-js-sdk/src/webrtc/call";
|
||||
|
||||
export interface DesktopCapturerSource {
|
||||
id: string;
|
||||
name: string;
|
||||
thumbnailURL;
|
||||
}
|
||||
|
||||
export enum Tabs {
|
||||
Screens = "screens",
|
||||
Windows = "windows",
|
||||
}
|
||||
|
||||
export interface DesktopCapturerSourceIProps {
|
||||
source: DesktopCapturerSource;
|
||||
onSelect(source: DesktopCapturerSource): void;
|
||||
}
|
||||
|
||||
export class ExistingSource extends React.Component<DesktopCapturerSourceIProps> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
onClick = (ev) => {
|
||||
this.props.onSelect(this.props.source);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AccessibleButton
|
||||
className="mx_desktopCapturerSourcePicker_stream_button"
|
||||
title={this.props.source.name}
|
||||
onClick={this.onClick} >
|
||||
<img
|
||||
className="mx_desktopCapturerSourcePicker_stream_thumbnail"
|
||||
src={this.props.source.thumbnailURL}
|
||||
/>
|
||||
<span className="mx_desktopCapturerSourcePicker_stream_name">{this.props.source.name}</span>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface DesktopCapturerSourcePickerIState {
|
||||
selectedTab: Tabs;
|
||||
sources: Array<DesktopCapturerSource>;
|
||||
}
|
||||
export interface DesktopCapturerSourcePickerIProps {
|
||||
onFinished(source: DesktopCapturerSource): void;
|
||||
}
|
||||
|
||||
export default class DesktopCapturerSourcePicker extends React.Component<
|
||||
DesktopCapturerSourcePickerIProps,
|
||||
DesktopCapturerSourcePickerIState
|
||||
> {
|
||||
interval;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
selectedTab: Tabs.Screens,
|
||||
sources: [],
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
// setInterval() first waits and then executes, therefore
|
||||
// we call getDesktopCapturerSources() here without any delay.
|
||||
// Otherwise the dialog would be left empty for some time.
|
||||
this.setState({
|
||||
sources: await getDesktopCapturerSources(),
|
||||
});
|
||||
|
||||
// We update the sources every 500ms to get newer thumbnails
|
||||
this.interval = setInterval(async () => {
|
||||
this.setState({
|
||||
sources: await getDesktopCapturerSources(),
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
onSelect = (source) => {
|
||||
this.props.onFinished(source);
|
||||
}
|
||||
|
||||
onScreensClick = (ev) => {
|
||||
this.setState({selectedTab: Tabs.Screens});
|
||||
}
|
||||
|
||||
onWindowsClick = (ev) => {
|
||||
this.setState({selectedTab: Tabs.Windows});
|
||||
}
|
||||
|
||||
onCloseClick = (ev) => {
|
||||
this.props.onFinished(null);
|
||||
}
|
||||
|
||||
render() {
|
||||
let sources;
|
||||
if (this.state.selectedTab === Tabs.Screens) {
|
||||
sources = this.state.sources
|
||||
.filter((source) => {
|
||||
return source.id.startsWith("screen");
|
||||
})
|
||||
.map((source) => {
|
||||
return <ExistingSource source={source} onSelect={this.onSelect} key={source.id} />;
|
||||
});
|
||||
} else {
|
||||
sources = this.state.sources
|
||||
.filter((source) => {
|
||||
return source.id.startsWith("window");
|
||||
})
|
||||
.map((source) => {
|
||||
return <ExistingSource source={source} onSelect={this.onSelect} key={source.id} />;
|
||||
});
|
||||
}
|
||||
|
||||
const buttonStyle = "mx_desktopCapturerSourcePicker_tabLabel";
|
||||
const screensButtonStyle = buttonStyle + ((this.state.selectedTab === Tabs.Screens) ? "_selected" : "");
|
||||
const windowsButtonStyle = buttonStyle + ((this.state.selectedTab === Tabs.Windows) ? "_selected" : "");
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_desktopCapturerSourcePicker"
|
||||
onFinished={this.onCloseClick}
|
||||
title={_t("Share your screen")}
|
||||
>
|
||||
<div className="mx_desktopCapturerSourcePicker_tabLabels">
|
||||
<AccessibleButton
|
||||
className={screensButtonStyle}
|
||||
onClick={this.onScreensClick}
|
||||
>
|
||||
{_t("Screens")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
className={windowsButtonStyle}
|
||||
onClick={this.onWindowsClick}
|
||||
>
|
||||
{_t("Windows")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div className="mx_desktopCapturerSourcePicker_panel">
|
||||
{ sources }
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@ import * as Avatar from '../../../Avatar';
|
|||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import EventTile from '../rooms/EventTile';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {Layout} from "../../../settings/Layout";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
|
||||
interface IProps {
|
||||
|
@ -33,7 +34,7 @@ interface IProps {
|
|||
/**
|
||||
* Whether to use the irc layout or not
|
||||
*/
|
||||
useIRCLayout: boolean;
|
||||
layout: Layout;
|
||||
|
||||
/**
|
||||
* classnames to apply to the wrapper of the preview
|
||||
|
@ -121,14 +122,14 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
|
|||
const event = this.fakeEvent(this.state);
|
||||
|
||||
const className = classnames(this.props.className, {
|
||||
"mx_IRCLayout": this.props.useIRCLayout,
|
||||
"mx_GroupLayout": !this.props.useIRCLayout,
|
||||
"mx_IRCLayout": this.props.layout == Layout.IRC,
|
||||
"mx_GroupLayout": this.props.layout == Layout.Group,
|
||||
});
|
||||
|
||||
return <div className={className}>
|
||||
<EventTile
|
||||
mxEvent={event}
|
||||
useIRCLayout={this.props.useIRCLayout}
|
||||
layout={this.props.layout}
|
||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||
/>
|
||||
</div>;
|
||||
|
|
|
@ -23,6 +23,7 @@ import ResizeObserver from 'resize-observer-polyfill';
|
|||
import dis from '../../../dispatcher/dispatcher';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {isNullOrUndefined} from "matrix-js-sdk/src/utils";
|
||||
|
||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||
|
@ -61,6 +62,9 @@ export default class PersistedElement extends React.Component {
|
|||
// Any PersistedElements with the same persistKey will use
|
||||
// the same DOM container.
|
||||
persistKey: PropTypes.string.isRequired,
|
||||
|
||||
// z-index for the element. Defaults to 9.
|
||||
zIndex: PropTypes.number,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
|
@ -165,6 +169,7 @@ export default class PersistedElement extends React.Component {
|
|||
|
||||
const parentRect = parent.getBoundingClientRect();
|
||||
Object.assign(child.style, {
|
||||
zIndex: isNullOrUndefined(this.props.zIndex) ? 9 : this.props.zIndex,
|
||||
position: 'absolute',
|
||||
top: parentRect.top + 'px',
|
||||
left: parentRect.left + 'px',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -23,23 +23,11 @@ import { Room, RoomMember } from 'matrix-js-sdk';
|
|||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import FlairStore from "../../../stores/FlairStore";
|
||||
import {getPrimaryPermalinkEntity} from "../../../utils/permalinks/Permalinks";
|
||||
import {getPrimaryPermalinkEntity, parseAppLocalLink} from "../../../utils/permalinks/Permalinks";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
|
||||
// For URLs of matrix.to links in the timeline which have been reformatted by
|
||||
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
|
||||
const REGEX_LOCAL_PERMALINK = /^#\/(?:user|room|group)\/(([#!@+]).*?)(?=\/|\?|$)/;
|
||||
|
||||
class Pill extends React.Component {
|
||||
static isPillUrl(url) {
|
||||
return !!getPrimaryPermalinkEntity(url);
|
||||
}
|
||||
|
||||
static isMessagePillUrl(url) {
|
||||
return !!REGEX_LOCAL_PERMALINK.exec(url);
|
||||
}
|
||||
|
||||
static roomNotifPos(text) {
|
||||
return text.indexOf("@room");
|
||||
}
|
||||
|
@ -56,7 +44,7 @@ class Pill extends React.Component {
|
|||
static propTypes = {
|
||||
// The Type of this Pill. If url is given, this is auto-detected.
|
||||
type: PropTypes.string,
|
||||
// The URL to pillify (no validation is done, see isPillUrl and isMessagePillUrl)
|
||||
// The URL to pillify (no validation is done)
|
||||
url: PropTypes.string,
|
||||
// Whether the pill is in a message
|
||||
inMessage: PropTypes.bool,
|
||||
|
@ -90,12 +78,9 @@ class Pill extends React.Component {
|
|||
|
||||
if (nextProps.url) {
|
||||
if (nextProps.inMessage) {
|
||||
// Default to the empty array if no match for simplicity
|
||||
// resource and prefix will be undefined instead of throwing
|
||||
const matrixToMatch = REGEX_LOCAL_PERMALINK.exec(nextProps.url) || [];
|
||||
|
||||
resourceId = matrixToMatch[1]; // The room/user ID
|
||||
prefix = matrixToMatch[2]; // The first character of prefix
|
||||
const parts = parseAppLocalLink(nextProps.url);
|
||||
resourceId = parts.primaryEntityId; // The room/user ID
|
||||
prefix = parts.sigil; // The first character of prefix
|
||||
} else {
|
||||
resourceId = getPrimaryPermalinkEntity(nextProps.url);
|
||||
prefix = resourceId ? resourceId[0] : undefined;
|
||||
|
|
|
@ -24,6 +24,7 @@ import {wantsDateSeparator} from '../../../DateUtils';
|
|||
import {MatrixEvent} from 'matrix-js-sdk';
|
||||
import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {LayoutPropType} from "../../../settings/Layout";
|
||||
import escapeHtml from "escape-html";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
|
@ -42,7 +43,7 @@ export default class ReplyThread extends React.Component {
|
|||
onHeightChanged: PropTypes.func.isRequired,
|
||||
permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
|
||||
// Specifies which layout to use.
|
||||
useIRCLayout: PropTypes.bool,
|
||||
layout: LayoutPropType,
|
||||
};
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
@ -209,7 +210,7 @@ export default class ReplyThread extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, useIRCLayout) {
|
||||
static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout) {
|
||||
if (!ReplyThread.getParentEventId(parentEv)) {
|
||||
return <div className="mx_ReplyThread_wrapper_empty" />;
|
||||
}
|
||||
|
@ -218,7 +219,7 @@ export default class ReplyThread extends React.Component {
|
|||
onHeightChanged={onHeightChanged}
|
||||
ref={ref}
|
||||
permalinkCreator={permalinkCreator}
|
||||
useIRCLayout={useIRCLayout}
|
||||
layout={layout}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
@ -386,7 +387,7 @@ export default class ReplyThread extends React.Component {
|
|||
permalinkCreator={this.props.permalinkCreator}
|
||||
isRedacted={ev.isRedacted()}
|
||||
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
|
||||
useIRCLayout={this.props.useIRCLayout}
|
||||
layout={this.props.layout}
|
||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||
replacingEventId={ev.replacingEventId()}
|
||||
/>
|
||||
|
|
|
@ -22,13 +22,33 @@ import {MatrixClient} from "matrix-js-sdk/src/client";
|
|||
import PlatformPeg from "../../../PlatformPeg";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {IIdentityProvider, ISSOFlow} from "../../../Login";
|
||||
import {IdentityProviderBrand, IIdentityProvider, ISSOFlow} from "../../../Login";
|
||||
import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
||||
|
||||
interface ISSOButtonProps extends Omit<IProps, "flow"> {
|
||||
idp: IIdentityProvider;
|
||||
mini?: boolean;
|
||||
}
|
||||
|
||||
const getIcon = (brand: IdentityProviderBrand | string) => {
|
||||
switch (brand) {
|
||||
case IdentityProviderBrand.Apple:
|
||||
return require(`../../../../res/img/element-icons/brands/apple.svg`);
|
||||
case IdentityProviderBrand.Facebook:
|
||||
return require(`../../../../res/img/element-icons/brands/facebook.svg`);
|
||||
case IdentityProviderBrand.Github:
|
||||
return require(`../../../../res/img/element-icons/brands/github.svg`);
|
||||
case IdentityProviderBrand.Gitlab:
|
||||
return require(`../../../../res/img/element-icons/brands/gitlab.svg`);
|
||||
case IdentityProviderBrand.Google:
|
||||
return require(`../../../../res/img/element-icons/brands/google.svg`);
|
||||
case IdentityProviderBrand.Twitter:
|
||||
return require(`../../../../res/img/element-icons/brands/twitter.svg`);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const SSOButton: React.FC<ISSOButtonProps> = ({
|
||||
matrixClient,
|
||||
loginType,
|
||||
|
@ -38,7 +58,6 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
|
|||
mini,
|
||||
...props
|
||||
}) => {
|
||||
const kind = primary ? "primary" : "primary_outline";
|
||||
const label = idp ? _t("Continue with %(provider)s", { provider: idp.name }) : _t("Sign in with single sign-on");
|
||||
|
||||
const onClick = () => {
|
||||
|
@ -46,30 +65,35 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
|
|||
};
|
||||
|
||||
let icon;
|
||||
if (typeof idp?.icon === "string" && (idp.icon.startsWith("mxc://") || idp.icon.startsWith("https://"))) {
|
||||
icon = <img
|
||||
src={matrixClient.mxcUrlToHttp(idp.icon, 24, 24, "crop", true)}
|
||||
height="24"
|
||||
width="24"
|
||||
alt={label}
|
||||
/>;
|
||||
let brandClass;
|
||||
const brandIcon = idp ? getIcon(idp.brand) : null;
|
||||
if (brandIcon) {
|
||||
const brandName = idp.brand.split(".").pop();
|
||||
brandClass = `mx_SSOButton_brand_${brandName}`;
|
||||
icon = <img src={brandIcon} height="24" width="24" alt={brandName} />;
|
||||
} else if (typeof idp?.icon === "string" && idp.icon.startsWith("mxc://")) {
|
||||
const src = matrixClient.mxcUrlToHttp(idp.icon, 24, 24, "crop", true);
|
||||
icon = <img src={src} height="24" width="24" alt={idp.name} />;
|
||||
}
|
||||
|
||||
const classes = classNames("mx_SSOButton", {
|
||||
[brandClass]: brandClass,
|
||||
mx_SSOButton_mini: mini,
|
||||
mx_SSOButton_default: !idp,
|
||||
mx_SSOButton_primary: primary,
|
||||
});
|
||||
|
||||
if (mini) {
|
||||
// TODO fallback icon
|
||||
return (
|
||||
<AccessibleButton {...props} className={classes} kind={kind} onClick={onClick}>
|
||||
<AccessibleTooltipButton {...props} title={label} className={classes} onClick={onClick}>
|
||||
{ icon }
|
||||
</AccessibleButton>
|
||||
</AccessibleTooltipButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AccessibleButton {...props} className={classes} kind={kind} onClick={onClick}>
|
||||
<AccessibleButton {...props} className={classes} onClick={onClick}>
|
||||
{ icon }
|
||||
{ label }
|
||||
</AccessibleButton>
|
||||
|
|
36
src/components/views/host_signup/HostSignupContainer.tsx
Normal file
36
src/components/views/host_signup/HostSignupContainer.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import HostSignupDialog from "../dialogs/HostSignupDialog";
|
||||
import { HostSignupStore } from "../../../stores/HostSignupStore";
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
|
||||
const HostSignupContainer = () => {
|
||||
const [isActive, setIsActive] = useState(HostSignupStore.instance.isHostSignupActive);
|
||||
useEventEmitter(HostSignupStore.instance, UPDATE_EVENT, () => {
|
||||
setIsActive(HostSignupStore.instance.isHostSignupActive);
|
||||
});
|
||||
|
||||
return <div className="mx_HostSignupContainer">
|
||||
{isActive &&
|
||||
<HostSignupDialog />
|
||||
}
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default HostSignupContainer
|
|
@ -288,7 +288,7 @@ export default class MFileBody extends React.Component {
|
|||
<a ref={this._dummyLink} />
|
||||
</div>
|
||||
<iframe
|
||||
src={`${url}?origin=${encodeURIComponent(window.location.origin)}`}
|
||||
src={url}
|
||||
onLoad={onIframeLoad}
|
||||
ref={this._iframe}
|
||||
sandbox="allow-scripts allow-downloads allow-downloads-without-user-activation" />
|
||||
|
|
|
@ -362,7 +362,7 @@ export default class MImageBody extends React.Component {
|
|||
}
|
||||
|
||||
// The maximum height of the thumbnail as it is rendered as an <img>
|
||||
const maxHeight = Math.min(this.props.maxImageHeight || 600, infoHeight);
|
||||
const maxHeight = Math.min(this.props.maxImageHeight || 240, infoHeight);
|
||||
// The maximum width of the thumbnail, as dictated by its natural
|
||||
// maximum height.
|
||||
const maxWidth = infoWidth * maxHeight / infoHeight;
|
||||
|
|
|
@ -37,11 +37,13 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
|
|||
const senderName = this.props.mxEvent.sender?.name || this.props.mxEvent.getSender();
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
const widgetId = this.props.mxEvent.getStateKey();
|
||||
const widget = WidgetStore.instance.getRoom(room.roomId).widgets.find(w => w.id === widgetId);
|
||||
const widget = WidgetStore.instance.getRoom(room.roomId, true).widgets.find(w => w.id === widgetId);
|
||||
|
||||
let joinCopy = _t('Join the conference at the top of this room');
|
||||
if (widget && WidgetLayoutStore.instance.isInContainer(room, widget, Container.Right)) {
|
||||
joinCopy = _t('Join the conference from the room information card on the right');
|
||||
} else if (!widget) {
|
||||
joinCopy = null;
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
|
|
|
@ -81,6 +81,7 @@ export default class TextualBody extends React.Component {
|
|||
}
|
||||
|
||||
_applyFormatting() {
|
||||
const showLineNumbers = SettingsStore.getValue("showCodeLineNumbers");
|
||||
this.activateSpoilers([this._content.current]);
|
||||
|
||||
// pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
|
||||
|
@ -91,29 +92,140 @@ export default class TextualBody extends React.Component {
|
|||
this.calculateUrlPreview();
|
||||
|
||||
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") {
|
||||
const blocks = ReactDOM.findDOMNode(this).getElementsByTagName("code");
|
||||
if (blocks.length > 0) {
|
||||
// Handle expansion and add buttons
|
||||
const pres = ReactDOM.findDOMNode(this).getElementsByTagName("pre");
|
||||
if (pres.length > 0) {
|
||||
for (let i = 0; i < pres.length; i++) {
|
||||
// If there already is a div wrapping the codeblock we want to skip this.
|
||||
// This happens after the codeblock was edited.
|
||||
if (pres[i].parentNode.className == "mx_EventTile_pre_container") continue;
|
||||
// Wrap a div around <pre> so that the copy button can be correctly positioned
|
||||
// when the <pre> overflows and is scrolled horizontally.
|
||||
const div = this._wrapInDiv(pres[i]);
|
||||
this._handleCodeBlockExpansion(pres[i]);
|
||||
this._addCodeExpansionButton(div, pres[i]);
|
||||
this._addCodeCopyButton(div);
|
||||
if (showLineNumbers) {
|
||||
this._addLineNumbers(pres[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Highlight code
|
||||
const codes = ReactDOM.findDOMNode(this).getElementsByTagName("code");
|
||||
if (codes.length > 0) {
|
||||
// Do this asynchronously: parsing code takes time and we don't
|
||||
// need to block the DOM update on it.
|
||||
setTimeout(() => {
|
||||
if (this._unmounted) return;
|
||||
for (let i = 0; i < blocks.length; i++) {
|
||||
if (SettingsStore.getValue("enableSyntaxHighlightLanguageDetection")) {
|
||||
highlight.highlightBlock(blocks[i]);
|
||||
} else {
|
||||
// Only syntax highlight if there's a class starting with language-
|
||||
const classes = blocks[i].className.split(/\s+/).filter(function(cl) {
|
||||
return cl.startsWith('language-') && !cl.startsWith('language-_');
|
||||
});
|
||||
|
||||
if (classes.length != 0) {
|
||||
highlight.highlightBlock(blocks[i]);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < codes.length; i++) {
|
||||
// If the code already has the hljs class we want to skip this.
|
||||
// This happens after the codeblock was edited.
|
||||
if (codes[i].className.includes("hljs")) continue;
|
||||
this._highlightCode(codes[i]);
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
this._addCodeCopyButton();
|
||||
}
|
||||
}
|
||||
|
||||
_addCodeExpansionButton(div, pre) {
|
||||
// Calculate how many percent does the pre element take up.
|
||||
// If it's less than 30% we don't add the expansion button.
|
||||
const percentageOfViewport = pre.offsetHeight / window.innerHeight * 100;
|
||||
if (percentageOfViewport < 30) return;
|
||||
|
||||
const button = document.createElement("span");
|
||||
button.className = "mx_EventTile_button ";
|
||||
if (pre.className == "mx_EventTile_collapsedCodeBlock") {
|
||||
button.className += "mx_EventTile_expandButton";
|
||||
} else {
|
||||
button.className += "mx_EventTile_collapseButton";
|
||||
}
|
||||
|
||||
button.onclick = async () => {
|
||||
button.className = "mx_EventTile_button ";
|
||||
if (pre.className == "mx_EventTile_collapsedCodeBlock") {
|
||||
pre.className = "";
|
||||
button.className += "mx_EventTile_collapseButton";
|
||||
} else {
|
||||
pre.className = "mx_EventTile_collapsedCodeBlock";
|
||||
button.className += "mx_EventTile_expandButton";
|
||||
}
|
||||
|
||||
// By expanding/collapsing we changed
|
||||
// the height, therefore we call this
|
||||
this.props.onHeightChanged();
|
||||
};
|
||||
|
||||
div.appendChild(button);
|
||||
}
|
||||
|
||||
_addCodeCopyButton(div) {
|
||||
const button = document.createElement("span");
|
||||
button.className = "mx_EventTile_button mx_EventTile_copyButton ";
|
||||
|
||||
// Check if expansion button exists. If so
|
||||
// we put the copy button to the bottom
|
||||
const expansionButtonExists = div.getElementsByClassName("mx_EventTile_button");
|
||||
if (expansionButtonExists.length > 0) button.className += "mx_EventTile_buttonBottom";
|
||||
|
||||
button.onclick = async () => {
|
||||
const copyCode = button.parentNode.getElementsByTagName("code")[0];
|
||||
const successful = await copyPlaintext(copyCode.textContent);
|
||||
|
||||
const buttonRect = button.getBoundingClientRect();
|
||||
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
||||
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
|
||||
...toRightOf(buttonRect, 2),
|
||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||
});
|
||||
button.onmouseleave = close;
|
||||
};
|
||||
|
||||
div.appendChild(button);
|
||||
}
|
||||
|
||||
_wrapInDiv(pre) {
|
||||
const div = document.createElement("div");
|
||||
div.className = "mx_EventTile_pre_container";
|
||||
|
||||
// Insert containing div in place of <pre> block
|
||||
pre.parentNode.replaceChild(div, pre);
|
||||
// Append <pre> block and copy button to container
|
||||
div.appendChild(pre);
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
_handleCodeBlockExpansion(pre) {
|
||||
if (!SettingsStore.getValue("expandCodeByDefault")) {
|
||||
pre.className = "mx_EventTile_collapsedCodeBlock";
|
||||
}
|
||||
}
|
||||
|
||||
_addLineNumbers(pre) {
|
||||
pre.innerHTML = '<span class="mx_EventTile_lineNumbers"></span>' + pre.innerHTML + '<span></span>';
|
||||
const lineNumbers = pre.getElementsByClassName("mx_EventTile_lineNumbers")[0];
|
||||
// Calculate number of lines in pre
|
||||
const number = pre.innerHTML.split(/\n/).length;
|
||||
// Iterate through lines starting with 1 (number of the first line is 1)
|
||||
for (let i = 1; i < number; i++) {
|
||||
lineNumbers.innerHTML += '<span class="mx_EventTile_lineNumber">' + i + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
_highlightCode(code) {
|
||||
if (SettingsStore.getValue("enableSyntaxHighlightLanguageDetection")) {
|
||||
highlight.highlightBlock(code);
|
||||
} else {
|
||||
// Only syntax highlight if there's a class starting with language-
|
||||
const classes = code.className.split(/\s+/).filter(function(cl) {
|
||||
return cl.startsWith('language-') && !cl.startsWith('language-_');
|
||||
});
|
||||
|
||||
if (classes.length != 0) {
|
||||
highlight.highlightBlock(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -254,38 +366,6 @@ export default class TextualBody extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_addCodeCopyButton() {
|
||||
// Add 'copy' buttons to pre blocks
|
||||
Array.from(ReactDOM.findDOMNode(this).querySelectorAll('.mx_EventTile_body pre')).forEach((p) => {
|
||||
const button = document.createElement("span");
|
||||
button.className = "mx_EventTile_copyButton";
|
||||
button.onclick = async () => {
|
||||
const copyCode = button.parentNode.getElementsByTagName("pre")[0];
|
||||
const successful = await copyPlaintext(copyCode.textContent);
|
||||
|
||||
const buttonRect = button.getBoundingClientRect();
|
||||
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
||||
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
|
||||
...toRightOf(buttonRect, 2),
|
||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||
});
|
||||
button.onmouseleave = close;
|
||||
};
|
||||
|
||||
// Wrap a div around <pre> so that the copy button can be correctly positioned
|
||||
// when the <pre> overflows and is scrolled horizontally.
|
||||
const div = document.createElement("div");
|
||||
div.className = "mx_EventTile_pre_container";
|
||||
|
||||
// Insert containing div in place of <pre> block
|
||||
p.parentNode.replaceChild(div, p);
|
||||
|
||||
// Append <pre> block and copy button to container
|
||||
div.appendChild(p);
|
||||
div.appendChild(button);
|
||||
});
|
||||
}
|
||||
|
||||
onCancelClick = event => {
|
||||
this.setState({ widgetHidden: true });
|
||||
// FIXME: persist this somewhere smarter than local storage
|
||||
|
|
|
@ -33,6 +33,7 @@ interface IProps {
|
|||
previousPhase?: RightPanelPhases;
|
||||
closeLabel?: string;
|
||||
onClose?(): void;
|
||||
refireParams?;
|
||||
}
|
||||
|
||||
interface IGroupProps {
|
||||
|
@ -56,6 +57,7 @@ const BaseCard: React.FC<IProps> = ({
|
|||
withoutScrollContainer,
|
||||
previousPhase,
|
||||
children,
|
||||
refireParams,
|
||||
}) => {
|
||||
let backButton;
|
||||
if (previousPhase) {
|
||||
|
@ -63,6 +65,7 @@ const BaseCard: React.FC<IProps> = ({
|
|||
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: previousPhase,
|
||||
refireParams: refireParams,
|
||||
});
|
||||
};
|
||||
backButton = <AccessibleButton className="mx_BaseCard_back" onClick={onBackClick} title={_t("Back")} />;
|
||||
|
|
|
@ -77,7 +77,7 @@ export const useWidgets = (room: Room) => {
|
|||
setApps([...WidgetStore.instance.getApps(room.roomId)]);
|
||||
}, [room]);
|
||||
|
||||
useEffect(updateApps, [room]);
|
||||
useEffect(updateApps, [room, updateApps]);
|
||||
useEventEmitter(WidgetStore.instance, room.roomId, updateApps);
|
||||
useEventEmitter(WidgetLayoutStore.instance, WidgetLayoutStore.emissionForRoom(room), updateApps);
|
||||
|
||||
|
|
|
@ -60,6 +60,7 @@ import QuestionDialog from "../dialogs/QuestionDialog";
|
|||
import ConfirmUserActionDialog from "../dialogs/ConfirmUserActionDialog";
|
||||
import InfoDialog from "../dialogs/InfoDialog";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
|
||||
interface IDevice {
|
||||
deviceId: string;
|
||||
|
@ -1534,6 +1535,24 @@ const UserInfo: React.FC<Props> = ({
|
|||
|
||||
const classes = ["mx_UserInfo"];
|
||||
|
||||
let refireParams;
|
||||
let previousPhase: RightPanelPhases;
|
||||
// We have no previousPhase for when viewing a UserInfo from a Group or without a Room at this time
|
||||
if (room && phase === RightPanelPhases.EncryptionPanel) {
|
||||
previousPhase = RightPanelPhases.RoomMemberInfo;
|
||||
refireParams = {member: member};
|
||||
} else if (room) {
|
||||
previousPhase = RightPanelPhases.RoomMemberList;
|
||||
}
|
||||
|
||||
const onEncryptionPanelClose = () => {
|
||||
dis.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: previousPhase,
|
||||
refireParams: refireParams,
|
||||
});
|
||||
}
|
||||
|
||||
let content;
|
||||
switch (phase) {
|
||||
case RightPanelPhases.RoomMemberInfo:
|
||||
|
@ -1553,19 +1572,13 @@ const UserInfo: React.FC<Props> = ({
|
|||
<EncryptionPanel
|
||||
{...props as React.ComponentProps<typeof EncryptionPanel>}
|
||||
member={member}
|
||||
onClose={onClose}
|
||||
onClose={onEncryptionPanelClose}
|
||||
isRoomEncrypted={isRoomEncrypted}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
let previousPhase: RightPanelPhases;
|
||||
// We have no previousPhase for when viewing a UserInfo from a Group or without a Room at this time
|
||||
if (room) {
|
||||
previousPhase = RightPanelPhases.RoomMemberList;
|
||||
}
|
||||
|
||||
let closeLabel = undefined;
|
||||
if (phase === RightPanelPhases.EncryptionPanel) {
|
||||
const verificationRequest = (props as React.ComponentProps<typeof EncryptionPanel>).verificationRequest;
|
||||
|
@ -1581,6 +1594,7 @@ const UserInfo: React.FC<Props> = ({
|
|||
onClose={onClose}
|
||||
closeLabel={closeLabel}
|
||||
previousPhase={previousPhase}
|
||||
refireParams={refireParams}
|
||||
>
|
||||
{ content }
|
||||
</BaseCard>;
|
||||
|
|
|
@ -69,19 +69,24 @@ export default class RoomProfileSettings extends React.Component {
|
|||
// clear file upload field so same file can be selected
|
||||
this._avatarUpload.current.value = "";
|
||||
this.setState({
|
||||
avatarUrl: undefined,
|
||||
avatarFile: undefined,
|
||||
avatarUrl: null,
|
||||
avatarFile: null,
|
||||
enableProfileSave: true,
|
||||
});
|
||||
};
|
||||
|
||||
_clearProfile = async (e) => {
|
||||
_cancelProfileChanges = async (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.state.enableProfileSave) return;
|
||||
this._removeAvatar();
|
||||
this.setState({enableProfileSave: false, displayName: this.state.originalDisplayName});
|
||||
this.setState({
|
||||
enableProfileSave: false,
|
||||
displayName: this.state.originalDisplayName,
|
||||
topic: this.state.originalTopic,
|
||||
avatarUrl: this.state.originalAvatarUrl,
|
||||
avatarFile: null,
|
||||
});
|
||||
};
|
||||
|
||||
_saveProfile = async (e) => {
|
||||
|
@ -108,7 +113,7 @@ export default class RoomProfileSettings extends React.Component {
|
|||
newState.originalAvatarUrl = newState.avatarUrl;
|
||||
newState.avatarFile = null;
|
||||
} else if (this.state.originalAvatarUrl !== this.state.avatarUrl) {
|
||||
await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {url: undefined}, '');
|
||||
await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {}, '');
|
||||
}
|
||||
|
||||
if (this.state.originalTopic !== this.state.topic) {
|
||||
|
@ -164,11 +169,15 @@ export default class RoomProfileSettings extends React.Component {
|
|||
const AvatarSetting = sdk.getComponent('settings.AvatarSetting');
|
||||
|
||||
let profileSettingsButtons;
|
||||
if (this.state.canSetTopic && this.state.canSetName) {
|
||||
if (
|
||||
this.state.canSetName ||
|
||||
this.state.canSetTopic ||
|
||||
this.state.canSetAvatar
|
||||
) {
|
||||
profileSettingsButtons = (
|
||||
<div className="mx_ProfileSettings_buttons">
|
||||
<AccessibleButton
|
||||
onClick={this._clearProfile}
|
||||
onClick={this._cancelProfileChanges}
|
||||
kind="link"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
>
|
||||
|
|
|
@ -519,7 +519,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
|
||||
private async tabCompleteName(event: React.KeyboardEvent) {
|
||||
try {
|
||||
await new Promise(resolve => this.setState({showVisualBell: false}, resolve));
|
||||
await new Promise<void>(resolve => this.setState({showVisualBell: false}, resolve));
|
||||
const {model} = this.props;
|
||||
const caret = this.getCaret();
|
||||
const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
|
||||
|
|
|
@ -27,6 +27,7 @@ import * as TextForEvent from "../../../TextForEvent";
|
|||
import * as sdk from "../../../index";
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {Layout, LayoutPropType} from "../../../settings/Layout";
|
||||
import {EventStatus} from 'matrix-js-sdk';
|
||||
import {formatTime} from "../../../DateUtils";
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
|
@ -227,8 +228,8 @@ export default class EventTile extends React.Component {
|
|||
// whether to show reactions for this event
|
||||
showReactions: PropTypes.bool,
|
||||
|
||||
// whether to use the irc layout
|
||||
useIRCLayout: PropTypes.bool,
|
||||
// which layout to use
|
||||
layout: LayoutPropType,
|
||||
|
||||
// whether or not to show flair at all
|
||||
enableFlair: PropTypes.bool,
|
||||
|
@ -734,7 +735,7 @@ export default class EventTile extends React.Component {
|
|||
// joins/parts/etc
|
||||
avatarSize = 14;
|
||||
needsSenderProfile = false;
|
||||
} else if (this.props.useIRCLayout) {
|
||||
} else if (this.props.layout == Layout.IRC) {
|
||||
avatarSize = 14;
|
||||
needsSenderProfile = true;
|
||||
} else if (this.props.continuation && this.props.tileShape !== "file_grid") {
|
||||
|
@ -845,10 +846,11 @@ export default class EventTile extends React.Component {
|
|||
{ timestamp }
|
||||
</a>;
|
||||
|
||||
const groupTimestamp = !this.props.useIRCLayout ? linkedTimestamp : null;
|
||||
const ircTimestamp = this.props.useIRCLayout ? linkedTimestamp : null;
|
||||
const groupPadlock = !this.props.useIRCLayout && !isBubbleMessage && this._renderE2EPadlock();
|
||||
const ircPadlock = this.props.useIRCLayout && !isBubbleMessage && this._renderE2EPadlock();
|
||||
const useIRCLayout = this.props.layout == Layout.IRC;
|
||||
const groupTimestamp = !useIRCLayout ? linkedTimestamp : null;
|
||||
const ircTimestamp = useIRCLayout ? linkedTimestamp : null;
|
||||
const groupPadlock = !useIRCLayout && !isBubbleMessage && this._renderE2EPadlock();
|
||||
const ircPadlock = useIRCLayout && !isBubbleMessage && this._renderE2EPadlock();
|
||||
|
||||
switch (this.props.tileShape) {
|
||||
case 'notif': {
|
||||
|
@ -943,7 +945,7 @@ export default class EventTile extends React.Component {
|
|||
this.props.onHeightChanged,
|
||||
this.props.permalinkCreator,
|
||||
this._replyThread,
|
||||
this.props.useIRCLayout,
|
||||
this.props.layout,
|
||||
);
|
||||
|
||||
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
|
||||
|
|
|
@ -107,7 +107,7 @@ export default class LinkPreviewWidget extends React.Component {
|
|||
if (!SettingsStore.getValue("showImages")) {
|
||||
image = null; // Don't render a button to show the image, just hide it outright
|
||||
}
|
||||
const imageMaxWidth = 100; const imageMaxHeight = 100;
|
||||
const imageMaxWidth = 320; const imageMaxHeight = 240;
|
||||
if (image && image.startsWith("mxc://")) {
|
||||
image = MatrixClientPeg.get().mxcUrlToHttp(image, imageMaxWidth, imageMaxHeight);
|
||||
}
|
||||
|
@ -134,6 +134,10 @@ export default class LinkPreviewWidget extends React.Component {
|
|||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<div className="mx_LinkPreviewWidget">
|
||||
<AccessibleButton className="mx_LinkPreviewWidget_cancel" onClick={this.props.onCancelClick} aria-label={_t("Close preview")}>
|
||||
<img className="mx_filterFlipColor" alt="" role="presentation"
|
||||
src={require("../../../../res/img/cancel.svg")} width="18" height="18" />
|
||||
</AccessibleButton>
|
||||
{ img }
|
||||
<div className="mx_LinkPreviewWidget_caption">
|
||||
<div className="mx_LinkPreviewWidget_title"><a href={this.props.link} target="_blank" rel="noreferrer noopener">{ p["og:title"] }</a></div>
|
||||
|
@ -142,10 +146,6 @@ export default class LinkPreviewWidget extends React.Component {
|
|||
{ description }
|
||||
</div>
|
||||
</div>
|
||||
<AccessibleButton className="mx_LinkPreviewWidget_cancel" onClick={this.props.onCancelClick} aria-label={_t("Close preview")}>
|
||||
<img className="mx_filterFlipColor" alt="" role="presentation"
|
||||
src={require("../../../../res/img/cancel.svg")} width="18" height="18" />
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -426,7 +426,8 @@ export default class MessageComposer extends React.Component {
|
|||
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
|
||||
);
|
||||
|
||||
if (SettingsStore.getValue(UIFeature.Widgets)) {
|
||||
if (SettingsStore.getValue(UIFeature.Widgets) &&
|
||||
SettingsStore.getValue("MessageComposerInput.showStickersButton")) {
|
||||
controls.push(<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />);
|
||||
}
|
||||
|
||||
|
|
|
@ -111,7 +111,7 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
|
|||
appear={true} in={this.state.doAnimation} timeout={640}
|
||||
classNames='mx_RoomBreadcrumbs'
|
||||
>
|
||||
<Toolbar className='mx_RoomBreadcrumbs'>
|
||||
<Toolbar className='mx_RoomBreadcrumbs' aria-label={_t("Recently visited rooms")}>
|
||||
{tiles.slice(this.state.skipFirst ? 1 : 0)}
|
||||
</Toolbar>
|
||||
</CSSTransition>
|
||||
|
|
|
@ -455,8 +455,9 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
const unfilteredLists = RoomListStore.instance.unfilteredLists
|
||||
const unfilteredRooms = unfilteredLists[DefaultTagID.Untagged] || [];
|
||||
const unfilteredHistorical = unfilteredLists[DefaultTagID.Archived] || [];
|
||||
const unfilteredFavourite = unfilteredLists[DefaultTagID.Favourite] || [];
|
||||
// show a prompt to join/create rooms if the user is in 0 rooms and no historical
|
||||
if (unfilteredRooms.length < 1 && unfilteredHistorical < 1) {
|
||||
if (unfilteredRooms.length < 1 && unfilteredHistorical < 1 && unfilteredFavourite < 1) {
|
||||
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
||||
<div>{_t("Use the + to make a new room or explore existing ones below")}</div>
|
||||
<AccessibleButton
|
||||
|
|
|
@ -110,6 +110,11 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
|
||||
this.onCommunityUpdate,
|
||||
);
|
||||
this.props.room.on("Room.name", this.onRoomNameUpdate);
|
||||
}
|
||||
|
||||
private onRoomNameUpdate = (room) => {
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
private onNotificationUpdate = () => {
|
||||
|
@ -150,6 +155,8 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
CommunityPrototypeStore.getUpdateEventName(this.props.room?.roomId),
|
||||
this.onCommunityUpdate,
|
||||
);
|
||||
prevProps.room?.off("Room.name", this.onRoomNameUpdate);
|
||||
this.props.room?.on("Room.name", this.onRoomNameUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -171,6 +178,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
|
||||
this.onCommunityUpdate,
|
||||
);
|
||||
this.props.room.off("Room.name", this.onRoomNameUpdate);
|
||||
}
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
this.notificationState.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
||||
|
|
|
@ -156,13 +156,14 @@ export default class SendMessageComposer extends React.Component {
|
|||
this.onVerticalArrow(event, true);
|
||||
} else if (event.key === Key.ARROW_DOWN) {
|
||||
this.onVerticalArrow(event, false);
|
||||
} else if (this._prepareToEncrypt) {
|
||||
this._prepareToEncrypt();
|
||||
} else if (event.key === Key.ESCAPE) {
|
||||
dis.dispatch({
|
||||
action: 'reply_to_event',
|
||||
event: null,
|
||||
});
|
||||
} else if (this._prepareToEncrypt) {
|
||||
// This needs to be last!
|
||||
this._prepareToEncrypt();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -402,6 +403,7 @@ export default class SendMessageComposer extends React.Component {
|
|||
this._editorRef.clearUndoHistory();
|
||||
this._editorRef.focus();
|
||||
this._clearStoredEditorState();
|
||||
dis.dispatch({action: "scroll_to_bottom"});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
|
|
@ -264,7 +264,7 @@ export default class Stickerpicker extends React.Component {
|
|||
width: this.popoverWidth,
|
||||
}}
|
||||
>
|
||||
<PersistedElement persistKey={PERSISTED_ELEMENT_KEY} style={{zIndex: STICKERPICKER_Z_INDEX}}>
|
||||
<PersistedElement persistKey={PERSISTED_ELEMENT_KEY} zIndex={STICKERPICKER_Z_INDEX}>
|
||||
<AppTile
|
||||
app={stickerApp}
|
||||
room={this.props.room}
|
||||
|
|
|
@ -81,7 +81,7 @@ export default class WhoIsTypingTile extends React.Component {
|
|||
};
|
||||
|
||||
onRoomTimeline = (event, room) => {
|
||||
if (room && room.roomId === this.props.room.roomId) {
|
||||
if (room?.roomId === this.props.room?.roomId) {
|
||||
const userId = event.getSender();
|
||||
// remove user from usersTyping
|
||||
const usersTyping = this.state.usersTyping.filter((m) => m.userId !== userId);
|
||||
|
|
|
@ -52,19 +52,23 @@ export default class ProfileSettings extends React.Component {
|
|||
// clear file upload field so same file can be selected
|
||||
this._avatarUpload.current.value = "";
|
||||
this.setState({
|
||||
avatarUrl: undefined,
|
||||
avatarFile: undefined,
|
||||
avatarUrl: null,
|
||||
avatarFile: null,
|
||||
enableProfileSave: true,
|
||||
});
|
||||
};
|
||||
|
||||
_clearProfile = async (e) => {
|
||||
_cancelProfileChanges = async (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.state.enableProfileSave) return;
|
||||
this._removeAvatar();
|
||||
this.setState({enableProfileSave: false, displayName: this.state.originalDisplayName});
|
||||
this.setState({
|
||||
enableProfileSave: false,
|
||||
displayName: this.state.originalDisplayName,
|
||||
avatarUrl: this.state.originalAvatarUrl,
|
||||
avatarFile: null,
|
||||
});
|
||||
};
|
||||
|
||||
_saveProfile = async (e) => {
|
||||
|
@ -186,7 +190,7 @@ export default class ProfileSettings extends React.Component {
|
|||
</div>
|
||||
<div className="mx_ProfileSettings_buttons">
|
||||
<AccessibleButton
|
||||
onClick={this._clearProfile}
|
||||
onClick={this._cancelProfileChanges}
|
||||
kind="link"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
>
|
||||
|
|
|
@ -28,15 +28,14 @@ import { FontWatcher } from "../../../../../settings/watchers/FontWatcher";
|
|||
import { RecheckThemePayload } from '../../../../../dispatcher/payloads/RecheckThemePayload';
|
||||
import { Action } from '../../../../../dispatcher/actions';
|
||||
import { IValidationResult, IFieldState } from '../../../elements/Validation';
|
||||
import StyledRadioButton from '../../../elements/StyledRadioButton';
|
||||
import StyledCheckbox from '../../../elements/StyledCheckbox';
|
||||
import SettingsFlag from '../../../elements/SettingsFlag';
|
||||
import Field from '../../../elements/Field';
|
||||
import EventTilePreview from '../../../elements/EventTilePreview';
|
||||
import StyledRadioGroup from "../../../elements/StyledRadioGroup";
|
||||
import classNames from 'classnames';
|
||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||
import {UIFeature} from "../../../../../settings/UIFeature";
|
||||
import {Layout} from "../../../../../settings/Layout";
|
||||
|
||||
interface IProps {
|
||||
}
|
||||
|
@ -62,7 +61,7 @@ interface IState extends IThemeState {
|
|||
useSystemFont: boolean;
|
||||
systemFont: string;
|
||||
showAdvanced: boolean;
|
||||
useIRCLayout: boolean;
|
||||
layout: Layout;
|
||||
}
|
||||
|
||||
|
||||
|
@ -83,7 +82,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
useSystemFont: SettingsStore.getValue("useSystemFont"),
|
||||
systemFont: SettingsStore.getValue("systemFont"),
|
||||
showAdvanced: false,
|
||||
useIRCLayout: SettingsStore.getValue("useIRCLayout"),
|
||||
layout: SettingsStore.getValue("layout"),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -213,15 +212,15 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
this.setState({customThemeUrl: e.target.value});
|
||||
};
|
||||
|
||||
private onLayoutChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const val = e.target.value === "true";
|
||||
|
||||
this.setState({
|
||||
useIRCLayout: val,
|
||||
});
|
||||
|
||||
SettingsStore.setValue("useIRCLayout", null, SettingLevel.DEVICE, val);
|
||||
};
|
||||
private onIRCLayoutChange = (enabled: boolean) => {
|
||||
if (enabled) {
|
||||
this.setState({layout: Layout.IRC});
|
||||
SettingsStore.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
|
||||
} else {
|
||||
this.setState({layout: Layout.Group});
|
||||
SettingsStore.setValue("layout", null, SettingLevel.DEVICE, Layout.Group);
|
||||
}
|
||||
}
|
||||
|
||||
private renderThemeSection() {
|
||||
const themeWatcher = new ThemeWatcher();
|
||||
|
@ -306,7 +305,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
<EventTilePreview
|
||||
className="mx_AppearanceUserSettingsTab_fontSlider_preview"
|
||||
message={this.MESSAGE_PREVIEW_TEXT}
|
||||
useIRCLayout={this.state.useIRCLayout}
|
||||
layout={this.state.layout}
|
||||
/>
|
||||
<div className="mx_AppearanceUserSettingsTab_fontSlider">
|
||||
<div className="mx_AppearanceUserSettingsTab_fontSlider_smallText">Aa</div>
|
||||
|
@ -342,50 +341,6 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
</div>;
|
||||
}
|
||||
|
||||
private renderLayoutSection = () => {
|
||||
return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_Layout">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Message layout")}</span>
|
||||
|
||||
<div className="mx_AppearanceUserSettingsTab_Layout_RadioButtons">
|
||||
<div className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
|
||||
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.useIRCLayout,
|
||||
})}>
|
||||
<EventTilePreview
|
||||
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
|
||||
message={this.MESSAGE_PREVIEW_TEXT}
|
||||
useIRCLayout={true}
|
||||
/>
|
||||
<StyledRadioButton
|
||||
name="layout"
|
||||
value="true"
|
||||
checked={this.state.useIRCLayout}
|
||||
onChange={this.onLayoutChange}
|
||||
>
|
||||
{_t("Compact")}
|
||||
</StyledRadioButton>
|
||||
</div>
|
||||
<div className="mx_AppearanceUserSettingsTab_spacer" />
|
||||
<div className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
|
||||
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: !this.state.useIRCLayout,
|
||||
})}>
|
||||
<EventTilePreview
|
||||
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
|
||||
message={this.MESSAGE_PREVIEW_TEXT}
|
||||
useIRCLayout={false}
|
||||
/>
|
||||
<StyledRadioButton
|
||||
name="layout"
|
||||
value="false"
|
||||
checked={!this.state.useIRCLayout}
|
||||
onChange={this.onLayoutChange}
|
||||
>
|
||||
{_t("Modern")}
|
||||
</StyledRadioButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
private renderAdvancedSection() {
|
||||
if (!SettingsStore.getValue(UIFeature.AdvancedSettings)) return null;
|
||||
|
||||
|
@ -409,14 +364,15 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
name="useCompactLayout"
|
||||
level={SettingLevel.DEVICE}
|
||||
useCheckbox={true}
|
||||
disabled={this.state.useIRCLayout}
|
||||
/>
|
||||
<SettingsFlag
|
||||
name="useIRCLayout"
|
||||
level={SettingLevel.DEVICE}
|
||||
useCheckbox={true}
|
||||
onChange={(checked) => this.setState({useIRCLayout: checked})}
|
||||
disabled={this.state.layout == Layout.IRC}
|
||||
/>
|
||||
<StyledCheckbox
|
||||
checked={this.state.layout == Layout.IRC}
|
||||
onChange={(ev) => this.onIRCLayoutChange(ev.target.checked)}
|
||||
>
|
||||
{_t("Enable experimental, compact IRC style layout")}
|
||||
</StyledCheckbox>
|
||||
|
||||
<SettingsFlag
|
||||
name="useSystemFont"
|
||||
level={SettingLevel.DEVICE}
|
||||
|
|
|
@ -34,6 +34,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
|
|||
'MessageComposerInput.suggestEmoji',
|
||||
'sendTypingNotifications',
|
||||
'MessageComposerInput.ctrlEnterToSend',
|
||||
'MessageComposerInput.showStickersButton',
|
||||
];
|
||||
|
||||
static TIMELINE_SETTINGS = [
|
||||
|
@ -46,12 +47,15 @@ export default class PreferencesUserSettingsTab extends React.Component {
|
|||
'alwaysShowTimestamps',
|
||||
'showRedactions',
|
||||
'enableSyntaxHighlightLanguageDetection',
|
||||
'expandCodeByDefault',
|
||||
'showCodeLineNumbers',
|
||||
'showJoinLeaves',
|
||||
'showAvatarChanges',
|
||||
'showDisplaynameChanges',
|
||||
'showImages',
|
||||
'showChatEffects',
|
||||
'Pill.shouldShowPillAvatar',
|
||||
'ctrlFForSearch',
|
||||
];
|
||||
|
||||
static GENERAL_SETTINGS = [
|
||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef, CSSProperties, ReactNode } from 'react';
|
||||
import React, { createRef, CSSProperties } from 'react';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
|
@ -494,6 +494,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
if (this.props.call.type === CallType.Video) {
|
||||
let localVideoFeed = null;
|
||||
let onHoldContent = null;
|
||||
let onHoldBackground = null;
|
||||
const backgroundStyle: CSSProperties = {};
|
||||
|
@ -512,6 +513,9 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
backgroundStyle.backgroundImage = 'url(' + backgroundAvatarUrl + ')';
|
||||
onHoldBackground = <div className="mx_CallView_video_holdBackground" style={backgroundStyle} />;
|
||||
}
|
||||
if (!this.state.vidMuted) {
|
||||
localVideoFeed = <VideoFeed type={VideoFeedType.Local} call={this.props.call} />;
|
||||
}
|
||||
|
||||
// if we're fullscreen, we don't want to set a maxHeight on the video element.
|
||||
const maxVideoHeight = getFullScreenElement() ? null : (
|
||||
|
@ -527,7 +531,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
<VideoFeed type={VideoFeedType.Remote} call={this.props.call} onResize={this.props.onResize}
|
||||
maxHeight={maxVideoHeight}
|
||||
/>
|
||||
<VideoFeed type={VideoFeedType.Local} call={this.props.call} />
|
||||
{localVideoFeed}
|
||||
{onHoldContent}
|
||||
{callControls}
|
||||
</div>;
|
||||
|
|
|
@ -24,6 +24,7 @@ import DialPad from './DialPad';
|
|||
import dis from '../../../dispatcher/dispatcher';
|
||||
import Modal from "../../../Modal";
|
||||
import ErrorDialog from "../../views/dialogs/ErrorDialog";
|
||||
import CallHandler from "../../../CallHandler";
|
||||
|
||||
interface IProps {
|
||||
onFinished: (boolean) => void;
|
||||
|
@ -64,9 +65,7 @@ export default class DialpadModal extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
onDialPress = async () => {
|
||||
const results = await MatrixClientPeg.get().getThirdpartyUser('im.vector.protocol.pstn', {
|
||||
'm.id.phone': this.state.value,
|
||||
});
|
||||
const results = await CallHandler.sharedInstance().pstnLookup(this.state.value);
|
||||
if (!results || results.length === 0 || !results[0].userid) {
|
||||
Modal.createTrackedDialog('', '', ErrorDialog, {
|
||||
title: _t("Unable to look up phone number"),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue