Files
ThreadNet-Web/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

382 lines
14 KiB
TypeScript
Raw Normal View History

/*
2024-09-09 14:57:16 +01:00
Copyright 2024 New Vector Ltd.
Copyright 2019-2023 The Matrix.org Foundation C.I.C.
2024-09-09 14:57:16 +01:00
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 { sleep } from "matrix-js-sdk/src/utils";
import { Room, RoomEvent } from "matrix-js-sdk/src/matrix";
import { KnownMembership, Membership } from "matrix-js-sdk/src/types";
2021-12-09 09:10:23 +00:00
import { logger } from "matrix-js-sdk/src/logger";
2021-06-29 13:11:58 +01:00
import { _t } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
2019-02-22 10:47:18 -07:00
import AccessibleButton from "../../../elements/AccessibleButton";
2020-05-13 20:41:41 -06:00
import dis from "../../../../../dispatcher/dispatcher";
2021-06-29 13:11:58 +01:00
import { SettingLevel } from "../../../../../settings/SettingLevel";
2020-09-04 11:37:06 +01:00
import SecureBackupPanel from "../../SecureBackupPanel";
import SettingsStore from "../../../../../settings/SettingsStore";
2021-06-29 13:11:58 +01:00
import { UIFeature } from "../../../../../settings/UIFeature";
2021-09-21 13:59:29 +02:00
import { ActionPayload } from "../../../../../dispatcher/payloads";
import CryptographyPanel from "../../CryptographyPanel";
2021-09-21 13:59:29 +02:00
import SettingsFlag from "../../../elements/SettingsFlag";
import CrossSigningPanel from "../../CrossSigningPanel";
import EventIndexPanel from "../../EventIndexPanel";
import InlineSpinner from "../../../elements/InlineSpinner";
2021-12-06 09:39:33 +11:00
import { PosthogAnalytics } from "../../../../../PosthogAnalytics";
import { showDialog as showAnalyticsLearnMoreDialog } from "../../../dialogs/AnalyticsLearnMoreDialog";
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 { useOwnDevices } from "../../devices/useOwnDevices";
import DiscoverySettings from "../../discovery/DiscoverySettings";
2024-07-10 20:10:19 +01:00
import SetIntegrationManager from "../../SetIntegrationManager";
2021-10-15 16:31:29 +02:00
2021-09-21 13:59:29 +02:00
interface IIgnoredUserProps {
userId: string;
onUnignored: (userId: string) => void;
inProgress: boolean;
}
const DehydratedDeviceStatus: React.FC = () => {
const { dehydratedDeviceId } = useOwnDevices();
if (dehydratedDeviceId) {
return (
<div className="mx_SettingsSubsection_content">
<div className="mx_SettingsFlag_label">{_t("settings|security|dehydrated_device_enabled")}</div>
<div className="mx_SettingsSubsection_text">
{_t("settings|security|dehydrated_device_description")}
</div>
</div>
);
} else {
return null;
}
};
2021-09-21 13:59:29 +02:00
export class IgnoredUser extends React.Component<IIgnoredUserProps> {
private onUnignoreClicked = (): void => {
this.props.onUnignored(this.props.userId);
};
public render(): React.ReactNode {
const id = `mx_SecurityUserSettingsTab_ignoredUser_${this.props.userId}`;
return (
2019-02-22 10:47:18 -07:00
<div className="mx_SecurityUserSettingsTab_ignoredUser">
2021-09-21 13:59:29 +02:00
<AccessibleButton
onClick={this.onUnignoreClicked}
kind="primary_sm"
aria-describedby={id}
disabled={this.props.inProgress}
>
{_t("action|unignore")}
</AccessibleButton>
<span id={id}>{this.props.userId}</span>
</div>
);
}
}
2021-09-21 13:59:29 +02:00
interface IProps {
closeSettingsFn: () => void;
}
2021-09-21 13:59:29 +02:00
interface IState {
ignoredUserIds: string[];
waitingUnignored: string[];
managingInvites: boolean;
invitedRoomIds: Set<string>;
versions?: IServerVersions;
2021-09-21 13:59:29 +02:00
}
export default class SecurityUserSettingsTab extends React.Component<IProps, IState> {
private dispatcherRef?: string;
2021-09-21 13:59:29 +02:00
public constructor(props: IProps) {
2021-09-21 13:59:29 +02:00
super(props);
// Get rooms we're invited to
const invitedRoomIds = new Set(this.getInvitedRooms().map((room) => room.roomId));
2019-03-06 13:58:47 +00:00
this.state = {
ignoredUserIds: MatrixClientPeg.safeGet().getIgnoredUsers(),
waitingUnignored: [],
2019-03-06 13:58:47 +00:00
managingInvites: false,
invitedRoomIds,
};
}
private onAction = ({ action }: ActionPayload): void => {
2020-04-22 16:44:47 +02:00
if (action === "ignore_state_changed") {
const ignoredUserIds = MatrixClientPeg.safeGet().getIgnoredUsers();
2022-01-10 12:57:20 +00:00
const newWaitingUnignored = this.state.waitingUnignored.filter((e) => ignoredUserIds.includes(e));
2021-06-29 13:11:58 +01:00
this.setState({ ignoredUserIds, waitingUnignored: newWaitingUnignored });
}
2021-09-21 13:59:29 +02:00
};
public componentDidMount(): void {
this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.safeGet().on(RoomEvent.MyMembership, this.onMyMembership);
MatrixClientPeg.safeGet()
.getVersions()
.then((versions) => this.setState({ versions }));
}
2021-09-21 13:59:29 +02:00
public componentWillUnmount(): void {
dis.unregister(this.dispatcherRef);
MatrixClientPeg.safeGet().removeListener(RoomEvent.MyMembership, this.onMyMembership);
}
2024-03-11 17:16:53 +00:00
private onMyMembership = (room: Room, membership: Membership): void => {
if (room.isSpaceRoom()) {
return;
}
if (membership === KnownMembership.Invite) {
this.addInvitedRoom(room);
} else if (this.state.invitedRoomIds.has(room.roomId)) {
// The user isn't invited anymore
this.removeInvitedRoom(room.roomId);
}
};
private addInvitedRoom = (room: Room): void => {
this.setState(({ invitedRoomIds }) => ({
invitedRoomIds: new Set(invitedRoomIds).add(room.roomId),
}));
};
private removeInvitedRoom = (roomId: string): void => {
this.setState(({ invitedRoomIds }) => {
const newInvitedRoomIds = new Set(invitedRoomIds);
newInvitedRoomIds.delete(roomId);
return {
invitedRoomIds: newInvitedRoomIds,
};
});
};
2021-09-21 13:59:29 +02:00
private onUserUnignored = async (userId: string): Promise<void> => {
2021-06-29 13:11:58 +01:00
const { ignoredUserIds, waitingUnignored } = this.state;
const currentlyIgnoredUserIds = ignoredUserIds.filter((e) => !waitingUnignored.includes(e));
const index = currentlyIgnoredUserIds.indexOf(userId);
if (index !== -1) {
currentlyIgnoredUserIds.splice(index, 1);
2021-06-29 13:11:58 +01:00
this.setState(({ waitingUnignored }) => ({ waitingUnignored: [...waitingUnignored, userId] }));
MatrixClientPeg.safeGet().setIgnoredUsers(currentlyIgnoredUserIds);
}
};
2021-09-21 13:59:29 +02:00
private getInvitedRooms = (): Room[] => {
return MatrixClientPeg.safeGet()
2019-03-06 13:58:47 +00:00
.getRooms()
.filter((r) => {
return r.hasMembershipState(MatrixClientPeg.safeGet().getUserId()!, KnownMembership.Invite);
2019-03-06 13:58:47 +00:00
});
};
2021-09-21 13:59:29 +02:00
private manageInvites = async (accept: boolean): Promise<void> => {
this.setState({
2019-03-06 13:58:47 +00:00
managingInvites: true,
});
2019-03-06 13:58:47 +00:00
// iterate with a normal for loop in order to retry on action failure
const invitedRoomIdsValues = Array.from(this.state.invitedRoomIds);
2019-03-06 13:58:47 +00:00
// Execute all acceptances/rejections sequentially
const cli = MatrixClientPeg.safeGet();
2019-03-06 13:58:47 +00:00
const action = accept ? cli.joinRoom.bind(cli) : cli.leave.bind(cli);
for (let i = 0; i < invitedRoomIdsValues.length; i++) {
const roomId = invitedRoomIdsValues[i];
2019-03-06 13:58:47 +00:00
// Accept/reject invite
await action(roomId).then(
() => {
// No error, update invited rooms button
this.removeInvitedRoom(roomId);
2019-03-06 13:58:47 +00:00
},
async (e): Promise<void> => {
2019-03-06 13:58:47 +00:00
// Action failure
if (e.errcode === "M_LIMIT_EXCEEDED") {
// Add a delay between each invite change in order to avoid rate
// limiting by the server.
await sleep(e.retry_after_ms || 2500);
2019-03-06 13:58:47 +00:00
// Redo last action
i--;
} else {
// Print out error with joining/leaving room
2021-10-15 16:31:29 +02:00
logger.warn(e);
2019-03-06 13:58:47 +00:00
}
},
);
2019-03-06 13:58:47 +00:00
}
this.setState({
managingInvites: false,
});
};
2021-09-21 13:59:29 +02:00
private onAcceptAllInvitesClicked = (): void => {
this.manageInvites(true);
2019-03-06 13:58:47 +00:00
};
2021-09-21 13:59:29 +02:00
private onRejectAllInvitesClicked = (): void => {
this.manageInvites(false);
2019-03-06 13:58:47 +00:00
};
2021-09-21 13:59:29 +02:00
private renderIgnoredUsers(): JSX.Element {
2021-06-29 13:11:58 +01:00
const { waitingUnignored, ignoredUserIds } = this.state;
const userIds = !ignoredUserIds?.length
? _t("settings|security|ignore_users_empty")
2021-04-29 20:08:14 +02:00
: ignoredUserIds.map((u) => {
return (
<IgnoredUser
userId={u}
2021-09-21 13:59:29 +02:00
onUnignored={this.onUserUnignored}
2021-04-29 20:08:14 +02:00
key={u}
inProgress={waitingUnignored.includes(u)}
/>
);
});
return (
<SettingsSubsection heading={_t("settings|security|ignore_users_section")}>
<SettingsSubsectionText>{userIds}</SettingsSubsectionText>
</SettingsSubsection>
);
}
private renderManageInvites(): ReactNode {
const { invitedRoomIds } = this.state;
if (invitedRoomIds.size === 0) {
return null;
}
return (
<SettingsSubsection heading={_t("settings|security|bulk_options_section")}>
<div className="mx_SecurityUserSettingsTab_bulkOptions">
<AccessibleButton
onClick={this.onAcceptAllInvitesClicked}
kind="primary_outline"
disabled={this.state.managingInvites}
>
{_t("settings|security|bulk_options_accept_all_invites", { invitedRooms: invitedRoomIds.size })}
</AccessibleButton>
<AccessibleButton
onClick={this.onRejectAllInvitesClicked}
kind="danger_outline"
disabled={this.state.managingInvites}
>
{_t("settings|security|bulk_options_reject_all_invites", { invitedRooms: invitedRoomIds.size })}
</AccessibleButton>
{this.state.managingInvites ? <InlineSpinner /> : <div />}
</div>
</SettingsSubsection>
);
}
public render(): React.ReactNode {
2020-09-04 11:37:06 +01:00
const secureBackup = (
<SettingsSubsection heading={_t("common|secure_backup")}>
<SecureBackupPanel />
<DehydratedDeviceStatus />
</SettingsSubsection>
2019-02-08 16:58:09 +00:00
);
2020-04-22 11:05:43 -06:00
const eventIndex = (
<SettingsSubsection heading={_t("settings|security|message_search_section")}>
2020-04-22 10:58:11 -06:00
<EventIndexPanel />
</SettingsSubsection>
2020-04-22 10:58:11 -06:00
);
2019-11-28 16:45:29 +00:00
// XXX: There's no such panel in the current cross-signing designs, but
// it's useful to have for testing the feature. If there's no interest
// in having advanced details here once all flows are implemented, we
// can remove this.
2020-05-27 10:28:25 +01:00
const crossSigning = (
<SettingsSubsection heading={_t("common|cross_signing")}>
<CrossSigningPanel />
</SettingsSubsection>
);
let warning;
if (!privateShouldBeEncrypted(MatrixClientPeg.safeGet())) {
warning = (
<div className="mx_SecurityUserSettingsTab_warning">
{_t("settings|security|e2ee_default_disabled_warning")}
</div>
);
}
let privacySection;
2022-06-14 17:51:51 +01:00
if (PosthogAnalytics.instance.isEnabled()) {
const onClickAnalyticsLearnMore = (): void => {
2022-06-14 17:51:51 +01:00
showAnalyticsLearnMoreDialog({
primaryButton: _t("action|ok"),
2022-06-14 17:51:51 +01:00
hasCancel: false,
});
2021-12-06 09:39:33 +11:00
};
privacySection = (
<SettingsSection heading={_t("common|privacy")}>
<DiscoverySettings />
<SettingsSubsection
heading={_t("common|analytics")}
description={_t("settings|security|analytics_description")}
>
<AccessibleButton kind="link" onClick={onClickAnalyticsLearnMore}>
{_t("action|learn_more")}
</AccessibleButton>
2022-06-14 17:51:51 +01:00
{PosthogAnalytics.instance.isEnabled() && (
<SettingsFlag name="pseudonymousAnalyticsOptIn" level={SettingLevel.ACCOUNT} />
2022-12-12 12:24:14 +01:00
)}
</SettingsSubsection>
<SettingsSubsection heading={_t("settings|sessions|title")}>
<SettingsFlag name="deviceClientInformationOptIn" level={SettingLevel.ACCOUNT} />
</SettingsSubsection>
</SettingsSection>
);
}
let advancedSection;
if (SettingsStore.getValue(UIFeature.AdvancedSettings)) {
2021-09-21 13:59:29 +02:00
const ignoreUsersPanel = this.renderIgnoredUsers();
const invitesPanel = this.renderManageInvites();
// only show the section if there's something to show
if (ignoreUsersPanel || invitesPanel) {
advancedSection = (
<SettingsSection heading={_t("common|advanced")}>
{ignoreUsersPanel}
{invitesPanel}
</SettingsSection>
);
}
}
2022-10-11 11:10:55 +02:00
return (
<SettingsTab>
2022-10-11 11:10:55 +02:00
{warning}
2024-07-10 20:10:19 +01:00
<SetIntegrationManager />
<SettingsSection heading={_t("settings|security|encryption_section")}>
{secureBackup}
{eventIndex}
{crossSigning}
<CryptographyPanel />
</SettingsSection>
{privacySection}
{advancedSection}
</SettingsTab>
);
}
}