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:
Michael Telatynski 2024-11-27 10:47:35 +00:00
commit 3dbcb5efa3
No known key found for this signature in database
GPG key ID: A2B008A5F49F5D0D
438 changed files with 7829 additions and 4692 deletions

View file

@ -31,4 +31,3 @@ export const BackdropPanel: React.FC<IProps> = ({ backgroundImage, blurMultiplie
</div>
);
};
export default BackdropPanel;

View file

@ -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();

View file

@ -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")),
);
}
});

View file

@ -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;

View file

@ -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();
}
};

View file

@ -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 {

View file

@ -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:

View file

@ -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" },
];

View file

@ -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,

View file

@ -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) {

View file

@ -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"
/>
)}

View file

@ -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,

View file

@ -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,
});

View file

@ -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;

View file

@ -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;

View file

@ -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>,

View file

@ -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);

View file

@ -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}

View file

@ -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}

View file

@ -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 (
<>

View file

@ -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);

View file

@ -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/*"

View file

@ -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;
}

View file

@ -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

View file

@ -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);
}
};

View file

@ -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"

View file

@ -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();

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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 {

View file

@ -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");
}
}

View file

@ -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";

View file

@ -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>();

View file

@ -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

View file

@ -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;
}

View file

@ -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 [];
}

View file

@ -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,

View file

@ -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;

View file

@ -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;
}

View file

@ -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 {

View file

@ -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 } = {

View file

@ -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) {

View file

@ -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";

View file

@ -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}

View file

@ -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

View 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>;
}

View file

@ -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({

View file

@ -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;

View file

@ -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

View file

@ -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],
);

View file

@ -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 },
);
};

View file

@ -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,
);

View file

@ -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";

View file

@ -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

View file

@ -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";

View file

@ -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";

View file

@ -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;

View file

@ -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";

View file

@ -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;

View file

@ -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";

View file

@ -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>
);
};

View file

@ -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 {

View file

@ -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";

View file

@ -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;

View file

@ -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 (

View file

@ -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";

View file

@ -65,5 +65,3 @@ export const SettingsSubsection: React.FC<SettingsSubsectionProps> = ({
{!legacy && <Separator />}
</div>
);
export default SettingsSubsection;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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>
);

View file

@ -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>

View file

@ -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";

View file

@ -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 {

View file

@ -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 {}

View file

@ -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";

View file

@ -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 {

View file

@ -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 => {

View file

@ -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;

View file

@ -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";

View file

@ -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 {

View file

@ -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)),
);
}
};

View file

@ -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>

View file

@ -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 {

View file

@ -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>

View file

@ -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,
});

View file

@ -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;

View file

@ -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}
>

View file

@ -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 => {