Merge remote-tracking branch 'origin/develop' into feat/add-message-edition-wysiwyg-composer

This commit is contained in:
Florian Duros 2022-10-19 16:34:22 +02:00
commit e77f333fb6
No known key found for this signature in database
GPG key ID: 9700AA5870258A0B
124 changed files with 4370 additions and 1039 deletions

View file

@ -137,6 +137,7 @@ import { TimelineRenderingType } from "../../contexts/RoomContext";
import { UseCaseSelection } from '../views/elements/UseCaseSelection';
import { ValidatedServerConfig } from '../../utils/ValidatedServerConfig';
import { isLocalRoom } from '../../utils/localRoom/isLocalRoom';
import { SdkContextClass, SDKContext } from '../../contexts/SDKContext';
import { viewUserDeviceSettings } from '../../actions/handlers/viewUserDeviceSettings';
// legacy export
@ -238,9 +239,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private readonly dispatcherRef: string;
private readonly themeWatcher: ThemeWatcher;
private readonly fontWatcher: FontWatcher;
private readonly stores: SdkContextClass;
constructor(props: IProps) {
super(props);
this.stores = SdkContextClass.instance;
this.stores.constructEagerStores();
this.state = {
view: Views.LOADING,
@ -762,6 +766,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
Modal.createDialog(DialPadModal, {}, "mx_Dialog_dialPadWrapper");
break;
case Action.OnLoggedIn:
this.stores.client = MatrixClientPeg.get();
if (
// Skip this handling for token login as that always calls onLoggedIn itself
!this.tokenLogin &&
@ -2087,7 +2092,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
return <ErrorBoundary>
{ view }
<SDKContext.Provider value={this.stores}>
{ view }
</SDKContext.Provider>
</ErrorBoundary>;
}
}

View file

@ -44,21 +44,18 @@ import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
import ResizeNotifier from '../../utils/ResizeNotifier';
import ContentMessages from '../../ContentMessages';
import Modal from '../../Modal';
import LegacyCallHandler, { LegacyCallHandlerEvent } from '../../LegacyCallHandler';
import { LegacyCallHandlerEvent } from '../../LegacyCallHandler';
import dis, { defaultDispatcher } from '../../dispatcher/dispatcher';
import * as Rooms from '../../Rooms';
import eventSearch, { searchPagination } from '../../Searching';
import MainSplit from './MainSplit';
import RightPanel from './RightPanel';
import { RoomViewStore } from '../../stores/RoomViewStore';
import RoomScrollStateStore, { ScrollState } from '../../stores/RoomScrollStateStore';
import WidgetEchoStore from '../../stores/WidgetEchoStore';
import SettingsStore from "../../settings/SettingsStore";
import { Layout } from "../../settings/enums/Layout";
import AccessibleButton from "../views/elements/AccessibleButton";
import RightPanelStore from "../../stores/right-panel/RightPanelStore";
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
import MatrixClientContext, { MatrixClientProps, withMatrixClientHOC } from "../../contexts/MatrixClientContext";
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
import { Action } from "../../dispatcher/actions";
import { IMatrixClientCreds } from "../../MatrixClientPeg";
@ -76,12 +73,10 @@ import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
import EffectsOverlay from "../views/elements/EffectsOverlay";
import { containsEmoji } from '../../effects/utils';
import { CHAT_EFFECTS } from '../../effects';
import WidgetStore from "../../stores/WidgetStore";
import { CallView } from "../views/voip/CallView";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
import Notifier from "../../Notifier";
import { showToast as showNotificationsToast } from "../../toasts/DesktopNotificationsToast";
import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
import { getKeyBindingsManager } from '../../KeyBindingsManager';
import { objectHasDiff } from "../../utils/objects";
@ -118,8 +113,8 @@ import { isLocalRoom } from '../../utils/localRoom/isLocalRoom';
import { ShowThreadPayload } from "../../dispatcher/payloads/ShowThreadPayload";
import { RoomStatusBarUnsentMessages } from './RoomStatusBarUnsentMessages';
import { LargeLoader } from './LargeLoader';
import { VoiceBroadcastInfoEventType } from '../../voice-broadcast';
import { isVideoRoom } from '../../utils/video-rooms';
import { SDKContext } from '../../contexts/SDKContext';
import { CallStore, CallStoreEvent } from "../../stores/CallStore";
import { Call } from "../../models/Call";
@ -133,7 +128,7 @@ if (DEBUG) {
debuglog = logger.log.bind(console);
}
interface IRoomProps extends MatrixClientProps {
interface IRoomProps {
threepidInvite: IThreepidInvite;
oobData?: IOOBData;
@ -203,7 +198,6 @@ export interface IRoomState {
upgradeRecommendation?: IRecommendedVersion;
canReact: boolean;
canSendMessages: boolean;
canSendVoiceBroadcasts: boolean;
tombstone?: MatrixEvent;
resizing: boolean;
layout: Layout;
@ -381,13 +375,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private messagePanel: TimelinePanel;
private roomViewBody = createRef<HTMLDivElement>();
static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>;
static contextType = SDKContext;
public context!: React.ContextType<typeof SDKContext>;
constructor(props: IRoomProps, context: React.ContextType<typeof MatrixClientContext>) {
constructor(props: IRoomProps, context: React.ContextType<typeof SDKContext>) {
super(props, context);
const llMembers = context.hasLazyLoadMembersEnabled();
const llMembers = context.client.hasLazyLoadMembersEnabled();
this.state = {
roomId: null,
roomLoading: true,
@ -408,7 +402,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
statusBarVisible: false,
canReact: false,
canSendMessages: false,
canSendVoiceBroadcasts: false,
resizing: false,
layout: SettingsStore.getValue("layout"),
lowBandwidth: SettingsStore.getValue("lowBandwidth"),
@ -422,7 +415,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
showJoinLeaves: true,
showAvatarChanges: true,
showDisplaynameChanges: true,
matrixClientIsReady: context?.isInitialSyncComplete(),
matrixClientIsReady: context.client?.isInitialSyncComplete(),
mainSplitContentType: MainSplitContentType.Timeline,
timelineRenderingType: TimelineRenderingType.Room,
liveTimeline: undefined,
@ -430,25 +423,25 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
};
this.dispatcherRef = dis.register(this.onAction);
context.on(ClientEvent.Room, this.onRoom);
context.on(RoomEvent.Timeline, this.onRoomTimeline);
context.on(RoomEvent.TimelineReset, this.onRoomTimelineReset);
context.on(RoomEvent.Name, this.onRoomName);
context.on(RoomStateEvent.Events, this.onRoomStateEvents);
context.on(RoomStateEvent.Update, this.onRoomStateUpdate);
context.on(RoomEvent.MyMembership, this.onMyMembership);
context.on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus);
context.on(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged);
context.on(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged);
context.on(CryptoEvent.KeysChanged, this.onCrossSigningKeysChanged);
context.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
context.client.on(ClientEvent.Room, this.onRoom);
context.client.on(RoomEvent.Timeline, this.onRoomTimeline);
context.client.on(RoomEvent.TimelineReset, this.onRoomTimelineReset);
context.client.on(RoomEvent.Name, this.onRoomName);
context.client.on(RoomStateEvent.Events, this.onRoomStateEvents);
context.client.on(RoomStateEvent.Update, this.onRoomStateUpdate);
context.client.on(RoomEvent.MyMembership, this.onMyMembership);
context.client.on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus);
context.client.on(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged);
context.client.on(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged);
context.client.on(CryptoEvent.KeysChanged, this.onCrossSigningKeysChanged);
context.client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
// Start listening for RoomViewStore updates
RoomViewStore.instance.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
context.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
context.rightPanelStore.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
context.widgetStore.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
CallStore.instance.on(CallStoreEvent.ActiveCalls, this.onActiveCalls);
@ -501,16 +494,16 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
action: "appsDrawer",
show: true,
});
if (WidgetLayoutStore.instance.hasMaximisedWidget(this.state.room)) {
if (this.context.widgetLayoutStore.hasMaximisedWidget(this.state.room)) {
// Show chat in right panel when a widget is maximised
RightPanelStore.instance.setCard({ phase: RightPanelPhases.Timeline });
this.context.rightPanelStore.setCard({ phase: RightPanelPhases.Timeline });
}
this.checkWidgets(this.state.room);
};
private checkWidgets = (room: Room): void => {
this.setState({
hasPinnedWidgets: WidgetLayoutStore.instance.hasPinnedWidgets(room),
hasPinnedWidgets: this.context.widgetLayoutStore.hasPinnedWidgets(room),
mainSplitContentType: this.getMainSplitContentType(room),
showApps: this.shouldShowApps(room),
});
@ -518,12 +511,12 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private getMainSplitContentType = (room: Room) => {
if (
(SettingsStore.getValue("feature_group_calls") && RoomViewStore.instance.isViewingCall())
(SettingsStore.getValue("feature_group_calls") && this.context.roomViewStore.isViewingCall())
|| isVideoRoom(room)
) {
return MainSplitContentType.Call;
}
if (WidgetLayoutStore.instance.hasMaximisedWidget(room)) {
if (this.context.widgetLayoutStore.hasMaximisedWidget(room)) {
return MainSplitContentType.MaximisedWidget;
}
return MainSplitContentType.Timeline;
@ -534,7 +527,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
return;
}
if (!initial && this.state.roomId !== RoomViewStore.instance.getRoomId()) {
if (!initial && this.state.roomId !== this.context.roomViewStore.getRoomId()) {
// RoomView explicitly does not support changing what room
// is being viewed: instead it should just be re-mounted when
// switching rooms. Therefore, if the room ID changes, we
@ -549,45 +542,45 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
return;
}
const roomId = RoomViewStore.instance.getRoomId();
const room = this.context.getRoom(roomId);
const roomId = this.context.roomViewStore.getRoomId();
const room = this.context.client.getRoom(roomId);
// This convoluted type signature ensures we get IntelliSense *and* correct typing
const newState: Partial<IRoomState> & Pick<IRoomState, any> = {
roomId,
roomAlias: RoomViewStore.instance.getRoomAlias(),
roomLoading: RoomViewStore.instance.isRoomLoading(),
roomLoadError: RoomViewStore.instance.getRoomLoadError(),
joining: RoomViewStore.instance.isJoining(),
replyToEvent: RoomViewStore.instance.getQuotingEvent(),
roomAlias: this.context.roomViewStore.getRoomAlias(),
roomLoading: this.context.roomViewStore.isRoomLoading(),
roomLoadError: this.context.roomViewStore.getRoomLoadError(),
joining: this.context.roomViewStore.isJoining(),
replyToEvent: this.context.roomViewStore.getQuotingEvent(),
// we should only peek once we have a ready client
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.instance.shouldPeek(),
shouldPeek: this.state.matrixClientIsReady && this.context.roomViewStore.shouldPeek(),
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
showRedactions: SettingsStore.getValue("showRedactions", roomId),
showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
wasContextSwitch: RoomViewStore.instance.getWasContextSwitch(),
wasContextSwitch: this.context.roomViewStore.getWasContextSwitch(),
mainSplitContentType: room === null ? undefined : this.getMainSplitContentType(room),
initialEventId: null, // default to clearing this, will get set later in the method if needed
showRightPanel: RightPanelStore.instance.isOpenForRoom(roomId),
showRightPanel: this.context.rightPanelStore.isOpenForRoom(roomId),
activeCall: CallStore.instance.getActiveCall(roomId),
};
if (
this.state.mainSplitContentType !== MainSplitContentType.Timeline
&& newState.mainSplitContentType === MainSplitContentType.Timeline
&& RightPanelStore.instance.isOpen
&& RightPanelStore.instance.currentCard.phase === RightPanelPhases.Timeline
&& RightPanelStore.instance.roomPhaseHistory.some(card => (card.phase === RightPanelPhases.Timeline))
&& this.context.rightPanelStore.isOpen
&& this.context.rightPanelStore.currentCard.phase === RightPanelPhases.Timeline
&& this.context.rightPanelStore.roomPhaseHistory.some(card => (card.phase === RightPanelPhases.Timeline))
) {
// We're returning to the main timeline, so hide the right panel timeline
RightPanelStore.instance.setCard({ phase: RightPanelPhases.RoomSummary });
RightPanelStore.instance.togglePanel(this.state.roomId ?? null);
this.context.rightPanelStore.setCard({ phase: RightPanelPhases.RoomSummary });
this.context.rightPanelStore.togglePanel(this.state.roomId ?? null);
newState.showRightPanel = false;
}
const initialEventId = RoomViewStore.instance.getInitialEventId();
const initialEventId = this.context.roomViewStore.getInitialEventId();
if (initialEventId) {
let initialEvent = room?.findEventById(initialEventId);
// The event does not exist in the current sync data
@ -600,7 +593,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
// becomes available to fetch a whole thread
if (!initialEvent) {
initialEvent = await fetchInitialEvent(
this.context,
this.context.client,
roomId,
initialEventId,
);
@ -616,21 +609,21 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
action: Action.ShowThread,
rootEvent: thread.rootEvent,
initialEvent,
highlighted: RoomViewStore.instance.isInitialEventHighlighted(),
scroll_into_view: RoomViewStore.instance.initialEventScrollIntoView(),
highlighted: this.context.roomViewStore.isInitialEventHighlighted(),
scroll_into_view: this.context.roomViewStore.initialEventScrollIntoView(),
});
} else {
newState.initialEventId = initialEventId;
newState.isInitialEventHighlighted = RoomViewStore.instance.isInitialEventHighlighted();
newState.initialEventScrollIntoView = RoomViewStore.instance.initialEventScrollIntoView();
newState.isInitialEventHighlighted = this.context.roomViewStore.isInitialEventHighlighted();
newState.initialEventScrollIntoView = this.context.roomViewStore.initialEventScrollIntoView();
if (thread && initialEvent?.isThreadRoot) {
dis.dispatch<ShowThreadPayload>({
action: Action.ShowThread,
rootEvent: thread.rootEvent,
initialEvent,
highlighted: RoomViewStore.instance.isInitialEventHighlighted(),
scroll_into_view: RoomViewStore.instance.initialEventScrollIntoView(),
highlighted: this.context.roomViewStore.isInitialEventHighlighted(),
scroll_into_view: this.context.roomViewStore.initialEventScrollIntoView(),
});
}
}
@ -657,7 +650,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (!initial && this.state.shouldPeek && !newState.shouldPeek) {
// Stop peeking because we have joined this room now
this.context.stopPeeking();
this.context.client.stopPeeking();
}
// Temporary logging to diagnose https://github.com/vector-im/element-web/issues/4307
@ -674,7 +667,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
// NB: This does assume that the roomID will not change for the lifetime of
// the RoomView instance
if (initial) {
newState.room = this.context.getRoom(newState.roomId);
newState.room = this.context.client.getRoom(newState.roomId);
if (newState.room) {
newState.showApps = this.shouldShowApps(newState.room);
this.onRoomLoaded(newState.room);
@ -784,7 +777,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
peekLoading: true,
isPeeking: true, // this will change to false if peeking fails
});
this.context.peekInRoom(roomId).then((room) => {
this.context.client.peekInRoom(roomId).then((room) => {
if (this.unmounted) {
return;
}
@ -817,7 +810,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
});
} else if (room) {
// Stop peeking because we have joined this room previously
this.context.stopPeeking();
this.context.client.stopPeeking();
this.setState({ isPeeking: false });
}
}
@ -835,7 +828,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
// Otherwise (in case the user set hideWidgetDrawer by clicking the button) follow the parameter.
const isManuallyShown = hideWidgetDrawer ? hideWidgetDrawer === "false": true;
const widgets = WidgetLayoutStore.instance.getContainerWidgets(room, Container.Top);
const widgets = this.context.widgetLayoutStore.getContainerWidgets(room, Container.Top);
return isManuallyShown && widgets.length > 0;
}
@ -848,7 +841,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
callState: callState,
});
LegacyCallHandler.instance.on(LegacyCallHandlerEvent.CallState, this.onCallState);
this.context.legacyCallHandler.on(LegacyCallHandlerEvent.CallState, this.onCallState);
window.addEventListener('beforeunload', this.onPageUnload);
}
@ -885,7 +878,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
// (We could use isMounted, but facebook have deprecated that.)
this.unmounted = true;
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallState, this.onCallState);
this.context.legacyCallHandler.removeListener(LegacyCallHandlerEvent.CallState, this.onCallState);
// update the scroll map before we get unmounted
if (this.state.roomId) {
@ -893,47 +886,47 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}
if (this.state.shouldPeek) {
this.context.stopPeeking();
this.context.client.stopPeeking();
}
// stop tracking room changes to format permalinks
this.stopAllPermalinkCreators();
dis.unregister(this.dispatcherRef);
if (this.context) {
this.context.removeListener(ClientEvent.Room, this.onRoom);
this.context.removeListener(RoomEvent.Timeline, this.onRoomTimeline);
this.context.removeListener(RoomEvent.TimelineReset, this.onRoomTimelineReset);
this.context.removeListener(RoomEvent.Name, this.onRoomName);
this.context.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
this.context.removeListener(RoomEvent.MyMembership, this.onMyMembership);
this.context.removeListener(RoomStateEvent.Update, this.onRoomStateUpdate);
this.context.removeListener(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus);
this.context.removeListener(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged);
this.context.removeListener(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged);
this.context.removeListener(CryptoEvent.KeysChanged, this.onCrossSigningKeysChanged);
this.context.removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted);
if (this.context.client) {
this.context.client.removeListener(ClientEvent.Room, this.onRoom);
this.context.client.removeListener(RoomEvent.Timeline, this.onRoomTimeline);
this.context.client.removeListener(RoomEvent.TimelineReset, this.onRoomTimelineReset);
this.context.client.removeListener(RoomEvent.Name, this.onRoomName);
this.context.client.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
this.context.client.removeListener(RoomEvent.MyMembership, this.onMyMembership);
this.context.client.removeListener(RoomStateEvent.Update, this.onRoomStateUpdate);
this.context.client.removeListener(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus);
this.context.client.removeListener(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged);
this.context.client.removeListener(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged);
this.context.client.removeListener(CryptoEvent.KeysChanged, this.onCrossSigningKeysChanged);
this.context.client.removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted);
}
window.removeEventListener('beforeunload', this.onPageUnload);
RoomViewStore.instance.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
this.context.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate);
this.context.rightPanelStore.off(UPDATE_EVENT, this.onRightPanelStoreUpdate);
WidgetEchoStore.removeListener(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
WidgetStore.instance.removeListener(UPDATE_EVENT, this.onWidgetStoreUpdate);
this.context.widgetStore.removeListener(UPDATE_EVENT, this.onWidgetStoreUpdate);
this.props.resizeNotifier.off("isResizing", this.onIsResizing);
if (this.state.room) {
WidgetLayoutStore.instance.off(
this.context.widgetLayoutStore.off(
WidgetLayoutStore.emissionForRoom(this.state.room),
this.onWidgetLayoutChange,
);
}
CallStore.instance.off(CallStoreEvent.ActiveCalls, this.onActiveCalls);
LegacyCallHandler.instance.off(LegacyCallHandlerEvent.CallState, this.onCallState);
this.context.legacyCallHandler.off(LegacyCallHandlerEvent.CallState, this.onCallState);
// cancel any pending calls to the throttled updated
this.updateRoomMembers.cancel();
@ -944,13 +937,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (this.viewsLocalRoom) {
// clean up if this was a local room
this.props.mxClient.store.removeRoom(this.state.room.roomId);
this.context.client.store.removeRoom(this.state.room.roomId);
}
}
private onRightPanelStoreUpdate = () => {
this.setState({
showRightPanel: RightPanelStore.instance.isOpenForRoom(this.state.roomId),
showRightPanel: this.context.rightPanelStore.isOpenForRoom(this.state.roomId),
});
};
@ -1017,7 +1010,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
break;
case 'picture_snapshot':
ContentMessages.sharedInstance().sendContentListToRoom(
[payload.file], this.state.room.roomId, null, this.context);
[payload.file], this.state.room.roomId, null, this.context.client);
break;
case 'notifier_enabled':
case Action.UploadStarted:
@ -1043,7 +1036,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
case 'MatrixActions.sync':
if (!this.state.matrixClientIsReady) {
this.setState({
matrixClientIsReady: this.context?.isInitialSyncComplete(),
matrixClientIsReady: this.context.client?.isInitialSyncComplete(),
}, () => {
// send another "initial" RVS update to trigger peeking if needed
this.onRoomViewStoreUpdate(true);
@ -1112,7 +1105,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private onLocalRoomEvent(roomId: string) {
if (roomId !== this.state.room.roomId) return;
createRoomFromLocalRoom(this.props.mxClient, this.state.room as LocalRoom);
createRoomFromLocalRoom(this.context.client, this.state.room as LocalRoom);
}
private onRoomTimeline = (ev: MatrixEvent, room: Room | null, toStartOfTimeline: boolean, removed, data) => {
@ -1145,7 +1138,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
this.handleEffects(ev);
}
if (ev.getSender() !== this.context.credentials.userId) {
if (ev.getSender() !== this.context.client.credentials.userId) {
// update unread count when scrolled up
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
// no change
@ -1165,7 +1158,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
};
private handleEffects = (ev: MatrixEvent) => {
const notifState = RoomNotificationStateStore.instance.getRoomState(this.state.room);
const notifState = this.context.roomNotificationStateStore.getRoomState(this.state.room);
if (!notifState.isUnread) return;
CHAT_EFFECTS.forEach(effect => {
@ -1202,7 +1195,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private onRoomLoaded = (room: Room) => {
if (this.unmounted) return;
// Attach a widget store listener only when we get a room
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(room), this.onWidgetLayoutChange);
this.context.widgetLayoutStore.on(WidgetLayoutStore.emissionForRoom(room), this.onWidgetLayoutChange);
this.calculatePeekRules(room);
this.updatePreviewUrlVisibility(room);
@ -1214,10 +1207,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (
this.getMainSplitContentType(room) !== MainSplitContentType.Timeline
&& RoomNotificationStateStore.instance.getRoomState(room).isUnread
&& this.context.roomNotificationStateStore.getRoomState(room).isUnread
) {
// Automatically open the chat panel to make unread messages easier to discover
RightPanelStore.instance.setCard({ phase: RightPanelPhases.Timeline }, true, room.roomId);
this.context.rightPanelStore.setCard({ phase: RightPanelPhases.Timeline }, true, room.roomId);
}
this.setState({
@ -1244,7 +1237,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private async loadMembersIfJoined(room: Room) {
// lazy load members if enabled
if (this.context.hasLazyLoadMembersEnabled()) {
if (this.context.client.hasLazyLoadMembersEnabled()) {
if (room && room.getMyMembership() === 'join') {
try {
await room.loadMembersIfNeeded();
@ -1270,7 +1263,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private updatePreviewUrlVisibility({ roomId }: Room) {
// URL Previews in E2EE rooms can be a privacy leak so use a different setting which is per-room explicit
const key = this.context.isRoomEncrypted(roomId) ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled';
const key = this.context.client.isRoomEncrypted(roomId) ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled';
this.setState({
showUrlPreview: SettingsStore.getValue(key, roomId),
});
@ -1283,7 +1276,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
// Detach the listener if the room is changing for some reason
if (this.state.room) {
WidgetLayoutStore.instance.off(
this.context.widgetLayoutStore.off(
WidgetLayoutStore.emissionForRoom(this.state.room),
this.onWidgetLayoutChange,
);
@ -1320,15 +1313,15 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
};
private async updateE2EStatus(room: Room) {
if (!this.context.isRoomEncrypted(room.roomId)) return;
if (!this.context.client.isRoomEncrypted(room.roomId)) return;
// If crypto is not currently enabled, we aren't tracking devices at all,
// so we don't know what the answer is. Let's error on the safe side and show
// a warning for this case.
let e2eStatus = E2EStatus.Warning;
if (this.context.isCryptoEnabled()) {
if (this.context.client.isCryptoEnabled()) {
/* At this point, the user has encryption on and cross-signing on */
e2eStatus = await shieldStatusForRoom(this.context, room);
e2eStatus = await shieldStatusForRoom(this.context.client, room);
}
if (this.unmounted) return;
@ -1374,19 +1367,17 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private updatePermissions(room: Room) {
if (room) {
const me = this.context.getUserId();
const me = this.context.client.getUserId();
const canReact = (
room.getMyMembership() === "join" &&
room.currentState.maySendEvent(EventType.Reaction, me)
);
const canSendMessages = room.maySendMessage();
const canSelfRedact = room.currentState.maySendEvent(EventType.RoomRedaction, me);
const canSendVoiceBroadcasts = room.currentState.maySendEvent(VoiceBroadcastInfoEventType, me);
this.setState({
canReact,
canSendMessages,
canSendVoiceBroadcasts,
canSelfRedact,
});
}
@ -1442,7 +1433,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private onJoinButtonClicked = () => {
// If the user is a ROU, allow them to transition to a PWLU
if (this.context?.isGuest()) {
if (this.context.client?.isGuest()) {
// Join this room once the user has registered and logged in
// (If we failed to peek, we may not have a valid room object.)
dis.dispatch<DoAfterSyncPreparedPayload<ViewRoomPayload>>({
@ -1499,13 +1490,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
};
private injectSticker(url: string, info: object, text: string, threadId: string | null) {
if (this.context.isGuest()) {
if (this.context.client.isGuest()) {
dis.dispatch({ action: 'require_registration' });
return;
}
ContentMessages.sharedInstance()
.sendStickerContentToRoom(url, this.state.room.roomId, threadId, info, text, this.context)
.sendStickerContentToRoom(url, this.state.room.roomId, threadId, info, text, this.context.client)
.then(undefined, (error) => {
if (error.name === "UnknownDeviceError") {
// Let the staus bar handle this
@ -1578,7 +1569,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
return b.length - a.length;
});
if (this.context.supportsExperimentalThreads()) {
if (this.context.client.supportsExperimentalThreads()) {
// Process all thread roots returned in this batch of search results
// XXX: This won't work for results coming from Seshat which won't include the bundled relationship
for (const result of results.results) {
@ -1586,7 +1577,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
const bundledRelationship = event
.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
if (!bundledRelationship || event.getThread()) continue;
const room = this.context.getRoom(event.getRoomId());
const room = this.context.client.getRoom(event.getRoomId());
const thread = room.findThreadForEvent(event);
if (thread) {
event.setThread(thread);
@ -1658,7 +1649,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
const mxEv = result.context.getEvent();
const roomId = mxEv.getRoomId();
const room = this.context.getRoom(roomId);
const room = this.context.client.getRoom(roomId);
if (!room) {
// if we do not have the room in js-sdk stores then hide it as we cannot easily show it
// As per the spec, an all rooms search can create this condition,
@ -1715,7 +1706,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
this.setState({
rejecting: true,
});
this.context.leave(this.state.roomId).then(() => {
this.context.client.leave(this.state.roomId).then(() => {
dis.dispatch({ action: Action.ViewHomePage });
this.setState({
rejecting: false,
@ -1742,13 +1733,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
});
try {
const myMember = this.state.room.getMember(this.context.getUserId());
const myMember = this.state.room.getMember(this.context.client.getUserId());
const inviteEvent = myMember.events.member;
const ignoredUsers = this.context.getIgnoredUsers();
const ignoredUsers = this.context.client.getIgnoredUsers();
ignoredUsers.push(inviteEvent.getSender()); // de-duped internally in the js-sdk
await this.context.setIgnoredUsers(ignoredUsers);
await this.context.client.setIgnoredUsers(ignoredUsers);
await this.context.leave(this.state.roomId);
await this.context.client.leave(this.state.roomId);
dis.dispatch({ action: Action.ViewHomePage });
this.setState({
rejecting: false,
@ -1911,7 +1902,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (!this.state.room) {
return null;
}
return LegacyCallHandler.instance.getCallForRoom(this.state.room.roomId);
return this.context.legacyCallHandler.getCallForRoom(this.state.room.roomId);
}
// this has to be a proper method rather than an unnamed function,
@ -1924,7 +1915,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
const createEvent = this.state.room.currentState.getStateEvents(EventType.RoomCreate, "");
if (!createEvent || !createEvent.getContent()['predecessor']) return null;
return this.context.getRoom(createEvent.getContent()['predecessor']['room_id']);
return this.context.client.getRoom(createEvent.getContent()['predecessor']['room_id']);
}
getHiddenHighlightCount() {
@ -1953,7 +1944,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
Array.from(dataTransfer.files),
this.state.room?.roomId ?? this.state.roomId,
null,
this.context,
this.context.client,
TimelineRenderingType.Room,
);
@ -1970,7 +1961,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}
private renderLocalRoomCreateLoader(): ReactElement {
const names = this.state.room.getDefaultRoomName(this.props.mxClient.getUserId());
const names = this.state.room.getDefaultRoomName(this.context.client.getUserId());
return <RoomContext.Provider value={this.state}>
<LocalRoomCreateLoader
names={names}
@ -2081,7 +2072,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
</ErrorBoundary>
);
} else {
const myUserId = this.context.credentials.userId;
const myUserId = this.context.client.credentials.userId;
const myMember = this.state.room.getMember(myUserId);
const inviteEvent = myMember ? myMember.events.member : null;
let inviterName = _t("Unknown");
@ -2162,7 +2153,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
const showRoomUpgradeBar = (
roomVersionRecommendation &&
roomVersionRecommendation.needsUpgrade &&
this.state.room.userMayUpgradeRoom(this.context.credentials.userId)
this.state.room.userMayUpgradeRoom(this.context.client.credentials.userId)
);
const hiddenHighlightCount = this.getHiddenHighlightCount();
@ -2174,7 +2165,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
searchInProgress={this.state.searchInProgress}
onCancelClick={this.onCancelSearchClick}
onSearch={this.onSearch}
isRoomEncrypted={this.context.isRoomEncrypted(this.state.room.roomId)}
isRoomEncrypted={this.context.client.isRoomEncrypted(this.state.room.roomId)}
/>;
} else if (showRoomUpgradeBar) {
aux = <RoomUpgradeWarningBar room={this.state.room} />;
@ -2236,7 +2227,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
const auxPanel = (
<AuxPanel
room={this.state.room}
userId={this.context.credentials.userId}
userId={this.context.client.credentials.userId}
showApps={this.state.showApps}
resizeNotifier={this.props.resizeNotifier}
>
@ -2257,7 +2248,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
resizeNotifier={this.props.resizeNotifier}
replyToEvent={this.state.replyToEvent}
permalinkCreator={this.permalinkCreator}
showVoiceBroadcastButton={this.state.canSendVoiceBroadcasts}
/>;
}
@ -2397,7 +2387,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
mainSplitBody = <>
<AppsDrawer
room={this.state.room}
userId={this.context.credentials.userId}
userId={this.context.client.credentials.userId}
resizeNotifier={this.props.resizeNotifier}
showApps={true}
/>
@ -2451,7 +2441,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
onAppsClick = null;
onForgetClick = null;
onSearchClick = null;
if (this.state.room.canInvite(this.context.credentials.userId)) {
if (this.state.room.canInvite(this.context.client.credentials.userId)) {
onInviteClick = this.onInviteClick;
}
viewingCall = true;
@ -2493,5 +2483,4 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}
}
const RoomViewWithMatrixClient = withMatrixClientHOC(RoomView);
export default RoomViewWithMatrixClient;
export default RoomView;

View file

@ -60,13 +60,13 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
import { useTypedEventEmitterState } from "../../hooks/useEventEmitter";
import { IOOBData } from "../../stores/ThreepidInviteStore";
import { awaitRoomDownSync } from "../../utils/RoomUpgrade";
import { RoomViewStore } from "../../stores/RoomViewStore";
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import { JoinRoomReadyPayload } from "../../dispatcher/payloads/JoinRoomReadyPayload";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
import { Alignment } from "../views/elements/Tooltip";
import { getTopic } from "../../hooks/room/useTopic";
import { SdkContextClass } from "../../contexts/SDKContext";
interface IProps {
space: Room;
@ -378,7 +378,7 @@ export const joinRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: st
metricsTrigger: "SpaceHierarchy",
});
}, err => {
RoomViewStore.instance.showJoinRoomError(err, roomId);
SdkContextClass.instance.roomViewStore.showJoinRoomError(err, roomId);
});
return prom;

View file

@ -51,10 +51,10 @@ import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
import Measured from '../views/elements/Measured';
import PosthogTrackers from "../../PosthogTrackers";
import { ButtonEvent } from "../views/elements/AccessibleButton";
import { RoomViewStore } from '../../stores/RoomViewStore';
import Spinner from "../views/elements/Spinner";
import { ComposerInsertPayload, ComposerType } from "../../dispatcher/payloads/ComposerInsertPayload";
import Heading from '../views/typography/Heading';
import { SdkContextClass } from '../../contexts/SDKContext';
interface IProps {
room: Room;
@ -113,7 +113,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
room.removeListener(ThreadEvent.New, this.onNewThread);
SettingsStore.unwatchSetting(this.layoutWatcherRef);
const hasRoomChanged = RoomViewStore.instance.getRoomId() !== roomId;
const hasRoomChanged = SdkContextClass.instance.roomViewStore.getRoomId() !== roomId;
if (this.props.isInitialEventHighlighted && !hasRoomChanged) {
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,

View file

@ -0,0 +1,396 @@
/*
Copyright 2022 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 { MSC3906Rendezvous, MSC3906RendezvousPayload, RendezvousFailureReason } from 'matrix-js-sdk/src/rendezvous';
import { MSC3886SimpleHttpRendezvousTransport } from 'matrix-js-sdk/src/rendezvous/transports';
import { MSC3903ECDHPayload, MSC3903ECDHv1RendezvousChannel } from 'matrix-js-sdk/src/rendezvous/channels';
import { logger } from 'matrix-js-sdk/src/logger';
import { MatrixClient } from 'matrix-js-sdk/src/client';
import { _t } from "../../../languageHandler";
import AccessibleButton from '../elements/AccessibleButton';
import QRCode from '../elements/QRCode';
import Spinner from '../elements/Spinner';
import { Icon as BackButtonIcon } from "../../../../res/img/element-icons/back.svg";
import { Icon as DevicesIcon } from "../../../../res/img/element-icons/devices.svg";
import { Icon as WarningBadge } from "../../../../res/img/element-icons/warning-badge.svg";
import { Icon as InfoIcon } from "../../../../res/img/element-icons/i.svg";
import { wrapRequestWithDialog } from '../../../utils/UserInteractiveAuth';
/**
* The intention of this enum is to have a mode that scans a QR code instead of generating one.
*/
export enum Mode {
/**
* A QR code with be generated and shown
*/
Show = "show",
}
enum Phase {
Loading,
ShowingQR,
Connecting,
Connected,
WaitingForDevice,
Verifying,
Error,
}
interface IProps {
client: MatrixClient;
mode: Mode;
onFinished(...args: any): void;
}
interface IState {
phase: Phase;
rendezvous?: MSC3906Rendezvous;
confirmationDigits?: string;
failureReason?: RendezvousFailureReason;
mediaPermissionError?: boolean;
}
/**
* A component that allows sign in and E2EE set up with a QR code.
*
* It implements both `login.start` and `login-reciprocate` capabilities as well as both scanning and showing QR codes.
*
* This uses the unstable feature of MSC3906: https://github.com/matrix-org/matrix-spec-proposals/pull/3906
*/
export default class LoginWithQR extends React.Component<IProps, IState> {
public constructor(props) {
super(props);
this.state = {
phase: Phase.Loading,
};
}
public componentDidMount(): void {
this.updateMode(this.props.mode).then(() => {});
}
public componentDidUpdate(prevProps: Readonly<IProps>): void {
if (prevProps.mode !== this.props.mode) {
this.updateMode(this.props.mode).then(() => {});
}
}
private async updateMode(mode: Mode) {
this.setState({ phase: Phase.Loading });
if (this.state.rendezvous) {
this.state.rendezvous.onFailure = undefined;
await this.state.rendezvous.cancel(RendezvousFailureReason.UserCancelled);
this.setState({ rendezvous: undefined });
}
if (mode === Mode.Show) {
await this.generateCode();
}
}
public componentWillUnmount(): void {
if (this.state.rendezvous) {
// eslint-disable-next-line react/no-direct-mutation-state
this.state.rendezvous.onFailure = undefined;
// calling cancel will call close() as well to clean up the resources
this.state.rendezvous.cancel(RendezvousFailureReason.UserCancelled).then(() => {});
}
}
private approveLogin = async (): Promise<void> => {
if (!this.state.rendezvous) {
throw new Error('Rendezvous not found');
}
this.setState({ phase: Phase.Loading });
try {
logger.info("Requesting login token");
const { login_token: loginToken } = await wrapRequestWithDialog(this.props.client.requestLoginToken, {
matrixClient: this.props.client,
title: _t("Sign in new device"),
})();
this.setState({ phase: Phase.WaitingForDevice });
const newDeviceId = await this.state.rendezvous.approveLoginOnExistingDevice(loginToken);
if (!newDeviceId) {
// user denied
return;
}
if (!this.props.client.crypto) {
// no E2EE to set up
this.props.onFinished(true);
return;
}
await this.state.rendezvous.verifyNewDeviceOnExistingDevice();
this.props.onFinished(true);
} catch (e) {
logger.error('Error whilst approving sign in', e);
this.setState({ phase: Phase.Error, failureReason: RendezvousFailureReason.Unknown });
}
};
private generateCode = async () => {
let rendezvous: MSC3906Rendezvous;
try {
const transport = new MSC3886SimpleHttpRendezvousTransport<MSC3903ECDHPayload>({
onFailure: this.onFailure,
client: this.props.client,
});
const channel = new MSC3903ECDHv1RendezvousChannel<MSC3906RendezvousPayload>(
transport, undefined, this.onFailure,
);
rendezvous = new MSC3906Rendezvous(channel, this.props.client, this.onFailure);
await rendezvous.generateCode();
this.setState({
phase: Phase.ShowingQR,
rendezvous,
failureReason: undefined,
});
} catch (e) {
logger.error('Error whilst generating QR code', e);
this.setState({ phase: Phase.Error, failureReason: RendezvousFailureReason.HomeserverLacksSupport });
return;
}
try {
const confirmationDigits = await rendezvous.startAfterShowingCode();
this.setState({ phase: Phase.Connected, confirmationDigits });
} catch (e) {
logger.error('Error whilst doing QR login', e);
// only set to error phase if it hasn't already been set by onFailure or similar
if (this.state.phase !== Phase.Error) {
this.setState({ phase: Phase.Error, failureReason: RendezvousFailureReason.Unknown });
}
}
};
private onFailure = (reason: RendezvousFailureReason) => {
logger.info(`Rendezvous failed: ${reason}`);
this.setState({ phase: Phase.Error, failureReason: reason });
};
public reset() {
this.setState({
rendezvous: undefined,
confirmationDigits: undefined,
failureReason: undefined,
});
}
private cancelClicked = async (e: React.FormEvent) => {
e.preventDefault();
await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled);
this.reset();
this.props.onFinished(false);
};
private declineClicked = async (e: React.FormEvent) => {
e.preventDefault();
await this.state.rendezvous?.declineLoginOnExistingDevice();
this.reset();
this.props.onFinished(false);
};
private tryAgainClicked = async (e: React.FormEvent) => {
e.preventDefault();
this.reset();
await this.updateMode(this.props.mode);
};
private onBackClick = async () => {
await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled);
this.props.onFinished(false);
};
private cancelButton = () => <AccessibleButton
kind="primary_outline"
onClick={this.cancelClicked}
>
{ _t("Cancel") }
</AccessibleButton>;
private simpleSpinner = (description?: string): JSX.Element => {
return <div className="mx_LoginWithQR_spinner">
<div>
<Spinner />
{ description && <p>{ description }</p> }
</div>
</div>;
};
public render() {
let title: string;
let titleIcon: JSX.Element | undefined;
let main: JSX.Element | undefined;
let buttons: JSX.Element | undefined;
let backButton = true;
let cancellationMessage: string | undefined;
let centreTitle = false;
switch (this.state.phase) {
case Phase.Error:
switch (this.state.failureReason) {
case RendezvousFailureReason.Expired:
cancellationMessage = _t("The linking wasn't completed in the required time.");
break;
case RendezvousFailureReason.InvalidCode:
cancellationMessage = _t("The scanned code is invalid.");
break;
case RendezvousFailureReason.UnsupportedAlgorithm:
cancellationMessage = _t("Linking with this device is not supported.");
break;
case RendezvousFailureReason.UserDeclined:
cancellationMessage = _t("The request was declined on the other device.");
break;
case RendezvousFailureReason.OtherDeviceAlreadySignedIn:
cancellationMessage = _t("The other device is already signed in.");
break;
case RendezvousFailureReason.OtherDeviceNotSignedIn:
cancellationMessage = _t("The other device isn't signed in.");
break;
case RendezvousFailureReason.UserCancelled:
cancellationMessage = _t("The request was cancelled.");
break;
case RendezvousFailureReason.Unknown:
cancellationMessage = _t("An unexpected error occurred.");
break;
case RendezvousFailureReason.HomeserverLacksSupport:
cancellationMessage = _t("The homeserver doesn't support signing in another device.");
break;
default:
cancellationMessage = _t("The request was cancelled.");
break;
}
title = _t("Connection failed");
centreTitle = true;
titleIcon = <WarningBadge className="error" />;
backButton = false;
main = <p data-testid="cancellation-message">{ cancellationMessage }</p>;
buttons = <>
<AccessibleButton
kind="primary"
onClick={this.tryAgainClicked}
>
{ _t("Try again") }
</AccessibleButton>
{ this.cancelButton() }
</>;
break;
case Phase.Connected:
title = _t("Devices connected");
titleIcon = <DevicesIcon className="normal" />;
backButton = false;
main = <>
<p>{ _t("Check that the code below matches with your other device:") }</p>
<div className="mx_LoginWithQR_confirmationDigits">
{ this.state.confirmationDigits }
</div>
<div className="mx_LoginWithQR_confirmationAlert">
<div>
<InfoIcon />
</div>
<div>{ _t("By approving access for this device, it will have full access to your account.") }</div>
</div>
</>;
buttons = <>
<AccessibleButton
data-testid="decline-login-button"
kind="primary_outline"
onClick={this.declineClicked}
>
{ _t("Cancel") }
</AccessibleButton>
<AccessibleButton
data-testid="approve-login-button"
kind="primary"
onClick={this.approveLogin}
>
{ _t("Approve") }
</AccessibleButton>
</>;
break;
case Phase.ShowingQR:
title =_t("Sign in with QR code");
if (this.state.rendezvous) {
const code = <div className="mx_LoginWithQR_qrWrapper">
<QRCode data={[{ data: Buffer.from(this.state.rendezvous.code), mode: 'byte' }]} className="mx_QRCode" />
</div>;
main = <>
<p>{ _t("Scan the QR code below with your device that's signed out.") }</p>
<ol>
<li>{ _t("Start at the sign in screen") }</li>
<li>{ _t("Select 'Scan QR code'") }</li>
<li>{ _t("Review and approve the sign in") }</li>
</ol>
{ code }
</>;
} else {
main = this.simpleSpinner();
buttons = this.cancelButton();
}
break;
case Phase.Loading:
main = this.simpleSpinner();
break;
case Phase.Connecting:
main = this.simpleSpinner(_t("Connecting..."));
buttons = this.cancelButton();
break;
case Phase.WaitingForDevice:
main = this.simpleSpinner(_t("Waiting for device to sign in"));
buttons = this.cancelButton();
break;
case Phase.Verifying:
title = _t("Success");
centreTitle = true;
main = this.simpleSpinner(_t("Completing set up of your new device"));
break;
}
return (
<div className="mx_LoginWithQR">
<div className={centreTitle ? "mx_LoginWithQR_centreTitle" : ""}>
{ backButton ?
<AccessibleButton
data-testid="back-button"
className="mx_LoginWithQR_BackButton"
onClick={this.onBackClick}
title="Back"
>
<BackButtonIcon />
</AccessibleButton>
: null }
<h1>{ titleIcon }{ title }</h1>
</div>
<div className="mx_LoginWithQR_main">
{ main }
</div>
<div className="mx_LoginWithQR_buttons">
{ buttons }
</div>
</div>
);
}
}

View file

@ -24,13 +24,13 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { Action } from "../../../dispatcher/actions";
import { Call, ConnectionState, ElementCall } from "../../../models/Call";
import { useCall } from "../../../hooks/useCall";
import { RoomViewStore } from "../../../stores/RoomViewStore";
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
import {
OwnBeaconStore,
OwnBeaconStoreEvent,
} from "../../../stores/OwnBeaconStore";
import { CallDurationFromEvent } from "../voip/CallDuration";
import { SdkContextClass } from "../../../contexts/SDKContext";
interface RoomCallBannerProps {
roomId: Room["roomId"];
@ -114,7 +114,7 @@ const RoomCallBanner: React.FC<Props> = ({ roomId }) => {
}
// Check if the call is already showing. No banner is needed in this case.
if (RoomViewStore.instance.isViewingCall()) {
if (SdkContextClass.instance.roomViewStore.isViewingCall()) {
return null;
}

View file

@ -37,7 +37,6 @@ import Modal from "../../../Modal";
import ExportDialog from "../dialogs/ExportDialog";
import { useFeatureEnabled } from "../../../hooks/useSettings";
import { usePinnedEvents } from "../right_panel/PinnedMessagesCard";
import { RoomViewStore } from "../../../stores/RoomViewStore";
import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases';
import { ROOM_NOTIFICATIONS_TAB } from "../dialogs/RoomSettingsDialog";
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
@ -50,6 +49,7 @@ import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import SettingsStore from "../../../settings/SettingsStore";
import DevtoolsDialog from "../dialogs/DevtoolsDialog";
import { SdkContextClass } from "../../../contexts/SDKContext";
interface IProps extends IContextMenuProps {
room: Room;
@ -332,7 +332,7 @@ const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => {
};
const ensureViewingRoom = (ev: ButtonEvent) => {
if (RoomViewStore.instance.getRoomId() === room.roomId) return;
if (SdkContextClass.instance.roomViewStore.getRoomId() === room.roomId) return;
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: room.roomId,
@ -377,7 +377,7 @@ const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => {
ev.stopPropagation();
Modal.createDialog(DevtoolsDialog, {
roomId: RoomViewStore.instance.getRoomId(),
roomId: SdkContextClass.instance.roomViewStore.getRoomId(),
}, "mx_DevtoolsDialog_wrapper");
onFinished();
}}

View file

@ -38,7 +38,7 @@ interface IDialogAesthetics {
};
}
interface IProps extends IDialogProps {
export interface InteractiveAuthDialogProps extends IDialogProps {
// matrix client to use for UI auth requests
matrixClient: MatrixClient;
@ -82,8 +82,8 @@ interface IState {
uiaStagePhase: number | string;
}
export default class InteractiveAuthDialog extends React.Component<IProps, IState> {
constructor(props: IProps) {
export default class InteractiveAuthDialog extends React.Component<InteractiveAuthDialogProps, IState> {
constructor(props: InteractiveAuthDialogProps) {
super(props);
this.state = {

View file

@ -66,7 +66,7 @@ import { BreadcrumbsStore } from "../../../../stores/BreadcrumbsStore";
import { RoomNotificationState } from "../../../../stores/notifications/RoomNotificationState";
import { RoomNotificationStateStore } from "../../../../stores/notifications/RoomNotificationStateStore";
import { RecentAlgorithm } from "../../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
import { RoomViewStore } from "../../../../stores/RoomViewStore";
import { SdkContextClass } from "../../../../contexts/SDKContext";
import { getMetaSpaceName } from "../../../../stores/spaces";
import SpaceStore from "../../../../stores/spaces/SpaceStore";
import { DirectoryMember, Member, startDmOnFirstMessage } from "../../../../utils/direct-messages";
@ -1060,7 +1060,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
</h4>
<div>
{ BreadcrumbsStore.instance.rooms
.filter(r => r.roomId !== RoomViewStore.instance.getRoomId())
.filter(r => r.roomId !== SdkContextClass.instance.roomViewStore.getRoomId())
.map(room => (
<TooltipOption
id={`mx_SpotlightDialog_button_recentlyViewed_${room.roomId}`}

View file

@ -43,13 +43,13 @@ import { IApp } from "../../../stores/WidgetStore";
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
import { OwnProfileStore } from '../../../stores/OwnProfileStore';
import { UPDATE_EVENT } from '../../../stores/AsyncStore';
import { RoomViewStore } from '../../../stores/RoomViewStore';
import WidgetUtils from '../../../utils/WidgetUtils';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { ActionPayload } from "../../../dispatcher/payloads";
import { Action } from '../../../dispatcher/actions';
import { ElementWidgetCapabilities } from '../../../stores/widgets/ElementWidgetCapabilities';
import { WidgetMessagingStore } from '../../../stores/widgets/WidgetMessagingStore';
import { SdkContextClass } from '../../../contexts/SDKContext';
interface IProps {
app: IApp;
@ -175,7 +175,7 @@ export default class AppTile extends React.Component<IProps, IState> {
);
if (isActiveWidget) {
// We just left the room that the active widget was from.
if (this.props.room && RoomViewStore.instance.getRoomId() !== this.props.room.roomId) {
if (this.props.room && SdkContextClass.instance.roomViewStore.getRoomId() !== this.props.room.roomId) {
// If we are not actively looking at the room then destroy this widget entirely.
this.endWidgetActions();
} else if (WidgetType.JITSI.matches(this.props.app.type)) {

View file

@ -83,7 +83,7 @@ const OptionsButton: React.FC<IOptionsButtonProps> = ({
getRelationsForEvent,
}) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
const [onFocus, isActive, ref] = useRovingTabIndex(button);
const [onFocus, isActive] = useRovingTabIndex(button);
useEffect(() => {
onFocusChange(menuDisplayed);
}, [onFocusChange, menuDisplayed]);
@ -123,7 +123,7 @@ const OptionsButton: React.FC<IOptionsButtonProps> = ({
onClick={onOptionsClick}
onContextMenu={onOptionsClick}
isExpanded={menuDisplayed}
inputRef={ref}
inputRef={button}
onFocus={onFocus}
tabIndex={isActive ? 0 : -1}
>
@ -141,7 +141,7 @@ interface IReactButtonProps {
const ReactButton: React.FC<IReactButtonProps> = ({ mxEvent, reactions, onFocusChange }) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
const [onFocus, isActive, ref] = useRovingTabIndex(button);
const [onFocus, isActive] = useRovingTabIndex(button);
useEffect(() => {
onFocusChange(menuDisplayed);
}, [onFocusChange, menuDisplayed]);
@ -173,7 +173,7 @@ const ReactButton: React.FC<IReactButtonProps> = ({ mxEvent, reactions, onFocusC
onClick={onClick}
onContextMenu={onClick}
isExpanded={menuDisplayed}
inputRef={ref}
inputRef={button}
onFocus={onFocus}
tabIndex={isActive ? 0 : -1}
>

View file

@ -43,8 +43,6 @@ import MjolnirBody from "./MjolnirBody";
import MBeaconBody from "./MBeaconBody";
import { IEventTileOps } from "../rooms/EventTile";
import { VoiceBroadcastBody, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from '../../../voice-broadcast';
import { Features } from '../../../settings/Settings';
import { SettingLevel } from '../../../settings/SettingLevel';
// onMessageAllowed is handled internally
interface IProps extends Omit<IBodyProps, "onMessageAllowed" | "mediaEventHelper"> {
@ -58,18 +56,10 @@ interface IProps extends Omit<IBodyProps, "onMessageAllowed" | "mediaEventHelper
isSeeingThroughMessageHiddenForModeration?: boolean;
}
interface State {
voiceBroadcastEnabled: boolean;
}
export interface IOperableEventTile {
getEventTileOps(): IEventTileOps;
}
interface State {
voiceBroadcastEnabled: boolean;
}
const baseBodyTypes = new Map<string, typeof React.Component>([
[MsgType.Text, TextualBody],
[MsgType.Notice, TextualBody],
@ -87,7 +77,7 @@ const baseEvTypes = new Map<string, React.ComponentType<Partial<IBodyProps>>>([
[M_BEACON_INFO.altName, MBeaconBody],
]);
export default class MessageEvent extends React.Component<IProps, State> implements IMediaBody, IOperableEventTile {
export default class MessageEvent extends React.Component<IProps> implements IMediaBody, IOperableEventTile {
private body: React.RefObject<React.Component | IOperableEventTile> = createRef();
private mediaHelper: MediaEventHelper;
private bodyTypes = new Map<string, typeof React.Component>(baseBodyTypes.entries());
@ -95,7 +85,6 @@ export default class MessageEvent extends React.Component<IProps, State> impleme
public static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>;
private voiceBroadcastSettingWatcherRef: string;
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
super(props, context);
@ -105,29 +94,15 @@ export default class MessageEvent extends React.Component<IProps, State> impleme
}
this.updateComponentMaps();
this.state = {
// only check voice broadcast settings for a voice broadcast event
voiceBroadcastEnabled: this.props.mxEvent.getType() === VoiceBroadcastInfoEventType
&& SettingsStore.getValue(Features.VoiceBroadcast),
};
}
public componentDidMount(): void {
this.props.mxEvent.addListener(MatrixEventEvent.Decrypted, this.onDecrypted);
if (this.props.mxEvent.getType() === VoiceBroadcastInfoEventType) {
this.watchVoiceBroadcastFeatureSetting();
}
}
public componentWillUnmount() {
this.props.mxEvent.removeListener(MatrixEventEvent.Decrypted, this.onDecrypted);
this.mediaHelper?.destroy();
if (this.voiceBroadcastSettingWatcherRef) {
SettingsStore.unwatchSetting(this.voiceBroadcastSettingWatcherRef);
}
}
public componentDidUpdate(prevProps: Readonly<IProps>) {
@ -171,16 +146,6 @@ export default class MessageEvent extends React.Component<IProps, State> impleme
this.forceUpdate();
};
private watchVoiceBroadcastFeatureSetting(): void {
this.voiceBroadcastSettingWatcherRef = SettingsStore.watchSetting(
Features.VoiceBroadcast,
null,
(settingName: string, roomId: string, atLevel: SettingLevel, newValAtLevel, newValue: boolean) => {
this.setState({ voiceBroadcastEnabled: newValue });
},
);
}
public render() {
const content = this.props.mxEvent.getContent();
const type = this.props.mxEvent.getType();
@ -209,8 +174,7 @@ export default class MessageEvent extends React.Component<IProps, State> impleme
}
if (
this.state.voiceBroadcastEnabled
&& type === VoiceBroadcastInfoEventType
type === VoiceBroadcastInfoEventType
&& content?.state === VoiceBroadcastInfoState.Started
) {
BodyType = VoiceBroadcastBody;

View file

@ -33,7 +33,6 @@ import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import { ActionPayload } from '../../../dispatcher/payloads';
import { Action } from '../../../dispatcher/actions';
import { RoomViewStore } from '../../../stores/RoomViewStore';
import ContentMessages from '../../../ContentMessages';
import UploadBar from '../../structures/UploadBar';
import SettingsStore from '../../../settings/SettingsStore';
@ -42,6 +41,7 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import Measured from '../elements/Measured';
import Heading from '../typography/Heading';
import { UPDATE_EVENT } from '../../../stores/AsyncStore';
import { SdkContextClass } from '../../../contexts/SDKContext';
interface IProps {
room: Room;
@ -91,7 +91,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
}
public componentDidMount(): void {
RoomViewStore.instance.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
SdkContextClass.instance.roomViewStore.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
this.dispatcherRef = dis.register(this.onAction);
this.readReceiptsSettingWatcher = SettingsStore.watchSetting("showReadReceipts", null, (...[,,, value]) =>
this.setState({ showReadReceipts: value as boolean }),
@ -102,7 +102,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
}
public componentWillUnmount(): void {
RoomViewStore.instance.removeListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
SdkContextClass.instance.roomViewStore.removeListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
if (this.readReceiptsSettingWatcher) {
SettingsStore.unwatchSetting(this.readReceiptsSettingWatcher);
@ -116,12 +116,9 @@ export default class TimelineCard extends React.Component<IProps, IState> {
private onRoomViewStoreUpdate = async (initial?: boolean): Promise<void> => {
const newState: Pick<IState, any> = {
// roomLoading: RoomViewStore.instance.isRoomLoading(),
// roomLoadError: RoomViewStore.instance.getRoomLoadError(),
initialEventId: RoomViewStore.instance.getInitialEventId(),
isInitialEventHighlighted: RoomViewStore.instance.isInitialEventHighlighted(),
replyToEvent: RoomViewStore.instance.getQuotingEvent(),
initialEventId: SdkContextClass.instance.roomViewStore.getInitialEventId(),
isInitialEventHighlighted: SdkContextClass.instance.roomViewStore.isInitialEventHighlighted(),
replyToEvent: SdkContextClass.instance.roomViewStore.getQuotingEvent(),
};
this.setState(newState);

View file

@ -36,7 +36,6 @@ import { _t } from '../../../languageHandler';
import DMRoomMap from '../../../utils/DMRoomMap';
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
import SdkConfig from '../../../SdkConfig';
import { RoomViewStore } from "../../../stores/RoomViewStore";
import MultiInviter from "../../../utils/MultiInviter";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import E2EIcon from "../rooms/E2EIcon";
@ -77,6 +76,7 @@ import UserIdentifierCustomisations from '../../../customisations/UserIdentifier
import PosthogTrackers from "../../../PosthogTrackers";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { DirectoryMember, startDmOnFirstMessage } from '../../../utils/direct-messages';
import { SdkContextClass } from '../../../contexts/SDKContext';
export interface IDevice {
deviceId: string;
@ -412,7 +412,7 @@ const UserOptionsSection: React.FC<{
}
if (canInvite && (member?.membership ?? 'leave') === 'leave' && shouldShowComponent(UIComponent.InviteUsers)) {
const roomId = member && member.roomId ? member.roomId : RoomViewStore.instance.getRoomId();
const roomId = member && member.roomId ? member.roomId : SdkContextClass.instance.roomViewStore.getRoomId();
const onInviteUserButton = async (ev: ButtonEvent) => {
try {
// We use a MultiInviter to re-use the invite logic, even though we're only inviting one user.

View file

@ -932,6 +932,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
rightClick={true}
reactions={this.state.reactions}
link={this.state.contextMenu.link}
getRelationsForEvent={this.props.getRelationsForEvent}
/>
);
}

View file

@ -86,7 +86,6 @@ interface IProps extends MatrixClientProps {
relation?: IEventRelation;
e2eStatus?: E2EStatus;
compact?: boolean;
showVoiceBroadcastButton?: boolean;
}
interface IState {
@ -395,10 +394,6 @@ class MessageComposer extends React.Component<IProps, IState> {
return this.state.showStickersButton && !isLocalRoom(this.props.room);
}
private get showVoiceBroadcastButton(): boolean {
return this.props.showVoiceBroadcastButton && this.state.showVoiceBroadcastButton;
}
public render() {
const isWysiwygComposerEnabled = SettingsStore.getValue("feature_wysiwyg_composer");
const controls = [
@ -420,6 +415,7 @@ class MessageComposer extends React.Component<IProps, IState> {
<SendWysiwygComposer key="controls_input"
disabled={this.state.haveRecording}
onChange={this.onWysiwygChange}
onSend={() => this.sendMessage}
/>,
);
} else {
@ -537,10 +533,10 @@ class MessageComposer extends React.Component<IProps, IState> {
showPollsButton={this.state.showPollsButton}
showStickersButton={this.showStickersButton}
toggleButtonMenu={this.toggleButtonMenu}
showVoiceBroadcastButton={this.showVoiceBroadcastButton}
showVoiceBroadcastButton={this.state.showVoiceBroadcastButton}
onStartVoiceBroadcastClick={() => {
startNewVoiceBroadcastRecording(
this.props.room.roomId,
this.props.room,
MatrixClientPeg.get(),
VoiceBroadcastRecordingsStore.instance(),
);

View file

@ -179,6 +179,7 @@ const EmojiButton: React.FC<IEmojiButtonProps> = ({ addEmoji, menuPosition }) =>
iconClassName="mx_MessageComposer_emoji"
onClick={openMenu}
title={_t("Emoji")}
inputRef={button}
/>
{ contextMenu }

View file

@ -38,7 +38,6 @@ import { ITagMap } from "../../../stores/room-list/algorithms/models";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import { RoomViewStore } from "../../../stores/RoomViewStore";
import {
isMetaSpace,
ISuggestedRoom,
@ -62,6 +61,7 @@ import IconizedContextMenu, {
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import ExtraTile from "./ExtraTile";
import RoomSublist, { IAuxButtonProps } from "./RoomSublist";
import { SdkContextClass } from "../../../contexts/SDKContext";
interface IProps {
onKeyDown: (ev: React.KeyboardEvent, state: IRovingTabIndexState) => void;
@ -421,7 +421,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
public componentDidMount(): void {
this.dispatcherRef = defaultDispatcher.register(this.onAction);
RoomViewStore.instance.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
SdkContextClass.instance.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
SpaceStore.instance.on(UPDATE_SUGGESTED_ROOMS, this.updateSuggestedRooms);
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
this.favouriteMessageWatcher =
@ -436,19 +436,19 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists);
SettingsStore.unwatchSetting(this.favouriteMessageWatcher);
defaultDispatcher.unregister(this.dispatcherRef);
RoomViewStore.instance.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
SdkContextClass.instance.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
}
private onRoomViewStoreUpdate = () => {
this.setState({
currentRoomId: RoomViewStore.instance.getRoomId(),
currentRoomId: SdkContextClass.instance.roomViewStore.getRoomId(),
});
};
private onAction = (payload: ActionPayload) => {
if (payload.action === Action.ViewRoomDelta) {
const viewRoomDeltaPayload = payload as ViewRoomDeltaPayload;
const currentRoomId = RoomViewStore.instance.getRoomId();
const currentRoomId = SdkContextClass.instance.roomViewStore.getRoomId();
const room = this.getRoomDelta(currentRoomId, viewRoomDeltaPayload.delta, viewRoomDeltaPayload.unread);
if (room) {
defaultDispatcher.dispatch<ViewRoomPayload>({

View file

@ -44,10 +44,10 @@ import PosthogTrackers from "../../../PosthogTrackers";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { RoomViewStore } from "../../../stores/RoomViewStore";
import { RoomTileCallSummary } from "./RoomTileCallSummary";
import { RoomGeneralContextMenu } from "../context_menus/RoomGeneralContextMenu";
import { CallStore, CallStoreEvent } from "../../../stores/CallStore";
import { SdkContextClass } from "../../../contexts/SDKContext";
interface IProps {
room: Room;
@ -86,7 +86,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
super(props);
this.state = {
selected: RoomViewStore.instance.getRoomId() === this.props.room.roomId,
selected: SdkContextClass.instance.roomViewStore.getRoomId() === this.props.room.roomId,
notificationsMenuPosition: null,
generalMenuPosition: null,
call: CallStore.instance.getCall(this.props.room.roomId),
@ -146,7 +146,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
this.scrollIntoView();
}
RoomViewStore.instance.addRoomListener(this.props.room.roomId, this.onActiveRoomUpdate);
SdkContextClass.instance.roomViewStore.addRoomListener(this.props.room.roomId, this.onActiveRoomUpdate);
this.dispatcherRef = defaultDispatcher.register(this.onAction);
MessagePreviewStore.instance.on(
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
@ -163,7 +163,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
}
public componentWillUnmount() {
RoomViewStore.instance.removeRoomListener(this.props.room.roomId, this.onActiveRoomUpdate);
SdkContextClass.instance.roomViewStore.removeRoomListener(this.props.room.roomId, this.onActiveRoomUpdate);
MessagePreviewStore.instance.off(
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
this.onRoomPreviewChanged,

View file

@ -14,58 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { forwardRef, RefObject, useMemo } from 'react';
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import React, { forwardRef, RefObject } from 'react';
import { useRoomContext } from '../../../../contexts/RoomContext';
import { useMatrixClientContext } from '../../../../contexts/MatrixClientContext';
import EditorStateTransfer from '../../../../utils/EditorStateTransfer';
import { CommandPartCreator, Part } from '../../../../editor/parts';
import { IRoomState } from '../../../structures/RoomView';
import SettingsStore from '../../../../settings/SettingsStore';
import { parseEvent } from '../../../../editor/deserialize';
import { WysiwygComposer } from './components/WysiwygComposer';
import { EditionButtons } from './components/EditionButtons';
import { useWysiwygEditActionHandler } from './hooks/useWysiwygEditActionHandler';
import { endEditing } from './utils/editing';
import { editMessage } from './utils/message';
function parseEditorStateTransfer(
editorStateTransfer: EditorStateTransfer,
roomContext: IRoomState,
mxClient: MatrixClient,
) {
if (!roomContext.room) {
return;
}
const { room } = roomContext;
const partCreator = new CommandPartCreator(room, mxClient);
let parts: Part[];
if (editorStateTransfer.hasEditorState()) {
// if restoring state from a previous editor,
// restore serialized parts from the state
parts = editorStateTransfer.getSerializedParts().map(p => partCreator.deserializePart(p));
} else {
// otherwise, either restore serialized parts from localStorage or parse the body of the event
// TODO local storage
// const restoredParts = this.restoreStoredEditorState(partCreator);
if (editorStateTransfer.getEvent().getContent().format === 'org.matrix.custom.html') {
return editorStateTransfer.getEvent().getContent().formatted_body || "";
}
parts = parseEvent(editorStateTransfer.getEvent(), partCreator, {
shouldEscape: SettingsStore.getValue("MessageComposerInput.useMarkdown"),
});
}
return parts.reduce((content, part) => content + part.text, '');
// Todo local storage
// this.saveStoredEditorState();
}
import { useEditing } from './hooks/useEditing';
import { useInitialContent } from './hooks/useInitialContent';
interface ContentProps {
disabled: boolean;
@ -81,25 +37,26 @@ const Content = forwardRef<HTMLElement, ContentProps>(
interface EditWysiwygComposerProps {
disabled?: boolean;
onChange?: (content: string) => void;
editorStateTransfer?: EditorStateTransfer;
editorStateTransfer: EditorStateTransfer;
}
export function EditWysiwygComposer({ editorStateTransfer, ...props }: EditWysiwygComposerProps) {
const roomContext = useRoomContext();
const mxClient = useMatrixClientContext();
const initialContent = useMemo(() => {
if (editorStateTransfer) {
return parseEditorStateTransfer(editorStateTransfer, roomContext, mxClient);
}
}, [editorStateTransfer, roomContext, mxClient]);
const initialContent = useInitialContent(editorStateTransfer);
const isReady = !editorStateTransfer || Boolean(initialContent);
return isReady && <WysiwygComposer initialContent={initialContent} {...props}>{ (ref, wysiwyg, content) => (
<>
<Content disabled={props.disabled} ref={ref} />
<EditionButtons onCancelClick={() => endEditing(roomContext)} onSaveClick={() => editMessage(content, { roomContext, mxClient, editorStateTransfer })} />
</>)
}
const { editMessage, endEditing, setContent } = useEditing(initialContent, editorStateTransfer);
return isReady && <WysiwygComposer
initialContent={initialContent}
onChange={setContent}
onSend={editMessage}
{...props}>
{ (ref, wysiwyg,
content) => (
<>
<Content disabled={props.disabled} ref={ref} />
<EditionButtons onCancelClick={endEditing} onSaveClick={editMessage} />
</>)
}
</WysiwygComposer>;
}

View file

@ -15,32 +15,33 @@ limitations under the License.
*/
import React, { forwardRef, RefObject } from 'react';
import { FormattingFunctions } from '@matrix-org/matrix-wysiwyg';
import { useWysiwygSendActionHandler } from './hooks/useWysiwygSendActionHandler';
import { WysiwygComposer } from './components/WysiwygComposer';
import { Wysiwyg } from './types';
interface SendWysiwygComposerProps {
disabled?: boolean;
onChange?: (content: string) => void;
}
export function SendWysiwygComposer(props: SendWysiwygComposerProps) {
return (
<WysiwygComposer {...props}>{ (ref, wysiwyg) => (
<Content disabled={props.disabled} ref={ref} wysiwyg={wysiwyg} />
) }
</WysiwygComposer>);
onSend(): () => void;
}
interface ContentProps {
disabled: boolean;
wysiwyg: Wysiwyg;
formattingFunctions: FormattingFunctions;
}
const Content = forwardRef<HTMLElement, ContentProps>(
function Content({ disabled, wysiwyg }: ContentProps, forwardRef: RefObject<HTMLElement>) {
function Content({ disabled, formattingFunctions: wysiwyg }: ContentProps, forwardRef: RefObject<HTMLElement>) {
useWysiwygSendActionHandler(disabled, forwardRef, wysiwyg);
return null;
},
);
export function SendWysiwygComposer(props: SendWysiwygComposerProps) {
return (
<WysiwygComposer {...props}>{ (ref, wysiwyg) => (
<Content disabled={props.disabled} ref={ref} formattingFunctions={wysiwyg} />
) }
</WysiwygComposer>);
}

View file

@ -14,24 +14,31 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { MutableRefObject, ReactNode, useEffect } from 'react';
import { useWysiwyg } from "@matrix-org/matrix-wysiwyg";
import React, { memo, MutableRefObject, ReactNode, useEffect } from 'react';
import { useWysiwyg, FormattingFunctions } from "@matrix-org/matrix-wysiwyg";
import { FormattingButtons } from './FormattingButtons';
import { Editor } from './Editor';
import { Wysiwyg } from '../types';
import { useInputEventProcessor } from '../hooks/useInputEventProcessor';
interface WysiwygComposerProps {
disabled?: boolean;
onChange?: (content: string) => void;
onSend: () => void;
initialContent?: string;
children?: (ref: MutableRefObject<HTMLDivElement | null>, wysiwyg: Wysiwyg, content: string) => ReactNode;
children?: (
ref: MutableRefObject<HTMLDivElement | null>,
wysiwyg: FormattingFunctions,
content: string) => ReactNode;
}
export function WysiwygComposer(
{ disabled = false, onChange, initialContent, children }: WysiwygComposerProps,
export const WysiwygComposer = memo(function WysiwygComposer(
{ disabled = false, onChange, onSend, initialContent, children }: WysiwygComposerProps,
) {
const { ref, isWysiwygReady, content, formattingStates, wysiwyg } = useWysiwyg({ initialContent });
const inputEventProcessor = useInputEventProcessor(onSend);
const { ref, isWysiwygReady, content, formattingStates, wysiwyg } =
useWysiwyg({ initialContent, inputEventProcessor });
useEffect(() => {
if (!disabled && content !== null) {
@ -46,4 +53,4 @@ export function WysiwygComposer(
{ children?.(ref, wysiwyg, content) }
</div>
);
}
});

View file

@ -0,0 +1,38 @@
/*
Copyright 2022 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 { useCallback, useState } from "react";
import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext";
import { useRoomContext } from "../../../../../contexts/RoomContext";
import EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
import { endEditing } from "../utils/editing";
import { editMessage } from "../utils/message";
export function useEditing(initialContent: string, editorStateTransfer: EditorStateTransfer) {
const roomContext = useRoomContext();
const mxClient = useMatrixClientContext();
const [content, setContent] = useState(initialContent);
const editMessageMemoized = useCallback(() =>
editMessage(content, { roomContext, mxClient, editorStateTransfer }),
[content, roomContext, mxClient, editorStateTransfer],
);
const endEditingMemoized = useCallback(() => endEditing(roomContext), [roomContext]);
return { setContent, editMessage: editMessageMemoized, endEditing: endEditingMemoized };
}

View file

@ -0,0 +1,67 @@
/*
Copyright 2022 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 { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import { useMemo } from "react";
import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext";
import { useRoomContext } from "../../../../../contexts/RoomContext";
import { parseEvent } from "../../../../../editor/deserialize";
import { CommandPartCreator, Part } from "../../../../../editor/parts";
import SettingsStore from "../../../../../settings/SettingsStore";
import EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
function parseEditorStateTransfer(
editorStateTransfer: EditorStateTransfer,
room: Room,
mxClient: MatrixClient,
): string {
const partCreator = new CommandPartCreator(room, mxClient);
let parts: Part[];
if (editorStateTransfer.hasEditorState()) {
// if restoring state from a previous editor,
// restore serialized parts from the state
parts = editorStateTransfer.getSerializedParts().map(p => partCreator.deserializePart(p));
} else {
// otherwise, either restore serialized parts from localStorage or parse the body of the event
// TODO local storage
// const restoredParts = this.restoreStoredEditorState(partCreator);
if (editorStateTransfer.getEvent().getContent().format === 'org.matrix.custom.html') {
return editorStateTransfer.getEvent().getContent().formatted_body || "";
}
parts = parseEvent(editorStateTransfer.getEvent(), partCreator, {
shouldEscape: SettingsStore.getValue("MessageComposerInput.useMarkdown"),
});
}
return parts.reduce((content, part) => content + part.text, '');
// Todo local storage
// this.saveStoredEditorState();
}
export function useInitialContent(editorStateTransfer: EditorStateTransfer) {
const roomContext = useRoomContext();
const mxClient = useMatrixClientContext();
return useMemo<string>(() => {
if (editorStateTransfer && roomContext.room) {
return parseEditorStateTransfer(editorStateTransfer, roomContext.room, mxClient);
}
}, [editorStateTransfer, roomContext, mxClient]);
}

View file

@ -0,0 +1,40 @@
/*
Copyright 2022 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 { WysiwygInputEvent } from "@matrix-org/matrix-wysiwyg";
import { useCallback } from "react";
import { useSettingValue } from "../../../../../hooks/useSettings";
export function useInputEventProcessor(onSend: () => void) {
const isCtrlEnter = useSettingValue("MessageComposerInput.ctrlEnterToSend") as boolean;
return useCallback((event: WysiwygInputEvent) => {
if (event instanceof ClipboardEvent) {
return event;
}
if (
(event.inputType === 'insertParagraph' && !isCtrlEnter) ||
event.inputType === 'sendMessage'
) {
onSend();
return null;
}
return event;
}
, [isCtrlEnter, onSend]);
}

View file

@ -15,19 +15,19 @@ limitations under the License.
*/
import { RefObject, useCallback, useRef } from "react";
import { FormattingFunctions } from "@matrix-org/matrix-wysiwyg";
import defaultDispatcher from "../../../../../dispatcher/dispatcher";
import { Action } from "../../../../../dispatcher/actions";
import { ActionPayload } from "../../../../../dispatcher/payloads";
import { TimelineRenderingType, useRoomContext } from "../../../../../contexts/RoomContext";
import { useDispatcher } from "../../../../../hooks/useDispatcher";
import { Wysiwyg } from "../types";
import { focusComposer } from "./utils";
export function useWysiwygSendActionHandler(
disabled: boolean,
composerElement: RefObject<HTMLElement>,
wysiwyg: Wysiwyg,
wysiwyg: FormattingFunctions,
) {
const roomContext = useRoomContext();
const timeoutId = useRef<number>();

View file

@ -1,21 +0,0 @@
/*
Copyright 2022 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 { useWysiwyg } from "@matrix-org/matrix-wysiwyg";
// TODO
// Change when the matrix-wysiwyg typescript definition will be refined
export type Wysiwyg = ReturnType<typeof useWysiwyg>['wysiwyg'];

View file

@ -19,13 +19,14 @@ import classNames from 'classnames';
import { IMyDevice } from "matrix-js-sdk/src/client";
import { logger } from "matrix-js-sdk/src/logger";
import { CrossSigningInfo } from "matrix-js-sdk/src/crypto/CrossSigning";
import { CryptoEvent } from 'matrix-js-sdk/src/crypto';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import DevicesPanelEntry from "./DevicesPanelEntry";
import Spinner from "../elements/Spinner";
import AccessibleButton from "../elements/AccessibleButton";
import { deleteDevicesWithInteractiveAuth } from './devices/deleteDevices';
import MatrixClientContext from '../../../contexts/MatrixClientContext';
interface IProps {
className?: string;
@ -40,6 +41,8 @@ interface IState {
}
export default class DevicesPanel extends React.Component<IProps, IState> {
public static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>;
private unmounted = false;
constructor(props: IProps) {
@ -52,15 +55,22 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
}
public componentDidMount(): void {
this.context.on(CryptoEvent.DevicesUpdated, this.onDevicesUpdated);
this.loadDevices();
}
public componentWillUnmount(): void {
this.context.off(CryptoEvent.DevicesUpdated, this.onDevicesUpdated);
this.unmounted = true;
}
private onDevicesUpdated = (users: string[]) => {
if (!users.includes(this.context.getUserId())) return;
this.loadDevices();
};
private loadDevices(): void {
const cli = MatrixClientPeg.get();
const cli = this.context;
cli.getDevices().then(
(resp) => {
if (this.unmounted) { return; }
@ -111,7 +121,7 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
private isDeviceVerified(device: IMyDevice): boolean | null {
try {
const cli = MatrixClientPeg.get();
const cli = this.context;
const deviceInfo = cli.getStoredDevice(cli.getUserId(), device.device_id);
return this.state.crossSigningInfo.checkDeviceTrust(
this.state.crossSigningInfo,
@ -184,7 +194,7 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
try {
await deleteDevicesWithInteractiveAuth(
MatrixClientPeg.get(),
this.context,
this.state.selectedDevices,
(success) => {
if (success) {
@ -208,7 +218,7 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
};
private renderDevice = (device: IMyDevice): JSX.Element => {
const myDeviceId = MatrixClientPeg.get().getDeviceId();
const myDeviceId = this.context.getDeviceId();
const myDevice = this.state.devices.find((device) => (device.device_id === myDeviceId));
const isOwnDevice = device.device_id === myDeviceId;
@ -246,7 +256,7 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
return <Spinner />;
}
const myDeviceId = MatrixClientPeg.get().getDeviceId();
const myDeviceId = this.context.getDeviceId();
const myDevice = devices.find((device) => (device.device_id === myDeviceId));
if (!myDevice) {

View file

@ -62,7 +62,6 @@ const DeviceDetails: React.FC<Props> = ({
id: 'session',
values: [
{ label: _t('Session ID'), value: device.device_id },
{ label: _t('Client'), value: device.client },
{
label: _t('Last activity'),
value: device.last_seen_ts && formatDate(new Date(device.last_seen_ts)),
@ -84,6 +83,7 @@ const DeviceDetails: React.FC<Props> = ({
values: [
{ label: _t('Model'), value: device.deviceModel },
{ label: _t('Operating system'), value: device.deviceOperatingSystem },
{ label: _t('Browser'), value: device.client },
{ label: _t('IP address'), value: device.last_seen_ip },
],
},

View file

@ -0,0 +1,63 @@
/*
Copyright 2022 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 type { IServerVersions } from 'matrix-js-sdk/src/matrix';
import { _t } from '../../../../languageHandler';
import AccessibleButton from '../../elements/AccessibleButton';
import SettingsSubsection from '../shared/SettingsSubsection';
import SettingsStore from '../../../../settings/SettingsStore';
interface IProps {
onShowQr: () => void;
versions: IServerVersions;
}
export default class LoginWithQRSection extends React.Component<IProps> {
public constructor(props: IProps) {
super(props);
}
public render(): JSX.Element {
const msc3882Supported = !!this.props.versions?.unstable_features?.['org.matrix.msc3882'];
const msc3886Supported = !!this.props.versions?.unstable_features?.['org.matrix.msc3886'];
// Needs to be enabled as a feature + server support MSC3886 or have a default rendezvous server configured:
const offerShowQr = SettingsStore.getValue("feature_qr_signin_reciprocate_show") &&
msc3882Supported && msc3886Supported; // We don't support configuration of a fallback at the moment so we just check the MSCs
// don't show anything if no method is available
if (!offerShowQr) {
return null;
}
return <SettingsSubsection
heading={_t('Sign in with QR code')}
>
<div className="mx_LoginWithQRSection">
<p className="mx_SettingsTab_subsectionText">{
_t("You can use this device to sign in a new device with a QR code. You will need to " +
"scan the QR code shown on this device with your device that's signed out.")
}</p>
<AccessibleButton
onClick={this.props.onShowQr}
kind="primary"
>{ _t("Show QR code") }</AccessibleButton>
</div>
</SettingsSubsection>;
}
}

View file

@ -31,6 +31,7 @@ import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/reque
import { MatrixError } from "matrix-js-sdk/src/http-api";
import { logger } from "matrix-js-sdk/src/logger";
import { LocalNotificationSettings } from "matrix-js-sdk/src/@types/local_notifications";
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
import { _t } from "../../../../languageHandler";
@ -179,6 +180,12 @@ export const useOwnDevices = (): DevicesState => {
refreshDevices();
}, [refreshDevices]);
useEventEmitter(matrixClient, CryptoEvent.DevicesUpdated, (users: string[]): void => {
if (users.includes(userId)) {
refreshDevices();
}
});
useEventEmitter(matrixClient, ClientEvent.AccountData, (event: MatrixEvent): void => {
const type = event.getType();
if (type.startsWith(LOCAL_NOTIFICATION_SETTINGS_PREFIX.name)) {

View file

@ -38,6 +38,9 @@ import InlineSpinner from "../../../elements/InlineSpinner";
import { PosthogAnalytics } from "../../../../../PosthogAnalytics";
import { showDialog as showAnalyticsLearnMoreDialog } from "../../../dialogs/AnalyticsLearnMoreDialog";
import { privateShouldBeEncrypted } from "../../../../../utils/rooms";
import LoginWithQR, { Mode } from '../../../auth/LoginWithQR';
import LoginWithQRSection from '../../devices/LoginWithQRSection';
import type { IServerVersions } from 'matrix-js-sdk/src/matrix';
interface IIgnoredUserProps {
userId: string;
@ -72,6 +75,8 @@ interface IState {
waitingUnignored: string[];
managingInvites: boolean;
invitedRoomIds: Set<string>;
showLoginWithQR: Mode | null;
versions?: IServerVersions;
}
export default class SecurityUserSettingsTab extends React.Component<IProps, IState> {
@ -88,6 +93,7 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
waitingUnignored: [],
managingInvites: false,
invitedRoomIds,
showLoginWithQR: null,
};
}
@ -102,6 +108,7 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
public componentDidMount(): void {
this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on(RoomEvent.MyMembership, this.onMyMembership);
MatrixClientPeg.get().getVersions().then(versions => this.setState({ versions }));
}
public componentWillUnmount(): void {
@ -251,6 +258,14 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
);
}
private onShowQRClicked = (): void => {
this.setState({ showLoginWithQR: Mode.Show });
};
private onLoginWithQRFinished = (): void => {
this.setState({ showLoginWithQR: null });
};
public render(): JSX.Element {
const secureBackup = (
<div className='mx_SettingsTab_section'>
@ -347,6 +362,7 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
}
const useNewSessionManager = SettingsStore.getValue("feature_new_device_manager");
const showQrCodeEnabled = SettingsStore.getValue("feature_qr_signin_reciprocate_show");
const devicesSection = useNewSessionManager
? null
: <>
@ -363,8 +379,20 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
</span>
<DevicesPanel />
</div>
{ showQrCodeEnabled ?
<LoginWithQRSection onShowQr={this.onShowQRClicked} versions={this.state.versions} />
: null
}
</>;
const client = MatrixClientPeg.get();
if (showQrCodeEnabled && this.state.showLoginWithQR) {
return <div className="mx_SettingsTab mx_SecurityUserSettingsTab">
<LoginWithQR onFinished={this.onLoginWithQRFinished} mode={this.state.showLoginWithQR} client={client} />
</div>;
}
return (
<div className="mx_SettingsTab mx_SecurityUserSettingsTab">
{ warning }

View file

@ -32,6 +32,10 @@ import SecurityRecommendations from '../../devices/SecurityRecommendations';
import { DeviceSecurityVariation, ExtendedDevice } from '../../devices/types';
import { deleteDevicesWithInteractiveAuth } from '../../devices/deleteDevices';
import SettingsTab from '../SettingsTab';
import LoginWithQRSection from '../../devices/LoginWithQRSection';
import LoginWithQR, { Mode } from '../../../auth/LoginWithQR';
import SettingsStore from '../../../../../settings/SettingsStore';
import { useAsyncMemo } from '../../../../../hooks/useAsyncMemo';
const useSignOut = (
matrixClient: MatrixClient,
@ -104,6 +108,7 @@ const SessionManagerTab: React.FC = () => {
const matrixClient = useContext(MatrixClientContext);
const userId = matrixClient.getUserId();
const currentUserMember = userId && matrixClient.getUser(userId) || undefined;
const clientVersions = useAsyncMemo(() => matrixClient.getVersions(), [matrixClient]);
const onDeviceExpandToggle = (deviceId: ExtendedDevice['device_id']): void => {
if (expandedDeviceIds.includes(deviceId)) {
@ -175,6 +180,26 @@ const SessionManagerTab: React.FC = () => {
onSignOutOtherDevices(Object.keys(otherDevices));
}: undefined;
const [signInWithQrMode, setSignInWithQrMode] = useState<Mode | null>();
const showQrCodeEnabled = SettingsStore.getValue("feature_qr_signin_reciprocate_show");
const onQrFinish = useCallback(() => {
setSignInWithQrMode(null);
}, [setSignInWithQrMode]);
const onShowQrClicked = useCallback(() => {
setSignInWithQrMode(Mode.Show);
}, [setSignInWithQrMode]);
if (showQrCodeEnabled && signInWithQrMode) {
return <LoginWithQR
mode={signInWithQrMode}
onFinished={onQrFinish}
client={matrixClient}
/>;
}
return <SettingsTab heading={_t('Sessions')}>
<SecurityRecommendations
devices={devices}
@ -222,6 +247,10 @@ const SessionManagerTab: React.FC = () => {
/>
</SettingsSubsection>
}
{ showQrCodeEnabled ?
<LoginWithQRSection onShowQr={onShowQrClicked} versions={clientVersions} />
: null
}
</SettingsTab>;
};

View file

@ -36,7 +36,7 @@ import { Icon as FavoriteIcon } from '../../../../res/img/element-icons/roomlist
import SettingsStore from "../../../settings/SettingsStore";
import Modal from "../../../Modal";
import DevtoolsDialog from "../dialogs/DevtoolsDialog";
import { RoomViewStore } from "../../../stores/RoomViewStore";
import { SdkContextClass } from "../../../contexts/SDKContext";
const QuickSettingsButton = ({ isPanelCollapsed = false }) => {
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
@ -72,7 +72,7 @@ const QuickSettingsButton = ({ isPanelCollapsed = false }) => {
onClick={() => {
closeMenu();
Modal.createDialog(DevtoolsDialog, {
roomId: RoomViewStore.instance.getRoomId(),
roomId: SdkContextClass.instance.roomViewStore.getRoomId(),
}, "mx_DevtoolsDialog_wrapper");
}}
kind="danger_outline"

View file

@ -21,7 +21,6 @@ import classNames from 'classnames';
import { Room } from "matrix-js-sdk/src/models/room";
import LegacyCallView from "./LegacyCallView";
import { RoomViewStore } from '../../../stores/RoomViewStore';
import LegacyCallHandler, { LegacyCallHandlerEvent } from '../../../LegacyCallHandler';
import PersistentApp from "../elements/PersistentApp";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
@ -34,6 +33,7 @@ import ActiveWidgetStore, { ActiveWidgetStoreEvent } from '../../../stores/Activ
import WidgetStore, { IApp } from "../../../stores/WidgetStore";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { UPDATE_EVENT } from '../../../stores/AsyncStore';
import { SdkContextClass } from '../../../contexts/SDKContext';
import { CallStore } from "../../../stores/CallStore";
import {
VoiceBroadcastRecording,
@ -129,7 +129,7 @@ class PipView extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
const roomId = RoomViewStore.instance.getRoomId();
const roomId = SdkContextClass.instance.roomViewStore.getRoomId();
const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(roomId);
@ -147,7 +147,7 @@ class PipView extends React.Component<IProps, IState> {
public componentDidMount() {
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCalls);
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallState, this.updateCalls);
RoomViewStore.instance.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
SdkContextClass.instance.roomViewStore.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
const room = MatrixClientPeg.get()?.getRoom(this.state.viewedRoomId);
if (room) {
@ -164,7 +164,7 @@ class PipView extends React.Component<IProps, IState> {
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallState, this.updateCalls);
const cli = MatrixClientPeg.get();
cli?.removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
RoomViewStore.instance.removeListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
SdkContextClass.instance.roomViewStore.removeListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
const room = cli?.getRoom(this.state.viewedRoomId);
if (room) {
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(room), this.updateCalls);
@ -186,7 +186,7 @@ class PipView extends React.Component<IProps, IState> {
private onMove = () => this.movePersistedElement.current?.();
private onRoomViewStoreUpdate = () => {
const newRoomId = RoomViewStore.instance.getRoomId();
const newRoomId = SdkContextClass.instance.roomViewStore.getRoomId();
const oldRoomId = this.state.viewedRoomId;
if (newRoomId === oldRoomId) return;
// The WidgetLayoutStore observer always tracks the currently viewed Room,