Merge branch 'develop' into midhun/fix-spotlight-1

This commit is contained in:
Michael Telatynski 2024-11-13 15:38:56 +00:00 committed by GitHub
commit ec96d33ed7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
215 changed files with 3574 additions and 2704 deletions

View file

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

View file

@ -49,11 +49,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 +124,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;

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

@ -45,7 +45,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";
@ -437,7 +437,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 +598,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 +704,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,
@ -850,7 +850,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
public componentDidMount(): 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 +967,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 +1045,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 +1145,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 +1188,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 +1197,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 +1303,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 });
}
}
});
@ -1363,7 +1363,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 => {
@ -1561,7 +1561,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 +1572,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 +1580,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 +1622,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 +1638,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 +1688,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 +1702,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 +1736,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 +1760,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 +1786,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 +1794,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 +1918,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 +2001,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 +2018,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,
});
@ -2547,5 +2547,3 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
);
}
}
export default RoomView;

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

@ -8,10 +8,10 @@ Please see LICENSE files in the repository root for full details.
import React, { ReactNode } from "react";
import { Tooltip } from "@vector-im/compound-web";
import { RestartIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import AccessibleButton from "../../../views/elements/AccessibleButton";
import { Icon as EMailPromptIcon } from "../../../../../res/img/element-icons/email-prompt.svg";
import { Icon as RetryIcon } from "../../../../../res/img/compound/retry-16px.svg";
import { _t } from "../../../../languageHandler";
import { useTimeoutToggle } from "../../../../hooks/useTimeoutToggle";
import { ErrorMessage } from "../../ErrorMessage";
@ -60,7 +60,7 @@ export const CheckEmail: React.FC<CheckEmailProps> = ({
<span className="mx_VerifyEMailDialog_text-light">{_t("auth|check_email_resend_prompt")}</span>
<Tooltip description={_t("auth|check_email_resend_tooltip")} placement="top" open={tooltipVisible}>
<AccessibleButton className="mx_AuthBody_resend-button" kind="link" onClick={onResendClickFn}>
<RetryIcon className="mx_Icon mx_Icon_16" />
<RestartIcon className="mx_Icon mx_Icon_16" />
{_t("action|resend")}
</AccessibleButton>
</Tooltip>

View file

@ -8,10 +8,10 @@ Please see LICENSE files in the repository root for full details.
import React, { ReactNode } from "react";
import { Tooltip } from "@vector-im/compound-web";
import { RestartIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { _t } from "../../../../languageHandler";
import AccessibleButton from "../../../views/elements/AccessibleButton";
import { Icon as RetryIcon } from "../../../../../res/img/compound/retry-16px.svg";
import { Icon as EmailPromptIcon } from "../../../../../res/img/element-icons/email-prompt.svg";
import { useTimeoutToggle } from "../../../../hooks/useTimeoutToggle";
import { ErrorMessage } from "../../ErrorMessage";
@ -59,7 +59,7 @@ export const VerifyEmailModal: React.FC<Props> = ({
<span className="mx_VerifyEMailDialog_text-light">{_t("auth|check_email_resend_prompt")}</span>
<Tooltip description={_t("auth|check_email_resend_tooltip")} placement="top" open={tooltipVisible}>
<AccessibleButton className="mx_AuthBody_resend-button" kind="link" onClick={onResendClickFn}>
<RetryIcon className="mx_Icon mx_Icon_16" />
<RestartIcon className="mx_Icon mx_Icon_16" />
{_t("action|resend")}
</AccessibleButton>
</Tooltip>

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

@ -12,6 +12,7 @@ import { Room, EventType } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { sleep } from "matrix-js-sdk/src/utils";
import { logger } from "matrix-js-sdk/src/logger";
import { ErrorIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { _t, _td, TranslationKey } from "../../../languageHandler";
import BaseDialog from "./BaseDialog";
@ -34,7 +35,6 @@ import LazyRenderList from "../elements/LazyRenderList";
import { useSettingValue } from "../../../hooks/useSettings";
import { filterBoolean } from "../../../utils/arrays";
import { NonEmptyArray } from "../../../@types/common";
import WarningBadgeSvg from "../../../../res/img/element-icons/warning-badge.svg";
// These values match CSS
const ROW_HEIGHT = 32 + 12;
@ -229,7 +229,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
if (error) {
footer = (
<>
<img src={WarningBadgeSvg} height="24" width="24" alt="" />
<ErrorIcon height="24px" width="24px" />
<span className="mx_AddExistingToSpaceDialog_error">
<div className="mx_AddExistingToSpaceDialog_errorHeading">

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";
@ -116,10 +114,8 @@ export default class LogoutDialog 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: 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

@ -22,6 +22,7 @@ import {
WidgetApiFromWidgetAction,
WidgetKind,
} from "matrix-widget-api";
import { ErrorIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import BaseDialog from "./BaseDialog";
import { _t, getUserLanguage } from "../../../languageHandler";
@ -33,7 +34,6 @@ import { arrayFastClone } from "../../../utils/arrays";
import { ElementWidget } from "../../../stores/widgets/StopGapWidget";
import { ELEMENT_CLIENT_ID } from "../../../identifiers";
import SettingsStore from "../../../settings/SettingsStore";
import WarningBadgeSvg from "../../../../res/img/element-icons/warning-badge.svg";
interface IProps {
widgetDefinition: IModalWidgetOpenRequestData;
@ -186,7 +186,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
onFinished={this.props.onFinished}
>
<div className="mx_ModalWidgetDialog_warning">
<img src={WarningBadgeSvg} height="16" width="16" alt="" />
<ErrorIcon width="16px" height="16px" />
{_t("widget|modal_data_warning", {
widgetDomain: parsed.hostname,
})}

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,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,
@ -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,
});
@ -270,7 +263,6 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
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

@ -8,9 +8,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, { createRef, CSSProperties } from "react";
import React, { createRef, CSSProperties, useRef, useState } from "react";
import FocusLock from "react-focus-lock";
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
import { MatrixEvent, parseErrorResponse } from "matrix-js-sdk/src/matrix";
import { _t } from "../../../languageHandler";
import MemberAvatar from "../avatars/MemberAvatar";
@ -30,6 +30,9 @@ import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { presentableTextForFile } from "../../../utils/FileUtils";
import AccessibleButton from "./AccessibleButton";
import Modal from "../../../Modal";
import ErrorDialog from "../dialogs/ErrorDialog";
import { FileDownloader } from "../../../utils/FileDownloader";
// Max scale to keep gaps around the image
const MAX_SCALE = 0.95;
@ -309,15 +312,6 @@ export default class ImageView extends React.Component<IProps, IState> {
this.setZoomAndRotation(cur + 90);
};
private onDownloadClick = (): void => {
const a = document.createElement("a");
a.href = this.props.src;
if (this.props.name) a.download = this.props.name;
a.target = "_blank";
a.rel = "noreferrer noopener";
a.click();
};
private onOpenContextMenu = (): void => {
this.setState({
contextMenuDisplayed: true,
@ -555,11 +549,7 @@ export default class ImageView extends React.Component<IProps, IState> {
title={_t("lightbox|rotate_right")}
onClick={this.onRotateClockwiseClick}
/>
<AccessibleButton
className="mx_ImageView_button mx_ImageView_button_download"
title={_t("action|download")}
onClick={this.onDownloadClick}
/>
<DownloadButton url={this.props.src} fileName={this.props.name} />
{contextMenuButton}
<AccessibleButton
className="mx_ImageView_button mx_ImageView_button_close"
@ -591,3 +581,61 @@ export default class ImageView extends React.Component<IProps, IState> {
);
}
}
function DownloadButton({ url, fileName }: { url: string; fileName?: string }): JSX.Element {
const downloader = useRef(new FileDownloader()).current;
const [loading, setLoading] = useState(false);
const blobRef = useRef<Blob>();
function showError(e: unknown): void {
Modal.createDialog(ErrorDialog, {
title: _t("timeline|download_failed"),
description: (
<>
<div>{_t("timeline|download_failed_description")}</div>
<div>{e instanceof Error ? e.toString() : ""}</div>
</>
),
});
setLoading(false);
}
const onDownloadClick = async (): Promise<void> => {
try {
if (loading) return;
setLoading(true);
if (blobRef.current) {
// Cheat and trigger a download, again.
return downloadBlob(blobRef.current);
}
const res = await fetch(url);
if (!res.ok) {
throw parseErrorResponse(res, await res.text());
}
const blob = await res.blob();
blobRef.current = blob;
await downloadBlob(blob);
} catch (e) {
showError(e);
}
};
async function downloadBlob(blob: Blob): Promise<void> {
await downloader.download({
blob,
name: fileName ?? _t("common|image"),
});
setLoading(false);
}
return (
<AccessibleButton
className="mx_ImageView_button mx_ImageView_button_download"
title={loading ? _t("timeline|download_action_downloading") : _t("action|download")}
onClick={onDownloadClick}
disabled={loading}
/>
);
}

View file

@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React, { MutableRefObject, ReactNode, StrictMode } from "react";
import ReactDOM from "react-dom";
import { createRoot, Root } from "react-dom/client";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import { TooltipProvider } from "@vector-im/compound-web";
@ -24,7 +24,7 @@ export const getPersistKey = (appId: string): string => "widget_" + appId;
// We contain all persisted elements within a master container to allow them all to be within the same
// CSS stacking context, and thus be able to control their z-indexes relative to each other.
function getOrCreateMasterContainer(): HTMLDivElement {
let container = getContainer("mx_PersistedElement_container");
let container = document.getElementById("mx_PersistedElement_container") as HTMLDivElement;
if (!container) {
container = document.createElement("div");
container.id = "mx_PersistedElement_container";
@ -34,18 +34,10 @@ function getOrCreateMasterContainer(): HTMLDivElement {
return container;
}
function getContainer(containerId: string): HTMLDivElement {
return document.getElementById(containerId) as HTMLDivElement;
}
function getOrCreateContainer(containerId: string): HTMLDivElement {
let container = getContainer(containerId);
if (!container) {
container = document.createElement("div");
container.id = containerId;
getOrCreateMasterContainer().appendChild(container);
}
const container = document.createElement("div");
container.id = containerId;
getOrCreateMasterContainer().appendChild(container);
return container;
}
@ -83,6 +75,8 @@ export default class PersistedElement extends React.Component<IProps> {
private childContainer?: HTMLDivElement;
private child?: HTMLDivElement;
private static rootMap: Record<string, [root: Root, container: Element]> = {};
public constructor(props: IProps) {
super(props);
@ -99,14 +93,16 @@ export default class PersistedElement extends React.Component<IProps> {
* @param {string} persistKey Key used to uniquely identify this PersistedElement
*/
public static destroyElement(persistKey: string): void {
const container = getContainer("mx_persistedElement_" + persistKey);
if (container) {
container.remove();
const pair = PersistedElement.rootMap[persistKey];
if (pair) {
pair[0].unmount();
pair[1].remove();
}
delete PersistedElement.rootMap[persistKey];
}
public static isMounted(persistKey: string): boolean {
return Boolean(getContainer("mx_persistedElement_" + persistKey));
return Boolean(PersistedElement.rootMap[persistKey]);
}
private collectChildContainer = (ref: HTMLDivElement): void => {
@ -179,7 +175,14 @@ export default class PersistedElement extends React.Component<IProps> {
</StrictMode>
);
ReactDOM.render(content, getOrCreateContainer("mx_persistedElement_" + this.props.persistKey));
let rootPair = PersistedElement.rootMap[this.props.persistKey];
if (!rootPair) {
const container = getOrCreateContainer("mx_persistedElement_" + this.props.persistKey);
const root = createRoot(container);
rootPair = [root, container];
PersistedElement.rootMap[this.props.persistKey] = rootPair;
}
rootPair[0].render(content);
}
private updateChildVisibility(child?: HTMLDivElement, visible = false): void {

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 { ErrorIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { Icon as WarningBadge } from "../../../../res/img/element-icons/warning-badge.svg";
import { _t } from "../../../languageHandler";
import { getLocationShareErrorMessage, LocationShareError } from "../../../utils/location";
import AccessibleButton from "../elements/AccessibleButton";
@ -29,7 +29,7 @@ export const MapError: React.FC<MapErrorProps> = ({ error, isMinimised, classNam
className={classNames("mx_MapError", className, { mx_MapError_isMinimised: isMinimised })}
onClick={onClick}
>
<WarningBadge className="mx_MapError_icon" />
<ErrorIcon className="mx_MapError_icon" />
<Heading className="mx_MapError_heading" size="3">
{_t("location_sharing|failed_load_map")}
</Heading>

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

@ -13,8 +13,8 @@ import classNames from "classnames";
import * as HtmlUtils from "../../../HtmlUtils";
import { editBodyDiffToHtml } from "../../../utils/MessageDiffUtils";
import { formatTime } from "../../../DateUtils";
import { pillifyLinks, unmountPills } from "../../../utils/pillify";
import { tooltipifyLinks, unmountTooltips } from "../../../utils/tooltipify";
import { pillifyLinks } from "../../../utils/pillify";
import { tooltipifyLinks } from "../../../utils/tooltipify";
import { _t } from "../../../languageHandler";
import Modal from "../../../Modal";
import RedactedBody from "./RedactedBody";
@ -23,6 +23,7 @@ import ConfirmAndWaitRedactDialog from "../dialogs/ConfirmAndWaitRedactDialog";
import ViewSource from "../../structures/ViewSource";
import SettingsStore from "../../../settings/SettingsStore";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { ReactRootManager } from "../../../utils/react";
function getReplacedContent(event: MatrixEvent): IContent {
const originalContent = event.getOriginalContent();
@ -47,8 +48,8 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
public declare context: React.ContextType<typeof MatrixClientContext>;
private content = createRef<HTMLDivElement>();
private pills: Element[] = [];
private tooltips: Element[] = [];
private pills = new ReactRootManager();
private tooltips = new ReactRootManager();
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
super(props, context);
@ -103,7 +104,7 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
private tooltipifyLinks(): void {
// not present for redacted events
if (this.content.current) {
tooltipifyLinks(this.content.current.children, this.pills, this.tooltips);
tooltipifyLinks(this.content.current.children, this.pills.elements, this.tooltips);
}
}
@ -113,8 +114,8 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
}
public componentWillUnmount(): void {
unmountPills(this.pills);
unmountTooltips(this.tooltips);
this.pills.unmount();
this.tooltips.unmount();
const event = this.props.mxEvent;
event.localRedactionEvent()?.off(MatrixEventEvent.Status, this.onAssociatedStatusChanged);
}

View file

@ -27,17 +27,17 @@ import {
OverflowHorizontalIcon,
ReplyIcon,
DeleteIcon,
RestartIcon,
} 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 ResendIcon } from "../../../../res/img/element-icons/retry.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";
@ -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,
@ -475,14 +475,14 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
0,
0,
<RovingAccessibleButton
className="mx_MessageActionBar_iconButton"
className="mx_MessageActionBar_iconButton mx_MessageActionBar_retryButton"
title={_t("action|retry")}
onClick={this.onResendClick}
onContextMenu={this.onResendClick}
key="resend"
placement="left"
>
<ResendIcon />
<RestartIcon />
</RovingAccessibleButton>,
);

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

@ -7,7 +7,6 @@ Please see LICENSE files in the repository root for full details.
*/
import React, { createRef, SyntheticEvent, MouseEvent, StrictMode } from "react";
import ReactDOM from "react-dom";
import { MsgType } from "matrix-js-sdk/src/matrix";
import { TooltipProvider } from "@vector-im/compound-web";
@ -17,8 +16,8 @@ import Modal from "../../../Modal";
import dis from "../../../dispatcher/dispatcher";
import { _t } from "../../../languageHandler";
import SettingsStore from "../../../settings/SettingsStore";
import { pillifyLinks, unmountPills } from "../../../utils/pillify";
import { tooltipifyLinks, unmountTooltips } from "../../../utils/tooltipify";
import { pillifyLinks } from "../../../utils/pillify";
import { tooltipifyLinks } from "../../../utils/tooltipify";
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
import { isPermalinkHost, tryTransformPermalinkToLocalHref } from "../../../utils/permalinks/Permalinks";
import { Action } from "../../../dispatcher/actions";
@ -36,6 +35,7 @@ import { EditWysiwygComposer } from "../rooms/wysiwyg_composer";
import { IEventTileOps } from "../rooms/EventTile";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import CodeBlock from "./CodeBlock";
import { ReactRootManager } from "../../../utils/react";
interface IState {
// the URLs (if any) to be previewed with a LinkPreviewWidget inside this TextualBody.
@ -48,9 +48,9 @@ interface IState {
export default class TextualBody extends React.Component<IBodyProps, IState> {
private readonly contentRef = createRef<HTMLDivElement>();
private pills: Element[] = [];
private tooltips: Element[] = [];
private reactRoots: Element[] = [];
private pills = new ReactRootManager();
private tooltips = new ReactRootManager();
private reactRoots = new ReactRootManager();
private ref = createRef<HTMLDivElement>();
@ -82,7 +82,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
// tooltipifyLinks AFTER calculateUrlPreview because the DOM inside the tooltip
// container is empty before the internal component has mounted so calculateUrlPreview
// won't find any anchors
tooltipifyLinks([content], this.pills, this.tooltips);
tooltipifyLinks([content], [...this.pills.elements, ...this.reactRoots.elements], this.tooltips);
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") {
// Handle expansion and add buttons
@ -113,12 +113,11 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
private wrapPreInReact(pre: HTMLPreElement): void {
const root = document.createElement("div");
root.className = "mx_EventTile_pre_container";
this.reactRoots.push(root);
// Insert containing div in place of <pre> block
pre.parentNode?.replaceChild(root, pre);
ReactDOM.render(
this.reactRoots.render(
<StrictMode>
<CodeBlock onHeightChanged={this.props.onHeightChanged}>{pre}</CodeBlock>
</StrictMode>,
@ -137,16 +136,9 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
}
public componentWillUnmount(): void {
unmountPills(this.pills);
unmountTooltips(this.tooltips);
for (const root of this.reactRoots) {
ReactDOM.unmountComponentAtNode(root);
}
this.pills = [];
this.tooltips = [];
this.reactRoots = [];
this.pills.unmount();
this.tooltips.unmount();
this.reactRoots.unmount();
}
public shouldComponentUpdate(nextProps: Readonly<IBodyProps>, nextState: Readonly<IState>): boolean {
@ -204,7 +196,8 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
</StrictMode>
);
ReactDOM.render(spoiler, spoilerContainer);
this.reactRoots.render(spoiler, spoilerContainer);
node.parentNode?.replaceChild(spoilerContainer, node);
node = spoilerContainer;

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

@ -414,7 +414,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 +424,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 +581,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 +595,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 +639,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,7 +668,6 @@ 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">
<ReplyPreview
replyToEvent={this.props.replyToEvent}

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

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

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

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";
@ -51,7 +51,6 @@ export const DiscoverySettings: React.FC = () => {
const [emails, setEmails] = useState<ThirdPartyIdentifier[]>([]);
const [phoneNumbers, setPhoneNumbers] = useState<ThirdPartyIdentifier[]>([]);
const [idServerName, setIdServerName] = useState<string | undefined>(abbreviateUrl(client.getIdentityServerUrl()));
const [canMake3pidChanges, setCanMake3pidChanges] = useState<boolean>(false);
const [requiredPolicyInfo, setRequiredPolicyInfo] = useState<RequiredPolicyInfo>({
// This object is passed along to a component for handling
@ -88,11 +87,6 @@ export const DiscoverySettings: React.FC = () => {
try {
await getThreepidState();
const capabilities = await client.getCapabilities();
setCanMake3pidChanges(
!capabilities["m.3pid_changes"] || capabilities["m.3pid_changes"].enabled === true,
);
// By starting the terms flow we get the logic for checking which terms the user has signed
// for free. So we might as well use that for our own purposes.
const idServerUrl = client.getIdentityServerUrl();
@ -166,7 +160,7 @@ export const DiscoverySettings: React.FC = () => {
medium={ThreepidMedium.Email}
threepids={emails}
onChange={getThreepidState}
disabled={!canMake3pidChanges}
disabled={!hasTerms}
isLoading={isLoadingThreepids}
/>
</SettingsSubsection>
@ -180,7 +174,7 @@ export const DiscoverySettings: React.FC = () => {
medium={ThreepidMedium.Phone}
threepids={phoneNumbers}
onChange={getThreepidState}
disabled={!canMake3pidChanges}
disabled={!hasTerms}
isLoading={isLoadingThreepids}
/>
</SettingsSubsection>
@ -196,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

@ -19,7 +19,7 @@ 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";

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

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

@ -22,7 +22,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";

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

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