Merge branch 'develop' into sort-imports
Signed-off-by: Aaron Raimist <aaron@raim.ist>
This commit is contained in:
commit
7b94e13a84
642 changed files with 30052 additions and 8035 deletions
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
79
src/components/views/settings/ImageSizePanel.tsx
Normal file
79
src/components/views/settings/ImageSizePanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 you’re 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>;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -76,7 +76,7 @@ export default class BridgeSettingsTab extends React.Component<IProps> {
|
|||
</div>;
|
||||
} else {
|
||||
content = <p>{ _t(
|
||||
"This room isn’t 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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 },
|
||||
) }
|
||||
|
||||
{ _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 you’re 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">
|
||||
|
|
|
@ -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;
|
Loading…
Add table
Add a link
Reference in a new issue