Merge branch 'develop' into sort-imports

Signed-off-by: Aaron Raimist <aaron@raim.ist>
This commit is contained in:
Aaron Raimist 2021-12-09 08:34:20 +00:00
commit 7b94e13a84
642 changed files with 30052 additions and 8035 deletions

View file

@ -23,7 +23,7 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
import AccessibleButton from '../elements/AccessibleButton';
import Spinner from '../elements/Spinner';
import withValidation, { IFieldState, IValidationResult } from '../elements/Validation';
import { _t } from '../../../languageHandler';
import { _t, _td } from '../../../languageHandler';
import Modal from "../../../Modal";
import PassphraseField from "../auth/PassphraseField";
import CountlyAnalytics from "../../../CountlyAnalytics";
@ -377,7 +377,7 @@ export default class ChangePassword extends React.Component<IProps, IState> {
<PassphraseField
fieldRef={field => this[FIELD_NEW_PASSWORD] = field}
type="password"
label='New Password'
label={_td("New Password")}
minScore={PASSWORD_MIN_SCORE}
value={this.state.newPassword}
autoFocus={this.props.autoFocusNewPasswordInput}

View file

@ -15,20 +15,21 @@ limitations under the License.
*/
import React from 'react';
import { MatrixEvent } from 'matrix-js-sdk/src';
import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import Modal from '../../../Modal';
import Spinner from '../elements/Spinner';
import InteractiveAuthDialog from '../dialogs/InteractiveAuthDialog';
import ConfirmDestroyCrossSigningDialog from '../dialogs/security/ConfirmDestroyCrossSigningDialog';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { MatrixEvent } from 'matrix-js-sdk/src';
import SetupEncryptionDialog from '../dialogs/security/SetupEncryptionDialog';
import { accessSecretStorage } from '../../../SecurityManager';
import { logger } from "matrix-js-sdk/src/logger";
import AccessibleButton from "../elements/AccessibleButton";
interface IState {
error?: Error;
crossSigningPublicKeysOnDevice?: boolean;
@ -164,7 +165,6 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
};
public render() {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
const {
error,
crossSigningPublicKeysOnDevice,

View file

@ -28,6 +28,7 @@ import InteractiveAuthDialog from "../dialogs/InteractiveAuthDialog";
import DevicesPanelEntry from "./DevicesPanelEntry";
import Spinner from "../elements/Spinner";
import AccessibleButton from "../elements/AccessibleButton";
import { CrossSigningInfo } from "matrix-js-sdk/src/crypto/CrossSigning";
interface IProps {
className?: string;
@ -35,6 +36,7 @@ interface IProps {
interface IState {
devices: IMyDevice[];
crossSigningInfo?: CrossSigningInfo;
deviceLoadError?: string;
selectedDevices: string[];
deleting?: boolean;
@ -50,6 +52,7 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
devices: [],
selectedDevices: [],
};
this.loadDevices = this.loadDevices.bind(this);
}
public componentDidMount(): void {
@ -61,20 +64,34 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
}
private loadDevices(): void {
MatrixClientPeg.get().getDevices().then(
const cli = MatrixClientPeg.get();
cli.getDevices().then(
(resp) => {
if (this.unmounted) { return; }
this.setState({ devices: resp.devices || [] });
const crossSigningInfo = cli.getStoredCrossSigningForUser(cli.getUserId());
this.setState((state, props) => {
const deviceIds = resp.devices.map((device) => device.device_id);
const selectedDevices = state.selectedDevices.filter(
(deviceId) => deviceIds.includes(deviceId),
);
return {
devices: resp.devices || [],
selectedDevices,
crossSigningInfo: crossSigningInfo,
};
});
console.log(this.state);
},
(error) => {
if (this.unmounted) { return; }
let errtxt;
if (error.httpStatus == 404) {
// 404 probably means the HS doesn't yet support the API.
errtxt = _t("Your homeserver does not support session management.");
errtxt = _t("Your homeserver does not support device management.");
} else {
logger.error("Error loading sessions:", error);
errtxt = _t("Unable to load session list");
errtxt = _t("Unable to load device list");
}
this.setState({ deviceLoadError: errtxt });
},
@ -97,6 +114,22 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
return (idA < idB) ? -1 : (idA > idB) ? 1 : 0;
}
private isDeviceVerified(device: IMyDevice): boolean | null {
try {
const cli = MatrixClientPeg.get();
const deviceInfo = cli.getStoredDevice(cli.getUserId(), device.device_id);
return this.state.crossSigningInfo.checkDeviceTrust(
this.state.crossSigningInfo,
deviceInfo,
false,
true,
).isCrossSigningVerified();
} catch (e) {
console.error("Error getting device cross-signing info", e);
return null;
}
}
private onDeviceSelectionToggled = (device: IMyDevice): void => {
if (this.unmounted) { return; }
@ -116,7 +149,40 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
});
};
private selectAll = (devices: IMyDevice[]): void => {
this.setState((state, props) => {
const selectedDevices = state.selectedDevices.slice();
for (const device of devices) {
const deviceId = device.device_id;
if (!selectedDevices.includes(deviceId)) {
selectedDevices.push(deviceId);
}
}
return { selectedDevices };
});
};
private deselectAll = (devices: IMyDevice[]): void => {
this.setState((state, props) => {
const selectedDevices = state.selectedDevices.slice();
for (const device of devices) {
const deviceId = device.device_id;
const i = selectedDevices.indexOf(deviceId);
if (i !== -1) {
selectedDevices.splice(i, 1);
}
}
return { selectedDevices };
});
};
private onDeleteClick = (): void => {
if (this.state.selectedDevices.length === 0) { return; }
this.setState({
deleting: true,
});
@ -134,18 +200,18 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("Use Single Sign On to continue"),
body: _t("Confirm deleting these sessions by using Single Sign On to prove your identity.", {
body: _t("Confirm logging out these devices by using Single Sign On to prove your identity.", {
count: numDevices,
}),
continueText: _t("Single Sign On"),
continueKind: "primary",
},
[SSOAuthEntry.PHASE_POSTAUTH]: {
title: _t("Confirm deleting these sessions"),
body: _t("Click the button below to confirm deleting these sessions.", {
title: _t("Confirm signing out these devices"),
body: _t("Click the button below to confirm signing out these devices.", {
count: numDevices,
}),
continueText: _t("Delete sessions", { count: numDevices }),
continueText: _t("Sign out devices", { count: numDevices }),
continueKind: "danger",
},
};
@ -173,34 +239,46 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
private makeDeleteRequest(auth?: any): Promise<any> {
return MatrixClientPeg.get().deleteMultipleDevices(this.state.selectedDevices, auth).then(
() => {
// Remove the deleted devices from `devices`, reset selection to []
// Reset selection to [], update device list
this.setState({
devices: this.state.devices.filter(
(d) => !this.state.selectedDevices.includes(d.device_id),
),
selectedDevices: [],
});
this.loadDevices();
},
);
}
private renderDevice = (device: IMyDevice): JSX.Element => {
const myDeviceId = MatrixClientPeg.get().getDeviceId();
const myDevice = this.state.devices.find((device) => (device.device_id === myDeviceId));
const isOwnDevice = device.device_id === myDeviceId;
// If our own device is unverified, it can't verify other
// devices, it can only request verification for itself
const canBeVerified = (myDevice && this.isDeviceVerified(myDevice)) || isOwnDevice;
return <DevicesPanelEntry
key={device.device_id}
device={device}
selected={this.state.selectedDevices.includes(device.device_id)}
isOwnDevice={isOwnDevice}
verified={this.isDeviceVerified(device)}
canBeVerified={canBeVerified}
onDeviceChange={this.loadDevices}
onDeviceToggled={this.onDeviceSelectionToggled}
/>;
};
public render(): JSX.Element {
const loadError = (
<div className={classNames(this.props.className, "error")}>
{ this.state.deviceLoadError }
</div>
);
if (this.state.deviceLoadError !== undefined) {
const classes = classNames(this.props.className, "error");
return (
<div className={classes}>
{ this.state.deviceLoadError }
</div>
);
return loadError;
}
const devices = this.state.devices;
@ -209,31 +287,121 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
return <Spinner />;
}
devices.sort(this.deviceCompare);
const myDeviceId = MatrixClientPeg.get().getDeviceId();
const myDevice = devices.find((device) => (device.device_id === myDeviceId));
if (!myDevice) {
return loadError;
}
const otherDevices = devices.filter((device) => (device.device_id !== myDeviceId));
otherDevices.sort(this.deviceCompare);
const verifiedDevices = [];
const unverifiedDevices = [];
const nonCryptoDevices = [];
for (const device of otherDevices) {
const verified = this.isDeviceVerified(device);
if (verified === true) {
verifiedDevices.push(device);
} else if (verified === false) {
unverifiedDevices.push(device);
} else {
nonCryptoDevices.push(device);
}
}
const section = (trustIcon: JSX.Element, title: string, deviceList: IMyDevice[]): JSX.Element => {
if (deviceList.length === 0) {
return <React.Fragment />;
}
let selectButton: JSX.Element;
if (deviceList.length > 1) {
const anySelected = deviceList.some((device) => this.state.selectedDevices.includes(device.device_id));
const buttonAction = anySelected ?
() => { this.deselectAll(deviceList); } :
() => { this.selectAll(deviceList); };
const buttonText = anySelected ? _t("Deselect all") : _t("Select all");
selectButton = <div className="mx_DevicesPanel_header_button">
<AccessibleButton
className="mx_DevicesPanel_selectButton"
kind="secondary"
onClick={buttonAction}
>
{ buttonText }
</AccessibleButton>
</div>;
}
return <React.Fragment>
<hr />
<div className="mx_DevicesPanel_header">
<div className="mx_DevicesPanel_header_trust">
{ trustIcon }
</div>
<div className="mx_DevicesPanel_header_title">
{ title }
</div>
{ selectButton }
</div>
{ deviceList.map(this.renderDevice) }
</React.Fragment>;
};
const verifiedDevicesSection = section(
<span className="mx_DevicesPanel_header_icon mx_E2EIcon mx_E2EIcon_verified" />,
_t("Verified devices"),
verifiedDevices,
);
const unverifiedDevicesSection = section(
<span className="mx_DevicesPanel_header_icon mx_E2EIcon mx_E2EIcon_warning" />,
_t("Unverified devices"),
unverifiedDevices,
);
const nonCryptoDevicesSection = section(
<React.Fragment />,
_t("Devices without encryption support"),
nonCryptoDevices,
);
const deleteButton = this.state.deleting ?
<Spinner w={22} h={22} /> :
<AccessibleButton onClick={this.onDeleteClick} kind="danger_sm">
{ _t("Delete %(count)s sessions", { count: this.state.selectedDevices.length }) }
<AccessibleButton
className="mx_DevicesPanel_deleteButton"
onClick={this.onDeleteClick}
kind="danger_outline"
disabled={this.state.selectedDevices.length === 0}
>
{ _t("Sign out %(count)s selected devices", { count: this.state.selectedDevices.length }) }
</AccessibleButton>;
const otherDevicesSection = (otherDevices.length > 0) ?
<React.Fragment>
{ verifiedDevicesSection }
{ unverifiedDevicesSection }
{ nonCryptoDevicesSection }
{ deleteButton }
</React.Fragment> :
<React.Fragment>
<hr />
<div className="mx_DevicesPanel_noOtherDevices">
{ _t("You aren't signed into any other devices.") }
</div>
</React.Fragment>;
const classes = classNames(this.props.className, "mx_DevicesPanel");
return (
<table className={classes}>
<thead className="mx_DevicesPanel_header">
<tr>
<th className="mx_DevicesPanel_deviceId">{ _t("ID") }</th>
<th className="mx_DevicesPanel_deviceName">{ _t("Public Name") }</th>
<th className="mx_DevicesPanel_deviceLastSeen">{ _t("Last seen") }</th>
<th className="mx_DevicesPanel_deviceButtons">
{ this.state.selectedDevices.length > 0 ? deleteButton : null }
</th>
</tr>
</thead>
<tbody>
{ devices.map(this.renderDevice) }
</tbody>
</table>
<div className={classes}>
<div className="mx_DevicesPanel_header">
<div className="mx_DevicesPanel_header_title">
{ _t("This device") }
</div>
</div>
{ this.renderDevice(myDevice) }
{ otherDevicesSection }
</div>
);
}
}

View file

@ -22,33 +22,98 @@ import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { formatDate } from '../../../DateUtils';
import StyledCheckbox from '../elements/StyledCheckbox';
import { CheckboxStyle } from '../elements/StyledCheckbox';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import EditableTextContainer from "../elements/EditableTextContainer";
import AccessibleButton from "../elements/AccessibleButton";
import Field from "../elements/Field";
import TextWithTooltip from "../elements/TextWithTooltip";
import Modal from "../../../Modal";
import SetupEncryptionDialog from '../dialogs/security/SetupEncryptionDialog';
import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog';
import LogoutDialog from '../dialogs/LogoutDialog';
interface IProps {
device?: IMyDevice;
onDeviceToggled?: (device: IMyDevice) => void;
selected?: boolean;
device: IMyDevice;
isOwnDevice: boolean;
verified: boolean | null;
canBeVerified: boolean;
onDeviceChange: () => void;
onDeviceToggled: (device: IMyDevice) => void;
selected: boolean;
}
interface IState {
renaming: boolean;
displayName: string;
}
@replaceableComponent("views.settings.DevicesPanelEntry")
export default class DevicesPanelEntry extends React.Component<IProps> {
public static defaultProps = {
onDeviceToggled: () => {},
export default class DevicesPanelEntry extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
renaming: false,
displayName: props.device.display_name,
};
}
private onDeviceToggled = (): void => {
this.props.onDeviceToggled(this.props.device);
};
private onDisplayNameChanged = (value: string): Promise<{}> => {
const device = this.props.device;
return MatrixClientPeg.get().setDeviceDetails(device.device_id, {
display_name: value,
private onRename = (): void => {
this.setState({ renaming: true });
};
private onChangeDisplayName = (ev: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({
displayName: ev.target.value,
});
};
private onRenameSubmit = async () => {
this.setState({ renaming: false });
await MatrixClientPeg.get().setDeviceDetails(this.props.device.device_id, {
display_name: this.state.displayName,
}).catch((e) => {
logger.error("Error setting session display name", e);
throw new Error(_t("Failed to set display name"));
});
this.props.onDeviceChange();
};
private onDeviceToggled = (): void => {
this.props.onDeviceToggled(this.props.device);
private onRenameCancel = (): void => {
this.setState({ renaming: false });
};
private onOwnDeviceSignOut = (): void => {
Modal.createTrackedDialog('Logout from device list', '', LogoutDialog,
/* props= */{}, /* className= */null,
/* isPriority= */false, /* isStatic= */true);
};
private verify = async () => {
if (this.props.isOwnDevice) {
Modal.createTrackedDialog("Verify session", "Verify session", SetupEncryptionDialog, {
onFinished: this.props.onDeviceChange,
});
} else {
const cli = MatrixClientPeg.get();
const userId = cli.getUserId();
const verificationRequestPromise = cli.requestVerification(
userId,
[this.props.device.device_id],
);
Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, {
verificationRequestPromise,
member: cli.getUser(userId),
onFinished: async () => {
const request = await verificationRequestPromise;
request.cancel();
this.props.onDeviceChange();
},
});
}
};
public render(): JSX.Element {
@ -56,34 +121,87 @@ export default class DevicesPanelEntry extends React.Component<IProps> {
let lastSeen = "";
if (device.last_seen_ts) {
const lastSeenDate = formatDate(new Date(device.last_seen_ts));
lastSeen = device.last_seen_ip + " @ " +
lastSeenDate.toLocaleString();
const lastSeenDate = new Date(device.last_seen_ts);
lastSeen = _t("Last seen %(date)s at %(ip)s", {
date: formatDate(lastSeenDate),
ip: device.last_seen_ip,
});
}
let myDeviceClass = '';
if (device.device_id === MatrixClientPeg.get().getDeviceId()) {
myDeviceClass = " mx_DevicesPanel_myDevice";
const myDeviceClass = this.props.isOwnDevice ? " mx_DevicesPanel_myDevice" : '';
let iconClass = '';
let verifyButton: JSX.Element;
if (this.props.verified !== null) {
iconClass = this.props.verified ? "mx_E2EIcon_verified" : "mx_E2EIcon_warning";
if (!this.props.verified && this.props.canBeVerified) {
verifyButton = <AccessibleButton kind="primary" onClick={this.verify}>
{ _t("Verify") }
</AccessibleButton>;
}
}
let signOutButton: JSX.Element;
if (this.props.isOwnDevice) {
signOutButton = <AccessibleButton kind="danger_outline" onClick={this.onOwnDeviceSignOut}>
{ _t("Sign Out") }
</AccessibleButton>;
}
const left = this.props.isOwnDevice ?
<div className="mx_DevicesPanel_deviceTrust">
<span className={"mx_DevicesPanel_icon mx_E2EIcon " + iconClass} />
</div> :
<div className="mx_DevicesPanel_checkbox">
<StyledCheckbox kind={CheckboxStyle.Outline} onChange={this.onDeviceToggled} checked={this.props.selected} />
</div>;
const deviceName = device.display_name ?
<React.Fragment>
<TextWithTooltip tooltip={device.display_name + " (" + device.device_id + ")"}>
{ device.display_name }
</TextWithTooltip>
</React.Fragment> :
<React.Fragment>
{ device.device_id }
</React.Fragment>;
const buttons = this.state.renaming ?
<form className="mx_DevicesPanel_renameForm" onSubmit={this.onRenameSubmit}>
<Field
label={_t("Display Name")}
type="text"
value={this.state.displayName}
autoComplete="off"
onChange={this.onChangeDisplayName}
autoFocus
/>
<AccessibleButton onClick={this.onRenameSubmit} kind="confirm_sm" />
<AccessibleButton onClick={this.onRenameCancel} kind="cancel_sm" />
</form> :
<React.Fragment>
{ signOutButton }
{ verifyButton }
<AccessibleButton kind="primary_outline" onClick={this.onRename}>
{ _t("Rename") }
</AccessibleButton>
</React.Fragment>;
return (
<tr className={"mx_DevicesPanel_device" + myDeviceClass}>
<td className="mx_DevicesPanel_deviceId">
{ device.device_id }
</td>
<td className="mx_DevicesPanel_deviceName">
<EditableTextContainer initialValue={device.display_name}
onSubmit={this.onDisplayNameChanged}
placeholder={device.device_id}
/>
</td>
<td className="mx_DevicesPanel_lastSeen">
{ lastSeen }
</td>
<td className="mx_DevicesPanel_deviceButtons">
<StyledCheckbox onChange={this.onDeviceToggled} checked={this.props.selected} />
</td>
</tr>
<div className={"mx_DevicesPanel_device" + myDeviceClass}>
{ left }
<div className="mx_DevicesPanel_deviceInfo">
<div className="mx_DevicesPanel_deviceName">
{ deviceName }
</div>
<div className="mx_DevicesPanel_lastSeen">
{ lastSeen }
</div>
</div>
<div className="mx_DevicesPanel_deviceButtons">
{ buttons }
</div>
</div>
);
}
}

View file

@ -23,7 +23,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import Slider from "../elements/Slider";
import { FontWatcher } from "../../../settings/watchers/FontWatcher";
import { IValidationResult, IFieldState } from '../elements/Validation';
import { Layout } from "../../../settings/Layout";
import { Layout } from "../../../settings/enums/Layout";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { SettingLevel } from "../../../settings/SettingLevel";
import { _t } from "../../../languageHandler";

View file

@ -0,0 +1,79 @@
/*
Copyright 2021 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 SettingsStore from "../../../settings/SettingsStore";
import StyledRadioButton from "../elements/StyledRadioButton";
import { _t } from "../../../languageHandler";
import { SettingLevel } from "../../../settings/SettingLevel";
import { ImageSize } from "../../../settings/enums/ImageSize";
interface IProps {
// none
}
interface IState {
size: ImageSize;
}
export default class ImageSizePanel extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
size: SettingsStore.getValue("Images.size"),
};
}
private onSizeChange = (ev: React.ChangeEvent<HTMLInputElement>): void => {
const newSize = ev.target.value as ImageSize;
this.setState({ size: newSize });
// noinspection JSIgnoredPromiseFromCall
SettingsStore.setValue("Images.size", null, SettingLevel.ACCOUNT, newSize);
};
public render(): JSX.Element {
return (
<div className="mx_SettingsTab_section mx_ImageSizePanel">
<span className="mx_SettingsTab_subheading">
{ _t("Image size in the timeline") }
</span>
<div className="mx_ImageSizePanel_radios">
<label>
<div className="mx_ImageSizePanel_size mx_ImageSizePanel_sizeDefault" />
<StyledRadioButton
name="image_size"
value={ImageSize.Normal}
checked={this.state.size === ImageSize.Normal}
onChange={this.onSizeChange}
>{ _t("Default") }</StyledRadioButton>
</label>
<label>
<div className="mx_ImageSizePanel_size mx_ImageSizePanel_sizeLarge" />
<StyledRadioButton
name="image_size"
value={ImageSize.Large}
checked={this.state.size === ImageSize.Large}
onChange={this.onSizeChange}
>{ _t("Large") }</StyledRadioButton>
</label>
</div>
</div>
);
}
}

View file

@ -23,7 +23,7 @@ import StyledRadioGroup, { IDefinition } from "../elements/StyledRadioGroup";
import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
import RoomAvatar from "../avatars/RoomAvatar";
import SpaceStore from "../../../stores/SpaceStore";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import Modal from "../../../Modal";
import ManageRestrictedJoinRuleDialog from "../dialogs/ManageRestrictedJoinRuleDialog";
@ -67,8 +67,8 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSet
const editRestrictedRoomIds = async (): Promise<string[] | undefined> => {
let selected = restrictedAllowRoomIds;
if (!selected?.length && SpaceStore.instance.activeSpace) {
selected = [SpaceStore.instance.activeSpace.roomId];
if (!selected?.length && SpaceStore.instance.activeSpaceRoom) {
selected = [SpaceStore.instance.activeSpaceRoom.roomId];
}
const matrixClient = MatrixClientPeg.get();
@ -176,9 +176,9 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSet
{ moreText && <span>{ moreText }</span> }
</div>
</div>;
} else if (SpaceStore.instance.activeSpace) {
} else if (SpaceStore.instance.activeSpaceRoom) {
description = _t("Anyone in <spaceName/> can find and join. You can select other spaces too.", {}, {
spaceName: () => <b>{ SpaceStore.instance.activeSpace.name }</b>,
spaceName: () => <b>{ SpaceStore.instance.activeSpaceRoom.name }</b>,
});
} else {
description = _t("Anyone in a space can find and join. You can select multiple spaces.");
@ -215,7 +215,7 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSet
.some(roomId => !cli.getRoom(roomId)?.currentState.maySendStateEvent(EventType.SpaceChild, userId));
if (unableToUpdateSomeParents) {
warning = <b>
{ _t("This room is in some spaces youre not an admin of. " +
{ _t("This room is in some spaces you're not an admin of. " +
"In those spaces, the old room will still be shown, " +
"but people will be prompted to join the new one.") }
</b>;

View file

@ -23,7 +23,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import EventTilePreview from "../elements/EventTilePreview";
import StyledRadioButton from "../elements/StyledRadioButton";
import { _t } from "../../../languageHandler";
import { Layout } from "../../../settings/Layout";
import { Layout } from "../../../settings/enums/Layout";
import { SettingLevel } from "../../../settings/SettingLevel";
interface IProps {

View file

@ -19,7 +19,7 @@ import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../../../languageHandler";
import SettingsStore from "../../../settings/SettingsStore";
import { enumerateThemes, findHighContrastTheme, findNonHighContrastTheme, isHighContrastTheme } from "../../../theme";
import { findHighContrastTheme, findNonHighContrastTheme, getOrderedThemes, isHighContrastTheme } from "../../../theme";
import ThemeWatcher from "../../../settings/watchers/ThemeWatcher";
import AccessibleButton from "../elements/AccessibleButton";
import dis from "../../../dispatcher/dispatcher";
@ -30,7 +30,6 @@ import Field from '../elements/Field';
import StyledRadioGroup from "../elements/StyledRadioGroup";
import { SettingLevel } from "../../../settings/SettingLevel";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { compare } from "../../../utils/strings";
interface IProps {
}
@ -58,13 +57,13 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
super(props);
this.state = {
...this.calculateThemeState(),
...ThemeChoicePanel.calculateThemeState(),
customThemeUrl: "",
customThemeMessage: { isError: false, text: "" },
};
}
private calculateThemeState(): IThemeState {
public static calculateThemeState(): IThemeState {
// We have to mirror the logic from ThemeWatcher.getEffectiveTheme so we
// show the right values for things.
@ -238,14 +237,7 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
);
}
// XXX: replace any type here
const themes = Object.entries<any>(enumerateThemes())
.map(p => ({ id: p[0], name: p[1] })) // convert pairs to objects for code readability
.filter(p => !isHighContrastTheme(p.id));
const builtInThemes = themes.filter(p => !p.id.startsWith("custom-"));
const customThemes = themes.filter(p => !builtInThemes.includes(p))
.sort((a, b) => compare(a.name, b.name));
const orderedThemes = [...builtInThemes, ...customThemes];
const orderedThemes = getOrderedThemes();
return (
<div className="mx_SettingsTab_section mx_ThemeChoicePanel">
<span className="mx_SettingsTab_subheading">{ _t("Theme") }</span>

View file

@ -24,6 +24,7 @@ import RoomUpgradeDialog from "../../../dialogs/RoomUpgradeDialog";
import DevtoolsDialog from "../../../dialogs/DevtoolsDialog";
import Modal from "../../../../../Modal";
import dis from "../../../../../dispatcher/dispatcher";
import { Action } from '../../../../../dispatcher/actions';
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
interface IProps {
@ -89,7 +90,7 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
e.stopPropagation();
dis.dispatch({
action: 'view_room',
action: Action.ViewRoom,
room_id: this.state.oldRoomId,
event_id: this.state.oldEventId,
});

View file

@ -76,7 +76,7 @@ export default class BridgeSettingsTab extends React.Component<IProps> {
</div>;
} else {
content = <p>{ _t(
"This room isnt bridging messages to any platforms. " +
"This room isn't bridging messages to any platforms. " +
"<a>Learn more.</a>", {},
{
// TODO: We don't have this link yet: this will prevent the translators

View file

@ -1,5 +1,5 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2019 - 2021 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.
@ -15,8 +15,6 @@ limitations under the License.
*/
import React, { createRef } from 'react';
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import AccessibleButton from "../../../elements/AccessibleButton";
@ -25,8 +23,19 @@ import SettingsStore from '../../../../../settings/SettingsStore';
import { SettingLevel } from "../../../../../settings/SettingLevel";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
import { logger } from "matrix-js-sdk/src/logger";
import { RoomEchoChamber } from "../../../../../stores/local-echo/RoomEchoChamber";
import { EchoChamber } from '../../../../../stores/local-echo/EchoChamber';
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
import StyledRadioGroup from "../../../elements/StyledRadioGroup";
import { RoomNotifState } from '../../../../../RoomNotifs';
import defaultDispatcher from "../../../../../dispatcher/dispatcher";
import { Action } from "../../../../../dispatcher/actions";
import { UserTab } from "../../../dialogs/UserSettingsDialog";
interface IProps {
roomId: string;
closeSettingsFn(): void;
}
interface IState {
@ -36,10 +45,16 @@ interface IState {
@replaceableComponent("views.settings.tabs.room.NotificationsSettingsTab")
export default class NotificationsSettingsTab extends React.Component<IProps, IState> {
private readonly roomProps: RoomEchoChamber;
private soundUpload = createRef<HTMLInputElement>();
constructor(props: IProps) {
super(props);
static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>;
constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
super(props, context);
this.roomProps = EchoChamber.forRoom(context.getRoom(this.props.roomId));
this.state = {
currentSound: "default",
@ -144,6 +159,19 @@ export default class NotificationsSettingsTab extends React.Component<IProps, IS
});
};
private onRoomNotificationChange = (value: RoomNotifState) => {
this.roomProps.notificationVolume = value;
this.forceUpdate();
};
private onOpenSettingsClick = () => {
this.props.closeSettingsFn();
defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Notifications,
});
};
public render(): JSX.Element {
let currentUploadedFile = null;
if (this.state.uploadedFile) {
@ -157,6 +185,63 @@ export default class NotificationsSettingsTab extends React.Component<IProps, IS
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{ _t("Notifications") }</div>
<div className="mx_SettingsTab_section mx_NotificationSettingsTab_notificationsSection">
<StyledRadioGroup
name="roomNotificationSetting"
definitions={[
{
value: RoomNotifState.AllMessages,
className: "mx_NotificationSettingsTab_defaultEntry",
label: <>
{ _t("Default") }
<div className="mx_NotificationSettingsTab_microCopy">
{ _t("Get notifications as set up in your <a>settings</a>", {}, {
a: sub => <AccessibleButton kind="link" onClick={this.onOpenSettingsClick}>
{ sub }
</AccessibleButton>,
}) }
</div>
</>,
}, {
value: RoomNotifState.AllMessagesLoud,
className: "mx_NotificationSettingsTab_allMessagesEntry",
label: <>
{ _t("All messages") }
<div className="mx_NotificationSettingsTab_microCopy">
{ _t("Get notified for every message") }
</div>
</>,
}, {
value: RoomNotifState.MentionsOnly,
className: "mx_NotificationSettingsTab_mentionsKeywordsEntry",
label: <>
{ _t("@mentions & keywords") }
<div className="mx_NotificationSettingsTab_microCopy">
{ _t("Get notified only with mentions and keywords " +
"as set up in your <a>settings</a>", {}, {
a: sub => <AccessibleButton kind="link" onClick={this.onOpenSettingsClick}>
{ sub }
</AccessibleButton>,
}) }
</div>
</>,
}, {
value: RoomNotifState.Mute,
className: "mx_NotificationSettingsTab_noneEntry",
label: <>
{ _t("Off") }
<div className="mx_NotificationSettingsTab_microCopy">
{ _t("You won't get any notifications") }
</div>
</>,
},
]}
onChange={this.onRoomNotificationChange}
value={this.roomProps.notificationVolume}
/>
</div>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<span className='mx_SettingsTab_subheading'>{ _t("Sounds") }</span>
<div>

View file

@ -15,24 +15,26 @@ limitations under the License.
*/
import React from 'react';
import { EventType } from "matrix-js-sdk/src/@types/event";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { RoomState } from "matrix-js-sdk/src/models/room-state";
import { logger } from "matrix-js-sdk/src/logger";
import { _t, _td } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import AccessibleButton from "../../../elements/AccessibleButton";
import Modal from "../../../../../Modal";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { RoomState } from "matrix-js-sdk/src/models/room-state";
import { compare } from "../../../../../utils/strings";
import ErrorDialog from '../../../dialogs/ErrorDialog';
import PowerSelector from "../../../elements/PowerSelector";
import { logger } from "matrix-js-sdk/src/logger";
import SettingsStore from "../../../../../settings/SettingsStore";
interface IEventShowOpts {
isState?: boolean;
hideForSpace?: boolean;
hideForRoom?: boolean;
}
interface IPowerLevelDescriptor {
@ -46,12 +48,14 @@ const plEventsToShow: Record<string, IEventShowOpts> = {
[EventType.RoomAvatar]: { isState: true },
[EventType.RoomName]: { isState: true },
[EventType.RoomCanonicalAlias]: { isState: true },
[EventType.SpaceChild]: { isState: true, hideForRoom: true },
[EventType.RoomHistoryVisibility]: { isState: true, hideForSpace: true },
[EventType.RoomPowerLevels]: { isState: true },
[EventType.RoomTopic]: { isState: true },
[EventType.RoomTombstone]: { isState: true, hideForSpace: true },
[EventType.RoomEncryption]: { isState: true, hideForSpace: true },
[EventType.RoomServerAcl]: { isState: true, hideForSpace: true },
[EventType.RoomPinnedEvents]: { isState: true, hideForSpace: true },
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
"im.vector.modular.widgets": { isState: true, hideForSpace: true },
@ -223,6 +227,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
[EventType.RoomCanonicalAlias]: isSpaceRoom
? _td("Change main address for the space")
: _td("Change main address for the room"),
[EventType.SpaceChild]: _td("Manage rooms in this space"),
[EventType.RoomHistoryVisibility]: _td("Change history visibility"),
[EventType.RoomPowerLevels]: _td("Change permissions"),
[EventType.RoomTopic]: isSpaceRoom ? _td("Change description") : _td("Change topic"),
@ -234,6 +239,10 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
"im.vector.modular.widgets": isSpaceRoom ? null : _td("Modify widgets"),
};
if (SettingsStore.getValue("feature_pinning")) {
plEventsToLabels[EventType.RoomPinnedEvents] = _td("Manage pinned events");
}
const powerLevelDescriptors: Record<string, IPowerLevelDescriptor> = {
"users_default": {
desc: _t('Default role'),
@ -411,7 +420,9 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
}
const eventPowerSelectors = Object.keys(eventsLevels).map((eventType, i) => {
if (isSpaceRoom && plEventsToShow[eventType].hideForSpace) {
if (isSpaceRoom && plEventsToShow[eventType]?.hideForSpace) {
return null;
} else if (!isSpaceRoom && plEventsToShow[eventType]?.hideForRoom) {
return null;
}

View file

@ -26,11 +26,12 @@ import SettingsFlag from '../../../elements/SettingsFlag';
import Field from '../../../elements/Field';
import { SettingLevel } from "../../../../../settings/SettingLevel";
import { UIFeature } from "../../../../../settings/UIFeature";
import { Layout } from "../../../../../settings/Layout";
import { Layout } from "../../../../../settings/enums/Layout";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
import LayoutSwitcher from "../../LayoutSwitcher";
import FontScalingPanel from '../../FontScalingPanel';
import ThemeChoicePanel from '../../ThemeChoicePanel';
import ImageSizePanel from "../../ImageSizePanel";
interface IProps {
}
@ -188,6 +189,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
{ layoutSection }
<FontScalingPanel />
{ this.renderAdvancedSection() }
<ImageSizePanel />
</div>
);
}

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import { sortBy } from "lodash";
import { _t } from "../../../../../languageHandler";
import SettingsStore from "../../../../../settings/SettingsStore";
@ -25,6 +26,8 @@ import SdkConfig from "../../../../../SdkConfig";
import BetaCard from "../../../beta/BetaCard";
import SettingsFlag from '../../../elements/SettingsFlag';
import { MatrixClientPeg } from '../../../../../MatrixClientPeg';
import { LabGroup, labGroupNames } from "../../../../../settings/Settings";
import { EnhancedMap } from "../../../../../utils/maps";
interface ILabsSettingToggleProps {
featureId: string;
@ -67,7 +70,7 @@ export default class LabsUserSettingsTab extends React.Component<{}, IState> {
const [labs, betas] = features.reduce((arr, f) => {
arr[SettingsStore.getBetaInfo(f) ? 1 : 0].push(f);
return arr;
}, [[], []]);
}, [[], []] as [string[], string[]]);
let betaSection;
if (betas.length) {
@ -78,22 +81,43 @@ export default class LabsUserSettingsTab extends React.Component<{}, IState> {
let labsSection;
if (SdkConfig.get()['showLabsSettings']) {
const flags = labs.map(f => <LabsSettingToggle featureId={f} key={f} />);
const groups = new EnhancedMap<LabGroup, JSX.Element[]>();
labs.forEach(f => {
groups.getOrCreate(SettingsStore.getLabGroup(f), []).push(
<LabsSettingToggle featureId={f} key={f} />,
);
});
groups.getOrCreate(LabGroup.Widgets, []).push(
<SettingsFlag name="enableWidgetScreenshots" level={SettingLevel.ACCOUNT} />,
);
groups.getOrCreate(LabGroup.Experimental, []).push(
<SettingsFlag name="lowBandwidth" level={SettingLevel.DEVICE} />,
);
groups.getOrCreate(LabGroup.Developer, []).push(
<SettingsFlag name="developerMode" level={SettingLevel.ACCOUNT} />,
<SettingsFlag name="showHiddenEventsInTimeline" level={SettingLevel.DEVICE} />,
);
groups.getOrCreate(LabGroup.Analytics, []).push(
<SettingsFlag name="automaticErrorReporting" level={SettingLevel.DEVICE} />,
);
let hiddenReadReceipts;
if (this.state.showHiddenReadReceipts) {
hiddenReadReceipts = (
<SettingsFlag name="feature_hidden_read_receipts" level={SettingLevel.DEVICE} />
groups.getOrCreate(LabGroup.Messaging, []).push(
<SettingsFlag name="feature_hidden_read_receipts" level={SettingLevel.DEVICE} />,
);
}
labsSection = <div className="mx_SettingsTab_section">
<SettingsFlag name="developerMode" level={SettingLevel.ACCOUNT} />
{ flags }
<SettingsFlag name="enableWidgetScreenshots" level={SettingLevel.ACCOUNT} />
<SettingsFlag name="showHiddenEventsInTimeline" level={SettingLevel.DEVICE} />
<SettingsFlag name="lowBandwidth" level={SettingLevel.DEVICE} />
{ hiddenReadReceipts }
{ sortBy(Array.from(groups.entries()), "0").map(([group, flags]) => (
<div key={group}>
<span className="mx_SettingsTab_subheading">{ _t(labGroupNames[group]) }</span>
{ flags }
</div>
)) }
</div>;
}

View file

@ -325,17 +325,19 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
minimizeToTrayOption = <LabelledToggleSwitch
value={this.state.minimizeToTray}
onChange={this.onMinimizeToTrayChange}
label={_t('Show tray icon and minimize window to it on close')} />;
label={_t('Show tray icon and minimise window to it on close')} />;
}
return (
<div className="mx_SettingsTab mx_PreferencesUserSettingsTab">
<div className="mx_SettingsTab_heading">{ _t("Preferences") }</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Room list") }</span>
{ this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS) }
</div>
{ !SettingsStore.getValue("feature_breadcrumbs_v2") &&
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Room list") }</span>
{ this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS) }
</div>
}
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Spaces") }</span>
@ -357,9 +359,13 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Keyboard shortcuts") }</span>
<AccessibleButton className="mx_SettingsFlag" onClick={KeyboardShortcuts.toggleDialog}>
{ _t("To view all keyboard shortcuts, click here.") }
</AccessibleButton>
<div className="mx_SettingsFlag">
{ _t("To view all keyboard shortcuts, <a>click here</a>.", {}, {
a: sub => <AccessibleButton kind="link" onClick={KeyboardShortcuts.toggleDialog}>
{ sub }
</AccessibleButton>,
}) }
</div>
{ this.renderGroup(PreferencesUserSettingsTab.KEYBINDINGS_SETTINGS) }
</div>

View file

@ -17,11 +17,8 @@ limitations under the License.
import React from 'react';
import { sleep } from "matrix-js-sdk/src/utils";
import { Room } from "matrix-js-sdk/src/models/room";
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../../../../../languageHandler";
import SdkConfig from "../../../../../SdkConfig";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import AccessibleButton from "../../../elements/AccessibleButton";
import Analytics from "../../../../../Analytics";
@ -34,14 +31,18 @@ import { UIFeature } from "../../../../../settings/UIFeature";
import E2eAdvancedPanel, { isE2eAdvancedPanelPossible } from "../../E2eAdvancedPanel";
import CountlyAnalytics from "../../../../../CountlyAnalytics";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
import { PosthogAnalytics } from "../../../../../PosthogAnalytics";
import { ActionPayload } from "../../../../../dispatcher/payloads";
import { Room } from "matrix-js-sdk/src/models/room";
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 { logger } from "matrix-js-sdk/src/logger";
import { showDialog as showAnalyticsLearnMoreDialog } from "../../../dialogs/AnalyticsLearnMoreDialog";
interface IIgnoredUserProps {
userId: string;
@ -75,7 +76,7 @@ interface IState {
ignoredUserIds: string[];
waitingUnignored: string[];
managingInvites: boolean;
invitedRoomAmt: number;
invitedRoomIds: Set<string>;
}
@replaceableComponent("views.settings.tabs.user.SecurityUserSettingsTab")
@ -85,14 +86,14 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
constructor(props: IProps) {
super(props);
// Get number of rooms we're invited to
const invitedRooms = this.getInvitedRooms();
// 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,
invitedRoomAmt: invitedRooms.length,
invitedRoomIds,
};
}
@ -106,16 +107,47 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
public componentDidMount(): void {
this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
}
public componentWillUnmount(): void {
dis.unregister(this.dispatcherRef);
MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership);
}
private updateAnalytics = (checked: boolean): void => {
checked ? Analytics.enable() : Analytics.disable();
CountlyAnalytics.instance.enable(/* anonymous = */ !checked);
PosthogAnalytics.instance.updateAnonymityFromSettings(MatrixClientPeg.get().getUserId());
};
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 onGoToUserProfileClick = (): void => {
@ -149,21 +181,19 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
managingInvites: true,
});
// Compile array of invitation room ids
const invitedRoomIds = this.getInvitedRooms().map((room) => {
return room.roomId;
});
// 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 < invitedRoomIds.length; i++) {
const roomId = invitedRoomIds[i];
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.setState({ invitedRoomAmt: this.state.invitedRoomAmt - 1 });
this.removeInvitedRoom(roomId);
}, async (e) => {
// Action failure
if (e.errcode === "M_LIMIT_EXCEEDED") {
@ -220,21 +250,20 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
}
private renderManageInvites(): JSX.Element {
if (this.state.invitedRoomAmt === 0) {
const { invitedRoomIds } = this.state;
if (invitedRoomIds.size === 0) {
return null;
}
const invitedRooms = this.getInvitedRooms();
const onClickAccept = this.onAcceptAllInvitesClicked.bind(this, invitedRooms);
const onClickReject = this.onRejectAllInvitesClicked.bind(this, invitedRooms);
return (
<div className='mx_SettingsTab_section mx_SecurityUserSettingsTab_bulkOptions'>
<span className='mx_SettingsTab_subheading'>{ _t('Bulk options') }</span>
<AccessibleButton onClick={onClickAccept} kind='primary' disabled={this.state.managingInvites}>
{ _t("Accept all %(invitedRooms)s invites", { invitedRooms: this.state.invitedRoomAmt }) }
<AccessibleButton onClick={this.onAcceptAllInvitesClicked} kind='primary' disabled={this.state.managingInvites}>
{ _t("Accept all %(invitedRooms)s invites", { invitedRooms: invitedRoomIds.size }) }
</AccessibleButton>
<AccessibleButton onClick={onClickReject} kind='danger' disabled={this.state.managingInvites}>
{ _t("Reject all %(invitedRooms)s invites", { invitedRooms: this.state.invitedRoomAmt }) }
<AccessibleButton onClick={this.onRejectAllInvitesClicked} kind='danger' disabled={this.state.managingInvites}>
{ _t("Reject all %(invitedRooms)s invites", { invitedRooms: invitedRoomIds.size }) }
</AccessibleButton>
{ this.state.managingInvites ? <InlineSpinner /> : <div /> }
</div>
@ -242,8 +271,6 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
}
public render(): JSX.Element {
const brand = SdkConfig.get().brand;
const secureBackup = (
<div className='mx_SettingsTab_section'>
<span className="mx_SettingsTab_subheading">{ _t("Secure Backup") }</span>
@ -282,24 +309,41 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
}
let privacySection;
if (Analytics.canEnable() || CountlyAnalytics.instance.canEnable()) {
if (Analytics.canEnable() || CountlyAnalytics.instance.canEnable() || PosthogAnalytics.instance.isEnabled()) {
const onClickAnalyticsLearnMore = () => {
if (PosthogAnalytics.instance.isEnabled()) {
showAnalyticsLearnMoreDialog({
primaryButton: _t("Okay"),
hasCancel: false,
});
} else {
Analytics.showDetailsModal();
}
};
privacySection = <React.Fragment>
<div className="mx_SettingsTab_heading">{ _t("Privacy") }</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Analytics") }</span>
<div className="mx_SettingsTab_subsectionText">
{ _t(
"%(brand)s collects anonymous analytics to allow us to improve the application.",
{ brand },
) }
&nbsp;
{ _t("Privacy is important to us, so we don't collect any personal or " +
"identifiable data for our analytics.") }
<AccessibleButton className="mx_SettingsTab_linkBtn" onClick={Analytics.showDetailsModal}>
{ _t("Learn more about how we use analytics.") }
</AccessibleButton>
<p>
{ _t("Share anonymous data to help us identify issues. Nothing personal. " +
"No third parties.") }
</p>
<p>
<AccessibleButton className="mx_SettingsTab_linkBtn" onClick={onClickAnalyticsLearnMore}>
{ _t("Learn more") }
</AccessibleButton>
</p>
</div>
<SettingsFlag name="analyticsOptIn" level={SettingLevel.DEVICE} onChange={this.updateAnalytics} />
{
PosthogAnalytics.instance.isEnabled() ?
<SettingsFlag name="pseudonymousAnalyticsOptIn"
level={SettingLevel.ACCOUNT}
onChange={this.updateAnalytics} /> :
<SettingsFlag name="analyticsOptIn"
level={SettingLevel.DEVICE}
onChange={this.updateAnalytics} />
}
</div>
</React.Fragment>;
}
@ -325,23 +369,15 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
return (
<div className="mx_SettingsTab mx_SecurityUserSettingsTab">
{ warning }
<div className="mx_SettingsTab_heading">{ _t("Where youre logged in") }</div>
<div className="mx_SettingsTab_heading">{ _t("Where you're signed in") }</div>
<div className="mx_SettingsTab_section">
<span>
{ _t(
"Manage the names of and sign out of your sessions below or " +
"<a>verify them in your User Profile</a>.", {},
{
a: sub => <AccessibleButton kind="link" onClick={this.onGoToUserProfileClick}>
{ sub }
</AccessibleButton>,
},
"Manage your signed-in devices below. " +
"A device's name is visible to people you communicate with.",
) }
</span>
<div className='mx_SettingsTab_subsectionText'>
{ _t("A session's public name is visible to people you communicate with") }
<DevicesPanel />
</div>
<DevicesPanel />
</div>
<div className="mx_SettingsTab_heading">{ _t("Encryption") }</div>
<div className="mx_SettingsTab_section">

View file

@ -0,0 +1,123 @@
/*
Copyright 2021 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, { ChangeEvent } from 'react';
import { _t } from "../../../../../languageHandler";
import SettingsStore from "../../../../../settings/SettingsStore";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import StyledCheckbox from "../../../elements/StyledCheckbox";
import { useSettingValue } from "../../../../../hooks/useSettings";
import { MetaSpace } from "../../../../../stores/spaces";
export const onMetaSpaceChangeFactory = (metaSpace: MetaSpace) => (e: ChangeEvent<HTMLInputElement>) => {
const currentValue = SettingsStore.getValue("Spaces.enabledMetaSpaces");
SettingsStore.setValue("Spaces.enabledMetaSpaces", null, SettingLevel.ACCOUNT, {
...currentValue,
[metaSpace]: e.target.checked,
});
};
const SidebarUserSettingsTab = () => {
const {
[MetaSpace.Home]: homeEnabled,
[MetaSpace.Favourites]: favouritesEnabled,
[MetaSpace.People]: peopleEnabled,
[MetaSpace.Orphans]: orphansEnabled,
} = useSettingValue<Record<MetaSpace, boolean>>("Spaces.enabledMetaSpaces");
const allRoomsInHome = useSettingValue<boolean>("Spaces.allRoomsInHome");
return (
<div className="mx_SettingsTab mx_SidebarUserSettingsTab">
<div className="mx_SettingsTab_heading">{ _t("Sidebar") }</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Spaces") }</span>
<div className="mx_SettingsTab_subsectionText">{ _t("Spaces are ways to group rooms and people.") }</div>
<div className="mx_SidebarUserSettingsTab_subheading">{ _t("Spaces to show") }</div>
<div className="mx_SettingsTab_subsectionText">
{ _t("Along with the spaces you're in, you can use some pre-built ones too.") }
</div>
<StyledCheckbox
checked={!!homeEnabled}
onChange={onMetaSpaceChangeFactory(MetaSpace.Home)}
className="mx_SidebarUserSettingsTab_homeCheckbox"
>
{ _t("Home") }
</StyledCheckbox>
<div className="mx_SidebarUserSettingsTab_checkboxMicrocopy">
{ _t("Home is useful for getting an overview of everything.") }
</div>
<StyledCheckbox
checked={allRoomsInHome}
disabled={!homeEnabled}
onChange={e => {
SettingsStore.setValue(
"Spaces.allRoomsInHome",
null,
SettingLevel.ACCOUNT,
e.target.checked,
);
}}
className="mx_SidebarUserSettingsTab_homeAllRoomsCheckbox"
>
{ _t("Show all rooms") }
</StyledCheckbox>
<div className="mx_SidebarUserSettingsTab_checkboxMicrocopy">
{ _t("Show all your rooms in Home, even if they're in a space.") }
</div>
<StyledCheckbox
checked={!!favouritesEnabled}
onChange={onMetaSpaceChangeFactory(MetaSpace.Favourites)}
className="mx_SidebarUserSettingsTab_favouritesCheckbox"
>
{ _t("Favourites") }
</StyledCheckbox>
<div className="mx_SidebarUserSettingsTab_checkboxMicrocopy">
{ _t("Automatically group all your favourite rooms and people together in one place.") }
</div>
<StyledCheckbox
checked={!!peopleEnabled}
onChange={onMetaSpaceChangeFactory(MetaSpace.People)}
className="mx_SidebarUserSettingsTab_peopleCheckbox"
>
{ _t("People") }
</StyledCheckbox>
<div className="mx_SidebarUserSettingsTab_checkboxMicrocopy">
{ _t("Automatically group all your people together in one place.") }
</div>
<StyledCheckbox
checked={!!orphansEnabled}
onChange={onMetaSpaceChangeFactory(MetaSpace.Orphans)}
className="mx_SidebarUserSettingsTab_orphansCheckbox"
>
{ _t("Rooms outside of a space") }
</StyledCheckbox>
<div className="mx_SidebarUserSettingsTab_checkboxMicrocopy">
{ _t("Automatically group all your rooms that aren't part of a space in one place.") }
</div>
</div>
</div>
);
};
export default SidebarUserSettingsTab;