Merge branch 'develop' of https://github.com/vector-im/element-web into dbkr/stateafter
# Conflicts: # test/unit-tests/components/structures/RoomView-test.tsx # test/unit-tests/components/structures/TimelinePanel-test.tsx
This commit is contained in:
commit
3dbcb5efa3
438 changed files with 7829 additions and 4692 deletions
|
@ -31,4 +31,3 @@ export const BackdropPanel: React.FC<IProps> = ({ backgroundImage, blurMultiplie
|
|||
</div>
|
||||
);
|
||||
};
|
||||
export default BackdropPanel;
|
||||
|
|
|
@ -23,7 +23,6 @@ import classNames from "classnames";
|
|||
import { isOnlyCtrlOrCmdKeyEvent, Key } from "../../Keyboard";
|
||||
import PageTypes from "../../PageTypes";
|
||||
import MediaDeviceHandler from "../../MediaDeviceHandler";
|
||||
import { fixupColorFonts } from "../../utils/FontManager";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import { IMatrixClientCreds } from "../../MatrixClientPeg";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
@ -49,11 +48,10 @@ import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../LegacyCallHandl
|
|||
import AudioFeedArrayForLegacyCall from "../views/voip/AudioFeedArrayForLegacyCall";
|
||||
import { OwnProfileStore } from "../../stores/OwnProfileStore";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import RoomView from "./RoomView";
|
||||
import type { RoomView as RoomViewType } from "./RoomView";
|
||||
import { RoomView } from "./RoomView";
|
||||
import ToastContainer from "./ToastContainer";
|
||||
import UserView from "./UserView";
|
||||
import BackdropPanel from "./BackdropPanel";
|
||||
import { BackdropPanel } from "./BackdropPanel";
|
||||
import { mediaFromMxc } from "../../customisations/Media";
|
||||
import { UserTab } from "../views/dialogs/UserTab";
|
||||
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
|
||||
|
@ -125,7 +123,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
public static displayName = "LoggedInView";
|
||||
|
||||
protected readonly _matrixClient: MatrixClient;
|
||||
protected readonly _roomView: React.RefObject<RoomViewType>;
|
||||
protected readonly _roomView: React.RefObject<RoomView>;
|
||||
protected readonly _resizeContainer: React.RefObject<HTMLDivElement>;
|
||||
protected readonly resizeHandler: React.RefObject<HTMLDivElement>;
|
||||
protected layoutWatcherRef?: string;
|
||||
|
@ -150,8 +148,6 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
|
||||
MediaDeviceHandler.loadDevices();
|
||||
|
||||
fixupColorFonts();
|
||||
|
||||
this._roomView = React.createRef();
|
||||
this._resizeContainer = React.createRef();
|
||||
this.resizeHandler = React.createRef();
|
||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { createRef } from "react";
|
||||
import React, { createRef, lazy } from "react";
|
||||
import {
|
||||
ClientEvent,
|
||||
createClient,
|
||||
|
@ -28,8 +28,6 @@ import { TooltipProvider } from "@vector-im/compound-web";
|
|||
// what-input helps improve keyboard accessibility
|
||||
import "what-input";
|
||||
|
||||
import type NewRecoveryMethodDialog from "../../async-components/views/dialogs/security/NewRecoveryMethodDialog";
|
||||
import type RecoveryMethodRemovedDialog from "../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog";
|
||||
import PosthogTrackers from "../../PosthogTrackers";
|
||||
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
|
||||
import { IMatrixClientCreds, MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
|
@ -429,7 +427,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
} else if (
|
||||
(await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) &&
|
||||
!shouldSkipSetupEncryption(cli)
|
||||
!(await shouldSkipSetupEncryption(cli))
|
||||
) {
|
||||
// if cross-signing is not yet set up, do so now if possible.
|
||||
this.setStateForNewView({ view: Views.E2E_SETUP });
|
||||
|
@ -1640,7 +1638,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
} else {
|
||||
// otherwise check the server to see if there's a new one
|
||||
try {
|
||||
newVersionInfo = await cli.getKeyBackupVersion();
|
||||
newVersionInfo = (await cli.getCrypto()?.getKeyBackupInfo()) ?? null;
|
||||
if (newVersionInfo !== null) haveNewVersion = true;
|
||||
} catch (e) {
|
||||
logger.error("Saw key backup error but failed to check backup version!", e);
|
||||
|
@ -1649,16 +1647,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
if (haveNewVersion) {
|
||||
Modal.createDialogAsync(
|
||||
import(
|
||||
"../../async-components/views/dialogs/security/NewRecoveryMethodDialog"
|
||||
) as unknown as Promise<typeof NewRecoveryMethodDialog>,
|
||||
Modal.createDialog(
|
||||
lazy(() => import("../../async-components/views/dialogs/security/NewRecoveryMethodDialog")),
|
||||
);
|
||||
} else {
|
||||
Modal.createDialogAsync(
|
||||
import(
|
||||
"../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog"
|
||||
) as unknown as Promise<typeof RecoveryMethodRemovedDialog>,
|
||||
Modal.createDialog(
|
||||
lazy(() => import("../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog")),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -9,7 +9,16 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { ChangeEvent, ComponentProps, createRef, ReactElement, ReactNode, RefObject, useContext } from "react";
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
ComponentProps,
|
||||
createRef,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
RefObject,
|
||||
useContext,
|
||||
JSX,
|
||||
} from "react";
|
||||
import classNames from "classnames";
|
||||
import {
|
||||
IRecommendedVersion,
|
||||
|
@ -29,6 +38,7 @@ import {
|
|||
MatrixError,
|
||||
ISearchResults,
|
||||
THREAD_RELATION_TYPE,
|
||||
MatrixClient,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
@ -45,7 +55,7 @@ import ResizeNotifier from "../../utils/ResizeNotifier";
|
|||
import ContentMessages from "../../ContentMessages";
|
||||
import Modal from "../../Modal";
|
||||
import { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
|
||||
import dis, { defaultDispatcher } from "../../dispatcher/dispatcher";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import * as Rooms from "../../Rooms";
|
||||
import MainSplit from "./MainSplit";
|
||||
import RightPanel from "./RightPanel";
|
||||
|
@ -233,6 +243,11 @@ export interface IRoomState {
|
|||
liveTimeline?: EventTimeline;
|
||||
narrow: boolean;
|
||||
msc3946ProcessDynamicPredecessor: boolean;
|
||||
/**
|
||||
* Whether the room is encrypted or not.
|
||||
* If null, we are still determining the encryption status.
|
||||
*/
|
||||
isRoomEncrypted: boolean | null;
|
||||
|
||||
canAskToJoin: boolean;
|
||||
promptAskToJoin: boolean;
|
||||
|
@ -417,6 +432,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
canAskToJoin: this.askToJoinEnabled,
|
||||
promptAskToJoin: false,
|
||||
viewRoomOpts: { buttons: [] },
|
||||
isRoomEncrypted: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -437,7 +453,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
|
||||
private onWidgetLayoutChange = (): void => {
|
||||
if (!this.state.room) return;
|
||||
dis.dispatch({
|
||||
defaultDispatcher.dispatch({
|
||||
action: "appsDrawer",
|
||||
show: true,
|
||||
});
|
||||
|
@ -598,7 +614,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
// Handle the use case of a link to a thread message
|
||||
// ie: #/room/roomId/eventId (eventId of a thread message)
|
||||
if (thread?.rootEvent && !initialEvent?.isThreadRoot) {
|
||||
dis.dispatch<ShowThreadPayload>({
|
||||
defaultDispatcher.dispatch<ShowThreadPayload>({
|
||||
action: Action.ShowThread,
|
||||
rootEvent: thread.rootEvent,
|
||||
initialEvent,
|
||||
|
@ -704,7 +720,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
const activeCall = CallStore.instance.getActiveCall(this.state.roomId);
|
||||
if (activeCall === null) {
|
||||
// We disconnected from the call, so stop viewing it
|
||||
dis.dispatch<ViewRoomPayload>(
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>(
|
||||
{
|
||||
action: Action.ViewRoom,
|
||||
room_id: this.state.roomId,
|
||||
|
@ -847,10 +863,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
return isManuallyShown && widgets.length > 0;
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
public async componentDidMount(): Promise<void> {
|
||||
this.unmounted = false;
|
||||
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
if (this.context.client) {
|
||||
this.context.client.on(ClientEvent.Room, this.onRoom);
|
||||
this.context.client.on(RoomEvent.Timeline, this.onRoomTimeline);
|
||||
|
@ -967,7 +983,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
// stop tracking room changes to format permalinks
|
||||
this.stopAllPermalinkCreators();
|
||||
|
||||
dis.unregister(this.dispatcherRef);
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
if (this.context.client) {
|
||||
this.context.client.removeListener(ClientEvent.Room, this.onRoom);
|
||||
this.context.client.removeListener(RoomEvent.Timeline, this.onRoomTimeline);
|
||||
|
@ -1045,7 +1061,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
handled = true;
|
||||
break;
|
||||
case KeyBindingAction.UploadFile: {
|
||||
dis.dispatch(
|
||||
defaultDispatcher.dispatch(
|
||||
{
|
||||
action: "upload_file",
|
||||
context: TimelineRenderingType.Room,
|
||||
|
@ -1145,7 +1161,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
if (payload.event && payload.event.getRoomId() !== this.state.roomId) {
|
||||
// If the event is in a different room (e.g. because the event to be edited is being displayed
|
||||
// in the results of an all-rooms search), we need to view that room first.
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: payload.event.getRoomId(),
|
||||
metricsTrigger: undefined,
|
||||
|
@ -1188,7 +1204,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
}
|
||||
|
||||
// re-dispatch to the correct composer
|
||||
dis.dispatch<ComposerInsertPayload>({
|
||||
defaultDispatcher.dispatch<ComposerInsertPayload>({
|
||||
...(payload as ComposerInsertPayload),
|
||||
timelineRenderingType,
|
||||
composerType: this.state.editState ? ComposerType.Edit : ComposerType.Send,
|
||||
|
@ -1197,7 +1213,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
}
|
||||
|
||||
case Action.FocusAComposer: {
|
||||
dis.dispatch<FocusComposerPayload>({
|
||||
defaultDispatcher.dispatch<FocusComposerPayload>({
|
||||
...(payload as FocusComposerPayload),
|
||||
// re-dispatch to the correct composer (the send message will still be on screen even when editing a message)
|
||||
action: this.state.editState ? Action.FocusEditMessageComposer : Action.FocusSendMessageComposer,
|
||||
|
@ -1303,7 +1319,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
if (containsEmoji(ev.getContent(), effect.emojis) || ev.getContent().msgtype === effect.msgType) {
|
||||
// For initial threads launch, chat effects are disabled see #19731
|
||||
if (!ev.isRelation(THREAD_RELATION_TYPE.name)) {
|
||||
dis.dispatch({ action: `effects.${effect.command}`, event: ev });
|
||||
defaultDispatcher.dispatch({ action: `effects.${effect.command}`, event: ev });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -1342,13 +1358,12 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
this.context.widgetLayoutStore.on(WidgetLayoutStore.emissionForRoom(room), this.onWidgetLayoutChange);
|
||||
|
||||
this.calculatePeekRules(room);
|
||||
this.updatePreviewUrlVisibility(room);
|
||||
this.loadMembersIfJoined(room);
|
||||
this.calculateRecommendedVersion(room);
|
||||
this.updateE2EStatus(room);
|
||||
this.updatePermissions(room);
|
||||
this.checkWidgets(room);
|
||||
this.loadVirtualRoom(room);
|
||||
this.updateRoomEncrypted(room);
|
||||
|
||||
if (
|
||||
this.getMainSplitContentType(room) !== MainSplitContentType.Timeline &&
|
||||
|
@ -1363,7 +1378,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
liveTimeline: room.getLiveTimeline(),
|
||||
});
|
||||
|
||||
dis.dispatch<ActionPayload>({ action: Action.RoomLoaded });
|
||||
defaultDispatcher.dispatch<ActionPayload>({ action: Action.RoomLoaded });
|
||||
};
|
||||
|
||||
private onRoomTimelineReset = (room?: Room): void => {
|
||||
|
@ -1377,6 +1392,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
return room?.currentState.getStateEvents(EventType.RoomTombstone, "") ?? undefined;
|
||||
}
|
||||
|
||||
private async getIsRoomEncrypted(roomId = this.state.roomId): Promise<boolean> {
|
||||
const crypto = this.context.client?.getCrypto();
|
||||
if (!crypto || !roomId) return false;
|
||||
|
||||
return await crypto.isEncryptionEnabledInRoom(roomId);
|
||||
}
|
||||
|
||||
private async calculateRecommendedVersion(room: Room): Promise<void> {
|
||||
const upgradeRecommendation = await room.getRecommendedVersion();
|
||||
if (this.unmounted) return;
|
||||
|
@ -1409,12 +1431,15 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
});
|
||||
}
|
||||
|
||||
private updatePreviewUrlVisibility({ roomId }: Room): void {
|
||||
// URL Previews in E2EE rooms can be a privacy leak so use a different setting which is per-room explicit
|
||||
const key = this.context.client?.isRoomEncrypted(roomId) ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled";
|
||||
this.setState({
|
||||
showUrlPreview: SettingsStore.getValue(key, roomId),
|
||||
});
|
||||
private updatePreviewUrlVisibility(room: Room): void {
|
||||
this.setState(({ isRoomEncrypted }) => ({
|
||||
showUrlPreview: this.getPreviewUrlVisibility(room, isRoomEncrypted),
|
||||
}));
|
||||
}
|
||||
|
||||
private getPreviewUrlVisibility({ roomId }: Room, isRoomEncrypted: boolean | null): boolean {
|
||||
const key = isRoomEncrypted ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled";
|
||||
return SettingsStore.getValue(key, roomId);
|
||||
}
|
||||
|
||||
private onRoom = (room: Room): void => {
|
||||
|
@ -1456,7 +1481,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
};
|
||||
|
||||
private async updateE2EStatus(room: Room): Promise<void> {
|
||||
if (!this.context.client?.isRoomEncrypted(room.roomId)) return;
|
||||
if (!this.context.client || !this.state.isRoomEncrypted) 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
|
||||
|
@ -1467,33 +1492,54 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
|
||||
if (this.context.client.getCrypto()) {
|
||||
/* At this point, the user has encryption on and cross-signing on */
|
||||
e2eStatus = await shieldStatusForRoom(this.context.client, room);
|
||||
RoomView.e2eStatusCache.set(room.roomId, e2eStatus);
|
||||
e2eStatus = await this.cacheAndGetE2EStatus(room, this.context.client);
|
||||
if (this.unmounted) return;
|
||||
this.setState({ e2eStatus });
|
||||
}
|
||||
}
|
||||
|
||||
private async cacheAndGetE2EStatus(room: Room, client: MatrixClient): Promise<E2EStatus> {
|
||||
const e2eStatus = await shieldStatusForRoom(client, room);
|
||||
RoomView.e2eStatusCache.set(room.roomId, e2eStatus);
|
||||
return e2eStatus;
|
||||
}
|
||||
|
||||
private onUrlPreviewsEnabledChange = (): void => {
|
||||
if (this.state.room) {
|
||||
this.updatePreviewUrlVisibility(this.state.room);
|
||||
}
|
||||
};
|
||||
|
||||
private onRoomStateEvents = (ev: MatrixEvent, state: RoomState): void => {
|
||||
private onRoomStateEvents = async (ev: MatrixEvent, state: RoomState): Promise<void> => {
|
||||
// ignore if we don't have a room yet
|
||||
if (!this.state.room || this.state.room.roomId !== state.roomId) return;
|
||||
if (!this.state.room || this.state.room.roomId !== state.roomId || !this.context.client) return;
|
||||
|
||||
switch (ev.getType()) {
|
||||
case EventType.RoomTombstone:
|
||||
this.setState({ tombstone: this.getRoomTombstone() });
|
||||
break;
|
||||
|
||||
case EventType.RoomEncryption: {
|
||||
await this.updateRoomEncrypted();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
this.updatePermissions(this.state.room);
|
||||
}
|
||||
};
|
||||
|
||||
private async updateRoomEncrypted(room = this.state.room): Promise<void> {
|
||||
if (!room || !this.context.client) return;
|
||||
|
||||
const isRoomEncrypted = await this.getIsRoomEncrypted(room.roomId);
|
||||
const newE2EStatus = isRoomEncrypted ? await this.cacheAndGetE2EStatus(room, this.context.client) : null;
|
||||
|
||||
this.setState({
|
||||
isRoomEncrypted,
|
||||
showUrlPreview: this.getPreviewUrlVisibility(room, isRoomEncrypted),
|
||||
...(newE2EStatus && { e2eStatus: newE2EStatus }),
|
||||
});
|
||||
}
|
||||
|
||||
private onRoomStateUpdate = (state: RoomState): void => {
|
||||
// ignore members in other rooms
|
||||
if (state.roomId !== this.state.room?.roomId) {
|
||||
|
@ -1561,7 +1607,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
|
||||
private onInviteClick = (): void => {
|
||||
// open the room inviter
|
||||
dis.dispatch({
|
||||
defaultDispatcher.dispatch({
|
||||
action: "view_invite",
|
||||
roomId: this.getRoomId(),
|
||||
});
|
||||
|
@ -1572,7 +1618,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
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>>({
|
||||
defaultDispatcher.dispatch<DoAfterSyncPreparedPayload<ViewRoomPayload>>({
|
||||
action: Action.DoAfterSyncPrepared,
|
||||
deferred_action: {
|
||||
action: Action.ViewRoom,
|
||||
|
@ -1580,13 +1626,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
metricsTrigger: undefined,
|
||||
},
|
||||
});
|
||||
dis.dispatch({ action: "require_registration" });
|
||||
defaultDispatcher.dispatch({ action: "require_registration" });
|
||||
} else {
|
||||
Promise.resolve().then(() => {
|
||||
const signUrl = this.props.threepidInvite?.signUrl;
|
||||
const roomId = this.getRoomId();
|
||||
if (isNotUndefined(roomId)) {
|
||||
dis.dispatch<JoinRoomPayload>({
|
||||
defaultDispatcher.dispatch<JoinRoomPayload>({
|
||||
action: Action.JoinRoom,
|
||||
roomId,
|
||||
opts: { inviteSignUrl: signUrl },
|
||||
|
@ -1622,7 +1668,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
this.state.initialEventId === eventId
|
||||
) {
|
||||
debuglog("Removing scroll_into_view flag from initial event");
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: this.getRoomId(),
|
||||
event_id: this.state.initialEventId,
|
||||
|
@ -1638,7 +1684,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
const roomId = this.getRoomId();
|
||||
if (!this.context.client || !roomId) return;
|
||||
if (this.context.client.isGuest()) {
|
||||
dis.dispatch({ action: "require_registration" });
|
||||
defaultDispatcher.dispatch({ action: "require_registration" });
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1688,7 +1734,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
};
|
||||
|
||||
private onForgetClick = (): void => {
|
||||
dis.dispatch({
|
||||
defaultDispatcher.dispatch({
|
||||
action: "forget_room",
|
||||
room_id: this.getRoomId(),
|
||||
});
|
||||
|
@ -1702,7 +1748,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
});
|
||||
this.context.client?.leave(roomId).then(
|
||||
() => {
|
||||
dis.dispatch({ action: Action.ViewHomePage });
|
||||
defaultDispatcher.dispatch({ action: Action.ViewHomePage });
|
||||
this.setState({
|
||||
rejecting: false,
|
||||
});
|
||||
|
@ -1736,7 +1782,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
await this.context.client!.setIgnoredUsers(ignoredUsers);
|
||||
|
||||
await this.context.client!.leave(this.state.roomId!);
|
||||
dis.dispatch({ action: Action.ViewHomePage });
|
||||
defaultDispatcher.dispatch({ action: Action.ViewHomePage });
|
||||
this.setState({
|
||||
rejecting: false,
|
||||
});
|
||||
|
@ -1760,7 +1806,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
// using /leave rather than /join. In the short term though, we
|
||||
// just ignore them.
|
||||
// https://github.com/vector-im/vector-web/issues/1134
|
||||
dis.fire(Action.ViewRoomDirectory);
|
||||
defaultDispatcher.fire(Action.ViewRoomDirectory);
|
||||
};
|
||||
|
||||
private onSearchChange = debounce((e: ChangeEvent): void => {
|
||||
|
@ -1786,7 +1832,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
// If we were viewing a highlighted event, firing view_room without
|
||||
// an event will take care of both clearing the URL fragment and
|
||||
// jumping to the bottom
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: this.getRoomId(),
|
||||
metricsTrigger: undefined, // room doesn't change
|
||||
|
@ -1794,7 +1840,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
} else {
|
||||
// Otherwise we have to jump manually
|
||||
this.messagePanel?.jumpToLiveTimeline();
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
defaultDispatcher.fire(Action.FocusSendMessageComposer);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1918,7 +1964,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
public onHiddenHighlightsClick = (): void => {
|
||||
const oldRoom = this.getOldRoom();
|
||||
if (!oldRoom) return;
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: oldRoom.roomId,
|
||||
metricsTrigger: "Predecessor",
|
||||
|
@ -2001,7 +2047,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
const roomId = this.getRoomId();
|
||||
|
||||
if (isNotUndefined(roomId)) {
|
||||
dis.dispatch<SubmitAskToJoinPayload>({
|
||||
defaultDispatcher.dispatch<SubmitAskToJoinPayload>({
|
||||
action: Action.SubmitAskToJoin,
|
||||
roomId,
|
||||
opts: { reason },
|
||||
|
@ -2018,7 +2064,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
const roomId = this.getRoomId();
|
||||
|
||||
if (isNotUndefined(roomId)) {
|
||||
dis.dispatch<CancelAskToJoinPayload>({
|
||||
defaultDispatcher.dispatch<CancelAskToJoinPayload>({
|
||||
action: Action.CancelAskToJoin,
|
||||
roomId,
|
||||
});
|
||||
|
@ -2027,6 +2073,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
|
||||
public render(): ReactNode {
|
||||
if (!this.context.client) return null;
|
||||
const { isRoomEncrypted } = this.state;
|
||||
const isRoomEncryptionLoading = isRoomEncrypted === null;
|
||||
|
||||
if (this.state.room instanceof LocalRoom) {
|
||||
if (this.state.room.state === LocalRoomState.CREATING) {
|
||||
|
@ -2242,14 +2290,16 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
let aux: JSX.Element | undefined;
|
||||
let previewBar;
|
||||
if (this.state.timelineRenderingType === TimelineRenderingType.Search) {
|
||||
aux = (
|
||||
<RoomSearchAuxPanel
|
||||
searchInfo={this.state.search}
|
||||
onCancelClick={this.onCancelSearchClick}
|
||||
onSearchScopeChange={this.onSearchScopeChange}
|
||||
isRoomEncrypted={this.context.client.isRoomEncrypted(this.state.room.roomId)}
|
||||
/>
|
||||
);
|
||||
if (!isRoomEncryptionLoading) {
|
||||
aux = (
|
||||
<RoomSearchAuxPanel
|
||||
searchInfo={this.state.search}
|
||||
onCancelClick={this.onCancelSearchClick}
|
||||
onSearchScopeChange={this.onSearchScopeChange}
|
||||
isRoomEncrypted={isRoomEncrypted}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else if (showRoomUpgradeBar) {
|
||||
aux = <RoomUpgradeWarningBar room={this.state.room} />;
|
||||
} else if (myMembership !== KnownMembership.Join) {
|
||||
|
@ -2325,8 +2375,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
|
||||
let messageComposer;
|
||||
const showComposer =
|
||||
!isRoomEncryptionLoading &&
|
||||
// joined and not showing search results
|
||||
myMembership === KnownMembership.Join && !this.state.search;
|
||||
myMembership === KnownMembership.Join &&
|
||||
!this.state.search;
|
||||
if (showComposer) {
|
||||
messageComposer = (
|
||||
<MessageComposer
|
||||
|
@ -2367,34 +2419,37 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
highlightedEventId = this.state.initialEventId;
|
||||
}
|
||||
|
||||
const messagePanel = (
|
||||
<TimelinePanel
|
||||
ref={this.gatherTimelinePanelRef}
|
||||
timelineSet={this.state.room.getUnfilteredTimelineSet()}
|
||||
overlayTimelineSet={this.state.virtualRoom?.getUnfilteredTimelineSet()}
|
||||
overlayTimelineSetFilter={isCallEvent}
|
||||
showReadReceipts={this.state.showReadReceipts}
|
||||
manageReadReceipts={!this.state.isPeeking}
|
||||
sendReadReceiptOnLoad={!this.state.wasContextSwitch}
|
||||
manageReadMarkers={!this.state.isPeeking}
|
||||
hidden={hideMessagePanel}
|
||||
highlightedEventId={highlightedEventId}
|
||||
eventId={this.state.initialEventId}
|
||||
eventScrollIntoView={this.state.initialEventScrollIntoView}
|
||||
eventPixelOffset={this.state.initialEventPixelOffset}
|
||||
onScroll={this.onMessageListScroll}
|
||||
onEventScrolledIntoView={this.resetJumpToEvent}
|
||||
onReadMarkerUpdated={this.updateTopUnreadMessagesBar}
|
||||
showUrlPreview={this.state.showUrlPreview}
|
||||
className={this.messagePanelClassNames}
|
||||
membersLoaded={this.state.membersLoaded}
|
||||
permalinkCreator={this.permalinkCreator}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
showReactions={true}
|
||||
layout={this.state.layout}
|
||||
editState={this.state.editState}
|
||||
/>
|
||||
);
|
||||
let messagePanel: JSX.Element | undefined;
|
||||
if (!isRoomEncryptionLoading) {
|
||||
messagePanel = (
|
||||
<TimelinePanel
|
||||
ref={this.gatherTimelinePanelRef}
|
||||
timelineSet={this.state.room.getUnfilteredTimelineSet()}
|
||||
overlayTimelineSet={this.state.virtualRoom?.getUnfilteredTimelineSet()}
|
||||
overlayTimelineSetFilter={isCallEvent}
|
||||
showReadReceipts={this.state.showReadReceipts}
|
||||
manageReadReceipts={!this.state.isPeeking}
|
||||
sendReadReceiptOnLoad={!this.state.wasContextSwitch}
|
||||
manageReadMarkers={!this.state.isPeeking}
|
||||
hidden={hideMessagePanel}
|
||||
highlightedEventId={highlightedEventId}
|
||||
eventId={this.state.initialEventId}
|
||||
eventScrollIntoView={this.state.initialEventScrollIntoView}
|
||||
eventPixelOffset={this.state.initialEventPixelOffset}
|
||||
onScroll={this.onMessageListScroll}
|
||||
onEventScrolledIntoView={this.resetJumpToEvent}
|
||||
onReadMarkerUpdated={this.updateTopUnreadMessagesBar}
|
||||
showUrlPreview={this.state.showUrlPreview}
|
||||
className={this.messagePanelClassNames}
|
||||
membersLoaded={this.state.membersLoaded}
|
||||
permalinkCreator={this.permalinkCreator}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
showReactions={true}
|
||||
layout={this.state.layout}
|
||||
editState={this.state.editState}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let topUnreadMessagesBar: JSX.Element | undefined;
|
||||
// Do not show TopUnreadMessagesBar if we have search results showing, it makes no sense
|
||||
|
@ -2415,7 +2470,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
);
|
||||
}
|
||||
|
||||
const showRightPanel = this.state.room && this.state.showRightPanel;
|
||||
const showRightPanel = !isRoomEncryptionLoading && this.state.room && this.state.showRightPanel;
|
||||
|
||||
const rightPanel = showRightPanel ? (
|
||||
<RightPanel
|
||||
|
@ -2547,5 +2602,3 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default RoomView;
|
||||
|
|
|
@ -114,7 +114,7 @@ const Tile: React.FC<ITileProps> = ({
|
|||
(room.room_type === RoomType.Space ? _t("common|unnamed_space") : _t("common|unnamed_room"));
|
||||
|
||||
const [showChildren, toggleShowChildren] = useStateToggle(true);
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex();
|
||||
const [onFocus, isActive, ref, nodeRef] = useRovingTabIndex();
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const onPreviewClick = (ev: ButtonEvent): void => {
|
||||
|
@ -288,7 +288,7 @@ const Tile: React.FC<ITileProps> = ({
|
|||
case KeyBindingAction.ArrowLeft:
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
ref.current?.focus();
|
||||
nodeRef.current?.focus();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -315,7 +315,7 @@ const Tile: React.FC<ITileProps> = ({
|
|||
case KeyBindingAction.ArrowRight:
|
||||
handled = true;
|
||||
if (showChildren) {
|
||||
const childSection = ref.current?.nextElementSibling;
|
||||
const childSection = nodeRef.current?.nextElementSibling;
|
||||
childSection?.querySelector<HTMLDivElement>(".mx_SpaceHierarchy_roomTile")?.focus();
|
||||
} else {
|
||||
toggleShowChildren();
|
||||
|
@ -790,7 +790,7 @@ const SpaceHierarchy: React.FC<IProps> = ({ space, initialText = "", showRoom, a
|
|||
const onKeyDown = (ev: KeyboardEvent, state: IState): void => {
|
||||
const action = getKeyBindingsManager().getAccessibilityAction(ev);
|
||||
if (action === KeyBindingAction.ArrowDown && ev.currentTarget.classList.contains("mx_SpaceHierarchy_search")) {
|
||||
state.refs[0]?.current?.focus();
|
||||
state.nodes[0]?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1217,7 +1217,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
return;
|
||||
}
|
||||
const lastDisplayedEvent = this.state.events[lastDisplayedIndex];
|
||||
this.setReadMarker(lastDisplayedEvent.getId()!, lastDisplayedEvent.getTs());
|
||||
await this.setReadMarker(lastDisplayedEvent.getId()!, lastDisplayedEvent.getTs());
|
||||
|
||||
// the read-marker should become invisible, so that if the user scrolls
|
||||
// down, they don't see it.
|
||||
|
@ -1335,7 +1335,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
// Update the read marker to the values we found
|
||||
this.setReadMarker(rmId, rmTs);
|
||||
await this.setReadMarker(rmId, rmTs);
|
||||
|
||||
// Send the receipts to the server immediately (don't wait for activity)
|
||||
await this.sendReadReceipts();
|
||||
|
@ -1866,7 +1866,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
return receiptStore?.getEventReadUpTo(myUserId, ignoreSynthesized) ?? null;
|
||||
}
|
||||
|
||||
private setReadMarker(eventId: string | null, eventTs?: number, inhibitSetState = false): void {
|
||||
private async setReadMarker(eventId: string | null, eventTs?: number, inhibitSetState = false): Promise<void> {
|
||||
const roomId = this.props.timelineSet.room?.roomId;
|
||||
|
||||
// don't update the state (and cause a re-render) if there is
|
||||
|
@ -1890,12 +1890,17 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
// Do the local echo of the RM
|
||||
// run the render cycle before calling the callback, so that
|
||||
// getReadMarkerPosition() returns the right thing.
|
||||
this.setState(
|
||||
{
|
||||
readMarkerEventId: eventId,
|
||||
},
|
||||
this.props.onReadMarkerUpdated,
|
||||
);
|
||||
await new Promise<void>((resolve) => {
|
||||
this.setState(
|
||||
{
|
||||
readMarkerEventId: eventId,
|
||||
},
|
||||
() => {
|
||||
this.props.onReadMarkerUpdated?.();
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private shouldPaginate(): boolean {
|
||||
|
|
|
@ -75,6 +75,7 @@ interface State {
|
|||
}
|
||||
|
||||
export default class ForgotPassword extends React.Component<Props, State> {
|
||||
private unmounted = false;
|
||||
private reset: PasswordReset;
|
||||
private fieldPassword: Field | null = null;
|
||||
private fieldPasswordConfirm: Field | null = null;
|
||||
|
@ -108,14 +109,20 @@ export default class ForgotPassword extends React.Component<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
private async checkServerLiveliness(serverConfig: ValidatedServerConfig): Promise<void> {
|
||||
public componentWillUnmount(): void {
|
||||
this.unmounted = true;
|
||||
}
|
||||
|
||||
private async checkServerLiveliness(serverConfig: ValidatedServerConfig): Promise<boolean> {
|
||||
try {
|
||||
await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(serverConfig.hsUrl, serverConfig.isUrl);
|
||||
if (this.unmounted) return false;
|
||||
|
||||
this.setState({
|
||||
serverIsAlive: true,
|
||||
});
|
||||
} catch (e: any) {
|
||||
if (this.unmounted) return false;
|
||||
const { serverIsAlive, serverDeadError } = AutoDiscoveryUtils.authComponentStateForError(
|
||||
e,
|
||||
"forgot_password",
|
||||
|
@ -124,7 +131,9 @@ export default class ForgotPassword extends React.Component<Props, State> {
|
|||
serverIsAlive,
|
||||
errorText: serverDeadError,
|
||||
});
|
||||
return serverIsAlive;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private async onPhaseEmailInputSubmit(): Promise<void> {
|
||||
|
@ -292,10 +301,10 @@ export default class ForgotPassword extends React.Component<Props, State> {
|
|||
});
|
||||
|
||||
// Refresh the server errors. Just in case the server came back online of went offline.
|
||||
await this.checkServerLiveliness(this.props.serverConfig);
|
||||
const serverIsAlive = await this.checkServerLiveliness(this.props.serverConfig);
|
||||
|
||||
// Server error
|
||||
if (!this.state.serverIsAlive) return;
|
||||
if (!serverIsAlive) return;
|
||||
|
||||
switch (this.state.phase) {
|
||||
case Phase.EnterEmail:
|
||||
|
|
|
@ -16,7 +16,7 @@ const AuthFooter = (): ReactElement => {
|
|||
const brandingConfig = SdkConfig.getObject("branding");
|
||||
const links = brandingConfig?.get("auth_footer_links") ?? [
|
||||
{ text: "Blog", url: "https://element.io/blog" },
|
||||
{ text: "Twitter", url: "https://twitter.com/element_hq" },
|
||||
{ text: "Mastodon", url: "https://mastodon.matrix.org/@Element" },
|
||||
{ text: "GitHub", url: "https://github.com/element-hq/element-web" },
|
||||
];
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import dispatcher, { defaultDispatcher } from "../../../dispatcher/dispatcher";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { ConnectionState, ElementCall } from "../../../models/Call";
|
||||
|
@ -53,7 +53,7 @@ const RoomCallBannerInner: React.FC<RoomCallBannerProps> = ({ roomId, call }) =>
|
|||
return;
|
||||
}
|
||||
|
||||
dispatcher.dispatch<ViewRoomPayload>({
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: roomId,
|
||||
metricsTrigger: undefined,
|
||||
|
|
|
@ -256,7 +256,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||
});
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const isVideoRoom = this.props.type === RoomType.ElementVideo;
|
||||
const isVideoRoom = this.props.type === RoomType.ElementVideo || this.props.type === RoomType.UnstableCall;
|
||||
|
||||
let aliasField: JSX.Element | undefined;
|
||||
if (this.state.joinRule === JoinRule.Public) {
|
||||
|
|
|
@ -294,7 +294,7 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
|
|||
const action = getKeyBindingsManager().getAccessibilityAction(ev);
|
||||
switch (action) {
|
||||
case KeyBindingAction.Enter: {
|
||||
state.activeRef?.current?.querySelector<HTMLButtonElement>(".mx_ForwardList_sendButton")?.click();
|
||||
state.activeNode?.querySelector<HTMLButtonElement>(".mx_ForwardList_sendButton")?.click();
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -347,13 +347,13 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
|
|||
onSearch={(query: string): void => {
|
||||
setQuery(query);
|
||||
setTimeout(() => {
|
||||
const ref = context.state.refs[0];
|
||||
if (ref) {
|
||||
const node = context.state.nodes[0];
|
||||
if (node) {
|
||||
context.dispatch({
|
||||
type: Type.SetFocus,
|
||||
payload: { ref },
|
||||
payload: { node },
|
||||
});
|
||||
ref.current?.scrollIntoView?.({
|
||||
node?.scrollIntoView?.({
|
||||
block: "nearest",
|
||||
});
|
||||
}
|
||||
|
@ -361,7 +361,7 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
|
|||
}}
|
||||
autoFocus={true}
|
||||
onKeyDown={onKeyDownHandler}
|
||||
aria-activedescendant={context.state.activeRef?.current?.id}
|
||||
aria-activedescendant={context.state.activeNode?.id}
|
||||
aria-owns="mx_ForwardDialog_resultsList"
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -7,12 +7,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { lazy } from "react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import type CreateKeyBackupDialog from "../../../async-components/views/dialogs/security/CreateKeyBackupDialog";
|
||||
import type ExportE2eKeysDialog from "../../../async-components/views/dialogs/security/ExportE2eKeysDialog";
|
||||
import Modal from "../../../Modal";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
@ -111,15 +109,13 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
// backup is not active. see if there is a backup version on the server we ought to back up to.
|
||||
const backupInfo = await client.getKeyBackupVersion();
|
||||
const backupInfo = await crypto.getKeyBackupInfo();
|
||||
this.setState({ backupStatus: backupInfo ? BackupStatus.SERVER_BACKUP_BUT_DISABLED : BackupStatus.NO_BACKUP });
|
||||
}
|
||||
|
||||
private onExportE2eKeysClicked = (): void => {
|
||||
Modal.createDialogAsync(
|
||||
import("../../../async-components/views/dialogs/security/ExportE2eKeysDialog") as unknown as Promise<
|
||||
typeof ExportE2eKeysDialog
|
||||
>,
|
||||
Modal.createDialog(
|
||||
lazy(() => import("../../../async-components/views/dialogs/security/ExportE2eKeysDialog")),
|
||||
{
|
||||
matrixClient: MatrixClientPeg.safeGet(),
|
||||
},
|
||||
|
@ -147,10 +143,8 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
|
|||
/* static = */ true,
|
||||
);
|
||||
} else {
|
||||
Modal.createDialogAsync(
|
||||
import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog") as unknown as Promise<
|
||||
typeof CreateKeyBackupDialog
|
||||
>,
|
||||
Modal.createDialog(
|
||||
lazy(() => import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog")),
|
||||
undefined,
|
||||
undefined,
|
||||
/* priority = */ false,
|
||||
|
|
|
@ -43,6 +43,10 @@ interface IState {
|
|||
// If we know it, the nature of the abuse, as specified by MSC3215.
|
||||
nature?: ExtendedNature;
|
||||
ignoreUserToo: boolean; // if true, user will be ignored/blocked on submit
|
||||
/*
|
||||
* Whether the room is encrypted.
|
||||
*/
|
||||
isRoomEncrypted: boolean;
|
||||
}
|
||||
|
||||
const MODERATED_BY_STATE_EVENT_TYPE = [
|
||||
|
@ -188,9 +192,20 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
|
|||
// If specified, the nature of the abuse, as specified by MSC3215.
|
||||
nature: undefined,
|
||||
ignoreUserToo: false, // default false, for now. Could easily be argued as default true
|
||||
isRoomEncrypted: false, // async, will be set later
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount = async (): Promise<void> => {
|
||||
const crypto = MatrixClientPeg.safeGet().getCrypto();
|
||||
const roomId = this.props.mxEvent.getRoomId();
|
||||
if (!crypto || !roomId) return;
|
||||
|
||||
this.setState({
|
||||
isRoomEncrypted: await crypto.isEncryptionEnabledInRoom(roomId),
|
||||
});
|
||||
};
|
||||
|
||||
private onIgnoreUserTooChanged = (newVal: boolean): void => {
|
||||
this.setState({ ignoreUserToo: newVal });
|
||||
};
|
||||
|
@ -319,7 +334,6 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
|
|||
if (this.moderation) {
|
||||
// Display report-to-moderator dialog.
|
||||
// We let the user pick a nature.
|
||||
const client = MatrixClientPeg.safeGet();
|
||||
const homeServerName = SdkConfig.get("validated_server_config")!.hsName;
|
||||
let subtitle: string;
|
||||
switch (this.state.nature) {
|
||||
|
@ -336,7 +350,7 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
|
|||
subtitle = _t("report_content|nature_spam");
|
||||
break;
|
||||
case NonStandardValue.Admin:
|
||||
if (client.isRoomEncrypted(this.props.mxEvent.getRoomId()!)) {
|
||||
if (this.state.isRoomEncrypted) {
|
||||
subtitle = _t("report_content|nature_nonstandard_admin_encrypted", {
|
||||
homeserver: homeServerName,
|
||||
});
|
||||
|
|
|
@ -21,7 +21,7 @@ import { SpacePreferenceTab } from "../../../dispatcher/payloads/OpenSpacePrefer
|
|||
import { NonEmptyArray } from "../../../@types/common";
|
||||
import SettingsTab from "../settings/tabs/SettingsTab";
|
||||
import { SettingsSection } from "../settings/shared/SettingsSection";
|
||||
import SettingsSubsection, { SettingsSubsectionText } from "../settings/shared/SettingsSubsection";
|
||||
import { SettingsSubsection, SettingsSubsectionText } from "../settings/shared/SettingsSubsection";
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
|
|
|
@ -8,8 +8,8 @@ Please see LICENSE files in the repository root for full details.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import { FilesIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { Icon as FileIcon } from "../../../../res/img/feather-customised/files.svg";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { getBlobSafeMimeType } from "../../../utils/blobs";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
|
@ -86,7 +86,7 @@ export default class UploadConfirmDialog extends React.Component<IProps> {
|
|||
/>
|
||||
);
|
||||
} else {
|
||||
placeholder = <FileIcon className="mx_UploadConfirmDialog_fileIcon" height={18} width={18} />;
|
||||
placeholder = <FilesIcon className="mx_UploadConfirmDialog_fileIcon" height="18px" width="18px" />;
|
||||
}
|
||||
|
||||
let uploadAllButton: JSX.Element | undefined;
|
||||
|
|
|
@ -17,6 +17,7 @@ import { determineUnreadState } from "../../../../RoomNotifs";
|
|||
import { humanReadableNotificationLevel } from "../../../../stores/notifications/NotificationLevel";
|
||||
import { doesRoomOrThreadHaveUnreadMessages } from "../../../../Unread";
|
||||
import BaseTool, { DevtoolsContext, IDevtoolsProps } from "./BaseTool";
|
||||
import { useIsEncrypted } from "../../../../hooks/useIsEncrypted.ts";
|
||||
|
||||
function UserReadUpTo({ target }: { target: ReadReceipt<any, any> }): JSX.Element {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
@ -59,6 +60,7 @@ function UserReadUpTo({ target }: { target: ReadReceipt<any, any> }): JSX.Elemen
|
|||
export default function RoomNotifications({ onBack }: IDevtoolsProps): JSX.Element {
|
||||
const { room } = useContext(DevtoolsContext);
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const isRoomEncrypted = useIsEncrypted(cli, room);
|
||||
|
||||
const { level, count } = determineUnreadState(room, undefined, false);
|
||||
const [notificationState] = useNotificationState(room);
|
||||
|
@ -93,9 +95,7 @@ export default function RoomNotifications({ onBack }: IDevtoolsProps): JSX.Eleme
|
|||
</li>
|
||||
<li>
|
||||
{_t(
|
||||
cli.isRoomEncrypted(room.roomId!)
|
||||
? _td("devtools|room_encrypted")
|
||||
: _td("devtools|room_not_encrypted"),
|
||||
isRoomEncrypted ? _td("devtools|room_encrypted") : _td("devtools|room_not_encrypted"),
|
||||
{},
|
||||
{
|
||||
strong: (sub) => <strong>{sub}</strong>,
|
||||
|
|
|
@ -19,7 +19,6 @@ import AccessibleButton, { ButtonEvent } from "../../elements/AccessibleButton";
|
|||
import { _t } from "../../../../languageHandler";
|
||||
import { accessSecretStorage } from "../../../../SecurityManager";
|
||||
import Modal from "../../../../Modal";
|
||||
import InteractiveAuthDialog from "../InteractiveAuthDialog";
|
||||
import DialogButtons from "../../elements/DialogButtons";
|
||||
import BaseDialog from "../BaseDialog";
|
||||
import { chromeFileInputFix } from "../../../../utils/BrowserWorkarounds";
|
||||
|
@ -226,28 +225,14 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
|
|||
|
||||
try {
|
||||
// Force reset secret storage (which resets the key backup)
|
||||
await accessSecretStorage(async (): Promise<void> => {
|
||||
// Now reset cross-signing so everything Just Works™ again.
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
await cli.getCrypto()?.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async (makeRequest): Promise<void> => {
|
||||
const { finished } = Modal.createDialog(InteractiveAuthDialog, {
|
||||
title: _t("encryption|bootstrap_title"),
|
||||
matrixClient: cli,
|
||||
makeRequest,
|
||||
});
|
||||
const [confirmed] = await finished;
|
||||
if (!confirmed) {
|
||||
throw new Error("Cross-signing key upload auth canceled");
|
||||
}
|
||||
},
|
||||
setupNewCrossSigning: true,
|
||||
});
|
||||
|
||||
// Now we can indicate that the user is done pressing buttons, finally.
|
||||
// Upstream flows will detect the new secret storage, key backup, etc and use it.
|
||||
this.props.onFinished({});
|
||||
}, true);
|
||||
await accessSecretStorage(
|
||||
async (): Promise<void> => {
|
||||
// Now we can indicate that the user is done pressing buttons, finally.
|
||||
// Upstream flows will detect the new secret storage, key backup, etc and use it.
|
||||
this.props.onFinished({});
|
||||
},
|
||||
{ forceReset: true, resetCrossSigning: true },
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
this.props.onFinished(false);
|
||||
|
|
|
@ -8,9 +8,8 @@ Please see LICENSE files in the repository root for full details.
|
|||
*/
|
||||
|
||||
import React, { ChangeEvent } from "react";
|
||||
import { MatrixClient, MatrixError, SecretStorage } from "matrix-js-sdk/src/matrix";
|
||||
import { decodeRecoveryKey, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||
import { IKeyBackupRestoreResult } from "matrix-js-sdk/src/crypto/keybackup";
|
||||
import { MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
|
||||
import { decodeRecoveryKey, KeyBackupInfo, KeyBackupRestoreResult } from "matrix-js-sdk/src/crypto-api";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||
|
@ -42,12 +41,11 @@ interface IProps {
|
|||
|
||||
interface IState {
|
||||
backupInfo: KeyBackupInfo | null;
|
||||
backupKeyStored: Record<string, SecretStorage.SecretStorageKeyDescription> | null;
|
||||
loading: boolean;
|
||||
loadError: boolean | null;
|
||||
restoreError: unknown | null;
|
||||
recoveryKey: string;
|
||||
recoverInfo: IKeyBackupRestoreResult | null;
|
||||
recoverInfo: KeyBackupRestoreResult | null;
|
||||
recoveryKeyValid: boolean;
|
||||
forceRecoveryKey: boolean;
|
||||
passPhrase: string;
|
||||
|
@ -72,7 +70,6 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
|
|||
super(props);
|
||||
this.state = {
|
||||
backupInfo: null,
|
||||
backupKeyStored: null,
|
||||
loading: false,
|
||||
loadError: null,
|
||||
restoreError: null,
|
||||
|
@ -112,7 +109,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
|
|||
|
||||
private onResetRecoveryClick = (): void => {
|
||||
this.props.onFinished(false);
|
||||
accessSecretStorage(async (): Promise<void> => {}, /* forceReset = */ true);
|
||||
accessSecretStorage(async (): Promise<void> => {}, { forceReset: true });
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -137,7 +134,8 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
|
|||
};
|
||||
|
||||
private onPassPhraseNext = async (): Promise<void> => {
|
||||
if (!this.state.backupInfo) return;
|
||||
const crypto = MatrixClientPeg.safeGet().getCrypto();
|
||||
if (!crypto) return;
|
||||
this.setState({
|
||||
loading: true,
|
||||
restoreError: null,
|
||||
|
@ -146,13 +144,9 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
|
|||
try {
|
||||
// We do still restore the key backup: we must ensure that the key backup key
|
||||
// is the right one and restoring it is currently the only way we can do this.
|
||||
const recoverInfo = await MatrixClientPeg.safeGet().restoreKeyBackupWithPassword(
|
||||
this.state.passPhrase,
|
||||
undefined,
|
||||
undefined,
|
||||
this.state.backupInfo,
|
||||
{ progressCallback: this.progressCallback },
|
||||
);
|
||||
const recoverInfo = await crypto.restoreKeyBackupWithPassphrase(this.state.passPhrase, {
|
||||
progressCallback: this.progressCallback,
|
||||
});
|
||||
|
||||
if (!this.props.showSummary) {
|
||||
this.props.onFinished(true);
|
||||
|
@ -172,7 +166,8 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
|
|||
};
|
||||
|
||||
private onRecoveryKeyNext = async (): Promise<void> => {
|
||||
if (!this.state.recoveryKeyValid || !this.state.backupInfo) return;
|
||||
const crypto = MatrixClientPeg.safeGet().getCrypto();
|
||||
if (!this.state.recoveryKeyValid || !this.state.backupInfo?.version || !crypto) return;
|
||||
|
||||
this.setState({
|
||||
loading: true,
|
||||
|
@ -180,13 +175,14 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
|
|||
restoreType: RestoreType.RecoveryKey,
|
||||
});
|
||||
try {
|
||||
const recoverInfo = await MatrixClientPeg.safeGet().restoreKeyBackupWithRecoveryKey(
|
||||
this.state.recoveryKey,
|
||||
undefined,
|
||||
undefined,
|
||||
this.state.backupInfo,
|
||||
{ progressCallback: this.progressCallback },
|
||||
await crypto.storeSessionBackupPrivateKey(
|
||||
decodeRecoveryKey(this.state.recoveryKey),
|
||||
this.state.backupInfo.version,
|
||||
);
|
||||
const recoverInfo = await crypto.restoreKeyBackup({
|
||||
progressCallback: this.progressCallback,
|
||||
});
|
||||
|
||||
if (!this.props.showSummary) {
|
||||
this.props.onFinished(true);
|
||||
return;
|
||||
|
@ -210,44 +206,41 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
|
|||
});
|
||||
};
|
||||
|
||||
private async restoreWithSecretStorage(): Promise<void> {
|
||||
private async restoreWithSecretStorage(): Promise<boolean> {
|
||||
const crypto = MatrixClientPeg.safeGet().getCrypto();
|
||||
if (!crypto) return false;
|
||||
|
||||
this.setState({
|
||||
loading: true,
|
||||
restoreError: null,
|
||||
restoreType: RestoreType.SecretStorage,
|
||||
});
|
||||
try {
|
||||
let recoverInfo: KeyBackupRestoreResult | null = null;
|
||||
// `accessSecretStorage` may prompt for storage access as needed.
|
||||
await accessSecretStorage(async (): Promise<void> => {
|
||||
if (!this.state.backupInfo) return;
|
||||
await MatrixClientPeg.safeGet().restoreKeyBackupWithSecretStorage(
|
||||
this.state.backupInfo,
|
||||
undefined,
|
||||
undefined,
|
||||
{ progressCallback: this.progressCallback },
|
||||
);
|
||||
await crypto.loadSessionBackupPrivateKeyFromSecretStorage();
|
||||
recoverInfo = await crypto.restoreKeyBackup({ progressCallback: this.progressCallback });
|
||||
});
|
||||
this.setState({
|
||||
loading: false,
|
||||
recoverInfo,
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
logger.log("Error restoring backup", e);
|
||||
logger.log("restoreWithSecretStorage failed:", e);
|
||||
this.setState({
|
||||
restoreError: e,
|
||||
loading: false,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async restoreWithCachedKey(backupInfo: KeyBackupInfo | null): Promise<boolean> {
|
||||
if (!backupInfo) return false;
|
||||
const crypto = MatrixClientPeg.safeGet().getCrypto();
|
||||
if (!crypto) return false;
|
||||
try {
|
||||
const recoverInfo = await MatrixClientPeg.safeGet().restoreKeyBackupWithCache(
|
||||
undefined /* targetRoomId */,
|
||||
undefined /* targetSessionId */,
|
||||
backupInfo,
|
||||
{ progressCallback: this.progressCallback },
|
||||
);
|
||||
const recoverInfo = await crypto.restoreKeyBackup({ progressCallback: this.progressCallback });
|
||||
this.setState({
|
||||
recoverInfo,
|
||||
});
|
||||
|
@ -265,12 +258,11 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
|
|||
});
|
||||
try {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
const backupInfo = await cli.getKeyBackupVersion();
|
||||
const backupInfo = (await cli.getCrypto()?.getKeyBackupInfo()) ?? null;
|
||||
const has4S = await cli.secretStorage.hasKey();
|
||||
const backupKeyStored = has4S ? await cli.isKeyBackupKeyStored() : null;
|
||||
this.setState({
|
||||
backupInfo,
|
||||
backupKeyStored,
|
||||
});
|
||||
|
||||
const gotCache = await this.restoreWithCachedKey(backupInfo);
|
||||
|
@ -282,9 +274,13 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
|
|||
return;
|
||||
}
|
||||
|
||||
// If the backup key is stored, we can proceed directly to restore.
|
||||
if (backupKeyStored) {
|
||||
return this.restoreWithSecretStorage();
|
||||
const hasBackupFromSS = backupKeyStored && (await this.restoreWithSecretStorage());
|
||||
if (hasBackupFromSS) {
|
||||
logger.log("RestoreKeyBackupDialog: found backup key in secret storage");
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
|
@ -398,6 +394,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
|
|||
|
||||
<form className="mx_RestoreKeyBackupDialog_primaryContainer">
|
||||
<input
|
||||
data-testid="passphraseInput"
|
||||
type="password"
|
||||
className="mx_RestoreKeyBackupDialog_passPhraseInput"
|
||||
onChange={this.onPassPhraseChange}
|
||||
|
|
|
@ -7,13 +7,12 @@ Please see LICENSE files in the repository root for full details.
|
|||
*/
|
||||
|
||||
import classNames from "classnames";
|
||||
import React, { ReactNode, RefObject } from "react";
|
||||
import React, { ReactNode } from "react";
|
||||
|
||||
import { useRovingTabIndex } from "../../../../accessibility/RovingTabIndex";
|
||||
import AccessibleButton, { ButtonEvent } from "../../elements/AccessibleButton";
|
||||
|
||||
interface OptionProps {
|
||||
inputRef?: RefObject<HTMLLIElement>;
|
||||
endAdornment?: ReactNode;
|
||||
id?: string;
|
||||
className?: string;
|
||||
|
@ -21,8 +20,8 @@ interface OptionProps {
|
|||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const Option: React.FC<OptionProps> = ({ inputRef, children, endAdornment, className, ...props }) => {
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex<HTMLLIElement>(inputRef);
|
||||
export const Option: React.FC<OptionProps> = ({ children, endAdornment, className, ...props }) => {
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex<HTMLLIElement>();
|
||||
return (
|
||||
<AccessibleButton
|
||||
{...props}
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
} from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { normalize } from "matrix-js-sdk/src/utils";
|
||||
import React, { ChangeEvent, RefObject, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import React, { ChangeEvent, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
|
||||
import { KeyBindingAction } from "../../../../accessibility/KeyboardShortcuts";
|
||||
|
@ -90,8 +90,8 @@ interface IProps {
|
|||
onFinished(): void;
|
||||
}
|
||||
|
||||
function refIsForRecentlyViewed(ref?: RefObject<HTMLElement>): boolean {
|
||||
return ref?.current?.id?.startsWith("mx_SpotlightDialog_button_recentlyViewed_") === true;
|
||||
function nodeIsForRecentlyViewed(node?: HTMLElement): boolean {
|
||||
return node?.id?.startsWith("mx_SpotlightDialog_button_recentlyViewed_") === true;
|
||||
}
|
||||
|
||||
function getRoomTypes(filter: Filter | null): Set<RoomType | null> {
|
||||
|
@ -498,13 +498,13 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
};
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
const ref = rovingContext.state.refs[0];
|
||||
if (ref) {
|
||||
const node = rovingContext.state.nodes[0];
|
||||
if (node) {
|
||||
rovingContext.dispatch({
|
||||
type: Type.SetFocus,
|
||||
payload: { ref },
|
||||
payload: { node },
|
||||
});
|
||||
ref.current?.scrollIntoView?.({
|
||||
node?.scrollIntoView?.({
|
||||
block: "nearest",
|
||||
});
|
||||
}
|
||||
|
@ -1128,7 +1128,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
break;
|
||||
}
|
||||
|
||||
let ref: RefObject<HTMLElement> | undefined;
|
||||
let node: HTMLElement | undefined;
|
||||
const accessibilityAction = getKeyBindingsManager().getAccessibilityAction(ev);
|
||||
switch (accessibilityAction) {
|
||||
case KeyBindingAction.Escape:
|
||||
|
@ -1141,20 +1141,20 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
if (rovingContext.state.activeRef && rovingContext.state.refs.length > 0) {
|
||||
let refs = rovingContext.state.refs;
|
||||
if (rovingContext.state.activeNode && rovingContext.state.nodes.length > 0) {
|
||||
let nodes = rovingContext.state.nodes;
|
||||
if (!query && !filter !== null) {
|
||||
// If the current selection is not in the recently viewed row then only include the
|
||||
// first recently viewed so that is the target when the user is switching into recently viewed.
|
||||
const keptRecentlyViewedRef = refIsForRecentlyViewed(rovingContext.state.activeRef)
|
||||
? rovingContext.state.activeRef
|
||||
: refs.find(refIsForRecentlyViewed);
|
||||
const keptRecentlyViewedRef = nodeIsForRecentlyViewed(rovingContext.state.activeNode)
|
||||
? rovingContext.state.activeNode
|
||||
: nodes.find(nodeIsForRecentlyViewed);
|
||||
// exclude all other recently viewed items from the list so up/down arrows skip them
|
||||
refs = refs.filter((ref) => ref === keptRecentlyViewedRef || !refIsForRecentlyViewed(ref));
|
||||
nodes = nodes.filter((ref) => ref === keptRecentlyViewedRef || !nodeIsForRecentlyViewed(ref));
|
||||
}
|
||||
|
||||
const idx = refs.indexOf(rovingContext.state.activeRef);
|
||||
ref = findSiblingElement(refs, idx + (accessibilityAction === KeyBindingAction.ArrowUp ? -1 : 1));
|
||||
const idx = nodes.indexOf(rovingContext.state.activeNode);
|
||||
node = findSiblingElement(nodes, idx + (accessibilityAction === KeyBindingAction.ArrowUp ? -1 : 1));
|
||||
}
|
||||
break;
|
||||
|
||||
|
@ -1164,27 +1164,30 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
if (
|
||||
!query &&
|
||||
!filter !== null &&
|
||||
rovingContext.state.activeRef &&
|
||||
rovingContext.state.refs.length > 0 &&
|
||||
refIsForRecentlyViewed(rovingContext.state.activeRef)
|
||||
rovingContext.state.activeNode &&
|
||||
rovingContext.state.nodes.length > 0 &&
|
||||
nodeIsForRecentlyViewed(rovingContext.state.activeNode)
|
||||
) {
|
||||
// we only intercept left/right arrows when the field is empty, and they'd do nothing anyway
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
const refs = rovingContext.state.refs.filter(refIsForRecentlyViewed);
|
||||
const idx = refs.indexOf(rovingContext.state.activeRef);
|
||||
ref = findSiblingElement(refs, idx + (accessibilityAction === KeyBindingAction.ArrowLeft ? -1 : 1));
|
||||
const nodes = rovingContext.state.nodes.filter(nodeIsForRecentlyViewed);
|
||||
const idx = nodes.indexOf(rovingContext.state.activeNode);
|
||||
node = findSiblingElement(
|
||||
nodes,
|
||||
idx + (accessibilityAction === KeyBindingAction.ArrowLeft ? -1 : 1),
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (ref) {
|
||||
if (node) {
|
||||
rovingContext.dispatch({
|
||||
type: Type.SetFocus,
|
||||
payload: { ref },
|
||||
payload: { node },
|
||||
});
|
||||
ref.current?.scrollIntoView({
|
||||
node?.scrollIntoView({
|
||||
block: "nearest",
|
||||
});
|
||||
}
|
||||
|
@ -1204,12 +1207,12 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
case KeyBindingAction.Enter:
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
rovingContext.state.activeRef?.current?.click();
|
||||
rovingContext.state.activeNode?.click();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const activeDescendant = rovingContext.state.activeRef?.current?.id;
|
||||
const activeDescendant = rovingContext.state.activeNode?.id;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -12,6 +12,9 @@ import React, {
|
|||
RefObject,
|
||||
createRef,
|
||||
ComponentProps,
|
||||
MutableRefObject,
|
||||
RefCallback,
|
||||
Ref,
|
||||
} from "react";
|
||||
import classNames from "classnames";
|
||||
import { debounce } from "lodash";
|
||||
|
@ -75,7 +78,7 @@ interface IProps {
|
|||
|
||||
export interface IInputProps extends IProps, InputHTMLAttributes<HTMLInputElement> {
|
||||
// The ref pass through to the input
|
||||
inputRef?: RefObject<HTMLInputElement>;
|
||||
inputRef?: Ref<HTMLInputElement>;
|
||||
// The element to create. Defaults to "input".
|
||||
element: "input";
|
||||
// The input's value. This is a controlled component, so the value is required.
|
||||
|
@ -84,7 +87,7 @@ export interface IInputProps extends IProps, InputHTMLAttributes<HTMLInputElemen
|
|||
|
||||
interface ISelectProps extends IProps, SelectHTMLAttributes<HTMLSelectElement> {
|
||||
// The ref pass through to the select
|
||||
inputRef?: RefObject<HTMLSelectElement>;
|
||||
inputRef?: Ref<HTMLSelectElement>;
|
||||
// To define options for a select, use <Field><option ... /></Field>
|
||||
element: "select";
|
||||
// The select's value. This is a controlled component, so the value is required.
|
||||
|
@ -93,7 +96,7 @@ interface ISelectProps extends IProps, SelectHTMLAttributes<HTMLSelectElement> {
|
|||
|
||||
interface ITextareaProps extends IProps, TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
// The ref pass through to the textarea
|
||||
inputRef?: RefObject<HTMLTextAreaElement>;
|
||||
inputRef?: Ref<HTMLTextAreaElement>;
|
||||
element: "textarea";
|
||||
// The textarea's value. This is a controlled component, so the value is required.
|
||||
value: string;
|
||||
|
@ -101,7 +104,7 @@ interface ITextareaProps extends IProps, TextareaHTMLAttributes<HTMLTextAreaElem
|
|||
|
||||
export interface INativeOnChangeInputProps extends IProps, InputHTMLAttributes<HTMLInputElement> {
|
||||
// The ref pass through to the input
|
||||
inputRef?: RefObject<HTMLInputElement>;
|
||||
inputRef?: Ref<HTMLInputElement>;
|
||||
element: "input";
|
||||
// The input's value. This is a controlled component, so the value is required.
|
||||
value: string;
|
||||
|
@ -118,7 +121,17 @@ interface IState {
|
|||
|
||||
export default class Field extends React.PureComponent<PropShapes, IState> {
|
||||
private readonly id: string;
|
||||
private readonly _inputRef = createRef<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>();
|
||||
private readonly _inputRef: MutableRefObject<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | null> =
|
||||
createRef();
|
||||
|
||||
/**
|
||||
* When props.inputRef is a callback ref, we will pass callbackRef to the DOM element.
|
||||
* This is so that other methods here can still access the DOM element via this._inputRef.
|
||||
*/
|
||||
private readonly callbackRef: RefCallback<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement> = (node) => {
|
||||
this._inputRef.current = node;
|
||||
(this.props.inputRef as RefCallback<unknown>)(node);
|
||||
};
|
||||
|
||||
public static readonly defaultProps = {
|
||||
element: "input",
|
||||
|
@ -230,7 +243,12 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
|||
}
|
||||
|
||||
private get inputRef(): RefObject<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement> {
|
||||
return this.props.inputRef ?? this._inputRef;
|
||||
const inputRef = this.props.inputRef;
|
||||
if (typeof inputRef === "function") {
|
||||
// This is a callback ref, so return _inputRef which will point to the actual DOM element.
|
||||
return this._inputRef;
|
||||
}
|
||||
return (inputRef ?? this._inputRef) as RefObject<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>;
|
||||
}
|
||||
|
||||
private onTooltipOpenChange = (open: boolean): void => {
|
||||
|
@ -284,7 +302,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
|||
const inputProps_: React.HTMLAttributes<HTMLSelectElement | HTMLInputElement | HTMLTextAreaElement> &
|
||||
React.ClassAttributes<HTMLSelectElement | HTMLInputElement | HTMLTextAreaElement> = {
|
||||
...inputProps,
|
||||
ref: this.inputRef,
|
||||
ref: typeof this.props.inputRef === "function" ? this.callbackRef : this.inputRef,
|
||||
};
|
||||
|
||||
const fieldInput = React.createElement(this.props.element, inputProps_, children);
|
||||
|
|
|
@ -17,6 +17,7 @@ import { useTimeout } from "../../../hooks/useTimeout";
|
|||
import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import Spinner from "./Spinner";
|
||||
import { getFileChanged } from "../settings/AvatarSetting.tsx";
|
||||
|
||||
export const AVATAR_SIZE = "52px";
|
||||
|
||||
|
@ -72,11 +73,12 @@ const MiniAvatarUploader: React.FC<IProps> = ({
|
|||
onClick?.(ev);
|
||||
}}
|
||||
onChange={async (ev): Promise<void> => {
|
||||
if (!ev.target.files?.length) return;
|
||||
setBusy(true);
|
||||
const file = ev.target.files[0];
|
||||
const { content_uri: uri } = await cli.uploadContent(file);
|
||||
await setAvatarUrl(uri);
|
||||
const file = getFileChanged(ev);
|
||||
if (file) {
|
||||
const { content_uri: uri } = await cli.uploadContent(file);
|
||||
await setAvatarUrl(uri);
|
||||
}
|
||||
setBusy(false);
|
||||
}}
|
||||
accept="image/*"
|
||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { Ref } from "react";
|
||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||
import classnames from "classnames";
|
||||
|
||||
|
@ -16,7 +16,7 @@ export enum CheckboxStyle {
|
|||
}
|
||||
|
||||
interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
inputRef?: React.RefObject<HTMLInputElement>;
|
||||
inputRef?: Ref<HTMLInputElement>;
|
||||
kind?: CheckboxStyle;
|
||||
id?: string;
|
||||
}
|
||||
|
|
|
@ -6,11 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { Ref } from "react";
|
||||
import classnames from "classnames";
|
||||
|
||||
interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
inputRef?: React.RefObject<HTMLInputElement>;
|
||||
inputRef?: Ref<HTMLInputElement>;
|
||||
outlined?: boolean;
|
||||
// If true (default), the children will be contained within a <label> element
|
||||
// If false, they'll be in a div. Putting interactive components that have labels
|
||||
|
|
|
@ -28,7 +28,6 @@ import {
|
|||
import { Key } from "../../../Keyboard";
|
||||
import { clamp } from "../../../utils/numbers";
|
||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import { Ref } from "../../../accessibility/roving/types";
|
||||
|
||||
export const CATEGORY_HEADER_HEIGHT = 20;
|
||||
export const EMOJI_HEIGHT = 35;
|
||||
|
@ -154,47 +153,47 @@ class EmojiPicker extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private keyboardNavigation(ev: React.KeyboardEvent, state: RovingState, dispatch: Dispatch<RovingAction>): void {
|
||||
const node = state.activeRef?.current;
|
||||
const node = state.activeNode;
|
||||
const parent = node?.parentElement;
|
||||
if (!parent || !state.activeRef) return;
|
||||
if (!parent || !state.activeNode) return;
|
||||
const rowIndex = Array.from(parent.children).indexOf(node);
|
||||
const refIndex = state.refs.indexOf(state.activeRef);
|
||||
const refIndex = state.nodes.indexOf(state.activeNode);
|
||||
|
||||
let focusRef: Ref | undefined;
|
||||
let focusNode: HTMLElement | undefined;
|
||||
let newParent: HTMLElement | undefined;
|
||||
switch (ev.key) {
|
||||
case Key.ARROW_LEFT:
|
||||
focusRef = state.refs[refIndex - 1];
|
||||
newParent = focusRef?.current?.parentElement ?? undefined;
|
||||
focusNode = state.nodes[refIndex - 1];
|
||||
newParent = focusNode?.parentElement ?? undefined;
|
||||
break;
|
||||
|
||||
case Key.ARROW_RIGHT:
|
||||
focusRef = state.refs[refIndex + 1];
|
||||
newParent = focusRef?.current?.parentElement ?? undefined;
|
||||
focusNode = state.nodes[refIndex + 1];
|
||||
newParent = focusNode?.parentElement ?? undefined;
|
||||
break;
|
||||
|
||||
case Key.ARROW_UP:
|
||||
case Key.ARROW_DOWN: {
|
||||
// For up/down we find the prev/next parent by inspecting the refs either side of our row
|
||||
const ref =
|
||||
const node =
|
||||
ev.key === Key.ARROW_UP
|
||||
? state.refs[refIndex - rowIndex - 1]
|
||||
: state.refs[refIndex - rowIndex + EMOJIS_PER_ROW];
|
||||
newParent = ref?.current?.parentElement ?? undefined;
|
||||
? state.nodes[refIndex - rowIndex - 1]
|
||||
: state.nodes[refIndex - rowIndex + EMOJIS_PER_ROW];
|
||||
newParent = node?.parentElement ?? undefined;
|
||||
const newTarget = newParent?.children[clamp(rowIndex, 0, newParent.children.length - 1)];
|
||||
focusRef = state.refs.find((r) => r.current === newTarget);
|
||||
focusNode = state.nodes.find((r) => r === newTarget);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (focusRef) {
|
||||
if (focusNode) {
|
||||
dispatch({
|
||||
type: Type.SetFocus,
|
||||
payload: { ref: focusRef },
|
||||
payload: { node: focusNode },
|
||||
});
|
||||
|
||||
if (parent !== newParent) {
|
||||
focusRef.current?.scrollIntoView({
|
||||
focusNode?.scrollIntoView({
|
||||
behavior: "auto",
|
||||
block: "center",
|
||||
inline: "center",
|
||||
|
@ -207,10 +206,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private onKeyDown = (ev: React.KeyboardEvent, state: RovingState, dispatch: Dispatch<RovingAction>): void => {
|
||||
if (
|
||||
state.activeRef?.current &&
|
||||
[Key.ARROW_DOWN, Key.ARROW_RIGHT, Key.ARROW_LEFT, Key.ARROW_UP].includes(ev.key)
|
||||
) {
|
||||
if (state.activeNode && [Key.ARROW_DOWN, Key.ARROW_RIGHT, Key.ARROW_LEFT, Key.ARROW_UP].includes(ev.key)) {
|
||||
this.keyboardNavigation(ev, state, dispatch);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -70,7 +70,7 @@ class Search extends React.PureComponent<IProps> {
|
|||
onChange={(ev) => this.props.onChange(ev.target.value)}
|
||||
onKeyDown={this.onKeyDown}
|
||||
ref={this.inputRef}
|
||||
aria-activedescendant={this.context.state.activeRef?.current?.id}
|
||||
aria-activedescendant={this.context.state.activeNode?.id}
|
||||
aria-controls="mx_EmojiPicker_body"
|
||||
aria-haspopup="grid"
|
||||
aria-autocomplete="list"
|
||||
|
|
|
@ -23,7 +23,7 @@ export interface IProps {
|
|||
relation?: IEventRelation;
|
||||
}
|
||||
|
||||
export const LocationButton: React.FC<IProps> = ({ roomId, sender, menuPosition, relation }) => {
|
||||
const LocationButton: React.FC<IProps> = ({ roomId, sender, menuPosition, relation }) => {
|
||||
const overflowMenuCloser = useContext(OverflowMenuContext);
|
||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
||||
|
||||
|
|
|
@ -8,8 +8,8 @@ Please see LICENSE files in the repository root for full details.
|
|||
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import LocationMarkerIcon from "@vector-im/compound-design-tokens/assets/web/icons/location-pin-solid";
|
||||
|
||||
import { Icon as LocationMarkerIcon } from "../../../../res/img/element-icons/location.svg";
|
||||
import { Icon as MapFallbackImage } from "../../../../res/img/location/map.svg";
|
||||
import Spinner from "../elements/Spinner";
|
||||
|
||||
|
|
|
@ -9,8 +9,8 @@ Please see LICENSE files in the repository root for full details.
|
|||
import React, { ReactNode, useState } from "react";
|
||||
import classNames from "classnames";
|
||||
import { RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import LocationIcon from "@vector-im/compound-design-tokens/assets/web/icons/location-pin-solid";
|
||||
|
||||
import { Icon as LocationIcon } from "../../../../res/img/element-icons/location.svg";
|
||||
import { getUserNameColorClass } from "../../../utils/FormattingUtils";
|
||||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
|||
*/
|
||||
|
||||
import React, { HTMLAttributes, useContext } from "react";
|
||||
import LocationIcon from "@vector-im/compound-design-tokens/assets/web/icons/location-pin-solid";
|
||||
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
@ -14,7 +15,6 @@ import { OwnProfileStore } from "../../../stores/OwnProfileStore";
|
|||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import Heading from "../typography/Heading";
|
||||
import { Icon as LocationIcon } from "../../../../res/img/element-icons/location.svg";
|
||||
import { LocationShareType } from "./shareLocation";
|
||||
import StyledLiveBeaconIcon from "../beacon/StyledLiveBeaconIcon";
|
||||
|
||||
|
|
|
@ -22,16 +22,6 @@ export function Map(props: ComponentProps<typeof MapComponent>): JSX.Element {
|
|||
);
|
||||
}
|
||||
|
||||
const LocationPickerComponent = lazy(() => import("./LocationPicker"));
|
||||
|
||||
export function LocationPicker(props: ComponentProps<typeof LocationPickerComponent>): JSX.Element {
|
||||
return (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<LocationPickerComponent {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
const SmartMarkerComponent = lazy(() => import("./SmartMarker"));
|
||||
|
||||
export function SmartMarker(props: ComponentProps<typeof SmartMarkerComponent>): JSX.Element {
|
||||
|
|
|
@ -98,25 +98,29 @@ export default class DateSeparator extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private getLabel(): string {
|
||||
const date = new Date(this.props.ts);
|
||||
const disableRelativeTimestamps = !SettingsStore.getValue(UIFeature.TimelineEnableRelativeDates);
|
||||
try {
|
||||
const date = new Date(this.props.ts);
|
||||
const disableRelativeTimestamps = !SettingsStore.getValue(UIFeature.TimelineEnableRelativeDates);
|
||||
|
||||
// During the time the archive is being viewed, a specific day might not make sense, so we return the full date
|
||||
if (this.props.forExport || disableRelativeTimestamps) return formatFullDateNoTime(date);
|
||||
// During the time the archive is being viewed, a specific day might not make sense, so we return the full date
|
||||
if (this.props.forExport || disableRelativeTimestamps) return formatFullDateNoTime(date);
|
||||
|
||||
const today = new Date();
|
||||
const yesterday = new Date();
|
||||
const days = getDaysArray("long");
|
||||
yesterday.setDate(today.getDate() - 1);
|
||||
const today = new Date();
|
||||
const yesterday = new Date();
|
||||
const days = getDaysArray("long");
|
||||
yesterday.setDate(today.getDate() - 1);
|
||||
|
||||
if (date.toDateString() === today.toDateString()) {
|
||||
return this.relativeTimeFormat.format(0, "day"); // Today
|
||||
} else if (date.toDateString() === yesterday.toDateString()) {
|
||||
return this.relativeTimeFormat.format(-1, "day"); // Yesterday
|
||||
} else if (today.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
|
||||
return days[date.getDay()]; // Sunday-Saturday
|
||||
} else {
|
||||
return formatFullDateNoTime(date);
|
||||
if (date.toDateString() === today.toDateString()) {
|
||||
return this.relativeTimeFormat.format(0, "day"); // Today
|
||||
} else if (date.toDateString() === yesterday.toDateString()) {
|
||||
return this.relativeTimeFormat.format(-1, "day"); // Yesterday
|
||||
} else if (today.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
|
||||
return days[date.getDay()]; // Sunday-Saturday
|
||||
} else {
|
||||
return formatFullDateNoTime(date);
|
||||
}
|
||||
} catch {
|
||||
return _t("common|message_timestamp_invalid");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,8 +9,8 @@ Please see LICENSE files in the repository root for full details.
|
|||
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import { DownloadIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { Icon as DownloadIcon } from "../../../../res/img/download.svg";
|
||||
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
|
||||
import { RovingAccessibleButton } from "../../../accessibility/RovingTabIndex";
|
||||
import Spinner from "../elements/Spinner";
|
||||
|
|
|
@ -6,18 +6,18 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { forwardRef, useContext } from "react";
|
||||
import React, { forwardRef } from "react";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import type { RoomEncryptionEventContent } from "matrix-js-sdk/src/types";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import EventTileBubble from "./EventTileBubble";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import { objectHasDiff } from "../../../utils/objects";
|
||||
import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom";
|
||||
import { MEGOLM_ENCRYPTION_ALGORITHM } from "../../../utils/crypto";
|
||||
import { useIsEncrypted } from "../../../hooks/useIsEncrypted.ts";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
|
@ -25,9 +25,9 @@ interface IProps {
|
|||
}
|
||||
|
||||
const EncryptionEvent = forwardRef<HTMLDivElement, IProps>(({ mxEvent, timestamp }, ref) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const cli = useMatrixClientContext();
|
||||
const roomId = mxEvent.getRoomId()!;
|
||||
const isRoomEncrypted = MatrixClientPeg.safeGet().isRoomEncrypted(roomId);
|
||||
const isRoomEncrypted = useIsEncrypted(cli, cli.getRoom(roomId) || undefined);
|
||||
|
||||
const prevContent = mxEvent.getPrevContent() as RoomEncryptionEventContent;
|
||||
const content = mxEvent.getContent<RoomEncryptionEventContent>();
|
||||
|
|
|
@ -23,7 +23,7 @@ const JumpToDatePicker: React.FC<IProps> = ({ ts, onDatePicked }: IProps) => {
|
|||
const dateInputDefaultValue = formatDateForInput(date);
|
||||
|
||||
const [dateValue, setDateValue] = useState(dateInputDefaultValue);
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex<HTMLInputElement>();
|
||||
const [onFocus, isActive, refCallback] = useRovingTabIndex<HTMLInputElement>();
|
||||
|
||||
const onDateValueInput = (ev: React.ChangeEvent<HTMLInputElement>): void => setDateValue(ev.target.value);
|
||||
const onJumpToDateSubmit = (ev: FormEvent): void => {
|
||||
|
@ -45,7 +45,7 @@ const JumpToDatePicker: React.FC<IProps> = ({ ts, onDatePicked }: IProps) => {
|
|||
className="mx_JumpToDatePicker_datePicker"
|
||||
label={_t("room|jump_to_date_prompt")}
|
||||
onFocus={onFocus}
|
||||
inputRef={ref}
|
||||
inputRef={refCallback}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
/>
|
||||
<RovingAccessibleButton
|
||||
|
|
|
@ -275,7 +275,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||
}
|
||||
|
||||
const content = this.props.mxEvent.getContent<ImageContent>();
|
||||
let isAnimated = mayBeAnimated(content.info?.mimetype);
|
||||
let isAnimated = content.info?.["org.matrix.msc4230.is_animated"] ?? mayBeAnimated(content.info?.mimetype);
|
||||
|
||||
// If there is no included non-animated thumbnail then we will generate our own, we can't depend on the server
|
||||
// because 1. encryption and 2. we can't ask the server specifically for a non-animated thumbnail.
|
||||
|
@ -298,8 +298,15 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||
}
|
||||
|
||||
try {
|
||||
const blob = await this.props.mediaEventHelper!.sourceBlob.value;
|
||||
if (!(await blobIsAnimated(content.info?.mimetype, blob))) {
|
||||
// If we didn't receive the MSC4230 is_animated flag
|
||||
// then we need to check if the image is animated by downloading it.
|
||||
if (
|
||||
content.info?.["org.matrix.msc4230.is_animated"] === false ||
|
||||
!(await blobIsAnimated(
|
||||
content.info?.mimetype,
|
||||
await this.props.mediaEventHelper!.sourceBlob.value,
|
||||
))
|
||||
) {
|
||||
isAnimated = false;
|
||||
}
|
||||
|
||||
|
|
|
@ -383,7 +383,10 @@ function userResponseFromPollResponseEvent(event: MatrixEvent): UserVote {
|
|||
|
||||
export function allVotes(voteRelations: Relations): Array<UserVote> {
|
||||
if (voteRelations) {
|
||||
return voteRelations.getRelations().map(userResponseFromPollResponseEvent);
|
||||
return voteRelations
|
||||
.getRelations()
|
||||
.filter((e) => !e.isRedacted())
|
||||
.map(userResponseFromPollResponseEvent);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
|
|
@ -28,16 +28,16 @@ import {
|
|||
ReplyIcon,
|
||||
DeleteIcon,
|
||||
RestartIcon,
|
||||
ThreadsIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { Icon as EditIcon } from "../../../../res/img/element-icons/room/message-bar/edit.svg";
|
||||
import { Icon as EmojiIcon } from "../../../../res/img/element-icons/room/message-bar/emoji.svg";
|
||||
import { Icon as ThreadIcon } from "../../../../res/img/element-icons/message/thread.svg";
|
||||
import { Icon as ExpandMessageIcon } from "../../../../res/img/element-icons/expand-message.svg";
|
||||
import { Icon as CollapseMessageIcon } from "../../../../res/img/element-icons/collapse-message.svg";
|
||||
import type { Relations } from "matrix-js-sdk/src/matrix";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import dis, { defaultDispatcher } from "../../../dispatcher/dispatcher";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import ContextMenu, { aboveLeftOf, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
|
||||
import { isContentActionable, canEditContent, editEvent, canCancel } from "../../../utils/EventUtils";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||
|
@ -243,7 +243,7 @@ const ReplyInThreadButton: React.FC<IReplyInThreadButton> = ({ mxEvent }) => {
|
|||
onContextMenu={onClick}
|
||||
placement="left"
|
||||
>
|
||||
<ThreadIcon />
|
||||
<ThreadsIcon />
|
||||
</RovingAccessibleButton>
|
||||
);
|
||||
};
|
||||
|
@ -323,7 +323,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
dis.dispatch({
|
||||
defaultDispatcher.dispatch({
|
||||
action: "reply_to_event",
|
||||
event: this.props.mxEvent,
|
||||
context: this.context.timelineRenderingType,
|
||||
|
|
|
@ -6,7 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import mime from "mime";
|
||||
import React, { createRef } from "react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import {
|
||||
EventType,
|
||||
MsgType,
|
||||
|
@ -15,6 +17,7 @@ import {
|
|||
M_LOCATION,
|
||||
M_POLL_END,
|
||||
M_POLL_START,
|
||||
IContent,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
@ -144,6 +147,103 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
|
|||
this.forceUpdate();
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates that the filename extension and advertised mimetype
|
||||
* of the supplied image/file message content match and are actuallly video/image content.
|
||||
* For image/video messages with a thumbnail it also validates the mimetype is an image.
|
||||
* @param content The mxEvent content of the message
|
||||
* @returns A boolean indicating whether the validation passed
|
||||
*/
|
||||
private validateImageOrVideoMimetype = (content: IContent): boolean => {
|
||||
// As per the spec if filename is not present the body represents the filename
|
||||
const filename = content.filename ?? content.body;
|
||||
if (!filename) {
|
||||
logger.log("Failed to validate image/video content, filename null");
|
||||
return false;
|
||||
}
|
||||
// Check mimetype of the thumbnail
|
||||
if (!this.validateThumbnailMimetype(content)) {
|
||||
logger.log("Failed to validate file/image thumbnail");
|
||||
return false;
|
||||
}
|
||||
|
||||
// if there is no mimetype from the extesion or the mimetype is not image/video validation fails
|
||||
const typeFromExtension = mime.getType(filename) ?? undefined;
|
||||
const extensionMajorMimetype = this.parseMajorMimetype(typeFromExtension);
|
||||
if (!typeFromExtension || !this.validateAllowedMimetype(typeFromExtension, ["image", "video"])) {
|
||||
logger.log("Failed to validate image/video content, invalid or missing extension");
|
||||
return false;
|
||||
}
|
||||
|
||||
// if the content mimetype is set check it is an image/video and that it matches the extesion mimetype otherwise validation fails
|
||||
const contentMimetype = content.info?.mimetype;
|
||||
if (contentMimetype) {
|
||||
const contentMajorMimetype = this.parseMajorMimetype(contentMimetype);
|
||||
if (
|
||||
!this.validateAllowedMimetype(contentMimetype, ["image", "video"]) ||
|
||||
extensionMajorMimetype !== contentMajorMimetype
|
||||
) {
|
||||
logger.log("Failed to validate image/video content, invalid or missing mimetype");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates that the advertised mimetype of the sticker content
|
||||
* is an image.
|
||||
* For stickers with a thumbnail it also validates the mimetype is an image.
|
||||
* @param content The mxEvent content of the message
|
||||
* @returns A boolean indicating whether the validation passed
|
||||
*/
|
||||
private validateStickerMimetype = (content: IContent): boolean => {
|
||||
// Validate mimetype of the thumbnail
|
||||
const thumbnailResult = this.validateThumbnailMimetype(content);
|
||||
if (!thumbnailResult) {
|
||||
logger.log("Failed to validate sticker thumbnail");
|
||||
return false;
|
||||
}
|
||||
// Validate mimetype of the content info is valid if it is set
|
||||
const contentMimetype = content.info?.mimetype;
|
||||
if (contentMimetype && !this.validateAllowedMimetype(contentMimetype, ["image"])) {
|
||||
logger.log("Failed to validate image/video content, invalid or missing mimetype/extensions");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* For image/video messages or stickers that have a thumnail mimetype specified,
|
||||
* validates that the major mimetime is image.
|
||||
* @param content The mxEvent content of the message
|
||||
* @returns A boolean indicating whether the validation passed
|
||||
*/
|
||||
private validateThumbnailMimetype = (content: IContent): boolean => {
|
||||
const thumbnailMimetype = content.info?.thumbnail_info?.mimetype;
|
||||
return !thumbnailMimetype || this.validateAllowedMimetype(thumbnailMimetype, ["image"]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates that the major part of a mimetime from an allowed list.
|
||||
* @param mimetype The mimetype to validate
|
||||
* @param allowedMajorMimeTypes The list of allowed major mimetimes
|
||||
* @returns A boolean indicating whether the validation passed
|
||||
*/
|
||||
private validateAllowedMimetype = (mimetype: string, allowedMajorMimeTypes: string[]): boolean => {
|
||||
const majorMimetype = this.parseMajorMimetype(mimetype);
|
||||
return !!majorMimetype && allowedMajorMimeTypes.includes(majorMimetype);
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses and returns the the major part of a mimetype(before the "/").
|
||||
* @param mimetype As optional mimetype string to parse
|
||||
* @returns The major part of the mimetype string or undefined
|
||||
*/
|
||||
private parseMajorMimetype(mimetype?: string): string | undefined {
|
||||
return mimetype?.split("/")[0];
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const type = this.props.mxEvent.getType();
|
||||
|
@ -165,6 +265,13 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
|
|||
BodyType = UnknownBody;
|
||||
}
|
||||
|
||||
if (
|
||||
((BodyType === MImageBody || BodyType == MVideoBody) && !this.validateImageOrVideoMimetype(content)) ||
|
||||
(BodyType === MStickerBody && !this.validateStickerMimetype(content))
|
||||
) {
|
||||
BodyType = this.bodyTypes.get(MsgType.File)!;
|
||||
}
|
||||
|
||||
// TODO: move to eventTypes when location sharing spec stabilises
|
||||
if (M_LOCATION.matches(type) || (type === EventType.RoomMessage && msgtype === MsgType.Location)) {
|
||||
BodyType = MLocationBody;
|
||||
|
|
|
@ -9,107 +9,144 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { ReactNode } from "react";
|
||||
import React, { ReactNode, JSX } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
import { InlineSpinner } from "@vector-im/compound-web";
|
||||
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
import SettingsFlag from "../elements/SettingsFlag";
|
||||
import SettingsFieldset from "../settings/SettingsFieldset";
|
||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import { useIsEncrypted } from "../../../hooks/useIsEncrypted.ts";
|
||||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext.tsx";
|
||||
import { useSettingValueAt } from "../../../hooks/useSettings.ts";
|
||||
|
||||
interface IProps {
|
||||
/**
|
||||
* The URL preview settings for a room
|
||||
*/
|
||||
interface UrlPreviewSettingsProps {
|
||||
/**
|
||||
* The room.
|
||||
*/
|
||||
room: Room;
|
||||
}
|
||||
|
||||
export default class UrlPreviewSettings extends React.Component<IProps> {
|
||||
private onClickUserSettings = (e: ButtonEvent): void => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dis.fire(Action.ViewUserSettings);
|
||||
};
|
||||
export function UrlPreviewSettings({ room }: UrlPreviewSettingsProps): JSX.Element {
|
||||
const { roomId } = room;
|
||||
const matrixClient = useMatrixClientContext();
|
||||
const isEncrypted = useIsEncrypted(matrixClient, room);
|
||||
const isLoading = isEncrypted === null;
|
||||
|
||||
public render(): ReactNode {
|
||||
const roomId = this.props.room.roomId;
|
||||
const isEncrypted = MatrixClientPeg.safeGet().isRoomEncrypted(roomId);
|
||||
|
||||
let previewsForAccount: ReactNode | undefined;
|
||||
let previewsForRoom: ReactNode | undefined;
|
||||
|
||||
if (!isEncrypted) {
|
||||
// Only show account setting state and room state setting state in non-e2ee rooms where they apply
|
||||
const accountEnabled = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled");
|
||||
if (accountEnabled) {
|
||||
previewsForAccount = _t(
|
||||
"room_settings|general|user_url_previews_default_on",
|
||||
{},
|
||||
{
|
||||
a: (sub) => (
|
||||
<AccessibleButton kind="link_inline" onClick={this.onClickUserSettings}>
|
||||
{sub}
|
||||
</AccessibleButton>
|
||||
),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
previewsForAccount = _t(
|
||||
"room_settings|general|user_url_previews_default_off",
|
||||
{},
|
||||
{
|
||||
a: (sub) => (
|
||||
<AccessibleButton kind="link_inline" onClick={this.onClickUserSettings}>
|
||||
{sub}
|
||||
</AccessibleButton>
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, SettingLevel.ROOM)) {
|
||||
previewsForRoom = (
|
||||
return (
|
||||
<SettingsFieldset
|
||||
legend={_t("room_settings|general|url_previews_section")}
|
||||
description={!isLoading && <Description isEncrypted={isEncrypted} />}
|
||||
>
|
||||
{isLoading ? (
|
||||
<InlineSpinner />
|
||||
) : (
|
||||
<>
|
||||
<PreviewsForRoom isEncrypted={isEncrypted} roomId={roomId} />
|
||||
<SettingsFlag
|
||||
name="urlPreviewsEnabled"
|
||||
level={SettingLevel.ROOM}
|
||||
name={isEncrypted ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled"}
|
||||
level={SettingLevel.ROOM_DEVICE}
|
||||
roomId={roomId}
|
||||
isExplicit={true}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
let str = _td("room_settings|general|default_url_previews_on");
|
||||
if (!SettingsStore.getValueAt(SettingLevel.ROOM, "urlPreviewsEnabled", roomId, /*explicit=*/ true)) {
|
||||
str = _td("room_settings|general|default_url_previews_off");
|
||||
}
|
||||
previewsForRoom = <div>{_t(str)}</div>;
|
||||
}
|
||||
} else {
|
||||
previewsForAccount = _t("room_settings|general|url_preview_encryption_warning");
|
||||
}
|
||||
</>
|
||||
)}
|
||||
</SettingsFieldset>
|
||||
);
|
||||
}
|
||||
|
||||
const previewsForRoomAccount = // in an e2ee room we use a special key to enforce per-room opt-in
|
||||
(
|
||||
<SettingsFlag
|
||||
name={isEncrypted ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled"}
|
||||
level={SettingLevel.ROOM_DEVICE}
|
||||
roomId={roomId}
|
||||
/>
|
||||
);
|
||||
/**
|
||||
* Click handler for the user settings link
|
||||
* @param e
|
||||
*/
|
||||
function onClickUserSettings(e: ButtonEvent): void {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dis.fire(Action.ViewUserSettings);
|
||||
}
|
||||
|
||||
const description = (
|
||||
<>
|
||||
<p>{_t("room_settings|general|url_preview_explainer")}</p>
|
||||
<p>{previewsForAccount}</p>
|
||||
</>
|
||||
/**
|
||||
* The description for the URL preview settings
|
||||
*/
|
||||
interface DescriptionProps {
|
||||
/**
|
||||
* Whether the room is encrypted
|
||||
*/
|
||||
isEncrypted: boolean;
|
||||
}
|
||||
|
||||
function Description({ isEncrypted }: DescriptionProps): JSX.Element {
|
||||
const urlPreviewsEnabled = useSettingValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled");
|
||||
|
||||
let previewsForAccount: ReactNode | undefined;
|
||||
if (isEncrypted) {
|
||||
previewsForAccount = _t("room_settings|general|url_preview_encryption_warning");
|
||||
} else {
|
||||
const button = {
|
||||
a: (sub: string) => (
|
||||
<AccessibleButton kind="link_inline" onClick={onClickUserSettings}>
|
||||
{sub}
|
||||
</AccessibleButton>
|
||||
),
|
||||
};
|
||||
|
||||
previewsForAccount = urlPreviewsEnabled
|
||||
? _t("room_settings|general|user_url_previews_default_on", {}, button)
|
||||
: _t("room_settings|general|user_url_previews_default_off", {}, button);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>{_t("room_settings|general|url_preview_explainer")}</p>
|
||||
<p>{previewsForAccount}</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The description for the URL preview settings
|
||||
*/
|
||||
interface PreviewsForRoomProps {
|
||||
/**
|
||||
* Whether the room is encrypted
|
||||
*/
|
||||
isEncrypted: boolean;
|
||||
/**
|
||||
* The room ID
|
||||
*/
|
||||
roomId: string;
|
||||
}
|
||||
|
||||
function PreviewsForRoom({ isEncrypted, roomId }: PreviewsForRoomProps): JSX.Element | null {
|
||||
const urlPreviewsEnabled = useSettingValueAt(
|
||||
SettingLevel.ACCOUNT,
|
||||
"urlPreviewsEnabled",
|
||||
roomId,
|
||||
/*explicit=*/ true,
|
||||
);
|
||||
if (isEncrypted) return null;
|
||||
|
||||
let previewsForRoom: ReactNode;
|
||||
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, SettingLevel.ROOM)) {
|
||||
previewsForRoom = (
|
||||
<SettingsFlag name="urlPreviewsEnabled" level={SettingLevel.ROOM} roomId={roomId} isExplicit={true} />
|
||||
);
|
||||
|
||||
return (
|
||||
<SettingsFieldset legend={_t("room_settings|general|url_previews_section")} description={description}>
|
||||
{previewsForRoom}
|
||||
{previewsForRoomAccount}
|
||||
</SettingsFieldset>
|
||||
} else {
|
||||
previewsForRoom = (
|
||||
<div>
|
||||
{urlPreviewsEnabled
|
||||
? _t("room_settings|general|default_url_previews_on")
|
||||
: _t("room_settings|general|default_url_previews_off")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return previewsForRoom;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import React, { createRef, KeyboardEvent, RefObject } from "react";
|
|||
import classNames from "classnames";
|
||||
import { flatMap } from "lodash";
|
||||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
import { defer } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import Autocompleter, { ICompletion, ISelectionRange, IProviderCompletions } from "../../../autocomplete/Autocompleter";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
@ -127,18 +128,21 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
private async processQuery(query: string, selection: ISelectionRange): Promise<void> {
|
||||
return this.autocompleter
|
||||
?.getCompletions(query, selection, this.state.forceComplete, MAX_PROVIDER_MATCHES)
|
||||
.then((completions) => {
|
||||
// Only ever process the completions for the most recent query being processed
|
||||
if (query !== this.queryRequested) {
|
||||
return;
|
||||
}
|
||||
this.processCompletions(completions);
|
||||
});
|
||||
if (!this.autocompleter) return;
|
||||
const completions = await this.autocompleter.getCompletions(
|
||||
query,
|
||||
selection,
|
||||
this.state.forceComplete,
|
||||
MAX_PROVIDER_MATCHES,
|
||||
);
|
||||
// Only ever process the completions for the most recent query being processed
|
||||
if (query !== this.queryRequested) {
|
||||
return;
|
||||
}
|
||||
await this.processCompletions(completions);
|
||||
}
|
||||
|
||||
private processCompletions(completions: IProviderCompletions[]): void {
|
||||
private async processCompletions(completions: IProviderCompletions[]): Promise<void> {
|
||||
const completionList = flatMap(completions, (provider) => provider.completions);
|
||||
|
||||
// Reset selection when completion list becomes empty.
|
||||
|
@ -169,14 +173,19 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
completions,
|
||||
completionList,
|
||||
selectionOffset,
|
||||
hide,
|
||||
// Force complete is turned off each time since we can't edit the query in that case
|
||||
forceComplete: false,
|
||||
});
|
||||
const deferred = defer<void>();
|
||||
this.setState(
|
||||
{
|
||||
completions,
|
||||
completionList,
|
||||
selectionOffset,
|
||||
hide,
|
||||
// Force complete is turned off each time since we can't edit the query in that case
|
||||
forceComplete: false,
|
||||
},
|
||||
deferred.resolve,
|
||||
);
|
||||
await deferred.promise;
|
||||
}
|
||||
|
||||
public hasSelection(): boolean {
|
||||
|
|
|
@ -19,9 +19,7 @@ import { XOR } from "../../../@types/common";
|
|||
export enum E2EState {
|
||||
Verified = "verified",
|
||||
Warning = "warning",
|
||||
Unknown = "unknown",
|
||||
Normal = "normal",
|
||||
Unauthenticated = "unauthenticated",
|
||||
}
|
||||
|
||||
const crossSigningUserTitles: { [key in E2EState]?: TranslationKey } = {
|
||||
|
|
|
@ -757,6 +757,14 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||
case EventShieldReason.MISMATCHED_SENDER_KEY:
|
||||
shieldReasonMessage = _t("encryption|event_shield_reason_mismatched_sender_key");
|
||||
break;
|
||||
|
||||
case EventShieldReason.SENT_IN_CLEAR:
|
||||
shieldReasonMessage = _t("common|unencrypted");
|
||||
break;
|
||||
|
||||
case EventShieldReason.VERIFICATION_VIOLATION:
|
||||
shieldReasonMessage = _t("timeline|decryption_failure|sender_identity_previously_verified");
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.state.shieldColour === EventShieldColour.GREY) {
|
||||
|
|
|
@ -7,11 +7,11 @@ Please see LICENSE files in the repository root for full details.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import LinkIcon from "@vector-im/compound-design-tokens/assets/web/icons/link";
|
||||
|
||||
import { RovingAccessibleButton } from "../../../../accessibility/RovingTabIndex";
|
||||
import Toolbar from "../../../../accessibility/Toolbar";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { Icon as LinkIcon } from "../../../../../res/img/element-icons/link.svg";
|
||||
import { Icon as ViewInRoomIcon } from "../../../../../res/img/element-icons/view-in-room.svg";
|
||||
import { ButtonEvent } from "../../elements/AccessibleButton";
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ import E2EIcon from "./E2EIcon";
|
|||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { aboveLeftOf, MenuProps } from "../../structures/ContextMenu";
|
||||
import ReplyPreview from "./ReplyPreview";
|
||||
import { UserIdentityWarning } from "./UserIdentityWarning";
|
||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
import VoiceRecordComposerTile from "./VoiceRecordComposerTile";
|
||||
import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore";
|
||||
|
@ -414,7 +415,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||
this.messageComposerInput.current?.sendMessage();
|
||||
|
||||
if (this.state.isWysiwygLabEnabled) {
|
||||
const { permalinkCreator, relation, replyToEvent } = this.props;
|
||||
const { relation, replyToEvent } = this.props;
|
||||
const composerContent = this.state.composerContent;
|
||||
this.setState({ composerContent: "", initialComposerContent: "" });
|
||||
dis.dispatch({
|
||||
|
@ -424,7 +425,6 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||
await sendMessage(composerContent, this.state.isRichTextEnabled, {
|
||||
mxClient: this.props.mxClient,
|
||||
roomContext: this.context,
|
||||
permalinkCreator,
|
||||
relation,
|
||||
replyToEvent,
|
||||
});
|
||||
|
@ -582,7 +582,6 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||
key="controls_input"
|
||||
room={this.props.room}
|
||||
placeholder={this.renderPlaceholderText()}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
relation={this.props.relation}
|
||||
replyToEvent={this.props.replyToEvent}
|
||||
onChange={this.onChange}
|
||||
|
@ -597,7 +596,6 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||
key="controls_voice_record"
|
||||
ref={this.voiceRecordingButton}
|
||||
room={this.props.room}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
relation={this.props.relation}
|
||||
replyToEvent={this.props.replyToEvent}
|
||||
/>,
|
||||
|
@ -642,8 +640,6 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
let recordingTooltip: JSX.Element | undefined;
|
||||
|
||||
const isTooltipOpen = Boolean(this.state.recordingTimeLeftSeconds);
|
||||
const secondsLeft = this.state.recordingTimeLeftSeconds ? Math.round(this.state.recordingTimeLeftSeconds) : 0;
|
||||
|
||||
|
@ -673,8 +669,8 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||
return (
|
||||
<Tooltip open={isTooltipOpen} description={formatTimeLeft(secondsLeft)} placement="bottom">
|
||||
<div className={classes} ref={this.ref} role="region" aria-label={_t("a11y|message_composer")}>
|
||||
{recordingTooltip}
|
||||
<div className="mx_MessageComposer_wrapper">
|
||||
<UserIdentityWarning room={this.props.room} key={this.props.room.roomId} />
|
||||
<ReplyPreview
|
||||
replyToEvent={this.props.replyToEvent}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
|
|
|
@ -47,7 +47,6 @@ import { CHAT_EFFECTS } from "../../../effects";
|
|||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../sendTimePerformanceMetrics";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||
|
@ -177,8 +176,6 @@ export function createMessageContent(
|
|||
model: EditorModel,
|
||||
replyToEvent: MatrixEvent | undefined,
|
||||
relation: IEventRelation | undefined,
|
||||
permalinkCreator?: RoomPermalinkCreator,
|
||||
includeReplyLegacyFallback = true,
|
||||
): RoomMessageEventContent {
|
||||
const isEmote = containsEmote(model);
|
||||
if (isEmote) {
|
||||
|
@ -209,10 +206,7 @@ export function createMessageContent(
|
|||
|
||||
attachRelation(content, relation);
|
||||
if (replyToEvent) {
|
||||
addReplyToMessageContent(content, replyToEvent, {
|
||||
permalinkCreator,
|
||||
includeLegacyFallback: includeReplyLegacyFallback,
|
||||
});
|
||||
addReplyToMessageContent(content, replyToEvent);
|
||||
}
|
||||
|
||||
return content;
|
||||
|
@ -238,12 +232,10 @@ export function isQuickReaction(model: EditorModel): boolean {
|
|||
interface ISendMessageComposerProps extends MatrixClientProps {
|
||||
room: Room;
|
||||
placeholder?: string;
|
||||
permalinkCreator?: RoomPermalinkCreator;
|
||||
relation?: IEventRelation;
|
||||
replyToEvent?: MatrixEvent;
|
||||
disabled?: boolean;
|
||||
onChange?(model: EditorModel): void;
|
||||
includeReplyLegacyFallback?: boolean;
|
||||
toggleStickerPickerOpen: () => void;
|
||||
}
|
||||
|
||||
|
@ -258,10 +250,6 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
private dispatcherRef?: string;
|
||||
private sendHistoryManager: SendHistoryManager;
|
||||
|
||||
public static defaultProps = {
|
||||
includeReplyLegacyFallback: true,
|
||||
};
|
||||
|
||||
public constructor(props: ISendMessageComposerProps, context: React.ContextType<typeof RoomContext>) {
|
||||
super(props, context);
|
||||
|
||||
|
@ -500,11 +488,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
attachMentions(this.props.mxClient.getSafeUserId(), content, model, replyToEvent);
|
||||
attachRelation(content, this.props.relation);
|
||||
if (replyToEvent) {
|
||||
addReplyToMessageContent(content, replyToEvent, {
|
||||
permalinkCreator: this.props.permalinkCreator,
|
||||
// Exclude the legacy fallback for custom event types such as those used by /fireworks
|
||||
includeLegacyFallback: content.msgtype?.startsWith("m.") ?? true,
|
||||
});
|
||||
addReplyToMessageContent(content, replyToEvent);
|
||||
}
|
||||
} else {
|
||||
shouldSend = false;
|
||||
|
@ -534,8 +518,6 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
model,
|
||||
replyToEvent,
|
||||
this.props.relation,
|
||||
this.props.permalinkCreator,
|
||||
this.props.includeReplyLegacyFallback,
|
||||
);
|
||||
}
|
||||
// don't bother sending an empty message
|
||||
|
|
328
src/components/views/rooms/UserIdentityWarning.tsx
Normal file
328
src/components/views/rooms/UserIdentityWarning.tsx
Normal file
|
@ -0,0 +1,328 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useRef, useState } from "react";
|
||||
import { EventType, KnownMembership, MatrixEvent, Room, RoomStateEvent, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { CryptoApi, CryptoEvent, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { Button, Separator } from "@vector-im/compound-web";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
||||
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
|
||||
interface UserIdentityWarningProps {
|
||||
/**
|
||||
* The current room being viewed.
|
||||
*/
|
||||
room: Room;
|
||||
/**
|
||||
* The ID of the room being viewed. This is used to ensure that the
|
||||
* component's state and references are cleared when the room changes.
|
||||
*/
|
||||
key: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the given user's identity need to be approved?
|
||||
*/
|
||||
async function userNeedsApproval(crypto: CryptoApi, userId: string): Promise<boolean> {
|
||||
const verificationStatus = await crypto.getUserVerificationStatus(userId);
|
||||
return verificationStatus.needsUserApproval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the component is uninitialised, is in the process of initialising, or
|
||||
* has completed initialising.
|
||||
*/
|
||||
enum InitialisationStatus {
|
||||
Uninitialised,
|
||||
Initialising,
|
||||
Completed,
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a banner warning when there is an issue with a user's identity.
|
||||
*
|
||||
* Warns when an unverified user's identity has changed, and gives the user a
|
||||
* button to acknowledge the change.
|
||||
*/
|
||||
export const UserIdentityWarning: React.FC<UserIdentityWarningProps> = ({ room }) => {
|
||||
const cli = useMatrixClientContext();
|
||||
const crypto = cli.getCrypto();
|
||||
|
||||
// The current room member that we are prompting the user to approve.
|
||||
// `undefined` means we are not currently showing a prompt.
|
||||
const [currentPrompt, setCurrentPrompt] = useState<RoomMember | undefined>(undefined);
|
||||
|
||||
// Whether or not we've already initialised the component by loading the
|
||||
// room membership.
|
||||
const initialisedRef = useRef<InitialisationStatus>(InitialisationStatus.Uninitialised);
|
||||
// Which room members need their identity approved.
|
||||
const membersNeedingApprovalRef = useRef<Map<string, RoomMember>>(new Map());
|
||||
// For each user, we assign a sequence number to each verification status
|
||||
// that we get, or fetch.
|
||||
//
|
||||
// Since fetching a verification status is asynchronous, we could get an
|
||||
// update in the middle of fetching the verification status, which could
|
||||
// mean that the status that we fetched is out of date. So if the current
|
||||
// sequence number is not the same as the sequence number when we started
|
||||
// the fetch, then we drop our fetched result, under the assumption that the
|
||||
// update that we received is the most up-to-date version. If it is in fact
|
||||
// not the most up-to-date version, then we should be receiving a new update
|
||||
// soon with the newer value, so it will fix itself in the end.
|
||||
//
|
||||
// We also assign a sequence number when the user leaves the room, in order
|
||||
// to prevent prompting about a user who leaves while we are fetching their
|
||||
// verification status.
|
||||
const verificationStatusSequencesRef = useRef<Map<string, number>>(new Map());
|
||||
const incrementVerificationStatusSequence = (userId: string): number => {
|
||||
const verificationStatusSequences = verificationStatusSequencesRef.current;
|
||||
const value = verificationStatusSequences.get(userId);
|
||||
const newValue = value === undefined ? 1 : value + 1;
|
||||
verificationStatusSequences.set(userId, newValue);
|
||||
return newValue;
|
||||
};
|
||||
|
||||
// Update the current prompt. Select a new user if needed, or hide the
|
||||
// warning if we don't have anyone to warn about.
|
||||
const updateCurrentPrompt = useCallback((): undefined => {
|
||||
const membersNeedingApproval = membersNeedingApprovalRef.current;
|
||||
// We have to do this in a callback to `setCurrentPrompt`
|
||||
// because this function could have been called after an
|
||||
// `await`, and the `currentPrompt` that this function would
|
||||
// have may be outdated.
|
||||
setCurrentPrompt((currentPrompt) => {
|
||||
// If we're already displaying a warning, and that user still needs
|
||||
// approval, continue showing that user.
|
||||
if (currentPrompt && membersNeedingApproval.has(currentPrompt.userId)) return currentPrompt;
|
||||
|
||||
if (membersNeedingApproval.size === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// We pick the user with the smallest user ID.
|
||||
const keys = Array.from(membersNeedingApproval.keys()).sort((a, b) => a.localeCompare(b));
|
||||
const selection = membersNeedingApproval.get(keys[0]!);
|
||||
return selection;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Add a user to the membersNeedingApproval map, and update the current
|
||||
// prompt if necessary. The user will only be added if they are actually a
|
||||
// member of the room. If they are not a member, this function will do
|
||||
// nothing.
|
||||
const addMemberNeedingApproval = useCallback(
|
||||
(userId: string, member?: RoomMember): void => {
|
||||
if (userId === cli.getUserId()) {
|
||||
// We always skip our own user, because we can't pin our own identity.
|
||||
return;
|
||||
}
|
||||
member = member ?? room.getMember(userId) ?? undefined;
|
||||
if (!member) return;
|
||||
|
||||
membersNeedingApprovalRef.current.set(userId, member);
|
||||
// We only select the prompt if we are done initialising,
|
||||
// because we will select the prompt after we're done
|
||||
// initialising, and we want to start by displaying a warning
|
||||
// for the user with the smallest ID.
|
||||
if (initialisedRef.current === InitialisationStatus.Completed) {
|
||||
updateCurrentPrompt();
|
||||
}
|
||||
},
|
||||
[cli, room, updateCurrentPrompt],
|
||||
);
|
||||
|
||||
// For each user in the list check if their identity needs approval, and if
|
||||
// so, add them to the membersNeedingApproval map and update the prompt if
|
||||
// needed.
|
||||
const addMembersWhoNeedApproval = useCallback(
|
||||
async (members: RoomMember[]): Promise<void> => {
|
||||
const verificationStatusSequences = verificationStatusSequencesRef.current;
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
for (const member of members) {
|
||||
const userId = member.userId;
|
||||
const sequenceNum = incrementVerificationStatusSequence(userId);
|
||||
promises.push(
|
||||
userNeedsApproval(crypto!, userId).then((needsApproval) => {
|
||||
if (needsApproval) {
|
||||
// Only actually update the list if we have the most
|
||||
// recent value.
|
||||
if (verificationStatusSequences.get(userId) === sequenceNum) {
|
||||
addMemberNeedingApproval(userId, member);
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
},
|
||||
[crypto, addMemberNeedingApproval],
|
||||
);
|
||||
|
||||
// Remove a user from the membersNeedingApproval map, and update the current
|
||||
// prompt if necessary.
|
||||
const removeMemberNeedingApproval = useCallback(
|
||||
(userId: string): void => {
|
||||
membersNeedingApprovalRef.current.delete(userId);
|
||||
updateCurrentPrompt();
|
||||
},
|
||||
[updateCurrentPrompt],
|
||||
);
|
||||
|
||||
// Initialise the component. Get the room members, check which ones need
|
||||
// their identity approved, and pick one to display.
|
||||
const loadMembers = useCallback(async (): Promise<void> => {
|
||||
if (!crypto || initialisedRef.current !== InitialisationStatus.Uninitialised) {
|
||||
return;
|
||||
}
|
||||
// If encryption is not enabled in the room, we don't need to do
|
||||
// anything. If encryption gets enabled later, we will retry, via
|
||||
// onRoomStateEvent.
|
||||
if (!(await crypto.isEncryptionEnabledInRoom(room.roomId))) {
|
||||
return;
|
||||
}
|
||||
initialisedRef.current = InitialisationStatus.Initialising;
|
||||
|
||||
const members = await room.getEncryptionTargetMembers();
|
||||
await addMembersWhoNeedApproval(members);
|
||||
|
||||
updateCurrentPrompt();
|
||||
initialisedRef.current = InitialisationStatus.Completed;
|
||||
}, [crypto, room, addMembersWhoNeedApproval, updateCurrentPrompt]);
|
||||
|
||||
loadMembers().catch((e) => {
|
||||
logger.error("Error initialising UserIdentityWarning:", e);
|
||||
});
|
||||
|
||||
// When a user's verification status changes, we check if they need to be
|
||||
// added/removed from the set of members needing approval.
|
||||
const onUserVerificationStatusChanged = useCallback(
|
||||
(userId: string, verificationStatus: UserVerificationStatus): void => {
|
||||
// If we haven't started initialising, that means that we're in a
|
||||
// room where we don't need to display any warnings.
|
||||
if (initialisedRef.current === InitialisationStatus.Uninitialised) {
|
||||
return;
|
||||
}
|
||||
|
||||
incrementVerificationStatusSequence(userId);
|
||||
|
||||
if (verificationStatus.needsUserApproval) {
|
||||
addMemberNeedingApproval(userId);
|
||||
} else {
|
||||
removeMemberNeedingApproval(userId);
|
||||
}
|
||||
},
|
||||
[addMemberNeedingApproval, removeMemberNeedingApproval],
|
||||
);
|
||||
useTypedEventEmitter(cli, CryptoEvent.UserTrustStatusChanged, onUserVerificationStatusChanged);
|
||||
|
||||
// We watch for encryption events (since we only display warnings in
|
||||
// encrypted rooms), and for membership changes (since we only display
|
||||
// warnings for users in the room).
|
||||
const onRoomStateEvent = useCallback(
|
||||
async (event: MatrixEvent): Promise<void> => {
|
||||
if (!crypto || event.getRoomId() !== room.roomId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventType = event.getType();
|
||||
if (eventType === EventType.RoomEncryption && event.getStateKey() === "") {
|
||||
// Room is now encrypted, so we can initialise the component.
|
||||
return loadMembers().catch((e) => {
|
||||
logger.error("Error initialising UserIdentityWarning:", e);
|
||||
});
|
||||
} else if (eventType !== EventType.RoomMember) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We're processing an m.room.member event
|
||||
|
||||
if (initialisedRef.current === InitialisationStatus.Uninitialised) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = event.getStateKey();
|
||||
|
||||
if (!userId) return;
|
||||
|
||||
if (
|
||||
event.getContent().membership === KnownMembership.Join ||
|
||||
(event.getContent().membership === KnownMembership.Invite && room.shouldEncryptForInvitedMembers())
|
||||
) {
|
||||
// Someone's membership changed and we will now encrypt to them. If
|
||||
// their identity needs approval, show a warning.
|
||||
const member = room.getMember(userId);
|
||||
if (member) {
|
||||
await addMembersWhoNeedApproval([member]).catch((e) => {
|
||||
logger.error("Error adding member in UserIdentityWarning:", e);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Someone's membership changed and we no longer encrypt to them.
|
||||
// If we're showing a warning about them, we don't need to any more.
|
||||
removeMemberNeedingApproval(userId);
|
||||
incrementVerificationStatusSequence(userId);
|
||||
}
|
||||
},
|
||||
[crypto, room, addMembersWhoNeedApproval, removeMemberNeedingApproval, loadMembers],
|
||||
);
|
||||
useTypedEventEmitter(cli, RoomStateEvent.Events, onRoomStateEvent);
|
||||
|
||||
if (!crypto || !currentPrompt) return null;
|
||||
|
||||
const confirmIdentity = async (): Promise<void> => {
|
||||
await crypto.pinCurrentUserIdentity(currentPrompt.userId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx_UserIdentityWarning">
|
||||
<Separator />
|
||||
<div className="mx_UserIdentityWarning_row">
|
||||
<MemberAvatar member={currentPrompt} title={currentPrompt.userId} size="30px" />
|
||||
<span className="mx_UserIdentityWarning_main">
|
||||
{currentPrompt.rawDisplayName === currentPrompt.userId
|
||||
? _t(
|
||||
"encryption|pinned_identity_changed_no_displayname",
|
||||
{ userId: currentPrompt.userId },
|
||||
{
|
||||
a: substituteATag,
|
||||
b: substituteBTag,
|
||||
},
|
||||
)
|
||||
: _t(
|
||||
"encryption|pinned_identity_changed",
|
||||
{ displayName: currentPrompt.rawDisplayName, userId: currentPrompt.userId },
|
||||
{
|
||||
a: substituteATag,
|
||||
b: substituteBTag,
|
||||
},
|
||||
)}
|
||||
</span>
|
||||
<Button kind="primary" size="sm" onClick={confirmIdentity}>
|
||||
{_t("action|ok")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function substituteATag(sub: string): React.ReactNode {
|
||||
return (
|
||||
<a href="https://element.io/help#encryption18" target="_blank" rel="noreferrer noopener">
|
||||
{sub}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
function substituteBTag(sub: string): React.ReactNode {
|
||||
return <b>{sub}</b>;
|
||||
}
|
|
@ -31,7 +31,6 @@ import { doMaybeLocalRoomAction } from "../../../utils/local-room";
|
|||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import { attachMentions, attachRelation } from "./SendMessageComposer";
|
||||
import { addReplyToMessageContent } from "../../../utils/Reply";
|
||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import { IUpload, VoiceMessageRecording } from "../../../audio/VoiceMessageRecording";
|
||||
import { createVoiceMessageContent } from "../../../utils/createVoiceMessageContent";
|
||||
|
@ -39,7 +38,6 @@ import AccessibleButton from "../elements/AccessibleButton";
|
|||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
permalinkCreator?: RoomPermalinkCreator;
|
||||
relation?: IEventRelation;
|
||||
replyToEvent?: MatrixEvent;
|
||||
}
|
||||
|
@ -93,7 +91,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
throw new Error("No recording started - cannot send anything");
|
||||
}
|
||||
|
||||
const { replyToEvent, relation, permalinkCreator } = this.props;
|
||||
const { replyToEvent, relation } = this.props;
|
||||
|
||||
await this.state.recorder.stop();
|
||||
|
||||
|
@ -124,10 +122,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
attachMentions(MatrixClientPeg.safeGet().getSafeUserId(), content, null, replyToEvent);
|
||||
attachRelation(content, relation);
|
||||
if (replyToEvent) {
|
||||
addReplyToMessageContent(content, replyToEvent, {
|
||||
permalinkCreator,
|
||||
includeLegacyFallback: true,
|
||||
});
|
||||
addReplyToMessageContent(content, replyToEvent);
|
||||
// Clear reply_to_event as we put the message into the queue
|
||||
// if the send fails, retry will handle resending.
|
||||
defaultDispatcher.dispatch({
|
||||
|
|
|
@ -11,7 +11,7 @@ import { IContent, IEventRelation, MatrixEvent, MsgType } from "matrix-js-sdk/sr
|
|||
import { ReplacementEvent, RoomMessageEventContent, RoomMessageTextEventContent } from "matrix-js-sdk/src/types";
|
||||
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import { parsePermalink, RoomPermalinkCreator } from "../../../../../utils/permalinks/Permalinks";
|
||||
import { parsePermalink } from "../../../../../utils/permalinks/Permalinks";
|
||||
import { addReplyToMessageContent } from "../../../../../utils/Reply";
|
||||
import { isNotNull } from "../../../../../Typeguards";
|
||||
|
||||
|
@ -52,8 +52,6 @@ function getTextReplyFallback(mxEvent: MatrixEvent): string {
|
|||
interface CreateMessageContentParams {
|
||||
relation?: IEventRelation;
|
||||
replyToEvent?: MatrixEvent;
|
||||
permalinkCreator?: RoomPermalinkCreator;
|
||||
includeReplyLegacyFallback?: boolean;
|
||||
editedEvent?: MatrixEvent;
|
||||
}
|
||||
|
||||
|
@ -62,13 +60,7 @@ const isMatrixEvent = (e: MatrixEvent | undefined): e is MatrixEvent => e instan
|
|||
export async function createMessageContent(
|
||||
message: string,
|
||||
isHTML: boolean,
|
||||
{
|
||||
relation,
|
||||
replyToEvent,
|
||||
permalinkCreator,
|
||||
includeReplyLegacyFallback = true,
|
||||
editedEvent,
|
||||
}: CreateMessageContentParams,
|
||||
{ relation, replyToEvent, editedEvent }: CreateMessageContentParams,
|
||||
): Promise<RoomMessageEventContent> {
|
||||
const isEditing = isMatrixEvent(editedEvent);
|
||||
const isReply = isEditing ? Boolean(editedEvent.replyEventId) : isMatrixEvent(replyToEvent);
|
||||
|
@ -126,11 +118,8 @@ export async function createMessageContent(
|
|||
// TODO Handle editing?
|
||||
attachRelation(content, newRelation);
|
||||
|
||||
if (!isEditing && replyToEvent && permalinkCreator) {
|
||||
addReplyToMessageContent(content, replyToEvent, {
|
||||
permalinkCreator,
|
||||
includeLegacyFallback: includeReplyLegacyFallback,
|
||||
});
|
||||
if (!isEditing && replyToEvent) {
|
||||
addReplyToMessageContent(content, replyToEvent);
|
||||
}
|
||||
|
||||
return content;
|
||||
|
|
|
@ -19,7 +19,6 @@ import { RoomMessageEventContent } from "matrix-js-sdk/src/types";
|
|||
import { PosthogAnalytics } from "../../../../../PosthogAnalytics";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../../../sendTimePerformanceMetrics";
|
||||
import { RoomPermalinkCreator } from "../../../../../utils/permalinks/Permalinks";
|
||||
import { doMaybeLocalRoomAction } from "../../../../../utils/local-room";
|
||||
import { CHAT_EFFECTS } from "../../../../../effects";
|
||||
import { containsEmoji } from "../../../../../effects/utils";
|
||||
|
@ -41,8 +40,6 @@ export interface SendMessageParams {
|
|||
relation?: IEventRelation;
|
||||
replyToEvent?: MatrixEvent;
|
||||
roomContext: IRoomState;
|
||||
permalinkCreator?: RoomPermalinkCreator;
|
||||
includeReplyLegacyFallback?: boolean;
|
||||
}
|
||||
|
||||
export async function sendMessage(
|
||||
|
@ -50,7 +47,7 @@ export async function sendMessage(
|
|||
isHTML: boolean,
|
||||
{ roomContext, mxClient, ...params }: SendMessageParams,
|
||||
): Promise<ISendEventResponse | undefined> {
|
||||
const { relation, replyToEvent, permalinkCreator } = params;
|
||||
const { relation, replyToEvent } = params;
|
||||
const { room } = roomContext;
|
||||
const roomId = room?.roomId;
|
||||
|
||||
|
@ -95,11 +92,7 @@ export async function sendMessage(
|
|||
) {
|
||||
attachRelation(content, relation);
|
||||
if (replyToEvent) {
|
||||
addReplyToMessageContent(content, replyToEvent, {
|
||||
permalinkCreator,
|
||||
// Exclude the legacy fallback for custom event types such as those used by /fireworks
|
||||
includeLegacyFallback: content.msgtype?.startsWith("m.") ?? true,
|
||||
});
|
||||
addReplyToMessageContent(content, replyToEvent);
|
||||
}
|
||||
} else {
|
||||
// instead of setting shouldSend to false as in SendMessageComposer, just return
|
||||
|
|
|
@ -19,6 +19,8 @@ import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds";
|
|||
import { useId } from "../../../utils/useId";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
import Modal from "../../../Modal.tsx";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog.tsx";
|
||||
|
||||
interface MenuProps {
|
||||
trigger: ReactNode;
|
||||
|
@ -103,6 +105,18 @@ interface IProps {
|
|||
placeholderName: string;
|
||||
}
|
||||
|
||||
export function getFileChanged(e: React.ChangeEvent<HTMLInputElement>): File | null {
|
||||
if (!e.target.files?.length) return null;
|
||||
const file = e.target.files[0];
|
||||
if (file.type.startsWith("image/")) return file;
|
||||
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("upload_failed_title"),
|
||||
description: _t("upload_file|not_image"),
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component for setting or removing an avatar on something (eg. a user or a room)
|
||||
*/
|
||||
|
@ -139,7 +153,10 @@ const AvatarSetting: React.FC<IProps> = ({
|
|||
|
||||
const onFileChanged = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files) onChange?.(e.target.files[0]);
|
||||
const file = getFileChanged(e);
|
||||
if (file) {
|
||||
onChange?.(file);
|
||||
}
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
|
|
@ -6,11 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { lazy } from "react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import type ExportE2eKeysDialog from "../../../async-components/views/dialogs/security/ExportE2eKeysDialog";
|
||||
import type ImportE2eKeysDialog from "../../../async-components/views/dialogs/security/ImportE2eKeysDialog";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import Modal from "../../../Modal";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
|
@ -18,7 +16,7 @@ import * as FormattingUtils from "../../../utils/FormattingUtils";
|
|||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import SettingsFlag from "../elements/SettingsFlag";
|
||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
import SettingsSubsection, { SettingsSubsectionText } from "./shared/SettingsSubsection";
|
||||
import { SettingsSubsection, SettingsSubsectionText } from "./shared/SettingsSubsection";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
interface IProps {}
|
||||
|
@ -129,19 +127,15 @@ export default class CryptographyPanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private onExportE2eKeysClicked = (): void => {
|
||||
Modal.createDialogAsync(
|
||||
import("../../../async-components/views/dialogs/security/ExportE2eKeysDialog") as unknown as Promise<
|
||||
typeof ExportE2eKeysDialog
|
||||
>,
|
||||
Modal.createDialog(
|
||||
lazy(() => import("../../../async-components/views/dialogs/security/ExportE2eKeysDialog")),
|
||||
{ matrixClient: this.context },
|
||||
);
|
||||
};
|
||||
|
||||
private onImportE2eKeysClicked = (): void => {
|
||||
Modal.createDialogAsync(
|
||||
import("../../../async-components/views/dialogs/security/ImportE2eKeysDialog") as unknown as Promise<
|
||||
typeof ImportE2eKeysDialog
|
||||
>,
|
||||
Modal.createDialog(
|
||||
lazy(() => import("../../../async-components/views/dialogs/security/ImportE2eKeysDialog")),
|
||||
{ matrixClient: this.context },
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { lazy } from "react";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
|
@ -94,14 +94,12 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
|
|||
}
|
||||
|
||||
private onManage = async (): Promise<void> => {
|
||||
Modal.createDialogAsync(
|
||||
// @ts-ignore: TS doesn't seem to like the type of this now that it
|
||||
// has also been converted to TS as well, but I can't figure out why...
|
||||
import("../../../async-components/views/dialogs/eventindex/ManageEventIndexDialog"),
|
||||
Modal.createDialog(
|
||||
lazy(() => import("../../../async-components/views/dialogs/eventindex/ManageEventIndexDialog")),
|
||||
{
|
||||
onFinished: () => {},
|
||||
},
|
||||
null,
|
||||
undefined,
|
||||
/* priority = */ false,
|
||||
/* static = */ true,
|
||||
);
|
||||
|
|
|
@ -14,7 +14,7 @@ import { Layout } from "../../../settings/enums/Layout";
|
|||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import SettingsSubsection from "./shared/SettingsSubsection";
|
||||
import { SettingsSubsection } from "./shared/SettingsSubsection";
|
||||
import Field from "../elements/Field";
|
||||
import { FontWatcher } from "../../../settings/watchers/FontWatcher";
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import StyledRadioButton from "../elements/StyledRadioButton";
|
|||
import { _t } from "../../../languageHandler";
|
||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
import { ImageSize } from "../../../settings/enums/ImageSize";
|
||||
import SettingsSubsection from "./shared/SettingsSubsection";
|
||||
import { SettingsSubsection } from "./shared/SettingsSubsection";
|
||||
|
||||
interface IProps {
|
||||
// none
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import React, { JSX, useEffect, useState } from "react";
|
||||
import { Field, HelpMessage, InlineField, Label, RadioControl, Root, ToggleControl } from "@vector-im/compound-web";
|
||||
|
||||
import SettingsSubsection from "./shared/SettingsSubsection";
|
||||
import { SettingsSubsection } from "./shared/SettingsSubsection";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
|
|
|
@ -48,7 +48,7 @@ import {
|
|||
} from "../../../utils/pushRules/updatePushRuleActions";
|
||||
import { Caption } from "../typography/Caption";
|
||||
import { SettingsSubsectionHeading } from "./shared/SettingsSubsectionHeading";
|
||||
import SettingsSubsection from "./shared/SettingsSubsection";
|
||||
import { SettingsSubsection } from "./shared/SettingsSubsection";
|
||||
import { doesRoomHaveUnreadMessages } from "../../../Unread";
|
||||
import SettingsFlag from "../elements/SettingsFlag";
|
||||
|
||||
|
|
|
@ -7,11 +7,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { ReactNode } from "react";
|
||||
import React, { lazy, ReactNode } from "react";
|
||||
import { CryptoEvent, BackupTrustInfo, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import type CreateKeyBackupDialog from "../../../async-components/views/dialogs/security/CreateKeyBackupDialog";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import Modal from "../../../Modal";
|
||||
|
@ -119,7 +118,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
|
|||
this.getUpdatedDiagnostics();
|
||||
try {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
const backupInfo = await cli.getKeyBackupVersion();
|
||||
const backupInfo = (await cli.getCrypto()?.getKeyBackupInfo()) ?? null;
|
||||
const backupTrustInfo = backupInfo ? await cli.getCrypto()?.isKeyBackupTrusted(backupInfo) : undefined;
|
||||
|
||||
const activeBackupVersion = (await cli.getCrypto()?.getActiveSessionBackupVersion()) ?? null;
|
||||
|
@ -170,10 +169,8 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
|
|||
}
|
||||
|
||||
private startNewBackup = (): void => {
|
||||
Modal.createDialogAsync(
|
||||
import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog") as unknown as Promise<
|
||||
typeof CreateKeyBackupDialog
|
||||
>,
|
||||
Modal.createDialog(
|
||||
lazy(() => import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog")),
|
||||
{
|
||||
onFinished: () => {
|
||||
this.loadBackupStatus();
|
||||
|
@ -195,12 +192,9 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
|
|||
if (!proceed) return;
|
||||
this.setState({ loading: true });
|
||||
const versionToDelete = this.state.backupInfo!.version!;
|
||||
MatrixClientPeg.safeGet()
|
||||
.getCrypto()
|
||||
?.deleteKeyBackupVersion(versionToDelete)
|
||||
.then(() => {
|
||||
this.loadBackupStatus();
|
||||
});
|
||||
// deleteKeyBackupVersion fires a key backup status event
|
||||
// which will update the UI
|
||||
MatrixClientPeg.safeGet().getCrypto()?.deleteKeyBackupVersion(versionToDelete);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -212,7 +206,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
|
|||
private resetSecretStorage = async (): Promise<void> => {
|
||||
this.setState({ error: false });
|
||||
try {
|
||||
await accessSecretStorage(async (): Promise<void> => {}, /* forceReset = */ true);
|
||||
await accessSecretStorage(async (): Promise<void> => {}, { forceReset: true });
|
||||
} catch (e) {
|
||||
logger.error("Error resetting secret storage", e);
|
||||
if (this.unmounted) return;
|
||||
|
|
|
@ -23,7 +23,7 @@ import classNames from "classnames";
|
|||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import SettingsSubsection from "./shared/SettingsSubsection";
|
||||
import { SettingsSubsection } from "./shared/SettingsSubsection";
|
||||
import ThemeWatcher from "../../../settings/watchers/ThemeWatcher";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
|
|
|
@ -12,7 +12,7 @@ import { Alert } from "@vector-im/compound-web";
|
|||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import InlineSpinner from "../elements/InlineSpinner";
|
||||
import SettingsSubsection from "./shared/SettingsSubsection";
|
||||
import { SettingsSubsection } from "./shared/SettingsSubsection";
|
||||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
||||
import { ThirdPartyIdentifier } from "../../../AddThreepid";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
@ -125,5 +125,3 @@ export const UserPersonalInfoSettings: React.FC<UserPersonalInfoSettingsProps> =
|
|||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserPersonalInfoSettings;
|
||||
|
|
|
@ -11,7 +11,7 @@ import { LocalNotificationSettings } from "matrix-js-sdk/src/matrix";
|
|||
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import Spinner from "../../elements/Spinner";
|
||||
import SettingsSubsection from "../shared/SettingsSubsection";
|
||||
import { SettingsSubsection } from "../shared/SettingsSubsection";
|
||||
import { SettingsSubsectionHeading } from "../shared/SettingsSubsectionHeading";
|
||||
import DeviceDetails from "./DeviceDetails";
|
||||
import { DeviceExpandDetailsButton } from "./DeviceExpandDetailsButton";
|
||||
|
|
|
@ -8,8 +8,8 @@ Please see LICENSE files in the repository root for full details.
|
|||
|
||||
import classNames from "classnames";
|
||||
import React, { ComponentProps } from "react";
|
||||
import { ChevronDownIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { Icon as CaretIcon } from "../../../../../res/img/feather-customised/dropdown-arrow.svg";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
|
||||
|
@ -38,7 +38,7 @@ export const DeviceExpandDetailsButton = <T extends keyof JSX.IntrinsicElements>
|
|||
})}
|
||||
onClick={onClick}
|
||||
>
|
||||
<CaretIcon className="mx_DeviceExpandDetailsButton_icon" />
|
||||
<ChevronDownIcon className="mx_DeviceExpandDetailsButton_icon" />
|
||||
</AccessibleButton>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -19,7 +19,7 @@ import { Text } from "@vector-im/compound-web";
|
|||
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
import SettingsSubsection from "../shared/SettingsSubsection";
|
||||
import { SettingsSubsection } from "../shared/SettingsSubsection";
|
||||
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
|
||||
|
||||
interface IProps {
|
||||
|
|
|
@ -10,7 +10,7 @@ import React from "react";
|
|||
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
import SettingsSubsection from "../shared/SettingsSubsection";
|
||||
import { SettingsSubsection } from "../shared/SettingsSubsection";
|
||||
import DeviceSecurityCard from "./DeviceSecurityCard";
|
||||
import { DeviceSecurityLearnMore } from "./DeviceSecurityLearnMore";
|
||||
import { filterDevicesBySecurityRecommendation, FilterVariation, INACTIVE_DEVICE_AGE_DAYS } from "./filter";
|
||||
|
|
|
@ -18,7 +18,7 @@ import SettingsStore from "../../../../settings/SettingsStore";
|
|||
import { UIFeature } from "../../../../settings/UIFeature";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import SetIdServer from "../SetIdServer";
|
||||
import SettingsSubsection from "../shared/SettingsSubsection";
|
||||
import { SettingsSubsection } from "../shared/SettingsSubsection";
|
||||
import InlineTermsAgreement from "../../terms/InlineTermsAgreement";
|
||||
import { Service, ServicePolicyPair, startTermsFlow } from "../../../../Terms";
|
||||
import IdentityAuthClient from "../../../../IdentityAuthClient";
|
||||
|
@ -190,5 +190,3 @@ export const DiscoverySettings: React.FC = () => {
|
|||
</SettingsSubsection>
|
||||
);
|
||||
};
|
||||
|
||||
export default DiscoverySettings;
|
||||
|
|
|
@ -20,7 +20,7 @@ import { UserTab } from "../../dialogs/UserTab";
|
|||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
import LabelledCheckbox from "../../elements/LabelledCheckbox";
|
||||
import { SettingsIndent } from "../shared/SettingsIndent";
|
||||
import SettingsSubsection, { SettingsSubsectionText } from "../shared/SettingsSubsection";
|
||||
import { SettingsSubsection, SettingsSubsectionText } from "../shared/SettingsSubsection";
|
||||
|
||||
function generalTabButton(content: string): JSX.Element {
|
||||
return (
|
||||
|
|
|
@ -31,7 +31,7 @@ import TagComposer from "../../elements/TagComposer";
|
|||
import { StatelessNotificationBadge } from "../../rooms/NotificationBadge/StatelessNotificationBadge";
|
||||
import { SettingsBanner } from "../shared/SettingsBanner";
|
||||
import { SettingsSection } from "../shared/SettingsSection";
|
||||
import SettingsSubsection from "../shared/SettingsSubsection";
|
||||
import { SettingsSubsection } from "../shared/SettingsSubsection";
|
||||
import { NotificationPusherSettings } from "./NotificationPusherSettings";
|
||||
import SettingsFlag from "../../elements/SettingsFlag";
|
||||
|
||||
|
|
|
@ -65,5 +65,3 @@ export const SettingsSubsection: React.FC<SettingsSubsectionProps> = ({
|
|||
{!legacy && <Separator />}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default SettingsSubsection;
|
||||
|
|
|
@ -20,7 +20,7 @@ import { ViewRoomPayload } from "../../../../../dispatcher/payloads/ViewRoomPayl
|
|||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import SettingsTab from "../SettingsTab";
|
||||
import { SettingsSection } from "../../shared/SettingsSection";
|
||||
import SettingsSubsection from "../../shared/SettingsSubsection";
|
||||
import { SettingsSubsection } from "../../shared/SettingsSubsection";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
|
|
@ -16,12 +16,12 @@ import dis from "../../../../../dispatcher/dispatcher";
|
|||
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import { UIFeature } from "../../../../../settings/UIFeature";
|
||||
import UrlPreviewSettings from "../../../room_settings/UrlPreviewSettings";
|
||||
import AliasSettings from "../../../room_settings/AliasSettings";
|
||||
import PosthogTrackers from "../../../../../PosthogTrackers";
|
||||
import SettingsSubsection from "../../shared/SettingsSubsection";
|
||||
import { SettingsSubsection } from "../../shared/SettingsSubsection";
|
||||
import SettingsTab from "../SettingsTab";
|
||||
import { SettingsSection } from "../../shared/SettingsSection";
|
||||
import { UrlPreviewSettings } from "../../../room_settings/UrlPreviewSettings";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
|
|
@ -25,7 +25,7 @@ import { UserTab } from "../../../dialogs/UserTab";
|
|||
import { chromeFileInputFix } from "../../../../../utils/BrowserWorkarounds";
|
||||
import SettingsTab from "../SettingsTab";
|
||||
import { SettingsSection } from "../../shared/SettingsSection";
|
||||
import SettingsSubsection from "../../shared/SettingsSubsection";
|
||||
import { SettingsSubsection } from "../../shared/SettingsSubsection";
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
|
|
|
@ -127,12 +127,30 @@ interface IProps {
|
|||
room: Room;
|
||||
}
|
||||
|
||||
export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
||||
interface RolesRoomSettingsTabState {
|
||||
isRoomEncrypted: boolean;
|
||||
isReady: boolean;
|
||||
}
|
||||
|
||||
export default class RolesRoomSettingsTab extends React.Component<IProps, RolesRoomSettingsTabState> {
|
||||
public static contextType = MatrixClientContext;
|
||||
public declare context: React.ContextType<typeof MatrixClientContext>;
|
||||
|
||||
public componentDidMount(): void {
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isReady: false,
|
||||
isRoomEncrypted: false,
|
||||
};
|
||||
}
|
||||
|
||||
public async componentDidMount(): Promise<void> {
|
||||
this.context.on(RoomStateEvent.Update, this.onRoomStateUpdate);
|
||||
this.setState({
|
||||
isRoomEncrypted:
|
||||
(await this.context.getCrypto()?.isEncryptionEnabledInRoom(this.props.room.roomId)) || false,
|
||||
isReady: true,
|
||||
});
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
|
@ -416,7 +434,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
.filter(Boolean);
|
||||
|
||||
// hide the power level selector for enabling E2EE if it the room is already encrypted
|
||||
if (client.isRoomEncrypted(this.props.room.roomId)) {
|
||||
if (this.state.isRoomEncrypted) {
|
||||
delete eventsLevels[EventType.RoomEncryption];
|
||||
}
|
||||
|
||||
|
@ -458,17 +476,19 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
{canChangeLevels && <AddPrivilegedUsers room={room} defaultUserLevel={defaultUserLevel} />}
|
||||
{mutedUsersSection}
|
||||
{bannedUsersSection}
|
||||
<SettingsFieldset
|
||||
legend={_t("room_settings|permissions|permissions_section")}
|
||||
description={
|
||||
isSpaceRoom
|
||||
? _t("room_settings|permissions|permissions_section_description_space")
|
||||
: _t("room_settings|permissions|permissions_section_description_room")
|
||||
}
|
||||
>
|
||||
{powerSelectors}
|
||||
{eventPowerSelectors}
|
||||
</SettingsFieldset>
|
||||
{this.state.isReady && (
|
||||
<SettingsFieldset
|
||||
legend={_t("room_settings|permissions|permissions_section")}
|
||||
description={
|
||||
isSpaceRoom
|
||||
? _t("room_settings|permissions|permissions_section_description_space")
|
||||
: _t("room_settings|permissions|permissions_section_description_room")
|
||||
}
|
||||
>
|
||||
{powerSelectors}
|
||||
{eventPowerSelectors}
|
||||
</SettingsFieldset>
|
||||
)}
|
||||
</SettingsSection>
|
||||
</SettingsTab>
|
||||
);
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
EventType,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { InlineSpinner } from "@vector-im/compound-web";
|
||||
|
||||
import { Icon as WarningIcon } from "../../../../../../res/img/warning.svg";
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
|
@ -53,7 +54,7 @@ interface IState {
|
|||
guestAccess: GuestAccess;
|
||||
history: HistoryVisibility;
|
||||
hasAliases: boolean;
|
||||
encrypted: boolean;
|
||||
encrypted: boolean | null;
|
||||
showAdvancedSection: boolean;
|
||||
}
|
||||
|
||||
|
@ -78,7 +79,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
HistoryVisibility.Shared,
|
||||
),
|
||||
hasAliases: false, // async loaded in componentDidMount
|
||||
encrypted: false, // async loaded in componentDidMount
|
||||
encrypted: null, // async loaded in componentDidMount
|
||||
showAdvancedSection: false,
|
||||
};
|
||||
}
|
||||
|
@ -419,6 +420,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
const client = this.context;
|
||||
const room = this.props.room;
|
||||
const isEncrypted = this.state.encrypted;
|
||||
const isEncryptionLoading = isEncrypted === null;
|
||||
const hasEncryptionPermission = room.currentState.mayClientSendStateEvent(EventType.RoomEncryption, client);
|
||||
const isEncryptionForceDisabled = shouldForceDisableEncryption(client);
|
||||
const canEnableEncryption = !isEncrypted && !isEncryptionForceDisabled && hasEncryptionPermission;
|
||||
|
@ -451,18 +453,23 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
: _t("room_settings|security|encryption_permanent")
|
||||
}
|
||||
>
|
||||
<LabelledToggleSwitch
|
||||
value={isEncrypted}
|
||||
onChange={this.onEncryptionChange}
|
||||
label={_t("common|encrypted")}
|
||||
disabled={!canEnableEncryption}
|
||||
/>
|
||||
{isEncryptionForceDisabled && !isEncrypted && (
|
||||
<Caption>{_t("room_settings|security|encryption_forced")}</Caption>
|
||||
{isEncryptionLoading ? (
|
||||
<InlineSpinner />
|
||||
) : (
|
||||
<>
|
||||
<LabelledToggleSwitch
|
||||
value={isEncrypted}
|
||||
onChange={this.onEncryptionChange}
|
||||
label={_t("common|encrypted")}
|
||||
disabled={!canEnableEncryption}
|
||||
/>
|
||||
{isEncryptionForceDisabled && !isEncrypted && (
|
||||
<Caption>{_t("room_settings|security|encryption_forced")}</Caption>
|
||||
)}
|
||||
{encryptionSettings}
|
||||
</>
|
||||
)}
|
||||
{encryptionSettings}
|
||||
</SettingsFieldset>
|
||||
|
||||
{this.renderJoinRule()}
|
||||
{historySection}
|
||||
</SettingsSection>
|
||||
|
|
|
@ -11,7 +11,7 @@ import { JoinRule, EventType, RoomState, Room } from "matrix-js-sdk/src/matrix";
|
|||
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||
import SettingsSubsection from "../../shared/SettingsSubsection";
|
||||
import { SettingsSubsection } from "../../shared/SettingsSubsection";
|
||||
import SettingsTab from "../SettingsTab";
|
||||
import { ElementCall } from "../../../../../models/Call";
|
||||
import { useRoomState } from "../../../../../hooks/useRoomState";
|
||||
|
|
|
@ -22,9 +22,9 @@ import ErrorDialog, { extractErrorMessageFromError } from "../../../dialogs/Erro
|
|||
import ChangePassword from "../../ChangePassword";
|
||||
import SettingsTab from "../SettingsTab";
|
||||
import { SettingsSection } from "../../shared/SettingsSection";
|
||||
import SettingsSubsection, { SettingsSubsectionText } from "../../shared/SettingsSubsection";
|
||||
import { SettingsSubsection, SettingsSubsectionText } from "../../shared/SettingsSubsection";
|
||||
import { SDKContext } from "../../../../../contexts/SDKContext";
|
||||
import UserPersonalInfoSettings from "../../UserPersonalInfoSettings";
|
||||
import { UserPersonalInfoSettings } from "../../UserPersonalInfoSettings";
|
||||
import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext";
|
||||
|
||||
interface IProps {
|
||||
|
|
|
@ -23,7 +23,7 @@ import { ThemeChoicePanel } from "../../ThemeChoicePanel";
|
|||
import ImageSizePanel from "../../ImageSizePanel";
|
||||
import SettingsTab from "../SettingsTab";
|
||||
import { SettingsSection } from "../../shared/SettingsSection";
|
||||
import SettingsSubsection from "../../shared/SettingsSubsection";
|
||||
import { SettingsSubsection } from "../../shared/SettingsSubsection";
|
||||
|
||||
interface IProps {}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ import BugReportDialog from "../../../dialogs/BugReportDialog";
|
|||
import CopyableText from "../../../elements/CopyableText";
|
||||
import SettingsTab from "../SettingsTab";
|
||||
import { SettingsSection } from "../../shared/SettingsSection";
|
||||
import SettingsSubsection, { SettingsSubsectionText } from "../../shared/SettingsSubsection";
|
||||
import { SettingsSubsection, SettingsSubsectionText } from "../../shared/SettingsSubsection";
|
||||
import ExternalLink from "../../../elements/ExternalLink";
|
||||
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
import { KeyboardShortcut } from "../../KeyboardShortcut";
|
||||
import SettingsTab from "../SettingsTab";
|
||||
import { SettingsSection } from "../../shared/SettingsSection";
|
||||
import SettingsSubsection from "../../shared/SettingsSubsection";
|
||||
import { SettingsSubsection } from "../../shared/SettingsSubsection";
|
||||
import { showLabsFlags } from "./LabsUserSettingsTab";
|
||||
|
||||
interface IKeyboardShortcutRowProps {
|
||||
|
|
|
@ -17,7 +17,7 @@ import SettingsFlag from "../../../elements/SettingsFlag";
|
|||
import { LabGroup, labGroupNames } from "../../../../../settings/Settings";
|
||||
import { EnhancedMap } from "../../../../../utils/maps";
|
||||
import { SettingsSection } from "../../shared/SettingsSection";
|
||||
import SettingsSubsection, { SettingsSubsectionText } from "../../shared/SettingsSubsection";
|
||||
import { SettingsSubsection, SettingsSubsectionText } from "../../shared/SettingsSubsection";
|
||||
import SettingsTab from "../SettingsTab";
|
||||
|
||||
export const showLabsFlags = (): boolean => {
|
||||
|
|
|
@ -22,7 +22,7 @@ import AccessibleButton from "../../../elements/AccessibleButton";
|
|||
import Field from "../../../elements/Field";
|
||||
import SettingsTab from "../SettingsTab";
|
||||
import { SettingsSection } from "../../shared/SettingsSection";
|
||||
import SettingsSubsection, { SettingsSubsectionText } from "../../shared/SettingsSubsection";
|
||||
import { SettingsSubsection, SettingsSubsectionText } from "../../shared/SettingsSubsection";
|
||||
|
||||
interface IState {
|
||||
busy: boolean;
|
||||
|
|
|
@ -24,7 +24,7 @@ import { OpenToTabPayload } from "../../../../../dispatcher/payloads/OpenToTabPa
|
|||
import { Action } from "../../../../../dispatcher/actions";
|
||||
import SdkConfig from "../../../../../SdkConfig";
|
||||
import { showUserOnboardingPage } from "../../../user-onboarding/UserOnboardingPage";
|
||||
import SettingsSubsection from "../../shared/SettingsSubsection";
|
||||
import { SettingsSubsection } from "../../shared/SettingsSubsection";
|
||||
import SettingsTab from "../SettingsTab";
|
||||
import { SettingsSection } from "../../shared/SettingsSection";
|
||||
import LanguageDropdown from "../../../elements/LanguageDropdown";
|
||||
|
|
|
@ -32,9 +32,9 @@ import { privateShouldBeEncrypted } from "../../../../../utils/rooms";
|
|||
import type { IServerVersions } from "matrix-js-sdk/src/matrix";
|
||||
import SettingsTab from "../SettingsTab";
|
||||
import { SettingsSection } from "../../shared/SettingsSection";
|
||||
import SettingsSubsection, { SettingsSubsectionText } from "../../shared/SettingsSubsection";
|
||||
import { SettingsSubsection, SettingsSubsectionText } from "../../shared/SettingsSubsection";
|
||||
import { useOwnDevices } from "../../devices/useOwnDevices";
|
||||
import DiscoverySettings from "../../discovery/DiscoverySettings";
|
||||
import { DiscoverySettings } from "../../discovery/DiscoverySettings";
|
||||
import SetIntegrationManager from "../../SetIntegrationManager";
|
||||
|
||||
interface IIgnoredUserProps {
|
||||
|
|
|
@ -9,10 +9,11 @@ Please see LICENSE files in the repository root for full details.
|
|||
import React, { lazy, Suspense, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { discoverAndValidateOIDCIssuerWellKnown, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { defer } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
import Modal from "../../../../../Modal";
|
||||
import SettingsSubsection from "../../shared/SettingsSubsection";
|
||||
import { SettingsSubsection } from "../../shared/SettingsSubsection";
|
||||
import SetupEncryptionDialog from "../../../dialogs/security/SetupEncryptionDialog";
|
||||
import VerificationRequestDialog from "../../../dialogs/VerificationRequestDialog";
|
||||
import LogoutDialog from "../../../dialogs/LogoutDialog";
|
||||
|
@ -108,31 +109,33 @@ const useSignOut = (
|
|||
}
|
||||
}
|
||||
|
||||
let success = false;
|
||||
try {
|
||||
setSigningOutDeviceIds([...signingOutDeviceIds, ...deviceIds]);
|
||||
|
||||
const onSignOutFinished = async (success: boolean): Promise<void> => {
|
||||
if (success) {
|
||||
await onSignoutResolvedCallback();
|
||||
}
|
||||
setSigningOutDeviceIds(signingOutDeviceIds.filter((deviceId) => !deviceIds.includes(deviceId)));
|
||||
};
|
||||
setSigningOutDeviceIds((signingOutDeviceIds) => [...signingOutDeviceIds, ...deviceIds]);
|
||||
|
||||
if (delegatedAuthAccountUrl) {
|
||||
const [deviceId] = deviceIds;
|
||||
try {
|
||||
setSigningOutDeviceIds([...signingOutDeviceIds, deviceId]);
|
||||
const success = await confirmDelegatedAuthSignOut(delegatedAuthAccountUrl, deviceId);
|
||||
await onSignOutFinished(success);
|
||||
success = await confirmDelegatedAuthSignOut(delegatedAuthAccountUrl, deviceId);
|
||||
} catch (error) {
|
||||
logger.error("Error deleting OIDC-aware sessions", error);
|
||||
}
|
||||
} else {
|
||||
await deleteDevicesWithInteractiveAuth(matrixClient, deviceIds, onSignOutFinished);
|
||||
const deferredSuccess = defer<boolean>();
|
||||
await deleteDevicesWithInteractiveAuth(matrixClient, deviceIds, async (success) => {
|
||||
deferredSuccess.resolve(success);
|
||||
});
|
||||
success = await deferredSuccess.promise;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error deleting sessions", error);
|
||||
setSigningOutDeviceIds(signingOutDeviceIds.filter((deviceId) => !deviceIds.includes(deviceId)));
|
||||
} finally {
|
||||
if (success) {
|
||||
await onSignoutResolvedCallback();
|
||||
}
|
||||
setSigningOutDeviceIds((signingOutDeviceIds) =>
|
||||
signingOutDeviceIds.filter((deviceId) => !deviceIds.includes(deviceId)),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -7,11 +7,13 @@ Please see LICENSE files in the repository root for full details.
|
|||
*/
|
||||
|
||||
import React, { ChangeEvent, useMemo } from "react";
|
||||
import CameraCircle from "@vector-im/compound-design-tokens/assets/web/icons/video-call-solid";
|
||||
import {
|
||||
VideoCallSolidIcon,
|
||||
HomeSolidIcon,
|
||||
UserProfileSolidIcon,
|
||||
FavouriteSolidIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { Icon as HomeIcon } from "../../../../../../res/img/element-icons/home.svg";
|
||||
import { Icon as FavoriteIcon } from "../../../../../../res/img/element-icons/roomlist/favorite.svg";
|
||||
import { Icon as MembersIcon } from "../../../../../../res/img/element-icons/room/members.svg";
|
||||
import { Icon as HashCircleIcon } from "../../../../../../res/img/element-icons/roomlist/hash-circle.svg";
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
|
@ -22,7 +24,7 @@ import { MetaSpace } from "../../../../../stores/spaces";
|
|||
import PosthogTrackers from "../../../../../PosthogTrackers";
|
||||
import SettingsTab from "../SettingsTab";
|
||||
import { SettingsSection } from "../../shared/SettingsSection";
|
||||
import SettingsSubsection, { SettingsSubsectionText } from "../../shared/SettingsSubsection";
|
||||
import { SettingsSubsection, SettingsSubsectionText } from "../../shared/SettingsSubsection";
|
||||
import SdkConfig from "../../../../../SdkConfig";
|
||||
|
||||
type InteractionName = "WebSettingsSidebarTabSpacesCheckbox" | "WebQuickSettingsPinToSidebarCheckbox";
|
||||
|
@ -84,7 +86,7 @@ const SidebarUserSettingsTab: React.FC = () => {
|
|||
disabled={homeEnabled}
|
||||
>
|
||||
<SettingsSubsectionText>
|
||||
<HomeIcon />
|
||||
<HomeSolidIcon />
|
||||
{_t("common|home")}
|
||||
</SettingsSubsectionText>
|
||||
<SettingsSubsectionText>
|
||||
|
@ -113,7 +115,7 @@ const SidebarUserSettingsTab: React.FC = () => {
|
|||
className="mx_SidebarUserSettingsTab_checkbox"
|
||||
>
|
||||
<SettingsSubsectionText>
|
||||
<FavoriteIcon />
|
||||
<FavouriteSolidIcon />
|
||||
{_t("common|favourites")}
|
||||
</SettingsSubsectionText>
|
||||
<SettingsSubsectionText>
|
||||
|
@ -127,7 +129,7 @@ const SidebarUserSettingsTab: React.FC = () => {
|
|||
className="mx_SidebarUserSettingsTab_checkbox"
|
||||
>
|
||||
<SettingsSubsectionText>
|
||||
<MembersIcon />
|
||||
<UserProfileSolidIcon />
|
||||
{_t("common|people")}
|
||||
</SettingsSubsectionText>
|
||||
<SettingsSubsectionText>
|
||||
|
@ -158,7 +160,7 @@ const SidebarUserSettingsTab: React.FC = () => {
|
|||
className="mx_SidebarUserSettingsTab_checkbox"
|
||||
>
|
||||
<SettingsSubsectionText>
|
||||
<CameraCircle />
|
||||
<VideoCallSolidIcon />
|
||||
{_t("settings|sidebar|metaspaces_video_rooms")}
|
||||
</SettingsSubsectionText>
|
||||
<SettingsSubsectionText>{conferenceSubsectionText}</SettingsSubsectionText>
|
||||
|
|
|
@ -21,7 +21,7 @@ import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
|||
import { requestMediaPermissions } from "../../../../../utils/media/requestMediaPermissions";
|
||||
import SettingsTab from "../SettingsTab";
|
||||
import { SettingsSection } from "../../shared/SettingsSection";
|
||||
import SettingsSubsection from "../../shared/SettingsSubsection";
|
||||
import { SettingsSubsection } from "../../shared/SettingsSubsection";
|
||||
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
|
||||
|
||||
interface IState {
|
||||
|
|
|
@ -8,7 +8,11 @@ Please see LICENSE files in the repository root for full details.
|
|||
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import EllipsisIcon from "@vector-im/compound-design-tokens/assets/web/icons/overflow-horizontal";
|
||||
import {
|
||||
OverflowHorizontalIcon,
|
||||
UserProfileSolidIcon,
|
||||
FavouriteSolidIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import ContextMenu, { alwaysAboveRightOf, ChevronFace, useContextMenu } from "../../structures/ContextMenu";
|
||||
|
@ -22,8 +26,6 @@ import { Action } from "../../../dispatcher/actions";
|
|||
import { UserTab } from "../dialogs/UserTab";
|
||||
import QuickThemeSwitcher from "./QuickThemeSwitcher";
|
||||
import { Icon as PinUprightIcon } from "../../../../res/img/element-icons/room/pin-upright.svg";
|
||||
import { Icon as MembersIcon } from "../../../../res/img/element-icons/room/members.svg";
|
||||
import { Icon as FavoriteIcon } from "../../../../res/img/element-icons/roomlist/favorite.svg";
|
||||
import Modal from "../../../Modal";
|
||||
import DevtoolsDialog from "../dialogs/DevtoolsDialog";
|
||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||
|
@ -89,7 +91,7 @@ const QuickSettingsButton: React.FC<{
|
|||
checked={!!favouritesEnabled}
|
||||
onChange={onMetaSpaceChangeFactory(MetaSpace.Favourites, "WebQuickSettingsPinToSidebarCheckbox")}
|
||||
>
|
||||
<FavoriteIcon className="mx_QuickSettingsButton_icon" />
|
||||
<FavouriteSolidIcon className="mx_QuickSettingsButton_icon" />
|
||||
{_t("common|favourites")}
|
||||
</StyledCheckbox>
|
||||
<StyledCheckbox
|
||||
|
@ -97,7 +99,7 @@ const QuickSettingsButton: React.FC<{
|
|||
checked={!!peopleEnabled}
|
||||
onChange={onMetaSpaceChangeFactory(MetaSpace.People, "WebQuickSettingsPinToSidebarCheckbox")}
|
||||
>
|
||||
<MembersIcon className="mx_QuickSettingsButton_icon" />
|
||||
<UserProfileSolidIcon className="mx_QuickSettingsButton_icon" />
|
||||
{_t("common|people")}
|
||||
</StyledCheckbox>
|
||||
<AccessibleButton
|
||||
|
@ -110,7 +112,7 @@ const QuickSettingsButton: React.FC<{
|
|||
});
|
||||
}}
|
||||
>
|
||||
<EllipsisIcon className="mx_QuickSettingsButton_icon" />
|
||||
<OverflowHorizontalIcon className="mx_QuickSettingsButton_icon" />
|
||||
{_t("quick_settings|sidebar_settings")}
|
||||
</AccessibleButton>
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@ import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
|||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { Filter } from "../dialogs/spotlight/Filter";
|
||||
import { OpenSpotlightPayload } from "../../../dispatcher/payloads/OpenSpotlightPayload.ts";
|
||||
|
||||
export const createSpace = async (
|
||||
client: MatrixClient,
|
||||
|
@ -265,7 +266,7 @@ const SpaceCreateMenu: React.FC<{
|
|||
};
|
||||
|
||||
const onSearchClick = (): void => {
|
||||
defaultDispatcher.dispatch({
|
||||
defaultDispatcher.dispatch<OpenSpotlightPayload>({
|
||||
action: Action.OpenSpotlight,
|
||||
initialFilter: Filter.PublicSpaces,
|
||||
});
|
||||
|
|
|
@ -19,7 +19,7 @@ import { leaveSpace } from "../../../utils/leave-behaviour";
|
|||
import { getTopic } from "../../../hooks/room/useTopic";
|
||||
import SettingsTab from "../settings/tabs/SettingsTab";
|
||||
import { SettingsSection } from "../settings/shared/SettingsSection";
|
||||
import SettingsSubsection from "../settings/shared/SettingsSubsection";
|
||||
import { SettingsSubsection } from "../settings/shared/SettingsSubsection";
|
||||
|
||||
interface IProps {
|
||||
matrixClient: MatrixClient;
|
||||
|
|
|
@ -73,7 +73,7 @@ export const SpaceButton = <T extends keyof JSX.IntrinsicElements>({
|
|||
...props
|
||||
}: ButtonProps<T>): JSX.Element => {
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLElement>(innerRef);
|
||||
const [onFocus, isActive] = useRovingTabIndex(handle);
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex(handle);
|
||||
const tabIndex = isActive ? 0 : -1;
|
||||
|
||||
const spaceKey = _spaceKey ?? space?.roomId;
|
||||
|
@ -144,7 +144,7 @@ export const SpaceButton = <T extends keyof JSX.IntrinsicElements>({
|
|||
title={!isNarrow || menuDisplayed ? undefined : label}
|
||||
onClick={onClick}
|
||||
onContextMenu={openMenu}
|
||||
ref={handle}
|
||||
ref={ref}
|
||||
tabIndex={tabIndex}
|
||||
onFocus={onFocus}
|
||||
>
|
||||
|
|
|
@ -14,7 +14,6 @@ import SasEmoji from "@matrix-org/spec/sas-emoji.json";
|
|||
import { _t, getNormalizedLanguageKeys, getUserLanguage } from "../../../languageHandler";
|
||||
import { PendingActionSpinner } from "../right_panel/EncryptionInfo";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { fixupColorFonts } from "../../../utils/FontManager";
|
||||
|
||||
interface IProps {
|
||||
pending?: boolean;
|
||||
|
@ -88,11 +87,6 @@ export default class VerificationShowSas extends React.Component<IProps, IState>
|
|||
this.state = {
|
||||
pending: false,
|
||||
};
|
||||
|
||||
// As this component is also used before login (during complete security),
|
||||
// also make sure we have a working emoji font to display the SAS emojis here.
|
||||
// This is also done from LoggedInView.
|
||||
fixupColorFonts();
|
||||
}
|
||||
|
||||
private onMatchClick = (): void => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue