/* Copyright 2019 - 2022 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ import React from "react"; import { sleep } from "matrix-js-sdk/src/utils"; import { Room, RoomEvent } from "matrix-js-sdk/src/models/room"; import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../../../languageHandler"; import { MatrixClientPeg } from "../../../../../MatrixClientPeg"; import AccessibleButton from "../../../elements/AccessibleButton"; import dis from "../../../../../dispatcher/dispatcher"; import { SettingLevel } from "../../../../../settings/SettingLevel"; import SecureBackupPanel from "../../SecureBackupPanel"; import SettingsStore from "../../../../../settings/SettingsStore"; import { UIFeature } from "../../../../../settings/UIFeature"; import E2eAdvancedPanel, { isE2eAdvancedPanelPossible } from "../../E2eAdvancedPanel"; import { ActionPayload } from "../../../../../dispatcher/payloads"; import CryptographyPanel from "../../CryptographyPanel"; import DevicesPanel from "../../DevicesPanel"; import SettingsFlag from "../../../elements/SettingsFlag"; import CrossSigningPanel from "../../CrossSigningPanel"; import EventIndexPanel from "../../EventIndexPanel"; import InlineSpinner from "../../../elements/InlineSpinner"; import { PosthogAnalytics } from "../../../../../PosthogAnalytics"; import { showDialog as showAnalyticsLearnMoreDialog } from "../../../dialogs/AnalyticsLearnMoreDialog"; import { privateShouldBeEncrypted } from "../../../../../utils/rooms"; import LoginWithQR, { Mode } from "../../../auth/LoginWithQR"; import LoginWithQRSection from "../../devices/LoginWithQRSection"; import type { IServerVersions } from "matrix-js-sdk/src/matrix"; interface IIgnoredUserProps { userId: string; onUnignored: (userId: string) => void; inProgress: boolean; } export class IgnoredUser extends React.Component { private onUnignoreClicked = (): void => { this.props.onUnignored(this.props.userId); }; public render(): React.ReactNode { const id = `mx_SecurityUserSettingsTab_ignoredUser_${this.props.userId}`; return (
{_t("Unignore")} {this.props.userId}
); } } interface IProps { closeSettingsFn: () => void; } interface IState { ignoredUserIds: string[]; waitingUnignored: string[]; managingInvites: boolean; invitedRoomIds: Set; showLoginWithQR: Mode | null; versions?: IServerVersions; } export default class SecurityUserSettingsTab extends React.Component { private dispatcherRef: string; public constructor(props: IProps) { super(props); // Get rooms we're invited to const invitedRoomIds = new Set(this.getInvitedRooms().map((room) => room.roomId)); this.state = { ignoredUserIds: MatrixClientPeg.get().getIgnoredUsers(), waitingUnignored: [], managingInvites: false, invitedRoomIds, showLoginWithQR: null, }; } private onAction = ({ action }: ActionPayload): void => { if (action === "ignore_state_changed") { const ignoredUserIds = MatrixClientPeg.get().getIgnoredUsers(); const newWaitingUnignored = this.state.waitingUnignored.filter((e) => ignoredUserIds.includes(e)); this.setState({ ignoredUserIds, waitingUnignored: newWaitingUnignored }); } }; public componentDidMount(): void { this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on(RoomEvent.MyMembership, this.onMyMembership); MatrixClientPeg.get() .getVersions() .then((versions) => this.setState({ versions })); } public componentWillUnmount(): void { dis.unregister(this.dispatcherRef); MatrixClientPeg.get().removeListener(RoomEvent.MyMembership, this.onMyMembership); } private onMyMembership = (room: Room, membership: string): void => { if (room.isSpaceRoom()) { return; } if (membership === "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, }; }); }; private onUserUnignored = async (userId: string): Promise => { 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); this.setState(({ waitingUnignored }) => ({ waitingUnignored: [...waitingUnignored, userId] })); MatrixClientPeg.get().setIgnoredUsers(currentlyIgnoredUserIds); } }; private getInvitedRooms = (): Room[] => { return MatrixClientPeg.get() .getRooms() .filter((r) => { return r.hasMembershipState(MatrixClientPeg.get().getUserId(), "invite"); }); }; private manageInvites = async (accept: boolean): Promise => { this.setState({ managingInvites: true, }); // iterate with a normal for loop in order to retry on action failure const invitedRoomIdsValues = Array.from(this.state.invitedRoomIds); // Execute all acceptances/rejections sequentially const cli = MatrixClientPeg.get(); const action = accept ? cli.joinRoom.bind(cli) : cli.leave.bind(cli); for (let i = 0; i < invitedRoomIdsValues.length; i++) { const roomId = invitedRoomIdsValues[i]; // Accept/reject invite await action(roomId).then( () => { // No error, update invited rooms button this.removeInvitedRoom(roomId); }, async (e): Promise => { // 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); // Redo last action i--; } else { // Print out error with joining/leaving room logger.warn(e); } }, ); } this.setState({ managingInvites: false, }); }; private onAcceptAllInvitesClicked = (): void => { this.manageInvites(true); }; private onRejectAllInvitesClicked = (): void => { this.manageInvites(false); }; private renderIgnoredUsers(): JSX.Element { const { waitingUnignored, ignoredUserIds } = this.state; const userIds = !ignoredUserIds?.length ? _t("You have no ignored users.") : ignoredUserIds.map((u) => { return ( ); }); return (
{_t("Ignored users")}
{userIds}
); } private renderManageInvites(): JSX.Element { const { invitedRoomIds } = this.state; if (invitedRoomIds.size === 0) { return null; } return (
{_t("Bulk options")} {_t("Accept all %(invitedRooms)s invites", { invitedRooms: invitedRoomIds.size })} {_t("Reject all %(invitedRooms)s invites", { invitedRooms: invitedRoomIds.size })} {this.state.managingInvites ? :
}
); } private onShowQRClicked = (): void => { this.setState({ showLoginWithQR: Mode.Show }); }; private onLoginWithQRFinished = (): void => { this.setState({ showLoginWithQR: null }); }; public render(): React.ReactNode { const secureBackup = (
{_t("Secure Backup")}
); const eventIndex = (
{_t("Message search")}
); // 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. const crossSigning = (
{_t("Cross-signing")}
); let warning; if (!privateShouldBeEncrypted()) { warning = (
{_t( "Your server admin has disabled end-to-end encryption by default " + "in private rooms & Direct Messages.", )}
); } let privacySection; if (PosthogAnalytics.instance.isEnabled()) { const onClickAnalyticsLearnMore = (): void => { showAnalyticsLearnMoreDialog({ primaryButton: _t("Okay"), hasCancel: false, }); }; privacySection = (
{_t("Privacy")}
{_t("Analytics")}

{_t( "Share anonymous data to help us identify issues. Nothing personal. " + "No third parties.", )}

{_t("Learn more")}
{PosthogAnalytics.instance.isEnabled() && ( )} {_t("Sessions")}
); } let advancedSection; if (SettingsStore.getValue(UIFeature.AdvancedSettings)) { const ignoreUsersPanel = this.renderIgnoredUsers(); const invitesPanel = this.renderManageInvites(); const e2ePanel = isE2eAdvancedPanelPossible() ? : null; // only show the section if there's something to show if (ignoreUsersPanel || invitesPanel || e2ePanel) { advancedSection = ( <>
{_t("Advanced")}
{ignoreUsersPanel} {invitesPanel} {e2ePanel}
); } } const useNewSessionManager = SettingsStore.getValue("feature_new_device_manager"); const showQrCodeEnabled = SettingsStore.getValue("feature_qr_signin_reciprocate_show"); const devicesSection = useNewSessionManager ? null : ( <>
{_t("Where you're signed in")}
{_t( "Manage your signed-in devices below. " + "A device's name is visible to people you communicate with.", )}
{showQrCodeEnabled ? ( ) : null} ); const client = MatrixClientPeg.get(); if (showQrCodeEnabled && this.state.showLoginWithQR) { return (
); } return (
{warning} {devicesSection}
{_t("Encryption")}
{secureBackup} {eventIndex} {crossSigning}
{privacySection} {advancedSection}
); } }