Merge branch 'develop' into jaywink/hosting-provider-iframe-minimize-wip
This commit is contained in:
commit
6ccce7142c
156 changed files with 7020 additions and 5218 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()) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -216,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 {
|
||||
|
@ -234,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();
|
||||
},
|
||||
};
|
||||
|
@ -426,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"
|
||||
|
|
|
@ -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) {
|
||||
|
@ -1186,6 +1232,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() {
|
||||
|
@ -1833,40 +1884,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 +1928,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,6 +23,7 @@ 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';
|
||||
|
@ -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 "message_sent":
|
||||
this.scrollToBottom();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onShowTypingNotificationsChange = () => {
|
||||
this.setState({
|
||||
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
|
||||
|
|
|
@ -487,7 +487,11 @@ export default class RoomDirectory extends React.Component {
|
|||
let previewButton;
|
||||
let joinOrViewButton;
|
||||
|
||||
if (room.world_readable && !hasJoinedRoom) {
|
||||
// Element Web currently does not allow guests to join rooms, so we
|
||||
// instead show them preview buttons for all rooms. If the room is not
|
||||
// world readable, a modal will appear asking you to register first. If
|
||||
// it is readable, the preview appears as normal.
|
||||
if (!hasJoinedRoom && (room.world_readable || isGuest)) {
|
||||
previewButton = (
|
||||
<AccessibleButton kind="secondary" onClick={(ev) => this.onPreviewClick(ev, room)}>{_t("Preview")}</AccessibleButton>
|
||||
);
|
||||
|
@ -496,7 +500,7 @@ export default class RoomDirectory extends React.Component {
|
|||
joinOrViewButton = (
|
||||
<AccessibleButton kind="secondary" onClick={(ev) => this.onViewClick(ev, room)}>{_t("View")}</AccessibleButton>
|
||||
);
|
||||
} else if (!isGuest || room.guest_can_join) {
|
||||
} else if (!isGuest) {
|
||||
joinOrViewButton = (
|
||||
<AccessibleButton kind="primary" onClick={(ev) => this.onJoinClick(ev, room)}>{_t("Join")}</AccessibleButton>
|
||||
);
|
||||
|
|
|
@ -21,15 +21,15 @@ limitations under the License.
|
|||
// - Search results component
|
||||
// - Drag and drop
|
||||
|
||||
import React, {createRef} from 'react';
|
||||
import React, { createRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import {EventSubscription} from "fbemitter";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { EventSubscription } from "fbemitter";
|
||||
|
||||
import shouldHideEvent from '../../shouldHideEvent';
|
||||
import {_t} from '../../languageHandler';
|
||||
import {RoomPermalinkCreator} from '../../utils/permalinks/Permalinks';
|
||||
import { _t } from '../../languageHandler';
|
||||
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
|
||||
import ResizeNotifier from '../../utils/ResizeNotifier';
|
||||
import ContentMessages from '../../ContentMessages';
|
||||
import Modal from '../../Modal';
|
||||
|
@ -40,8 +40,8 @@ import Tinter from '../../Tinter';
|
|||
import rateLimitedFunc from '../../ratelimitedfunc';
|
||||
import * as ObjectUtils from '../../ObjectUtils';
|
||||
import * as Rooms from '../../Rooms';
|
||||
import eventSearch, {searchPagination} from '../../Searching';
|
||||
import {isOnlyCtrlOrCmdIgnoreShiftKeyEvent, Key} from '../../Keyboard';
|
||||
import eventSearch, { searchPagination } from '../../Searching';
|
||||
import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent, Key } from '../../Keyboard';
|
||||
import MainSplit from './MainSplit';
|
||||
import RightPanel from './RightPanel';
|
||||
import RoomViewStore from '../../stores/RoomViewStore';
|
||||
|
@ -50,13 +50,13 @@ import WidgetEchoStore from '../../stores/WidgetEchoStore';
|
|||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import {haveTileForEvent} from "../views/rooms/EventTile";
|
||||
import { haveTileForEvent } from "../views/rooms/EventTile";
|
||||
import RoomContext from "../../contexts/RoomContext";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import {E2EStatus, shieldStatusForRoom} from '../../utils/ShieldUtils';
|
||||
import {Action} from "../../dispatcher/actions";
|
||||
import {SettingLevel} from "../../settings/SettingLevel";
|
||||
import {IMatrixClientCreds} from "../../MatrixClientPeg";
|
||||
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { SettingLevel } from "../../settings/SettingLevel";
|
||||
import { IMatrixClientCreds } from "../../MatrixClientPeg";
|
||||
import ScrollPanel from "./ScrollPanel";
|
||||
import TimelinePanel from "./TimelinePanel";
|
||||
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||
|
@ -67,17 +67,18 @@ import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
|||
import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel";
|
||||
import AuxPanel from "../views/rooms/AuxPanel";
|
||||
import RoomHeader from "../views/rooms/RoomHeader";
|
||||
import {XOR} from "../../@types/common";
|
||||
import { XOR } from "../../@types/common";
|
||||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||
import EffectsOverlay from "../views/elements/EffectsOverlay";
|
||||
import {containsEmoji} from '../../effects/utils';
|
||||
import {CHAT_EFFECTS} from '../../effects';
|
||||
import { containsEmoji } from '../../effects/utils';
|
||||
import { CHAT_EFFECTS } from '../../effects';
|
||||
import { CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||
import WidgetStore from "../../stores/WidgetStore";
|
||||
import {UPDATE_EVENT} from "../../stores/AsyncStore";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import Notifier from "../../Notifier";
|
||||
import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast";
|
||||
import { showToast as showNotificationsToast } from "../../toasts/DesktopNotificationsToast";
|
||||
import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
|
||||
import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function(msg: string) {};
|
||||
|
@ -266,12 +267,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
this.layoutWatcherRef = SettingsStore.watchSetting("useIRCLayout", null, this.onLayoutChange);
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Move into constructor
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillMount() {
|
||||
this.onRoomViewStoreUpdate(true);
|
||||
}
|
||||
|
||||
private onWidgetStoreUpdate = () => {
|
||||
if (this.state.room) {
|
||||
this.checkWidgets(this.state.room);
|
||||
|
@ -280,8 +275,9 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
|
||||
private checkWidgets = (room) => {
|
||||
this.setState({
|
||||
hasPinnedWidgets: WidgetStore.instance.getPinnedApps(room.roomId).length > 0,
|
||||
})
|
||||
hasPinnedWidgets: WidgetLayoutStore.instance.getContainerWidgets(room, Container.Top).length > 0,
|
||||
showApps: this.shouldShowApps(room),
|
||||
});
|
||||
};
|
||||
|
||||
private onReadReceiptsChange = () => {
|
||||
|
@ -418,11 +414,17 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private onWidgetEchoStoreUpdate = () => {
|
||||
if (!this.state.room) return;
|
||||
this.setState({
|
||||
hasPinnedWidgets: WidgetLayoutStore.instance.getContainerWidgets(this.state.room, Container.Top).length > 0,
|
||||
showApps: this.shouldShowApps(this.state.room),
|
||||
});
|
||||
};
|
||||
|
||||
private onWidgetLayoutChange = () => {
|
||||
this.onWidgetEchoStoreUpdate(); // we cheat here by calling the thing that matters
|
||||
};
|
||||
|
||||
private setupRoom(room: Room, roomId: string, joining: boolean, shouldPeek: boolean) {
|
||||
// if this is an unknown room then we're in one of three states:
|
||||
// - This is a room we can peek into (search engine) (we can /peek)
|
||||
|
@ -488,7 +490,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private shouldShowApps(room: Room) {
|
||||
if (!BROWSER_SUPPORTS_SANDBOX) return false;
|
||||
if (!BROWSER_SUPPORTS_SANDBOX || !room) return false;
|
||||
|
||||
// Check if user has previously chosen to hide the app drawer for this
|
||||
// room. If so, do not show apps
|
||||
|
@ -497,10 +499,15 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
|
||||
// This is confusing, but it means to say that we default to the tray being
|
||||
// hidden unless the user clicked to open it.
|
||||
return hideWidgetDrawer === "false";
|
||||
const isManuallyShown = hideWidgetDrawer === "false";
|
||||
|
||||
const widgets = WidgetLayoutStore.instance.getContainerWidgets(room, Container.Top);
|
||||
return widgets.length > 0 || isManuallyShown;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.onRoomViewStoreUpdate(true);
|
||||
|
||||
const call = this.getCallForRoom();
|
||||
const callState = call ? call.state : null;
|
||||
this.setState({
|
||||
|
@ -608,6 +615,13 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
WidgetEchoStore.removeListener(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
|
||||
WidgetStore.instance.removeListener(UPDATE_EVENT, this.onWidgetStoreUpdate);
|
||||
|
||||
if (this.state.room) {
|
||||
WidgetLayoutStore.instance.off(
|
||||
WidgetLayoutStore.emissionForRoom(this.state.room),
|
||||
this.onWidgetLayoutChange,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.showReadReceiptsWatchRef) {
|
||||
SettingsStore.unwatchSetting(this.showReadReceiptsWatchRef);
|
||||
}
|
||||
|
@ -748,6 +762,9 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
});
|
||||
}
|
||||
break;
|
||||
case 'focus_search':
|
||||
this.onSearchClick();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -835,6 +852,10 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
// called when state.room is first initialised (either at initial load,
|
||||
// after a successful peek, or after we join the room).
|
||||
private onRoomLoaded = (room: Room) => {
|
||||
// Attach a widget store listener only when we get a room
|
||||
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(room), this.onWidgetLayoutChange);
|
||||
this.onWidgetLayoutChange(); // provoke an update
|
||||
|
||||
this.calculatePeekRules(room);
|
||||
this.updatePreviewUrlVisibility(room);
|
||||
this.loadMembersIfJoined(room);
|
||||
|
@ -897,6 +918,15 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
if (!room || room.roomId !== this.state.roomId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Detach the listener if the room is changing for some reason
|
||||
if (this.state.room) {
|
||||
WidgetLayoutStore.instance.off(
|
||||
WidgetLayoutStore.emissionForRoom(this.state.room),
|
||||
this.onWidgetLayoutChange,
|
||||
);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
room: room,
|
||||
}, () => {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -120,9 +120,9 @@ export default class SetupEncryptionBody extends React.Component {
|
|||
const store = SetupEncryptionStore.sharedInstance();
|
||||
let recoveryKeyPrompt;
|
||||
if (store.keyInfo && keyHasPassphrase(store.keyInfo)) {
|
||||
recoveryKeyPrompt = _t("Use Recovery Key or Passphrase");
|
||||
recoveryKeyPrompt = _t("Use Security Key or Phrase");
|
||||
} else if (store.keyInfo) {
|
||||
recoveryKeyPrompt = _t("Use Recovery Key");
|
||||
recoveryKeyPrompt = _t("Use Security Key");
|
||||
}
|
||||
|
||||
let useRecoveryKeyButton;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -20,7 +20,7 @@ import {MatrixCapabilities} from "matrix-widget-api";
|
|||
import IconizedContextMenu, {IconizedContextMenuOption, IconizedContextMenuOptionList} from "./IconizedContextMenu";
|
||||
import {ChevronFace} from "../../structures/ContextMenu";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import WidgetStore, {IApp} from "../../../stores/WidgetStore";
|
||||
import {IApp} from "../../../stores/WidgetStore";
|
||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||
import {WidgetMessagingStore} from "../../../stores/widgets/WidgetMessagingStore";
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
|
@ -30,6 +30,7 @@ import Modal from "../../../Modal";
|
|||
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
import {WidgetType} from "../../../widgets/WidgetType";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
|
||||
interface IProps extends React.ComponentProps<typeof IconizedContextMenu> {
|
||||
app: IApp;
|
||||
|
@ -56,7 +57,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
|||
let unpinButton;
|
||||
if (showUnpin) {
|
||||
const onUnpinClick = () => {
|
||||
WidgetStore.instance.unpinWidget(room.roomId, app.id);
|
||||
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Right);
|
||||
onFinished();
|
||||
};
|
||||
|
||||
|
@ -137,13 +138,13 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
|||
revokeButton = <IconizedContextMenuOption onClick={onRevokeClick} label={_t("Revoke permissions")} />;
|
||||
}
|
||||
|
||||
const pinnedWidgets = WidgetStore.instance.getPinnedApps(roomId);
|
||||
const pinnedWidgets = WidgetLayoutStore.instance.getContainerWidgets(room, Container.Top);
|
||||
const widgetIndex = pinnedWidgets.findIndex(widget => widget.id === app.id);
|
||||
|
||||
let moveLeftButton;
|
||||
if (showUnpin && widgetIndex > 0) {
|
||||
const onClick = () => {
|
||||
WidgetStore.instance.movePinnedWidget(roomId, app.id, -1);
|
||||
WidgetLayoutStore.instance.moveWithinContainer(room, Container.Top, app, -1);
|
||||
onFinished();
|
||||
};
|
||||
|
||||
|
@ -153,7 +154,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
|||
let moveRightButton;
|
||||
if (showUnpin && widgetIndex < pinnedWidgets.length - 1) {
|
||||
const onClick = () => {
|
||||
WidgetStore.instance.movePinnedWidget(roomId, app.id, 1);
|
||||
WidgetLayoutStore.instance.moveWithinContainer(room, Container.Top, app, 1);
|
||||
onFinished();
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -47,7 +47,7 @@ export default class NewSessionReviewDialog extends React.PureComponent {
|
|||
<li>{_t("The internet connection either session is using")}</li>
|
||||
</ul>
|
||||
<div>
|
||||
{_t("We recommend you change your password and recovery key in Settings immediately")}
|
||||
{_t("We recommend you change your password and Security Key in Settings immediately")}
|
||||
</div>
|
||||
</div>,
|
||||
onFinished: () => this.props.onFinished(false),
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -151,13 +151,13 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
|||
|
||||
const valid = await this.fieldRef.current.validate({ allowEmpty: false });
|
||||
|
||||
if (!valid) {
|
||||
if (!valid && !this.state.defaultChosen) {
|
||||
this.fieldRef.current.focus();
|
||||
this.fieldRef.current.validate({ allowEmpty: false, focused: true });
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onFinished(this.validatedConf);
|
||||
this.props.onFinished(this.state.defaultChosen ? this.defaultServer : this.validatedConf);
|
||||
};
|
||||
|
||||
public render() {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -199,11 +199,11 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
} else if (this.state.recoveryKeyCorrect) {
|
||||
return _t("Looks good!");
|
||||
} else if (this.state.recoveryKeyValid) {
|
||||
return _t("Wrong Recovery Key");
|
||||
return _t("Wrong Security Key");
|
||||
} else if (this.state.recoveryKeyValid === null) {
|
||||
return '';
|
||||
} else {
|
||||
return _t("Invalid Recovery Key");
|
||||
return _t("Invalid Security Key");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -231,7 +231,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
|
||||
{"\uD83D\uDC4E "}{_t(
|
||||
"Unable to access secret storage. " +
|
||||
"Please verify that you entered the correct recovery passphrase.",
|
||||
"Please verify that you entered the correct Security Phrase.",
|
||||
)}
|
||||
</div>;
|
||||
} else {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -297,19 +297,19 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
} else if (this.state.restoreError) {
|
||||
if (this.state.restoreError.errcode === MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY) {
|
||||
if (this.state.restoreType === RESTORE_TYPE_RECOVERYKEY) {
|
||||
title = _t("Recovery key mismatch");
|
||||
title = _t("Security Key mismatch");
|
||||
content = <div>
|
||||
<p>{_t(
|
||||
"Backup could not be decrypted with this recovery key: " +
|
||||
"please verify that you entered the correct recovery key.",
|
||||
"Backup could not be decrypted with this Security Key: " +
|
||||
"please verify that you entered the correct Security Key.",
|
||||
)}</p>
|
||||
</div>;
|
||||
} else {
|
||||
title = _t("Incorrect recovery passphrase");
|
||||
title = _t("Incorrect Security Phrase");
|
||||
content = <div>
|
||||
<p>{_t(
|
||||
"Backup could not be decrypted with this recovery passphrase: " +
|
||||
"please verify that you entered the correct recovery passphrase.",
|
||||
"Backup could not be decrypted with this Security Phrase: " +
|
||||
"please verify that you entered the correct Security Phrase.",
|
||||
)}</p>
|
||||
</div>;
|
||||
}
|
||||
|
@ -342,7 +342,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
} else if (backupHasPassphrase && !this.state.forceRecoveryKey) {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
title = _t("Enter recovery passphrase");
|
||||
title = _t("Enter Security Phrase");
|
||||
content = <div>
|
||||
<p>{_t(
|
||||
"<b>Warning</b>: you should only set up key backup " +
|
||||
|
@ -351,7 +351,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
)}</p>
|
||||
<p>{_t(
|
||||
"Access your secure message history and set up secure " +
|
||||
"messaging by entering your recovery passphrase.",
|
||||
"messaging by entering your Security Phrase.",
|
||||
)}</p>
|
||||
|
||||
<form className="mx_RestoreKeyBackupDialog_primaryContainer">
|
||||
|
@ -371,8 +371,8 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
/>
|
||||
</form>
|
||||
{_t(
|
||||
"If you've forgotten your recovery passphrase you can "+
|
||||
"<button1>use your recovery key</button1> or " +
|
||||
"If you've forgotten your Security Phrase you can "+
|
||||
"<button1>use your Security Key</button1> or " +
|
||||
"<button2>set up new recovery options</button2>"
|
||||
, {}, {
|
||||
button1: s => <AccessibleButton className="mx_linkButton"
|
||||
|
@ -390,7 +390,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
})}
|
||||
</div>;
|
||||
} else {
|
||||
title = _t("Enter recovery key");
|
||||
title = _t("Enter Security Key");
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
|
@ -399,11 +399,11 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
keyStatus = <div className="mx_RestoreKeyBackupDialog_keyStatus"></div>;
|
||||
} else if (this.state.recoveryKeyValid) {
|
||||
keyStatus = <div className="mx_RestoreKeyBackupDialog_keyStatus">
|
||||
{"\uD83D\uDC4D "}{_t("This looks like a valid recovery key!")}
|
||||
{"\uD83D\uDC4D "}{_t("This looks like a valid Security Key!")}
|
||||
</div>;
|
||||
} else {
|
||||
keyStatus = <div className="mx_RestoreKeyBackupDialog_keyStatus">
|
||||
{"\uD83D\uDC4E "}{_t("Not a valid recovery key")}
|
||||
{"\uD83D\uDC4E "}{_t("Not a valid Security Key")}
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
@ -415,7 +415,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
)}</p>
|
||||
<p>{_t(
|
||||
"Access your secure message history and set up secure " +
|
||||
"messaging by entering your recovery key.",
|
||||
"messaging by entering your Security Key.",
|
||||
)}</p>
|
||||
|
||||
<div className="mx_RestoreKeyBackupDialog_primaryContainer">
|
||||
|
@ -434,7 +434,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
/>
|
||||
</div>
|
||||
{_t(
|
||||
"If you've forgotten your recovery key you can "+
|
||||
"If you've forgotten your Security Key you can "+
|
||||
"<button>set up new recovery options</button>"
|
||||
, {}, {
|
||||
button: s => <AccessibleButton className="mx_linkButton"
|
||||
|
|
|
@ -124,7 +124,7 @@ export default class EditableItemList extends React.Component {
|
|||
<Field label={this.props.placeholder} type="text"
|
||||
autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged}
|
||||
list={this.props.suggestionsListId} />
|
||||
<AccessibleButton onClick={this._onItemAdded} kind="primary" type="submit">
|
||||
<AccessibleButton onClick={this._onItemAdded} kind="primary" type="submit" disabled={!this.props.newItem}>
|
||||
{_t("Add")}
|
||||
</AccessibleButton>
|
||||
</form>
|
||||
|
|
|
@ -165,6 +165,7 @@ export default class PersistedElement extends React.Component {
|
|||
|
||||
const parentRect = parent.getBoundingClientRect();
|
||||
Object.assign(child.style, {
|
||||
zIndex: 9,
|
||||
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;
|
||||
|
|
|
@ -15,19 +15,40 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import { chunk } from "lodash";
|
||||
import classNames from "classnames";
|
||||
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 classNames from "classnames";
|
||||
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,
|
||||
|
@ -37,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 = () => {
|
||||
|
@ -45,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>
|
||||
|
@ -83,6 +108,8 @@ interface IProps {
|
|||
primary?: boolean;
|
||||
}
|
||||
|
||||
const MAX_PER_ROW = 6;
|
||||
|
||||
const SSOButtons: React.FC<IProps> = ({matrixClient, flow, loginType, fragmentAfterLogin, primary}) => {
|
||||
const providers = flow["org.matrix.msc2858.identity_providers"] || [];
|
||||
if (providers.length < 2) {
|
||||
|
@ -97,17 +124,24 @@ const SSOButtons: React.FC<IProps> = ({matrixClient, flow, loginType, fragmentAf
|
|||
</div>;
|
||||
}
|
||||
|
||||
const rows = Math.ceil(providers.length / MAX_PER_ROW);
|
||||
const size = Math.ceil(providers.length / rows);
|
||||
|
||||
return <div className="mx_SSOButtons">
|
||||
{ providers.map(idp => (
|
||||
<SSOButton
|
||||
key={idp.id}
|
||||
matrixClient={matrixClient}
|
||||
loginType={loginType}
|
||||
fragmentAfterLogin={fragmentAfterLogin}
|
||||
idp={idp}
|
||||
mini={true}
|
||||
primary={primary}
|
||||
/>
|
||||
{ chunk(providers, size).map(chunk => (
|
||||
<div key={chunk[0].id} className="mx_SSOButtons_row">
|
||||
{ chunk.map(idp => (
|
||||
<SSOButton
|
||||
key={idp.id}
|
||||
matrixClient={matrixClient}
|
||||
loginType={loginType}
|
||||
fragmentAfterLogin={fragmentAfterLogin}
|
||||
idp={idp}
|
||||
mini={true}
|
||||
primary={primary}
|
||||
/>
|
||||
)) }
|
||||
</div>
|
||||
)) }
|
||||
</div>;
|
||||
};
|
||||
|
|
|
@ -19,6 +19,8 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|||
import { _t } from "../../../languageHandler";
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
import EventTileBubble from "./EventTileBubble";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
|
@ -33,10 +35,15 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
|
|||
const url = this.props.mxEvent.getContent()['url'];
|
||||
const prevUrl = this.props.mxEvent.getPrevContent()['url'];
|
||||
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, true).widgets.find(w => w.id === widgetId);
|
||||
|
||||
let joinCopy = _t('Join the conference at the top of this room');
|
||||
if (!WidgetStore.instance.isPinned(this.props.mxEvent.getRoomId(), this.props.mxEvent.getStateKey())) {
|
||||
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) {
|
||||
|
|
|
@ -37,13 +37,14 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import TextWithTooltip from "../elements/TextWithTooltip";
|
||||
import WidgetAvatar from "../avatars/WidgetAvatar";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import WidgetStore, {IApp, MAX_PINNED} from "../../../stores/WidgetStore";
|
||||
import WidgetStore, {IApp} from "../../../stores/WidgetStore";
|
||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
import {ChevronFace, ContextMenuTooltipButton, useContextMenu} from "../../structures/ContextMenu";
|
||||
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
|
||||
import {useRoomMemberCount} from "../../../hooks/useRoomMembers";
|
||||
import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -76,8 +77,9 @@ 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);
|
||||
|
||||
return apps;
|
||||
};
|
||||
|
@ -102,10 +104,10 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
|
|||
});
|
||||
};
|
||||
|
||||
const isPinned = WidgetStore.instance.isPinned(room.roomId, app.id);
|
||||
const isPinned = WidgetLayoutStore.instance.isInContainer(room, app, Container.Top);
|
||||
const togglePin = isPinned
|
||||
? () => { WidgetStore.instance.unpinWidget(room.roomId, app.id); }
|
||||
: () => { WidgetStore.instance.pinWidget(room.roomId, app.id); };
|
||||
? () => { WidgetLayoutStore.instance.moveToContainer(room, app, Container.Right); }
|
||||
: () => { WidgetLayoutStore.instance.moveToContainer(room, app, Container.Top); };
|
||||
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
|
||||
let contextMenu;
|
||||
|
@ -120,7 +122,7 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
|
|||
/>;
|
||||
}
|
||||
|
||||
const cannotPin = !isPinned && !WidgetStore.instance.canPin(room.roomId, app.id);
|
||||
const cannotPin = !isPinned && !WidgetLayoutStore.instance.canAddToContainer(room, Container.Top);
|
||||
|
||||
let pinTitle: string;
|
||||
if (cannotPin) {
|
||||
|
@ -184,9 +186,18 @@ const AppsSection: React.FC<IAppsSectionProps> = ({ room }) => {
|
|||
}
|
||||
};
|
||||
|
||||
let copyLayoutBtn = null;
|
||||
if (apps.length > 0 && WidgetLayoutStore.instance.canCopyLayoutToRoom(room)) {
|
||||
copyLayoutBtn = (
|
||||
<AccessibleButton kind="link" onClick={() => WidgetLayoutStore.instance.copyLayoutToRoom(room)}>
|
||||
{ _t("Set my room layout for everyone") }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return <Group className="mx_RoomSummaryCard_appsGroup" title={_t("Widgets")}>
|
||||
{ apps.map(app => <AppRow key={app.id} app={app} room={room} />) }
|
||||
|
||||
{ copyLayoutBtn }
|
||||
<AccessibleButton kind="link" onClick={onManageIntegrations}>
|
||||
{ apps.length > 0 ? _t("Edit widgets, bridges & bots") : _t("Add widgets, bridges & bots") }
|
||||
</AccessibleButton>
|
||||
|
|
|
@ -14,22 +14,22 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useContext, useEffect} from "react";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import React, { useContext, useEffect } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import BaseCard from "./BaseCard";
|
||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||
import AppTile from "../elements/AppTile";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {useWidgets} from "./RoomSummaryCard";
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { useWidgets } from "./RoomSummaryCard";
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
import {ChevronFace, ContextMenuButton, useContextMenu} from "../../structures/ContextMenu";
|
||||
import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { ChevronFace, ContextMenuButton, useContextMenu } from "../../structures/ContextMenu";
|
||||
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -42,7 +42,7 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
|
|||
|
||||
const apps = useWidgets(room);
|
||||
const app = apps.find(a => a.id === widgetId);
|
||||
const isPinned = app && WidgetStore.instance.isPinned(room.roomId, app.id);
|
||||
const isPinned = app && WidgetLayoutStore.instance.isInContainer(room, app, Container.Top);
|
||||
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
||||
|
||||
|
|
|
@ -120,17 +120,21 @@ export default class RoomProfileSettings extends React.Component {
|
|||
};
|
||||
|
||||
_onDisplayNameChanged = (e) => {
|
||||
this.setState({
|
||||
displayName: e.target.value,
|
||||
enableProfileSave: true,
|
||||
});
|
||||
this.setState({displayName: e.target.value});
|
||||
if (this.state.originalDisplayName === e.target.value) {
|
||||
this.setState({enableProfileSave: false});
|
||||
} else {
|
||||
this.setState({enableProfileSave: true});
|
||||
}
|
||||
};
|
||||
|
||||
_onTopicChanged = (e) => {
|
||||
this.setState({
|
||||
topic: e.target.value,
|
||||
enableProfileSave: true,
|
||||
});
|
||||
this.setState({topic: e.target.value});
|
||||
if (this.state.originalTopic === e.target.value) {
|
||||
this.setState({enableProfileSave: false});
|
||||
} else {
|
||||
this.setState({enableProfileSave: true});
|
||||
}
|
||||
};
|
||||
|
||||
_onAvatarChanged = (e) => {
|
||||
|
@ -158,31 +162,10 @@ export default class RoomProfileSettings extends React.Component {
|
|||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const AvatarSetting = sdk.getComponent('settings.AvatarSetting');
|
||||
return (
|
||||
<form
|
||||
onSubmit={this._saveProfile}
|
||||
autoComplete="off"
|
||||
noValidate={true}
|
||||
className="mx_ProfileSettings_profileForm"
|
||||
>
|
||||
<input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
|
||||
onChange={this._onAvatarChanged} accept="image/*" />
|
||||
<div className="mx_ProfileSettings_profile">
|
||||
<div className="mx_ProfileSettings_controls">
|
||||
<Field label={_t("Room Name")}
|
||||
type="text" value={this.state.displayName} autoComplete="off"
|
||||
onChange={this._onDisplayNameChanged} disabled={!this.state.canSetName} />
|
||||
<Field id="profileTopic" label={_t("Room Topic")} disabled={!this.state.canSetTopic}
|
||||
type="text" value={this.state.topic} autoComplete="off"
|
||||
onChange={this._onTopicChanged} element="textarea" />
|
||||
</div>
|
||||
<AvatarSetting
|
||||
avatarUrl={this.state.avatarUrl}
|
||||
avatarName={this.state.displayName || this.props.roomId}
|
||||
avatarAltText={_t("Room avatar")}
|
||||
uploadAvatar={this.state.canSetAvatar ? this._uploadAvatar : undefined}
|
||||
removeAvatar={this.state.canSetAvatar ? this._removeAvatar : undefined} />
|
||||
</div>
|
||||
|
||||
let profileSettingsButtons;
|
||||
if (this.state.canSetTopic && this.state.canSetName) {
|
||||
profileSettingsButtons = (
|
||||
<div className="mx_ProfileSettings_buttons">
|
||||
<AccessibleButton
|
||||
onClick={this._clearProfile}
|
||||
|
@ -199,6 +182,35 @@ export default class RoomProfileSettings extends React.Component {
|
|||
{_t("Save")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={this._saveProfile}
|
||||
autoComplete="off"
|
||||
noValidate={true}
|
||||
className="mx_ProfileSettings_profileForm"
|
||||
>
|
||||
<input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
|
||||
onChange={this._onAvatarChanged} accept="image/*" />
|
||||
<div className="mx_ProfileSettings_profile">
|
||||
<div className="mx_ProfileSettings_controls">
|
||||
<Field label={_t("Room Name")}
|
||||
type="text" value={this.state.displayName} autoComplete="off"
|
||||
onChange={this._onDisplayNameChanged} disabled={!this.state.canSetName} />
|
||||
<Field className="mx_ProfileSettings_controls_topic" id="profileTopic" label={_t("Room Topic")} disabled={!this.state.canSetTopic}
|
||||
type="text" value={this.state.topic} autoComplete="off"
|
||||
onChange={this._onTopicChanged} element="textarea" />
|
||||
</div>
|
||||
<AvatarSetting
|
||||
avatarUrl={this.state.avatarUrl}
|
||||
avatarName={this.state.displayName || this.props.roomId}
|
||||
avatarAltText={_t("Room avatar")}
|
||||
uploadAvatar={this.state.canSetAvatar ? this._uploadAvatar : undefined}
|
||||
removeAvatar={this.state.canSetAvatar ? this._removeAvatar : undefined} />
|
||||
</div>
|
||||
{ profileSettingsButtons }
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -28,12 +28,13 @@ import WidgetUtils from '../../../utils/WidgetUtils';
|
|||
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
|
||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {useLocalStorageState} from "../../../hooks/useLocalStorageState";
|
||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
import ResizeHandle from "../elements/ResizeHandle";
|
||||
import Resizer from "../../../resizer/resizer";
|
||||
import PercentageDistributor from "../../../resizer/distributors/percentage";
|
||||
import {Container, WidgetLayoutStore} from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import {clamp, percentageOf, percentageWithin} from "../../../utils/numbers";
|
||||
import {useStateCallback} from "../../../hooks/useStateCallback";
|
||||
|
||||
export default class AppsDrawer extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -62,13 +63,13 @@ export default class AppsDrawer extends React.Component {
|
|||
|
||||
componentDidMount() {
|
||||
ScalarMessaging.startListening();
|
||||
WidgetStore.instance.on(this.props.room.roomId, this._updateApps);
|
||||
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(this.props.room), this._updateApps);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ScalarMessaging.stopListening();
|
||||
WidgetStore.instance.off(this.props.room.roomId, this._updateApps);
|
||||
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(this.props.room), this._updateApps);
|
||||
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
||||
if (this._resizeContainer) {
|
||||
this.resizer.detach();
|
||||
|
@ -102,11 +103,10 @@ export default class AppsDrawer extends React.Component {
|
|||
},
|
||||
onResizeStop: () => {
|
||||
this._resizeContainer.classList.remove("mx_AppsDrawer_resizing");
|
||||
// persist to localStorage
|
||||
localStorage.setItem(this._getStorageKey(), JSON.stringify([
|
||||
this.state.apps.map(app => app.id),
|
||||
...this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
|
||||
]));
|
||||
WidgetLayoutStore.instance.setResizerDistributions(
|
||||
this.props.room, Container.Top,
|
||||
this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
|
||||
);
|
||||
},
|
||||
};
|
||||
// pass a truthy container for now, we won't call attach until we update it
|
||||
|
@ -128,8 +128,6 @@ export default class AppsDrawer extends React.Component {
|
|||
this._loadResizerPreferences();
|
||||
};
|
||||
|
||||
_getStorageKey = () => `mx_apps_drawer-${this.props.room.roomId}`;
|
||||
|
||||
_getAppsHash = (apps) => apps.map(app => app.id).join("~");
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
|
@ -147,24 +145,16 @@ export default class AppsDrawer extends React.Component {
|
|||
};
|
||||
|
||||
_loadResizerPreferences = () => {
|
||||
try {
|
||||
const [[...lastIds], ...sizes] = JSON.parse(localStorage.getItem(this._getStorageKey()));
|
||||
// Every app was included in the last split, reuse the last sizes
|
||||
if (this.state.apps.length <= lastIds.length && this.state.apps.every((app, i) => lastIds[i] === app.id)) {
|
||||
sizes.forEach((size, i) => {
|
||||
const distributor = this.resizer.forHandleAt(i);
|
||||
if (distributor) {
|
||||
distributor.size = size;
|
||||
distributor.finish();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// this is expected
|
||||
}
|
||||
|
||||
if (this.state.apps) {
|
||||
const distributions = WidgetLayoutStore.instance.getResizerDistributions(this.props.room, Container.Top);
|
||||
if (this.state.apps && (this.state.apps.length - 1) === distributions.length) {
|
||||
distributions.forEach((size, i) => {
|
||||
const distributor = this.resizer.forHandleAt(i);
|
||||
if (distributor) {
|
||||
distributor.size = size;
|
||||
distributor.finish();
|
||||
}
|
||||
});
|
||||
} else if (this.state.apps) {
|
||||
const distributors = this.resizer.getDistributors();
|
||||
distributors.forEach(d => d.item.clearSize());
|
||||
distributors.forEach(d => d.start());
|
||||
|
@ -190,7 +180,7 @@ export default class AppsDrawer extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
_getApps = () => WidgetStore.instance.getPinnedApps(this.props.room.roomId);
|
||||
_getApps = () => WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top);
|
||||
|
||||
_updateApps = () => {
|
||||
this.setState({
|
||||
|
@ -248,10 +238,11 @@ export default class AppsDrawer extends React.Component {
|
|||
return (
|
||||
<div className={classes}>
|
||||
<PersistentVResizer
|
||||
id={"apps-drawer_" + this.props.room.roomId}
|
||||
room={this.props.room}
|
||||
minHeight={100}
|
||||
maxHeight={this.props.maxHeight ? this.props.maxHeight - 50 : undefined}
|
||||
handleClass="mx_AppsContainer_resizerHandle"
|
||||
handleWrapperClass="mx_AppsContainer_resizerHandleContainer"
|
||||
className="mx_AppsContainer_resizer"
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
>
|
||||
|
@ -272,7 +263,7 @@ export default class AppsDrawer extends React.Component {
|
|||
}
|
||||
|
||||
const PersistentVResizer = ({
|
||||
id,
|
||||
room,
|
||||
minHeight,
|
||||
maxHeight,
|
||||
className,
|
||||
|
@ -281,7 +272,24 @@ const PersistentVResizer = ({
|
|||
resizeNotifier,
|
||||
children,
|
||||
}) => {
|
||||
const [height, setHeight] = useLocalStorageState("pvr_" + id, 280); // old fixed height was 273px
|
||||
let defaultHeight = WidgetLayoutStore.instance.getContainerHeight(room, Container.Top);
|
||||
|
||||
// Arbitrary defaults to avoid NaN problems. 100 px or 3/4 of the visible window.
|
||||
if (!minHeight) minHeight = 100;
|
||||
if (!maxHeight) maxHeight = (window.innerHeight / 4) * 3;
|
||||
|
||||
// Convert from percentage to height. Note that the default height is 280px.
|
||||
if (defaultHeight) {
|
||||
defaultHeight = clamp(defaultHeight, 0, 100);
|
||||
defaultHeight = percentageWithin(defaultHeight / 100, minHeight, maxHeight);
|
||||
} else {
|
||||
defaultHeight = 280;
|
||||
}
|
||||
|
||||
const [height, setHeight] = useStateCallback(defaultHeight, newHeight => {
|
||||
newHeight = percentageOf(newHeight, minHeight, maxHeight) * 100;
|
||||
WidgetLayoutStore.instance.setContainerHeight(room, Container.Top, newHeight);
|
||||
});
|
||||
|
||||
return <Resizable
|
||||
size={{height: Math.min(height, maxHeight)}}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -37,6 +37,7 @@ import {E2E_STATE} from "./E2EIcon";
|
|||
import {toRem} from "../../../utils/units";
|
||||
import {WidgetType} from "../../../widgets/WidgetType";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import {WIDGET_LAYOUT_EVENT_TYPE} from "../../../stores/widgets/WidgetLayoutStore";
|
||||
|
||||
const eventTileTypes = {
|
||||
'm.room.message': 'messages.MessageEvent',
|
||||
|
@ -65,6 +66,7 @@ const stateEventTileTypes = {
|
|||
'm.room.server_acl': 'messages.TextualEvent',
|
||||
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
||||
'im.vector.modular.widgets': 'messages.TextualEvent',
|
||||
[WIDGET_LAYOUT_EVENT_TYPE]: 'messages.TextualEvent',
|
||||
'm.room.tombstone': 'messages.TextualEvent',
|
||||
'm.room.join_rules': 'messages.TextualEvent',
|
||||
'm.room.guest_access': 'messages.TextualEvent',
|
||||
|
|
|
@ -109,9 +109,12 @@ function HangupButton(props) {
|
|||
|
||||
dis.dispatch({
|
||||
action,
|
||||
// hangup the call for this room, which may not be the room in props
|
||||
// (e.g. conferences which will hangup the 1:1 room instead)
|
||||
room_id: call.roomId,
|
||||
// hangup the call for this room. NB. We use the room in props as the room ID
|
||||
// as call.roomId may be the 'virtual room', and the dispatch actions always
|
||||
// use the user-facing room (there was a time when we deliberately used
|
||||
// call.roomId and *not* props.roomId, but that was for the old
|
||||
// style Freeswitch conference calls and those times are gone.)
|
||||
room_id: props.roomId,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -29,7 +29,7 @@ import ActiveRoomObserver from "../../../ActiveRoomObserver";
|
|||
import { _t } from "../../../languageHandler";
|
||||
import { ChevronFace, ContextMenuTooltipButton } from "../../structures/ContextMenu";
|
||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
import { MessagePreviewStore, ROOM_PREVIEW_CHANGED } from "../../../stores/room-list/MessagePreviewStore";
|
||||
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import { ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE } from "../../../RoomNotifs";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
|
@ -51,7 +51,6 @@ import IconizedContextMenu, {
|
|||
IconizedContextMenuRadio,
|
||||
} from "../context_menus/IconizedContextMenu";
|
||||
import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/CommunityPrototypeStore";
|
||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -99,12 +98,23 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
|
||||
ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
MessagePreviewStore.instance.on(ROOM_PREVIEW_CHANGED, this.onRoomPreviewChanged);
|
||||
MessagePreviewStore.instance.on(
|
||||
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
|
||||
this.onRoomPreviewChanged,
|
||||
);
|
||||
this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room);
|
||||
this.notificationState.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
||||
this.roomProps = EchoChamber.forRoom(this.props.room);
|
||||
this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
|
||||
CommunityPrototypeStore.instance.on(UPDATE_EVENT, this.onCommunityUpdate);
|
||||
CommunityPrototypeStore.instance.on(
|
||||
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
|
||||
this.onCommunityUpdate,
|
||||
);
|
||||
this.props.room.on("Room.name", this.onRoomNameUpdate);
|
||||
}
|
||||
|
||||
private onRoomNameUpdate = (room) => {
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
private onNotificationUpdate = () => {
|
||||
|
@ -128,6 +138,26 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
if (prevProps.showMessagePreview !== this.props.showMessagePreview && this.showMessagePreview) {
|
||||
this.setState({messagePreview: this.generatePreview()});
|
||||
}
|
||||
if (prevProps.room?.roomId !== this.props.room?.roomId) {
|
||||
MessagePreviewStore.instance.off(
|
||||
MessagePreviewStore.getPreviewChangedEventName(prevProps.room),
|
||||
this.onRoomPreviewChanged,
|
||||
);
|
||||
MessagePreviewStore.instance.on(
|
||||
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
|
||||
this.onRoomPreviewChanged,
|
||||
);
|
||||
CommunityPrototypeStore.instance.off(
|
||||
CommunityPrototypeStore.getUpdateEventName(prevProps.room?.roomId),
|
||||
this.onCommunityUpdate,
|
||||
);
|
||||
CommunityPrototypeStore.instance.on(
|
||||
CommunityPrototypeStore.getUpdateEventName(this.props.room?.roomId),
|
||||
this.onCommunityUpdate,
|
||||
);
|
||||
prevProps.room?.off("Room.name", this.onRoomNameUpdate);
|
||||
this.props.room?.on("Room.name", this.onRoomNameUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
|
@ -140,11 +170,18 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
public componentWillUnmount() {
|
||||
if (this.props.room) {
|
||||
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||
MessagePreviewStore.instance.off(
|
||||
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
|
||||
this.onRoomPreviewChanged,
|
||||
);
|
||||
CommunityPrototypeStore.instance.off(
|
||||
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
|
||||
this.onCommunityUpdate,
|
||||
);
|
||||
this.props.room.off("Room.name", this.onRoomNameUpdate);
|
||||
}
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
MessagePreviewStore.instance.off(ROOM_PREVIEW_CHANGED, this.onRoomPreviewChanged);
|
||||
this.notificationState.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
||||
CommunityPrototypeStore.instance.off(UPDATE_EVENT, this.onCommunityUpdate);
|
||||
}
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -65,7 +65,7 @@ const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, remo
|
|||
|
||||
const avatarClasses = classNames({
|
||||
"mx_AvatarSetting_avatar": true,
|
||||
"mx_AvatarSetting_avatar_hovering": isHovering,
|
||||
"mx_AvatarSetting_avatar_hovering": isHovering && uploadAvatar,
|
||||
});
|
||||
return <div className={avatarClasses}>
|
||||
{avatarElement}
|
||||
|
|
|
@ -1,115 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 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 PropTypes from 'prop-types';
|
||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import Pill from "../elements/Pill";
|
||||
import {makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
||||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
@replaceableComponent("views.settings.BridgeTile")
|
||||
export default class BridgeTile extends React.PureComponent {
|
||||
static propTypes = {
|
||||
ev: PropTypes.object.isRequired,
|
||||
room: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
state = {
|
||||
visible: false,
|
||||
}
|
||||
|
||||
_toggleVisible() {
|
||||
this.setState({
|
||||
visible: !this.state.visible,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const content = this.props.ev.getContent();
|
||||
const { channel, network, protocol } = content;
|
||||
const protocolName = protocol.displayname || protocol.id;
|
||||
const channelName = channel.displayname || channel.id;
|
||||
const networkName = network ? network.displayname || network.id : protocolName;
|
||||
|
||||
let creator = null;
|
||||
if (content.creator) {
|
||||
creator = _t("This bridge was provisioned by <user />.", {}, {
|
||||
user: <Pill
|
||||
type={Pill.TYPE_USER_MENTION}
|
||||
room={this.props.room}
|
||||
url={makeUserPermalink(content.creator)}
|
||||
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
|
||||
/>,
|
||||
});
|
||||
}
|
||||
|
||||
const bot = _t("This bridge is managed by <user />.", {}, {
|
||||
user: <Pill
|
||||
type={Pill.TYPE_USER_MENTION}
|
||||
room={this.props.room}
|
||||
url={makeUserPermalink(this.props.ev.getSender())}
|
||||
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
|
||||
/>,
|
||||
});
|
||||
|
||||
let networkIcon;
|
||||
|
||||
if (protocol.avatar) {
|
||||
const avatarUrl = getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
protocol.avatar, 64, 64, "crop",
|
||||
);
|
||||
|
||||
networkIcon = <BaseAvatar className="protocol-icon"
|
||||
width={48}
|
||||
height={48}
|
||||
resizeMethod='crop'
|
||||
name={ protocolName }
|
||||
idName={ protocolName }
|
||||
url={ avatarUrl }
|
||||
/>;
|
||||
} else {
|
||||
networkIcon = <div class="noProtocolIcon"></div>;
|
||||
}
|
||||
|
||||
const id = this.props.ev.getId();
|
||||
const metadataClassname = "metadata" + (this.state.visible ? " visible" : "");
|
||||
return (<li key={id}>
|
||||
<div className="column-icon">
|
||||
{networkIcon}
|
||||
</div>
|
||||
<div className="column-data">
|
||||
<h3>{protocolName}</h3>
|
||||
<p className="workspace-channel-details">
|
||||
<span>{_t("Workspace: %(networkName)s", {networkName})}</span>
|
||||
<span className="channel">{_t("Channel: %(channelName)s", {channelName})}</span>
|
||||
</p>
|
||||
<p className={metadataClassname}>
|
||||
{creator} {bot}
|
||||
</p>
|
||||
<AccessibleButton className="mx_showMore" kind="secondary" onClick={this._toggleVisible.bind(this)}>
|
||||
{ this.state.visible ? _t("Show less") : _t("Show more") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</li>);
|
||||
}
|
||||
}
|
167
src/components/views/settings/BridgeTile.tsx
Normal file
167
src/components/views/settings/BridgeTile.tsx
Normal file
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
Copyright 2020 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 PropTypes from 'prop-types';
|
||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import Pill from "../elements/Pill";
|
||||
import {makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
||||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { isUrlPermitted } from '../../../HtmlUtils';
|
||||
|
||||
interface IProps {
|
||||
ev: MatrixEvent;
|
||||
room: Room;
|
||||
}
|
||||
|
||||
/**
|
||||
* This should match https://github.com/matrix-org/matrix-doc/blob/hs/msc-bridge-inf/proposals/2346-bridge-info-state-event.md#mbridge
|
||||
*/
|
||||
interface IBridgeStateEvent {
|
||||
bridgebot: string;
|
||||
creator?: string;
|
||||
protocol: {
|
||||
id: string;
|
||||
displayname?: string;
|
||||
// eslint-disable-next-line camelcase
|
||||
avatar_url?: string;
|
||||
// eslint-disable-next-line camelcase
|
||||
external_url?: string;
|
||||
};
|
||||
network?: {
|
||||
id: string;
|
||||
displayname?: string;
|
||||
// eslint-disable-next-line camelcase
|
||||
avatar_url?: string;
|
||||
// eslint-disable-next-line camelcase
|
||||
external_url?: string;
|
||||
};
|
||||
channel: {
|
||||
id: string;
|
||||
displayname?: string;
|
||||
// eslint-disable-next-line camelcase
|
||||
avatar_url?: string;
|
||||
// eslint-disable-next-line camelcase
|
||||
external_url?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default class BridgeTile extends React.PureComponent<IProps> {
|
||||
static propTypes = {
|
||||
ev: PropTypes.object.isRequired,
|
||||
room: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const content: IBridgeStateEvent = this.props.ev.getContent();
|
||||
// Validate
|
||||
if (!content.channel?.id || !content.protocol?.id) {
|
||||
console.warn(`Bridge info event ${this.props.ev.getId()} has missing content. Tile will not render`);
|
||||
return null;
|
||||
}
|
||||
if (!content.bridgebot) {
|
||||
// Bridgebot was not required previously, so in order to not break rooms we are allowing
|
||||
// the sender to be used in place. When the proposal is merged, this should be removed.
|
||||
console.warn(`Bridge info event ${this.props.ev.getId()} does not provide a 'bridgebot' key which`
|
||||
+ "is deprecated behaviour. Using sender for now.");
|
||||
content.bridgebot = this.props.ev.getSender();
|
||||
}
|
||||
const { channel, network, protocol } = content;
|
||||
const protocolName = protocol.displayname || protocol.id;
|
||||
const channelName = channel.displayname || channel.id;
|
||||
|
||||
let creator = null;
|
||||
if (content.creator) {
|
||||
creator = <li>{_t("This bridge was provisioned by <user />.", {}, {
|
||||
user: () => <Pill
|
||||
type={Pill.TYPE_USER_MENTION}
|
||||
room={this.props.room}
|
||||
url={makeUserPermalink(content.creator)}
|
||||
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
|
||||
/>,
|
||||
})}</li>;
|
||||
}
|
||||
|
||||
const bot = <li>{_t("This bridge is managed by <user />.", {}, {
|
||||
user: () => <Pill
|
||||
type={Pill.TYPE_USER_MENTION}
|
||||
room={this.props.room}
|
||||
url={makeUserPermalink(content.bridgebot)}
|
||||
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
|
||||
/>,
|
||||
})}</li>;
|
||||
|
||||
let networkIcon;
|
||||
|
||||
if (protocol.avatar_url) {
|
||||
const avatarUrl = getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
protocol.avatar_url, 64, 64, "crop",
|
||||
);
|
||||
|
||||
networkIcon = <BaseAvatar className="protocol-icon"
|
||||
width={48}
|
||||
height={48}
|
||||
resizeMethod='crop'
|
||||
name={ protocolName }
|
||||
idName={ protocolName }
|
||||
url={ avatarUrl }
|
||||
/>;
|
||||
} else {
|
||||
networkIcon = <div className="noProtocolIcon"></div>;
|
||||
}
|
||||
let networkItem = null;
|
||||
if (network) {
|
||||
const networkName = network.displayname || network.id;
|
||||
let networkLink = <span>{networkName}</span>;
|
||||
if (typeof network.external_url === "string" && isUrlPermitted(network.external_url)) {
|
||||
networkLink = <a href={network.external_url} target="_blank" rel="noreferrer noopener">{networkName}</a>
|
||||
}
|
||||
networkItem = _t("Workspace: <networkLink/>", {}, {
|
||||
networkLink: () => networkLink,
|
||||
});
|
||||
}
|
||||
|
||||
let channelLink = <span>{channelName}</span>;
|
||||
if (typeof channel.external_url === "string" && isUrlPermitted(channel.external_url)) {
|
||||
channelLink = <a href={channel.external_url} target="_blank" rel="noreferrer noopener">{channelName}</a>
|
||||
}
|
||||
|
||||
const id = this.props.ev.getId();
|
||||
return (<li key={id}>
|
||||
<div className="column-icon">
|
||||
{networkIcon}
|
||||
</div>
|
||||
<div className="column-data">
|
||||
<h3>{protocolName}</h3>
|
||||
<p className="workspace-channel-details">
|
||||
{networkItem}
|
||||
<span className="channel">{_t("Channel: <channelLink/>", {}, {
|
||||
channelLink: () => channelLink,
|
||||
})}</span>
|
||||
</p>
|
||||
<ul className="metadata">
|
||||
{creator} {bot}
|
||||
</ul>
|
||||
</div>
|
||||
</li>);
|
||||
}
|
||||
}
|
|
@ -424,7 +424,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
<p>{_t(
|
||||
"Back up your encryption keys with your account data in case you " +
|
||||
"lose access to your sessions. Your keys will be secured with a " +
|
||||
"unique Recovery Key.",
|
||||
"unique Security Key.",
|
||||
)}</p>
|
||||
{statusDescription}
|
||||
<details>
|
||||
|
|
|
@ -52,6 +52,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
|
|||
'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';
|
||||
|
@ -212,9 +212,10 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private onExpandClick = () => {
|
||||
const userFacingRoomId = CallHandler.roomIdForCall(this.props.call);
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.props.call.roomId,
|
||||
room_id: userFacingRoomId,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -340,27 +341,33 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private onRoomAvatarClick = () => {
|
||||
const userFacingRoomId = CallHandler.roomIdForCall(this.props.call);
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.props.call.roomId,
|
||||
room_id: userFacingRoomId,
|
||||
});
|
||||
}
|
||||
|
||||
private onSecondaryRoomAvatarClick = () => {
|
||||
const userFacingRoomId = CallHandler.roomIdForCall(this.props.secondaryCall);
|
||||
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.props.secondaryCall.roomId,
|
||||
room_id: userFacingRoomId,
|
||||
});
|
||||
}
|
||||
|
||||
private onCallResumeClick = () => {
|
||||
CallHandler.sharedInstance().setActiveCallRoomId(this.props.call.roomId);
|
||||
const userFacingRoomId = CallHandler.roomIdForCall(this.props.call);
|
||||
CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const client = MatrixClientPeg.get();
|
||||
const callRoom = client.getRoom(this.props.call.roomId);
|
||||
const secCallRoom = this.props.secondaryCall ? client.getRoom(this.props.secondaryCall.roomId) : null;
|
||||
const callRoomId = CallHandler.roomIdForCall(this.props.call);
|
||||
const secondaryCallRoomId = CallHandler.roomIdForCall(this.props.secondaryCall);
|
||||
const callRoom = client.getRoom(callRoomId);
|
||||
const secCallRoom = this.props.secondaryCall ? client.getRoom(secondaryCallRoomId) : null;
|
||||
|
||||
let dialPad;
|
||||
let contextMenu;
|
||||
|
@ -456,7 +463,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
onClick={() => {
|
||||
dis.dispatch({
|
||||
action: 'hangup',
|
||||
room_id: this.props.call.roomId,
|
||||
room_id: callRoomId,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
@ -487,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 = {};
|
||||
|
@ -505,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 : (
|
||||
|
@ -520,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>;
|
||||
|
|
|
@ -70,7 +70,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
|
|||
e.stopPropagation();
|
||||
dis.dispatch({
|
||||
action: 'answer',
|
||||
room_id: this.state.incomingCall.roomId,
|
||||
room_id: CallHandler.roomIdForCall(this.state.incomingCall),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -78,7 +78,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
|
|||
e.stopPropagation();
|
||||
dis.dispatch({
|
||||
action: 'reject',
|
||||
room_id: this.state.incomingCall.roomId,
|
||||
room_id: CallHandler.roomIdForCall(this.state.incomingCall),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -89,7 +89,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
|
|||
|
||||
let room = null;
|
||||
if (this.state.incomingCall) {
|
||||
room = MatrixClientPeg.get().getRoom(this.state.incomingCall.roomId);
|
||||
room = MatrixClientPeg.get().getRoom(CallHandler.roomIdForCall(this.state.incomingCall));
|
||||
}
|
||||
|
||||
const caller = room ? room.name : _t("Unknown caller");
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue