Apply prettier formatting

This commit is contained in:
Michael Weimann 2022-12-12 12:24:14 +01:00
parent 1cac306093
commit 526645c791
No known key found for this signature in database
GPG key ID: 53F535A266BB9584
1576 changed files with 65385 additions and 62478 deletions

View file

@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { FormEvent, useCallback, useContext, useRef, useState } from 'react';
import { Room } from 'matrix-js-sdk/src/models/room';
import React, { FormEvent, useCallback, useContext, useRef, useState } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { _t } from "../../../languageHandler";
import { ICompletion } from '../../../autocomplete/Autocompleter';
import { ICompletion } from "../../../autocomplete/Autocompleter";
import UserProvider from "../../../autocomplete/UserProvider";
import { AutocompleteInput } from "../../structures/AutocompleteInput";
import PowerSelector from "../elements/PowerSelector";
@ -77,10 +77,10 @@ export const AddPrivilegedUsers: React.FC<AddPrivilegedUsersProps> = ({ room, de
};
return (
<form style={{ display: 'flex' }} onSubmit={onSubmit}>
<form style={{ display: "flex" }} onSubmit={onSubmit}>
<SettingsFieldset
legend={_t('Add privileged users')}
description={_t('Give one or multiple users in this room more privileges')}
legend={_t("Add privileged users")}
description={_t("Give one or multiple users in this room more privileges")}
style={{ flexGrow: 1 }}
>
<AutocompleteInput
@ -92,25 +92,21 @@ export const AddPrivilegedUsers: React.FC<AddPrivilegedUsersProps> = ({ room, de
/>
<PowerSelector value={powerLevel} onChange={setPowerLevel} />
<AccessibleButton
type='submit'
element='button'
kind='primary'
type="submit"
element="button"
kind="primary"
disabled={!selectedUsers.length || isLoading}
onClick={null}
data-testid='add-privileged-users-submit-button'
data-testid="add-privileged-users-submit-button"
>
{ _t('Apply') }
{_t("Apply")}
</AccessibleButton>
</SettingsFieldset>
</form>
);
};
export const hasLowerOrEqualLevelThanDefaultLevel = (
room: Room,
user: ICompletion,
defaultUserLevel: number,
) => {
export const hasLowerOrEqualLevelThanDefaultLevel = (room: Room, user: ICompletion, defaultUserLevel: number) => {
if (user.completionId === undefined) {
return false;
}
@ -125,8 +121,8 @@ export const hasLowerOrEqualLevelThanDefaultLevel = (
};
export const getUserIdsFromCompletions = (completions: ICompletion[]) => {
const completionsWithId = completions.filter(completion => completion.completionId !== undefined);
const completionsWithId = completions.filter((completion) => completion.completionId !== undefined);
// undefined completionId's are filtered out above but TypeScript does not seem to understand.
return completionsWithId.map(completion => completion.completionId!);
return completionsWithId.map((completion) => completion.completionId!);
};

View file

@ -35,12 +35,14 @@ const AvatarSetting: React.FC<IProps> = ({ avatarUrl, avatarAltText, avatarName,
onMouseLeave: () => setIsHovering(false),
};
let avatarElement = <AccessibleButton
element="div"
onClick={uploadAvatar}
className="mx_AvatarSetting_avatarPlaceholder"
{...hoveringProps}
/>;
let avatarElement = (
<AccessibleButton
element="div"
onClick={uploadAvatar}
className="mx_AvatarSetting_avatarPlaceholder"
{...hoveringProps}
/>
);
if (avatarUrl) {
avatarElement = (
<AccessibleButton
@ -57,33 +59,35 @@ const AvatarSetting: React.FC<IProps> = ({ avatarUrl, avatarAltText, avatarName,
let uploadAvatarBtn;
if (uploadAvatar) {
// insert an empty div to be the host for a css mask containing the upload.svg
uploadAvatarBtn = <AccessibleButton
onClick={uploadAvatar}
className='mx_AvatarSetting_uploadButton'
{...hoveringProps}
/>;
uploadAvatarBtn = (
<AccessibleButton onClick={uploadAvatar} className="mx_AvatarSetting_uploadButton" {...hoveringProps} />
);
}
let removeAvatarBtn;
if (avatarUrl && removeAvatar) {
removeAvatarBtn = <AccessibleButton onClick={removeAvatar} kind="link_sm">
{ _t("Remove") }
</AccessibleButton>;
removeAvatarBtn = (
<AccessibleButton onClick={removeAvatar} kind="link_sm">
{_t("Remove")}
</AccessibleButton>
);
}
const avatarClasses = classNames({
"mx_AvatarSetting_avatar": true,
"mx_AvatarSetting_avatar_hovering": isHovering && uploadAvatar,
mx_AvatarSetting_avatar: true,
mx_AvatarSetting_avatar_hovering: isHovering && uploadAvatar,
});
return <div className={avatarClasses}>
{ avatarElement }
<div className="mx_AvatarSetting_hover">
<div className="mx_AvatarSetting_hoverBg" />
<span>{ _t("Upload") }</span>
return (
<div className={avatarClasses}>
{avatarElement}
<div className="mx_AvatarSetting_hover">
<div className="mx_AvatarSetting_hoverBg" />
<span>{_t("Upload")}</span>
</div>
{uploadAvatarBtn}
{removeAvatarBtn}
</div>
{ uploadAvatarBtn }
{ removeAvatarBtn }
</div>;
);
};
export default AvatarSetting;

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { logger } from "matrix-js-sdk/src/logger";
@ -24,7 +24,7 @@ import Pill, { PillType } from "../elements/Pill";
import { makeUserPermalink } from "../../../utils/permalinks/Permalinks";
import BaseAvatar from "../avatars/BaseAvatar";
import SettingsStore from "../../../settings/SettingsStore";
import { isUrlPermitted } from '../../../HtmlUtils';
import { isUrlPermitted } from "../../../HtmlUtils";
import { mediaFromMxc } from "../../../customisations/Media";
interface IProps {
@ -75,8 +75,10 @@ export default class BridgeTile extends React.PureComponent<IProps> {
if (!content.bridgebot) {
// Bridgebot was not required previously, so in order to not break rooms we are allowing
// the sender to be used in place. When the proposal is merged, this should be removed.
logger.warn(`Bridge info event ${this.props.ev.getId()} does not provide a 'bridgebot' key which`
+ "is deprecated behaviour. Using sender for now.");
logger.warn(
`Bridge info event ${this.props.ev.getId()} does not provide a 'bridgebot' key which` +
"is deprecated behaviour. Using sender for now.",
);
content.bridgebot = this.props.ev.getSender();
}
const { channel, network, protocol } = content;
@ -85,77 +87,116 @@ export default class BridgeTile extends React.PureComponent<IProps> {
let creator = null;
if (content.creator) {
creator = <li>{ _t("This bridge was provisioned by <user />.", {}, {
user: () => <Pill
type={PillType.UserMention}
room={this.props.room}
url={makeUserPermalink(content.creator)}
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
/>,
}) }</li>;
creator = (
<li>
{_t(
"This bridge was provisioned by <user />.",
{},
{
user: () => (
<Pill
type={PillType.UserMention}
room={this.props.room}
url={makeUserPermalink(content.creator)}
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
/>
),
},
)}
</li>
);
}
const bot = <li>{ _t("This bridge is managed by <user />.", {}, {
user: () => <Pill
type={PillType.UserMention}
room={this.props.room}
url={makeUserPermalink(content.bridgebot)}
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
/>,
}) }</li>;
const bot = (
<li>
{_t(
"This bridge is managed by <user />.",
{},
{
user: () => (
<Pill
type={PillType.UserMention}
room={this.props.room}
url={makeUserPermalink(content.bridgebot)}
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
/>
),
},
)}
</li>
);
let networkIcon;
if (protocol.avatar_url) {
const avatarUrl = mediaFromMxc(protocol.avatar_url).getSquareThumbnailHttp(64);
networkIcon = <BaseAvatar className="mx_RoomSettingsDialog_protocolIcon"
width={48}
height={48}
resizeMethod='crop'
name={protocolName}
idName={protocolName}
url={avatarUrl}
/>;
networkIcon = (
<BaseAvatar
className="mx_RoomSettingsDialog_protocolIcon"
width={48}
height={48}
resizeMethod="crop"
name={protocolName}
idName={protocolName}
url={avatarUrl}
/>
);
} else {
networkIcon = <div className="mx_RoomSettingsDialog_noProtocolIcon" />;
}
let networkItem = null;
if (network) {
const networkName = network.displayname || network.id;
let networkLink = <span>{ networkName }</span>;
let networkLink = <span>{networkName}</span>;
if (typeof network.external_url === "string" && isUrlPermitted(network.external_url)) {
networkLink = (
<a href={network.external_url} target="_blank" rel="noreferrer noopener">{ networkName }</a>
<a href={network.external_url} target="_blank" rel="noreferrer noopener">
{networkName}
</a>
);
}
networkItem = _t("Workspace: <networkLink/>", {}, {
networkLink: () => networkLink,
});
networkItem = _t(
"Workspace: <networkLink/>",
{},
{
networkLink: () => networkLink,
},
);
}
let channelLink = <span>{ channelName }</span>;
let channelLink = <span>{channelName}</span>;
if (typeof channel.external_url === "string" && isUrlPermitted(channel.external_url)) {
channelLink = <a href={channel.external_url} target="_blank" rel="noreferrer noopener">{ channelName }</a>;
channelLink = (
<a href={channel.external_url} target="_blank" rel="noreferrer noopener">
{channelName}
</a>
);
}
const id = this.props.ev.getId();
return (<li key={id} className="mx_RoomSettingsDialog_BridgeList_listItem">
<div className="mx_RoomSettingsDialog_column_icon">
{ networkIcon }
</div>
<div className="mx_RoomSettingsDialog_column_data">
<h3 className="mx_RoomSettingsDialog_column_data_protocolName">{ protocolName }</h3>
<p className="mx_RoomSettingsDialog_column_data_details mx_RoomSettingsDialog_workspace_channel_details">
{ networkItem }
<span className="mx_RoomSettingsDialog_channel">{ _t("Channel: <channelLink/>", {}, {
channelLink: () => channelLink,
}) }</span>
</p>
<ul className="mx_RoomSettingsDialog_column_data_metadata mx_RoomSettingsDialog_metadata">
{ creator } { bot }
</ul>
</div>
</li>);
return (
<li key={id} className="mx_RoomSettingsDialog_BridgeList_listItem">
<div className="mx_RoomSettingsDialog_column_icon">{networkIcon}</div>
<div className="mx_RoomSettingsDialog_column_data">
<h3 className="mx_RoomSettingsDialog_column_data_protocolName">{protocolName}</h3>
<p className="mx_RoomSettingsDialog_column_data_details mx_RoomSettingsDialog_workspace_channel_details">
{networkItem}
<span className="mx_RoomSettingsDialog_channel">
{_t(
"Channel: <channelLink/>",
{},
{
channelLink: () => channelLink,
},
)}
</span>
</p>
<ul className="mx_RoomSettingsDialog_column_data_metadata mx_RoomSettingsDialog_metadata">
{creator} {bot}
</ul>
</div>
</li>
);
}
}

View file

@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from "../../../languageHandler";
import EditableTextContainer from "../elements/EditableTextContainer";
export default class ChangeDisplayName extends React.Component {
@ -33,7 +33,7 @@ export default class ChangeDisplayName extends React.Component {
private changeDisplayName = (newDisplayname: string): Promise<{}> => {
const cli = MatrixClientPeg.get();
return cli.setDisplayName(newDisplayname).catch(function() {
return cli.setDisplayName(newDisplayname).catch(function () {
throw new Error("Failed to set display name");
});
};
@ -44,7 +44,8 @@ export default class ChangeDisplayName extends React.Component {
getInitialValue={this.getDisplayName}
placeholder={_t("No display name")}
blurToSubmit={true}
onSubmit={this.changeDisplayName} />
onSubmit={this.changeDisplayName}
/>
);
}
}

View file

@ -15,24 +15,24 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ComponentType } from 'react';
import React, { ComponentType } from "react";
import { MatrixClient } from "matrix-js-sdk/src/client";
import Field from "../elements/Field";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import AccessibleButton from '../elements/AccessibleButton';
import Spinner from '../elements/Spinner';
import withValidation, { IFieldState, IValidationResult } from '../elements/Validation';
import { _t, _td } from '../../../languageHandler';
import AccessibleButton from "../elements/AccessibleButton";
import Spinner from "../elements/Spinner";
import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
import { _t, _td } from "../../../languageHandler";
import Modal from "../../../Modal";
import PassphraseField from "../auth/PassphraseField";
import { PASSWORD_MIN_SCORE } from '../auth/RegistrationForm';
import { PASSWORD_MIN_SCORE } from "../auth/RegistrationForm";
import SetEmailDialog from "../dialogs/SetEmailDialog";
import QuestionDialog from "../dialogs/QuestionDialog";
const FIELD_OLD_PASSWORD = 'field_old_password';
const FIELD_NEW_PASSWORD = 'field_new_password';
const FIELD_NEW_PASSWORD_CONFIRM = 'field_new_password_confirm';
const FIELD_OLD_PASSWORD = "field_old_password";
const FIELD_NEW_PASSWORD = "field_new_password";
const FIELD_NEW_PASSWORD_CONFIRM = "field_new_password_confirm";
enum Phase {
Edit = "edit",
@ -46,7 +46,7 @@ interface IProps {
/** Was one or more other devices logged out whilst changing the password */
didLogoutOutOtherDevices: boolean;
}) => void;
onError?: (error: {error: string}) => void;
onError?: (error: { error: string }) => void;
rowClassName?: string;
buttonClassName?: string;
buttonKind?: string;
@ -97,29 +97,32 @@ export default class ChangePassword extends React.Component<IProps, IState> {
// warn about logging out all devices
const { finished } = Modal.createDialog<[boolean]>(QuestionDialog, {
title: _t("Warning!"),
description:
description: (
<div>
<p>{ _t(
'Changing your password on this homeserver will cause all of your other devices to be ' +
'signed out. This will delete the message encryption keys stored on them, and may make ' +
'encrypted chat history unreadable.',
) }</p>
<p>{ _t(
'If you want to retain access to your chat history in encrypted rooms you should first ' +
'export your room keys and re-import them afterwards.',
) }</p>
<p>{ _t(
'You can also ask your homeserver admin to upgrade the server to change this behaviour.',
) }</p>
</div>,
<p>
{_t(
"Changing your password on this homeserver will cause all of your other devices to be " +
"signed out. This will delete the message encryption keys stored on them, and may make " +
"encrypted chat history unreadable.",
)}
</p>
<p>
{_t(
"If you want to retain access to your chat history in encrypted rooms you should first " +
"export your room keys and re-import them afterwards.",
)}
</p>
<p>
{_t(
"You can also ask your homeserver admin to upgrade the server to change this behaviour.",
)}
</p>
</div>
),
button: _t("Continue"),
extraButtons: [
<button
key="exportRoomKeys"
className="mx_Dialog_primary"
onClick={this.onExportE2eKeysClicked}
>
{ _t('Export E2E room keys') }
<button key="exportRoomKeys" className="mx_Dialog_primary" onClick={this.onExportE2eKeysClicked}>
{_t("Export E2E room keys")}
</button>,
],
});
@ -139,9 +142,9 @@ export default class ChangePassword extends React.Component<IProps, IState> {
userHasOtherDevices: boolean,
): void {
const authDict = {
type: 'm.login.password',
type: "m.login.password",
identifier: {
type: 'm.id.user',
type: "m.id.user",
user: cli.credentials.userId,
},
// TODO: Remove `user` once servers support proper UIA
@ -159,30 +162,35 @@ export default class ChangePassword extends React.Component<IProps, IState> {
// undefined or true mean all devices signed out
const didLogoutOutOtherDevices = !serverSupportsControlOfDevicesLogout && userHasOtherDevices;
cli.setPassword(authDict, newPassword, logoutDevices).then(() => {
if (this.props.shouldAskForEmail) {
return this.optionallySetEmail().then((confirmed) => {
this.props.onFinished({
didSetEmail: confirmed,
didLogoutOutOtherDevices,
});
cli.setPassword(authDict, newPassword, logoutDevices)
.then(
() => {
if (this.props.shouldAskForEmail) {
return this.optionallySetEmail().then((confirmed) => {
this.props.onFinished({
didSetEmail: confirmed,
didLogoutOutOtherDevices,
});
});
} else {
this.props.onFinished({ didLogoutOutOtherDevices });
}
},
(err) => {
this.props.onError(err);
},
)
.finally(() => {
this.setState({
phase: Phase.Edit,
oldPassword: "",
newPassword: "",
newPasswordConfirm: "",
});
} else {
this.props.onFinished({ didLogoutOutOtherDevices });
}
}, (err) => {
this.props.onError(err);
}).finally(() => {
this.setState({
phase: Phase.Edit,
oldPassword: "",
newPassword: "",
newPasswordConfirm: "",
});
});
}
private checkPassword(oldPass: string, newPass: string, confirmPass: string): {error: string} {
private checkPassword(oldPass: string, newPass: string, confirmPass: string): { error: string } {
if (newPass !== confirmPass) {
return {
error: _t("New passwords don't match"),
@ -197,16 +205,16 @@ export default class ChangePassword extends React.Component<IProps, IState> {
private optionallySetEmail(): Promise<boolean> {
// Ask for an email otherwise the user has no way to reset their password
const modal = Modal.createDialog(SetEmailDialog, {
title: _t('Do you want to set an email address?'),
title: _t("Do you want to set an email address?"),
});
return modal.finished.then(([confirmed]) => confirmed);
}
private onExportE2eKeysClicked = (): void => {
Modal.createDialogAsync(
import(
'../../../async-components/views/dialogs/security/ExportE2eKeysDialog'
) as unknown as Promise<ComponentType<{}>>,
import("../../../async-components/views/dialogs/security/ExportE2eKeysDialog") as unknown as Promise<
ComponentType<{}>
>,
{
matrixClient: MatrixClientPeg.get(),
},
@ -293,9 +301,7 @@ export default class ChangePassword extends React.Component<IProps, IState> {
const oldPassword = this.state.oldPassword;
const newPassword = this.state.newPassword;
const confirmPassword = this.state.newPasswordConfirm;
const err = this.checkPassword(
oldPassword, newPassword, confirmPassword,
);
const err = this.checkPassword(oldPassword, newPassword, confirmPassword);
if (err) {
this.props.onError(err);
} else {
@ -311,11 +317,7 @@ export default class ChangePassword extends React.Component<IProps, IState> {
activeElement.blur();
}
const fieldIDsInDisplayOrder = [
FIELD_OLD_PASSWORD,
FIELD_NEW_PASSWORD,
FIELD_NEW_PASSWORD_CONFIRM,
];
const fieldIDsInDisplayOrder = [FIELD_OLD_PASSWORD, FIELD_NEW_PASSWORD, FIELD_NEW_PASSWORD_CONFIRM];
// Run all fields with stricter validation that no longer allows empty
// values for required fields.
@ -375,9 +377,9 @@ export default class ChangePassword extends React.Component<IProps, IState> {
<form className={this.props.className} onSubmit={this.onClickChange}>
<div className={rowClassName}>
<Field
ref={field => this[FIELD_OLD_PASSWORD] = field}
ref={(field) => (this[FIELD_OLD_PASSWORD] = field)}
type="password"
label={_t('Current password')}
label={_t("Current password")}
value={this.state.oldPassword}
onChange={this.onChangeOldPassword}
onValidate={this.onOldPasswordValidate}
@ -385,7 +387,7 @@ export default class ChangePassword extends React.Component<IProps, IState> {
</div>
<div className={rowClassName}>
<PassphraseField
fieldRef={field => this[FIELD_NEW_PASSWORD] = field}
fieldRef={(field) => (this[FIELD_NEW_PASSWORD] = field)}
type="password"
label={_td("New Password")}
minScore={PASSWORD_MIN_SCORE}
@ -398,7 +400,7 @@ export default class ChangePassword extends React.Component<IProps, IState> {
</div>
<div className={rowClassName}>
<Field
ref={field => this[FIELD_NEW_PASSWORD_CONFIRM] = field}
ref={(field) => (this[FIELD_NEW_PASSWORD_CONFIRM] = field)}
type="password"
label={_t("Confirm password")}
value={this.state.newPasswordConfirm}
@ -407,8 +409,12 @@ export default class ChangePassword extends React.Component<IProps, IState> {
autoComplete="new-password"
/>
</div>
<AccessibleButton className={buttonClassName} kind={this.props.buttonKind} onClick={this.onClickChange}>
{ this.props.buttonLabel || _t('Change Password') }
<AccessibleButton
className={buttonClassName}
kind={this.props.buttonKind}
onClick={this.onClickChange}
>
{this.props.buttonLabel || _t("Change Password")}
</AccessibleButton>
</form>
);

View file

@ -14,19 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { ClientEvent, MatrixEvent } from 'matrix-js-sdk/src/matrix';
import React from "react";
import { ClientEvent, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import Spinner from '../elements/Spinner';
import InteractiveAuthDialog from '../dialogs/InteractiveAuthDialog';
import ConfirmDestroyCrossSigningDialog from '../dialogs/security/ConfirmDestroyCrossSigningDialog';
import SetupEncryptionDialog from '../dialogs/security/SetupEncryptionDialog';
import { accessSecretStorage } from '../../../SecurityManager';
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from "../../../languageHandler";
import Modal from "../../../Modal";
import Spinner from "../elements/Spinner";
import InteractiveAuthDialog from "../dialogs/InteractiveAuthDialog";
import ConfirmDestroyCrossSigningDialog from "../dialogs/security/ConfirmDestroyCrossSigningDialog";
import SetupEncryptionDialog from "../dialogs/security/SetupEncryptionDialog";
import { accessSecretStorage } from "../../../SecurityManager";
import AccessibleButton from "../elements/AccessibleButton";
interface IState {
@ -97,8 +97,9 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
const masterPrivateKeyCached = !!(pkCache && (await pkCache.getCrossSigningKeyCache("master")));
const selfSigningPrivateKeyCached = !!(pkCache && (await pkCache.getCrossSigningKeyCache("self_signing")));
const userSigningPrivateKeyCached = !!(pkCache && (await pkCache.getCrossSigningKeyCache("user_signing")));
const homeserverSupportsCrossSigning =
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
const homeserverSupportsCrossSigning = await cli.doesServerSupportUnstableFeature(
"org.matrix.e2e_cross_signing",
);
const crossSigningReady = await cli.isCrossSigningReady();
this.setState({
@ -170,49 +171,43 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
let errorSection;
if (error) {
errorSection = <div className="error">{ error.toString() }</div>;
errorSection = <div className="error">{error.toString()}</div>;
}
let summarisedStatus;
if (homeserverSupportsCrossSigning === undefined) {
summarisedStatus = <Spinner />;
} else if (!homeserverSupportsCrossSigning) {
summarisedStatus = <p>{ _t(
"Your homeserver does not support cross-signing.",
) }</p>;
summarisedStatus = <p>{_t("Your homeserver does not support cross-signing.")}</p>;
} else if (crossSigningReady && crossSigningPrivateKeysInStorage) {
summarisedStatus = <p> { _t(
"Cross-signing is ready for use.",
) }</p>;
summarisedStatus = <p> {_t("Cross-signing is ready for use.")}</p>;
} else if (crossSigningReady && !crossSigningPrivateKeysInStorage) {
summarisedStatus = <p> { _t(
"Cross-signing is ready but keys are not backed up.",
) }</p>;
summarisedStatus = <p> {_t("Cross-signing is ready but keys are not backed up.")}</p>;
} else if (crossSigningPrivateKeysInStorage) {
summarisedStatus = <p>{ _t(
"Your account has a cross-signing identity in secret storage, " +
"but it is not yet trusted by this session.",
) }</p>;
summarisedStatus = (
<p>
{_t(
"Your account has a cross-signing identity in secret storage, " +
"but it is not yet trusted by this session.",
)}
</p>
);
} else {
summarisedStatus = <p>{ _t(
"Cross-signing is not set up.",
) }</p>;
summarisedStatus = <p>{_t("Cross-signing is not set up.")}</p>;
}
const keysExistAnywhere = (
const keysExistAnywhere =
crossSigningPublicKeysOnDevice ||
crossSigningPrivateKeysInStorage ||
masterPrivateKeyCached ||
selfSigningPrivateKeyCached ||
userSigningPrivateKeyCached
);
const keysExistEverywhere = (
userSigningPrivateKeyCached;
const keysExistEverywhere =
crossSigningPublicKeysOnDevice &&
crossSigningPrivateKeysInStorage &&
masterPrivateKeyCached &&
selfSigningPrivateKeyCached &&
userSigningPrivateKeyCached
);
userSigningPrivateKeyCached;
const actions = [];
@ -224,7 +219,7 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
}
actions.push(
<AccessibleButton key="setup" kind="primary" onClick={this.onBootstrapClick}>
{ buttonCaption }
{buttonCaption}
</AccessibleButton>,
);
}
@ -232,52 +227,56 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
if (keysExistAnywhere) {
actions.push(
<AccessibleButton key="reset" kind="danger" onClick={this.resetCrossSigning}>
{ _t("Reset") }
{_t("Reset")}
</AccessibleButton>,
);
}
let actionRow;
if (actions.length) {
actionRow = <div className="mx_CrossSigningPanel_buttonRow">
{ actions }
</div>;
actionRow = <div className="mx_CrossSigningPanel_buttonRow">{actions}</div>;
}
return (
<div>
{ summarisedStatus }
{summarisedStatus}
<details>
<summary>{ _t("Advanced") }</summary>
<table className="mx_CrossSigningPanel_statusList"><tbody>
<tr>
<td>{ _t("Cross-signing public keys:") }</td>
<td>{ crossSigningPublicKeysOnDevice ? _t("in memory") : _t("not found") }</td>
</tr>
<tr>
<td>{ _t("Cross-signing private keys:") }</td>
<td>{ crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found in storage") }</td>
</tr>
<tr>
<td>{ _t("Master private key:") }</td>
<td>{ masterPrivateKeyCached ? _t("cached locally") : _t("not found locally") }</td>
</tr>
<tr>
<td>{ _t("Self signing private key:") }</td>
<td>{ selfSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally") }</td>
</tr>
<tr>
<td>{ _t("User signing private key:") }</td>
<td>{ userSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally") }</td>
</tr>
<tr>
<td>{ _t("Homeserver feature support:") }</td>
<td>{ homeserverSupportsCrossSigning ? _t("exists") : _t("not found") }</td>
</tr>
</tbody></table>
<summary>{_t("Advanced")}</summary>
<table className="mx_CrossSigningPanel_statusList">
<tbody>
<tr>
<td>{_t("Cross-signing public keys:")}</td>
<td>{crossSigningPublicKeysOnDevice ? _t("in memory") : _t("not found")}</td>
</tr>
<tr>
<td>{_t("Cross-signing private keys:")}</td>
<td>
{crossSigningPrivateKeysInStorage
? _t("in secret storage")
: _t("not found in storage")}
</td>
</tr>
<tr>
<td>{_t("Master private key:")}</td>
<td>{masterPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td>
</tr>
<tr>
<td>{_t("Self signing private key:")}</td>
<td>{selfSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td>
</tr>
<tr>
<td>{_t("User signing private key:")}</td>
<td>{userSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td>
</tr>
<tr>
<td>{_t("Homeserver feature support:")}</td>
<td>{homeserverSupportsCrossSigning ? _t("exists") : _t("not found")}</td>
</tr>
</tbody>
</table>
</details>
{ errorSection }
{ actionRow }
{errorSection}
{actionRow}
</div>
);
}

View file

@ -14,22 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ComponentType } from 'react';
import React, { ComponentType } from "react";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from "../../../languageHandler";
import Modal from "../../../Modal";
import AccessibleButton from "../elements/AccessibleButton";
import * as FormattingUtils from "../../../utils/FormattingUtils";
import SettingsStore from "../../../settings/SettingsStore";
import SettingsFlag from "../elements/SettingsFlag";
import { SettingLevel } from "../../../settings/SettingLevel";
interface IProps {
}
interface IProps {}
interface IState {
}
interface IState {}
export default class CryptographyPanel extends React.Component<IProps, IState> {
constructor(props: IProps) {
@ -49,12 +47,12 @@ export default class CryptographyPanel extends React.Component<IProps, IState> {
let importExportButtons = null;
if (client.isCryptoEnabled()) {
importExportButtons = (
<div className='mx_CryptographyPanel_importExportButtons'>
<AccessibleButton kind='primary' onClick={this.onExportE2eKeysClicked}>
{ _t("Export E2E room keys") }
<div className="mx_CryptographyPanel_importExportButtons">
<AccessibleButton kind="primary" onClick={this.onExportE2eKeysClicked}>
{_t("Export E2E room keys")}
</AccessibleButton>
<AccessibleButton kind='primary' onClick={this.onImportE2eKeysClicked}>
{ _t("Import E2E room keys") }
<AccessibleButton kind="primary" onClick={this.onImportE2eKeysClicked}>
{_t("Import E2E room keys")}
</AccessibleButton>
</div>
);
@ -62,48 +60,56 @@ export default class CryptographyPanel extends React.Component<IProps, IState> {
let noSendUnverifiedSetting;
if (SettingsStore.isEnabled("blacklistUnverifiedDevices")) {
noSendUnverifiedSetting = <SettingsFlag
name='blacklistUnverifiedDevices'
level={SettingLevel.DEVICE}
onChange={this.updateBlacklistDevicesFlag}
/>;
noSendUnverifiedSetting = (
<SettingsFlag
name="blacklistUnverifiedDevices"
level={SettingLevel.DEVICE}
onChange={this.updateBlacklistDevicesFlag}
/>
);
}
return (
<div className='mx_SettingsTab_section mx_CryptographyPanel'>
<span className='mx_SettingsTab_subheading'>{ _t("Cryptography") }</span>
<table className='mx_SettingsTab_subsectionText mx_CryptographyPanel_sessionInfo'>
<div className="mx_SettingsTab_section mx_CryptographyPanel">
<span className="mx_SettingsTab_subheading">{_t("Cryptography")}</span>
<table className="mx_SettingsTab_subsectionText mx_CryptographyPanel_sessionInfo">
<tbody>
<tr>
<td>{ _t("Session ID:") }</td>
<td><code>{ deviceId }</code></td>
<td>{_t("Session ID:")}</td>
<td>
<code>{deviceId}</code>
</td>
</tr>
<tr>
<td>{ _t("Session key:") }</td>
<td><code><b>{ identityKey }</b></code></td>
<td>{_t("Session key:")}</td>
<td>
<code>
<b>{identityKey}</b>
</code>
</td>
</tr>
</tbody>
</table>
{ importExportButtons }
{ noSendUnverifiedSetting }
{importExportButtons}
{noSendUnverifiedSetting}
</div>
);
}
private onExportE2eKeysClicked = (): void => {
Modal.createDialogAsync(
import(
'../../../async-components/views/dialogs/security/ExportE2eKeysDialog'
) as unknown as Promise<ComponentType<{}>>,
import("../../../async-components/views/dialogs/security/ExportE2eKeysDialog") as unknown as Promise<
ComponentType<{}>
>,
{ matrixClient: MatrixClientPeg.get() },
);
};
private onImportE2eKeysClicked = (): void => {
Modal.createDialogAsync(
import(
'../../../async-components/views/dialogs/security/ImportE2eKeysDialog'
) as unknown as Promise<ComponentType<{}>>,
import("../../../async-components/views/dialogs/security/ImportE2eKeysDialog") as unknown as Promise<
ComponentType<{}>
>,
{ matrixClient: MatrixClientPeg.get() },
);
};

View file

@ -14,19 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import classNames from 'classnames';
import React from "react";
import classNames from "classnames";
import { IMyDevice } from "matrix-js-sdk/src/client";
import { logger } from "matrix-js-sdk/src/logger";
import { CrossSigningInfo } from "matrix-js-sdk/src/crypto/CrossSigning";
import { CryptoEvent } from 'matrix-js-sdk/src/crypto';
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import { _t } from '../../../languageHandler';
import { _t } from "../../../languageHandler";
import DevicesPanelEntry from "./DevicesPanelEntry";
import Spinner from "../elements/Spinner";
import AccessibleButton from "../elements/AccessibleButton";
import { deleteDevicesWithInteractiveAuth } from './devices/deleteDevices';
import MatrixClientContext from '../../../contexts/MatrixClientContext';
import { deleteDevicesWithInteractiveAuth } from "./devices/deleteDevices";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
interface IProps {
className?: string;
@ -73,14 +73,14 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
const cli = this.context;
cli.getDevices().then(
(resp) => {
if (this.unmounted) { return; }
if (this.unmounted) {
return;
}
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),
);
const selectedDevices = state.selectedDevices.filter((deviceId) => deviceIds.includes(deviceId));
return {
devices: resp.devices || [],
selectedDevices,
@ -89,7 +89,9 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
});
},
(error) => {
if (this.unmounted) { return; }
if (this.unmounted) {
return;
}
let errtxt;
if (error.httpStatus == 404) {
// 404 probably means the HS doesn't yet support the API.
@ -109,26 +111,24 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
*/
private deviceCompare(a: IMyDevice, b: IMyDevice): number {
// return < 0 if a comes before b, > 0 if a comes after b.
const lastSeenDelta =
(b.last_seen_ts || 0) - (a.last_seen_ts || 0);
const lastSeenDelta = (b.last_seen_ts || 0) - (a.last_seen_ts || 0);
if (lastSeenDelta !== 0) { return lastSeenDelta; }
if (lastSeenDelta !== 0) {
return lastSeenDelta;
}
const idA = a.device_id;
const idB = b.device_id;
return (idA < idB) ? -1 : (idA > idB) ? 1 : 0;
return idA < idB ? -1 : idA > idB ? 1 : 0;
}
private isDeviceVerified(device: IMyDevice): boolean | null {
try {
const cli = this.context;
const deviceInfo = cli.getStoredDevice(cli.getUserId(), device.device_id);
return this.state.crossSigningInfo.checkDeviceTrust(
this.state.crossSigningInfo,
deviceInfo,
false,
true,
).isCrossSigningVerified();
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;
@ -136,7 +136,9 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
}
private onDeviceSelectionToggled = (device: IMyDevice): void => {
if (this.unmounted) { return; }
if (this.unmounted) {
return;
}
const deviceId = device.device_id;
this.setState((state, props) => {
@ -186,29 +188,27 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
};
private onDeleteClick = async (): Promise<void> => {
if (this.state.selectedDevices.length === 0) { return; }
if (this.state.selectedDevices.length === 0) {
return;
}
this.setState({
deleting: true,
});
try {
await deleteDevicesWithInteractiveAuth(
this.context,
this.state.selectedDevices,
(success) => {
if (success) {
// Reset selection to [], update device list
this.setState({
selectedDevices: [],
});
this.loadDevices();
}
await deleteDevicesWithInteractiveAuth(this.context, this.state.selectedDevices, (success) => {
if (success) {
// Reset selection to [], update device list
this.setState({
deleting: false,
selectedDevices: [],
});
},
);
this.loadDevices();
}
this.setState({
deleting: false,
});
});
} catch (error) {
logger.error("Error deleting sessions", error);
this.setState({
@ -219,7 +219,7 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
private renderDevice = (device: IMyDevice): JSX.Element => {
const myDeviceId = this.context.getDeviceId();
const myDevice = this.state.devices.find((device) => (device.device_id === myDeviceId));
const myDevice = this.state.devices.find((device) => device.device_id === myDeviceId);
const isOwnDevice = device.device_id === myDeviceId;
@ -227,24 +227,22 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
// 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}
/>;
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>
);
const loadError = <div className={classNames(this.props.className, "error")}>{this.state.deviceLoadError}</div>;
if (this.state.deviceLoadError !== undefined) {
return loadError;
@ -257,13 +255,13 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
}
const myDeviceId = this.context.getDeviceId();
const myDevice = devices.find((device) => (device.device_id === myDeviceId));
const myDevice = devices.find((device) => device.device_id === myDeviceId);
if (!myDevice) {
return loadError;
}
const otherDevices = devices.filter((device) => (device.device_id !== myDeviceId));
const otherDevices = devices.filter((device) => device.device_id !== myDeviceId);
otherDevices.sort(this.deviceCompare);
const verifiedDevices = [];
@ -288,34 +286,38 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
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 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>;
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 }
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>
<div className="mx_DevicesPanel_header_title">
{ title }
</div>
{ selectButton }
</div>
{ deviceList.map(this.renderDevice) }
</React.Fragment>;
{deviceList.map(this.renderDevice)}
</React.Fragment>
);
};
const verifiedDevicesSection = section(
@ -336,42 +338,45 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
nonCryptoDevices,
);
const deleteButton = this.state.deleting ?
<Spinner w={22} h={22} /> :
const deleteButton = this.state.deleting ? (
<Spinner w={22} h={22} />
) : (
<AccessibleButton
className="mx_DevicesPanel_deleteButton"
onClick={this.onDeleteClick}
kind="danger_outline"
disabled={this.state.selectedDevices.length === 0}
data-testid='sign-out-devices-btn'
data-testid="sign-out-devices-btn"
>
{ _t("Sign out %(count)s selected devices", { count: this.state.selectedDevices.length }) }
</AccessibleButton>;
{_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 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 (
<div className={classes}>
<div className="mx_DevicesPanel_header">
<div className="mx_DevicesPanel_header_title">
{ _t("This device") }
</div>
<div className="mx_DevicesPanel_header_title">{_t("This device")}</div>
</div>
{ this.renderDevice(myDevice) }
{ otherDevicesSection }
{this.renderDevice(myDevice)}
{otherDevicesSection}
</div>
);
}

View file

@ -14,22 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { IMyDevice } from 'matrix-js-sdk/src/client';
import React from "react";
import { IMyDevice } from "matrix-js-sdk/src/client";
import { logger } from "matrix-js-sdk/src/logger";
import classNames from 'classnames';
import classNames from "classnames";
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import AccessibleButton from "../elements/AccessibleButton";
import Field from "../elements/Field";
import Modal from "../../../Modal";
import SetupEncryptionDialog from '../dialogs/security/SetupEncryptionDialog';
import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog';
import LogoutDialog from '../dialogs/LogoutDialog';
import DeviceTile from './devices/DeviceTile';
import SelectableDeviceTile from './devices/SelectableDeviceTile';
import { DeviceType } from '../../../utils/device/parseUserAgent';
import SetupEncryptionDialog from "../dialogs/security/SetupEncryptionDialog";
import VerificationRequestDialog from "../../views/dialogs/VerificationRequestDialog";
import LogoutDialog from "../dialogs/LogoutDialog";
import DeviceTile from "./devices/DeviceTile";
import SelectableDeviceTile from "./devices/SelectableDeviceTile";
import { DeviceType } from "../../../utils/device/parseUserAgent";
interface IProps {
device: IMyDevice;
@ -71,12 +71,14 @@ export default class DevicesPanelEntry extends React.Component<IProps, IState> {
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"));
});
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();
};
@ -85,9 +87,13 @@ export default class DevicesPanelEntry extends React.Component<IProps, IState> {
};
private onOwnDeviceSignOut = (): void => {
Modal.createDialog(LogoutDialog,
/* props= */{}, /* className= */null,
/* isPriority= */false, /* isStatic= */true);
Modal.createDialog(
LogoutDialog,
/* props= */ {},
/* className= */ null,
/* isPriority= */ false,
/* isStatic= */ true,
);
};
private verify = async () => {
@ -98,10 +104,7 @@ export default class DevicesPanelEntry extends React.Component<IProps, IState> {
} else {
const cli = MatrixClientPeg.get();
const userId = cli.getUserId();
const verificationRequestPromise = cli.requestVerification(
userId,
[this.props.device.device_id],
);
const verificationRequestPromise = cli.requestVerification(userId, [this.props.device.device_id]);
Modal.createDialog(VerificationRequestDialog, {
verificationRequestPromise,
member: cli.getUser(userId),
@ -115,25 +118,29 @@ export default class DevicesPanelEntry extends React.Component<IProps, IState> {
};
public render(): JSX.Element {
let iconClass = '';
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>;
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>;
signOutButton = (
<AccessibleButton kind="danger_outline" onClick={this.onOwnDeviceSignOut}>
{_t("Sign Out")}
</AccessibleButton>
);
}
const buttons = this.state.renaming ?
const buttons = this.state.renaming ? (
<form className="mx_DevicesPanel_renameForm" onSubmit={this.onRenameSubmit}>
<Field
label={_t("Display Name")}
@ -145,14 +152,16 @@ export default class DevicesPanelEntry extends React.Component<IProps, IState> {
/>
<AccessibleButton onClick={this.onRenameSubmit} kind="confirm_sm" />
<AccessibleButton onClick={this.onRenameCancel} kind="cancel_sm" />
</form> :
</form>
) : (
<React.Fragment>
{ signOutButton }
{ verifyButton }
{signOutButton}
{verifyButton}
<AccessibleButton kind="primary_outline" onClick={this.onRename}>
{ _t("Rename") }
{_t("Rename")}
</AccessibleButton>
</React.Fragment>;
</React.Fragment>
);
const extendedDevice = {
...this.props.device,
@ -161,20 +170,24 @@ export default class DevicesPanelEntry extends React.Component<IProps, IState> {
};
if (this.props.isOwnDevice) {
return <div className={classNames("mx_DevicesPanel_device", "mx_DevicesPanel_myDevice")}>
<div className="mx_DevicesPanel_deviceTrust">
<span className={"mx_DevicesPanel_icon mx_E2EIcon " + iconClass} />
return (
<div className={classNames("mx_DevicesPanel_device", "mx_DevicesPanel_myDevice")}>
<div className="mx_DevicesPanel_deviceTrust">
<span className={"mx_DevicesPanel_icon mx_E2EIcon " + iconClass} />
</div>
<DeviceTile device={extendedDevice}>{buttons}</DeviceTile>
</div>
<DeviceTile device={extendedDevice}>
{ buttons }
</DeviceTile>
</div>;
);
}
return (
<div className="mx_DevicesPanel_device">
<SelectableDeviceTile device={extendedDevice} onSelect={this.onDeviceToggled} isSelected={this.props.selected}>
{ buttons }
<SelectableDeviceTile
device={extendedDevice}
onSelect={this.onDeviceToggled}
isSelected={this.props.selected}
>
{buttons}
</SelectableDeviceTile>
</div>
);

View file

@ -14,26 +14,28 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { _t } from "../../../languageHandler";
import { SettingLevel } from "../../../settings/SettingLevel";
import SettingsStore from "../../../settings/SettingsStore";
import SettingsFlag from '../elements/SettingsFlag';
import SettingsFlag from "../elements/SettingsFlag";
const SETTING_MANUALLY_VERIFY_ALL_SESSIONS = "e2ee.manuallyVerifyAllSessions";
const E2eAdvancedPanel = props => {
return <div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Encryption") }</span>
const E2eAdvancedPanel = (props) => {
return (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Encryption")}</span>
<SettingsFlag name={SETTING_MANUALLY_VERIFY_ALL_SESSIONS}
level={SettingLevel.DEVICE}
/>
<div className="mx_SettingsTab_subsectionText">{ _t(
"Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.",
) }</div>
</div>;
<SettingsFlag name={SETTING_MANUALLY_VERIFY_ALL_SESSIONS} level={SettingLevel.DEVICE} />
<div className="mx_SettingsTab_subsectionText">
{_t(
"Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.",
)}
</div>
</div>
);
};
export default E2eAdvancedPanel;

View file

@ -14,18 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { _t } from '../../../languageHandler';
import { _t } from "../../../languageHandler";
import SdkConfig from "../../../SdkConfig";
import Modal from '../../../Modal';
import Modal from "../../../Modal";
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from "../elements/AccessibleButton";
import { formatBytes, formatCountLong } from "../../../utils/FormattingUtils";
import EventIndexPeg from "../../../indexing/EventIndexPeg";
import { SettingLevel } from "../../../settings/SettingLevel";
import SeshatResetDialog from '../dialogs/SeshatResetDialog';
import InlineSpinner from '../elements/InlineSpinner';
import SeshatResetDialog from "../dialogs/SeshatResetDialog";
import InlineSpinner from "../elements/InlineSpinner";
interface IState {
enabling: boolean;
@ -42,8 +42,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
enabling: false,
eventIndexSize: 0,
roomCount: 0,
eventIndexingEnabled:
SettingsStore.getValueAt(SettingLevel.DEVICE, 'enableEventIndexing'),
eventIndexingEnabled: SettingsStore.getValueAt(SettingLevel.DEVICE, "enableEventIndexing"),
};
}
@ -79,7 +78,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
async updateState() {
const eventIndex = EventIndexPeg.get();
const eventIndexingEnabled = SettingsStore.getValueAt(SettingLevel.DEVICE, 'enableEventIndexing');
const eventIndexingEnabled = SettingsStore.getValueAt(SettingLevel.DEVICE, "enableEventIndexing");
const enabling = false;
let eventIndexSize = 0;
@ -111,10 +110,13 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
Modal.createDialogAsync(
// @ts-ignore: TS doesn't seem to like the type of this now that it
// has also been converted to TS as well, but I can't figure out why...
import('../../../async-components/views/dialogs/eventindex/ManageEventIndexDialog'),
import("../../../async-components/views/dialogs/eventindex/ManageEventIndexDialog"),
{
onFinished: () => {},
}, null, /* priority = */ false, /* static = */ true,
},
null,
/* priority = */ false,
/* static = */ true,
);
};
@ -126,7 +128,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
await EventIndexPeg.initEventIndex();
await EventIndexPeg.get().addInitialCheckpoints();
EventIndexPeg.get().startCrawler();
await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, true);
await SettingsStore.setValue("enableEventIndexing", null, SettingLevel.DEVICE, true);
await this.updateState();
};
@ -134,7 +136,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
const { close } = Modal.createDialog(SeshatResetDialog, {
onFinished: async (success) => {
if (success) {
await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false);
await SettingsStore.setValue("enableEventIndexing", null, SettingLevel.DEVICE, false);
await EventIndexPeg.deleteEventIndex();
await this.onEnable();
close();
@ -150,20 +152,22 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
if (EventIndexPeg.get() !== null) {
eventIndexingSettings = (
<div>
<div className='mx_SettingsTab_subsectionText'>{ _t(
"Securely cache encrypted messages locally for them " +
"to appear in search results, using %(size)s to store messages from %(rooms)s rooms.",
{
size: formatBytes(this.state.eventIndexSize, 0),
// This drives the singular / plural string
// selection for "room" / "rooms" only.
count: this.state.roomCount,
rooms: formatCountLong(this.state.roomCount),
},
) }</div>
<div className="mx_SettingsTab_subsectionText">
{_t(
"Securely cache encrypted messages locally for them " +
"to appear in search results, using %(size)s to store messages from %(rooms)s rooms.",
{
size: formatBytes(this.state.eventIndexSize, 0),
// This drives the singular / plural string
// selection for "room" / "rooms" only.
count: this.state.roomCount,
rooms: formatCountLong(this.state.roomCount),
},
)}
</div>
<div>
<AccessibleButton kind="primary" onClick={this.onManage}>
{ _t("Manage") }
{_t("Manage")}
</AccessibleButton>
</div>
</div>
@ -171,87 +175,78 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
} else if (!this.state.eventIndexingEnabled && EventIndexPeg.supportIsInstalled()) {
eventIndexingSettings = (
<div>
<div className='mx_SettingsTab_subsectionText'>{ _t(
"Securely cache encrypted messages locally for them to " +
"appear in search results.",
) }</div>
<div className="mx_SettingsTab_subsectionText">
{_t("Securely cache encrypted messages locally for them to " + "appear in search results.")}
</div>
<div>
<AccessibleButton
kind="primary"
disabled={this.state.enabling}
onClick={this.onEnable}
>
{ _t("Enable") }
<AccessibleButton kind="primary" disabled={this.state.enabling} onClick={this.onEnable}>
{_t("Enable")}
</AccessibleButton>
{ this.state.enabling ? <InlineSpinner /> : <div /> }
{this.state.enabling ? <InlineSpinner /> : <div />}
</div>
</div>
);
} else if (EventIndexPeg.platformHasSupport() && !EventIndexPeg.supportIsInstalled()) {
const nativeLink = (
const nativeLink =
"https://github.com/vector-im/element-desktop/blob/develop/" +
"docs/native-node-modules.md#" +
"adding-seshat-for-search-in-e2e-encrypted-rooms"
);
"adding-seshat-for-search-in-e2e-encrypted-rooms";
eventIndexingSettings = (
<div className='mx_SettingsTab_subsectionText'>{ _t(
"%(brand)s is missing some components required for securely " +
"caching encrypted messages locally. If you'd like to " +
"experiment with this feature, build a custom %(brand)s Desktop " +
"with <nativeLink>search components added</nativeLink>.",
{
brand,
},
{
nativeLink: sub => <a
href={nativeLink}
target="_blank"
rel="noreferrer noopener"
>{ sub }</a>,
},
) }</div>
<div className="mx_SettingsTab_subsectionText">
{_t(
"%(brand)s is missing some components required for securely " +
"caching encrypted messages locally. If you'd like to " +
"experiment with this feature, build a custom %(brand)s Desktop " +
"with <nativeLink>search components added</nativeLink>.",
{
brand,
},
{
nativeLink: (sub) => (
<a href={nativeLink} target="_blank" rel="noreferrer noopener">
{sub}
</a>
),
},
)}
</div>
);
} else if (!EventIndexPeg.platformHasSupport()) {
eventIndexingSettings = (
<div className='mx_SettingsTab_subsectionText'>{ _t(
"%(brand)s can't securely cache encrypted messages locally " +
"while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> " +
"for encrypted messages to appear in search results.",
{
brand,
},
{
desktopLink: sub => <a
href="https://element.io/get-started"
target="_blank"
rel="noreferrer noopener"
>{ sub }</a>,
},
) }</div>
<div className="mx_SettingsTab_subsectionText">
{_t(
"%(brand)s can't securely cache encrypted messages locally " +
"while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> " +
"for encrypted messages to appear in search results.",
{
brand,
},
{
desktopLink: (sub) => (
<a href="https://element.io/get-started" target="_blank" rel="noreferrer noopener">
{sub}
</a>
),
},
)}
</div>
);
} else {
eventIndexingSettings = (
<div className='mx_SettingsTab_subsectionText'>
<p>
{ this.state.enabling
? <InlineSpinner />
: _t("Message search initialisation failed")
}
</p>
{ EventIndexPeg.error && (
<div className="mx_SettingsTab_subsectionText">
<p>{this.state.enabling ? <InlineSpinner /> : _t("Message search initialisation failed")}</p>
{EventIndexPeg.error && (
<details>
<summary>{ _t("Advanced") }</summary>
<code>
{ EventIndexPeg.error.message }
</code>
<summary>{_t("Advanced")}</summary>
<code>{EventIndexPeg.error.message}</code>
<p>
<AccessibleButton key="delete" kind="danger" onClick={this.confirmEventStoreReset}>
{ _t("Reset") }
{_t("Reset")}
</AccessibleButton>
</p>
</details>
) }
)}
</div>
);
}

View file

@ -14,22 +14,21 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ChangeEvent } from 'react';
import React, { ChangeEvent } from "react";
import EventTilePreview from '../elements/EventTilePreview';
import Field from '../elements/Field';
import SettingsFlag from '../elements/SettingsFlag';
import EventTilePreview from "../elements/EventTilePreview";
import Field from "../elements/Field";
import SettingsFlag from "../elements/SettingsFlag";
import SettingsStore from "../../../settings/SettingsStore";
import Slider from "../elements/Slider";
import { FontWatcher } from "../../../settings/watchers/FontWatcher";
import { IValidationResult, IFieldState } from '../elements/Validation';
import { IValidationResult, IFieldState } from "../elements/Validation";
import { Layout } from "../../../settings/enums/Layout";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { SettingLevel } from "../../../settings/SettingLevel";
import { _t } from "../../../languageHandler";
interface IProps {
}
interface IProps {}
interface IState {
// String displaying the current selected fontSize.
@ -97,66 +96,59 @@ export default class FontScalingPanel extends React.Component<IProps, IState> {
if (!(min <= parsedSize && parsedSize <= max)) {
return {
valid: false,
feedback: _t('Custom font size can only be between %(min)s pt and %(max)s pt', { min, max }),
feedback: _t("Custom font size can only be between %(min)s pt and %(max)s pt", { min, max }),
};
}
SettingsStore.setValue(
"baseFontSize",
null,
SettingLevel.DEVICE,
parseInt(value, 10) - FontWatcher.SIZE_DIFF,
);
SettingsStore.setValue("baseFontSize", null, SettingLevel.DEVICE, parseInt(value, 10) - FontWatcher.SIZE_DIFF);
return { valid: true, feedback: _t('Use between %(min)s pt and %(max)s pt', { min, max }) };
return { valid: true, feedback: _t("Use between %(min)s pt and %(max)s pt", { min, max }) };
};
public render() {
return <div className="mx_SettingsTab_section mx_FontScalingPanel">
<span className="mx_SettingsTab_subheading">{ _t("Font size") }</span>
<EventTilePreview
className="mx_FontScalingPanel_preview"
message={this.MESSAGE_PREVIEW_TEXT}
layout={this.state.layout}
userId={this.state.userId}
displayName={this.state.displayName}
avatarUrl={this.state.avatarUrl}
/>
<div className="mx_FontScalingPanel_fontSlider">
<div className="mx_FontScalingPanel_fontSlider_smallText">Aa</div>
<Slider
values={[13, 14, 15, 16, 18]}
value={parseInt(this.state.fontSize, 10)}
onSelectionChange={this.onFontSizeChanged}
displayFunc={_ => ""}
disabled={this.state.useCustomFontSize}
return (
<div className="mx_SettingsTab_section mx_FontScalingPanel">
<span className="mx_SettingsTab_subheading">{_t("Font size")}</span>
<EventTilePreview
className="mx_FontScalingPanel_preview"
message={this.MESSAGE_PREVIEW_TEXT}
layout={this.state.layout}
userId={this.state.userId}
displayName={this.state.displayName}
avatarUrl={this.state.avatarUrl}
/>
<div className="mx_FontScalingPanel_fontSlider">
<div className="mx_FontScalingPanel_fontSlider_smallText">Aa</div>
<Slider
values={[13, 14, 15, 16, 18]}
value={parseInt(this.state.fontSize, 10)}
onSelectionChange={this.onFontSizeChanged}
displayFunc={(_) => ""}
disabled={this.state.useCustomFontSize}
/>
<div className="mx_FontScalingPanel_fontSlider_largeText">Aa</div>
</div>
<SettingsFlag
name="useCustomFontSize"
level={SettingLevel.ACCOUNT}
onChange={(checked) => this.setState({ useCustomFontSize: checked })}
useCheckbox={true}
/>
<Field
type="number"
label={_t("Font size")}
autoComplete="off"
placeholder={this.state.fontSize.toString()}
value={this.state.fontSize.toString()}
id="font_size_field"
onValidate={this.onValidateFontSize}
onChange={(value: ChangeEvent<HTMLInputElement>) => this.setState({ fontSize: value.target.value })}
disabled={!this.state.useCustomFontSize}
className="mx_FontScalingPanel_customFontSizeField"
/>
<div className="mx_FontScalingPanel_fontSlider_largeText">Aa</div>
</div>
<SettingsFlag
name="useCustomFontSize"
level={SettingLevel.ACCOUNT}
onChange={(checked) => this.setState({ useCustomFontSize: checked })}
useCheckbox={true}
/>
<Field
type="number"
label={_t("Font size")}
autoComplete="off"
placeholder={this.state.fontSize.toString()}
value={this.state.fontSize.toString()}
id="font_size_field"
onValidate={this.onValidateFontSize}
onChange={
(value: ChangeEvent<HTMLInputElement>) =>
this.setState({ fontSize: value.target.value })
}
disabled={!this.state.useCustomFontSize}
className="mx_FontScalingPanel_customFontSizeField"
/>
</div>;
);
}
}

View file

@ -50,9 +50,7 @@ export default class ImageSizePanel extends React.Component<IProps, IState> {
public render(): JSX.Element {
return (
<div className="mx_SettingsTab_section mx_ImageSizePanel">
<span className="mx_SettingsTab_subheading">
{ _t("Image size in the timeline") }
</span>
<span className="mx_SettingsTab_subheading">{_t("Image size in the timeline")}</span>
<div className="mx_ImageSizePanel_radios">
<label>
@ -62,7 +60,9 @@ export default class ImageSizePanel extends React.Component<IProps, IState> {
value={ImageSize.Normal}
checked={this.state.size === ImageSize.Normal}
onChange={this.onSizeChange}
>{ _t("Default") }</StyledRadioButton>
>
{_t("Default")}
</StyledRadioButton>
</label>
<label>
<div className="mx_ImageSizePanel_size mx_ImageSizePanel_sizeLarge" />
@ -71,7 +71,9 @@ export default class ImageSizePanel extends React.Component<IProps, IState> {
value={ImageSize.Large}
checked={this.state.size === ImageSize.Large}
onChange={this.onSizeChange}
>{ _t("Large") }</StyledRadioButton>
>
{_t("Large")}
</StyledRadioButton>
</label>
</div>
</div>

View file

@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher/dispatcher';
import { ActionPayload } from '../../../dispatcher/payloads';
import { _t } from "../../../languageHandler";
import dis from "../../../dispatcher/dispatcher";
import { ActionPayload } from "../../../dispatcher/payloads";
import Spinner from "../elements/Spinner";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import Heading from '../typography/Heading';
import Heading from "../typography/Heading";
interface IProps {
// false to display an error saying that we couldn't connect to the integration manager
@ -76,7 +76,7 @@ export default class IntegrationManager extends React.Component<IProps, IState>
};
private onAction = (payload: ActionPayload): void => {
if (payload.action === 'close_scalar') {
if (payload.action === "close_scalar") {
this.props.onFinished();
}
};
@ -88,8 +88,8 @@ export default class IntegrationManager extends React.Component<IProps, IState>
public render(): JSX.Element {
if (this.props.loading) {
return (
<div className='mx_IntegrationManager_loading'>
<Heading size="h3">{ _t("Connecting to integration manager...") }</Heading>
<div className="mx_IntegrationManager_loading">
<Heading size="h3">{_t("Connecting to integration manager...")}</Heading>
<Spinner />
</div>
);
@ -97,9 +97,9 @@ export default class IntegrationManager extends React.Component<IProps, IState>
if (!this.props.connected || this.state.errored) {
return (
<div className='mx_IntegrationManager_error'>
<Heading size="h3">{ _t("Cannot connect to integration manager") }</Heading>
<p>{ _t("The integration manager is offline or it cannot reach your homeserver.") }</p>
<div className="mx_IntegrationManager_error">
<Heading size="h3">{_t("Cannot connect to integration manager")}</Heading>
<p>{_t("The integration manager is offline or it cannot reach your homeserver.")}</p>
</div>
);
}

View file

@ -50,22 +50,22 @@ const JoinRuleSettings = ({ room, promptUpgrade, aliasWarning, onError, beforeCh
const cli = room.client;
const roomSupportsRestricted = doesRoomVersionSupport(room.getVersion(), PreferredRoomVersions.RestrictedRooms);
const preferredRestrictionVersion = !roomSupportsRestricted && promptUpgrade
? PreferredRoomVersions.RestrictedRooms
: undefined;
const preferredRestrictionVersion =
!roomSupportsRestricted && promptUpgrade ? PreferredRoomVersions.RestrictedRooms : undefined;
const disabled = !room.currentState.mayClientSendStateEvent(EventType.RoomJoinRules, cli);
const [content, setContent] = useLocalEcho<IJoinRuleEventContent>(
() => room.currentState.getStateEvents(EventType.RoomJoinRules, "")?.getContent(),
content => cli.sendStateEvent(room.roomId, EventType.RoomJoinRules, content, ""),
(content) => cli.sendStateEvent(room.roomId, EventType.RoomJoinRules, content, ""),
onError,
);
const { join_rule: joinRule = JoinRule.Invite } = content || {};
const restrictedAllowRoomIds = joinRule === JoinRule.Restricted
? content.allow?.filter(o => o.type === RestrictedAllowType.RoomMembership).map(o => o.room_id)
: undefined;
const restrictedAllowRoomIds =
joinRule === JoinRule.Restricted
? content.allow?.filter((o) => o.type === RestrictedAllowType.RoomMembership).map((o) => o.room_id)
: undefined;
const editRestrictedRoomIds = async (): Promise<string[] | undefined> => {
let selected = restrictedAllowRoomIds;
@ -74,44 +74,52 @@ const JoinRuleSettings = ({ room, promptUpgrade, aliasWarning, onError, beforeCh
}
const matrixClient = MatrixClientPeg.get();
const { finished } = Modal.createDialog(ManageRestrictedJoinRuleDialog, {
matrixClient,
room,
selected,
}, "mx_ManageRestrictedJoinRuleDialog_wrapper");
const { finished } = Modal.createDialog(
ManageRestrictedJoinRuleDialog,
{
matrixClient,
room,
selected,
},
"mx_ManageRestrictedJoinRuleDialog_wrapper",
);
const [roomIds] = await finished;
return roomIds;
};
const definitions: IDefinition<JoinRule>[] = [{
value: JoinRule.Invite,
label: _t("Private (invite only)"),
description: _t("Only invited people can join."),
checked: joinRule === JoinRule.Invite || (joinRule === JoinRule.Restricted && !restrictedAllowRoomIds?.length),
}, {
value: JoinRule.Public,
label: _t("Public"),
description: <>
{ _t("Anyone can find and join.") }
{ aliasWarning }
</>,
}];
const definitions: IDefinition<JoinRule>[] = [
{
value: JoinRule.Invite,
label: _t("Private (invite only)"),
description: _t("Only invited people can join."),
checked:
joinRule === JoinRule.Invite || (joinRule === JoinRule.Restricted && !restrictedAllowRoomIds?.length),
},
{
value: JoinRule.Public,
label: _t("Public"),
description: (
<>
{_t("Anyone can find and join.")}
{aliasWarning}
</>
),
},
];
if (roomSupportsRestricted || preferredRestrictionVersion || joinRule === JoinRule.Restricted) {
let upgradeRequiredPill;
if (preferredRestrictionVersion) {
upgradeRequiredPill = <span className="mx_JoinRuleSettings_upgradeRequired">
{ _t("Upgrade required") }
</span>;
upgradeRequiredPill = <span className="mx_JoinRuleSettings_upgradeRequired">{_t("Upgrade required")}</span>;
}
let description;
if (joinRule === JoinRule.Restricted && restrictedAllowRoomIds?.length) {
// only show the first 4 spaces we know about, so that the UI doesn't grow out of proportion there are lots.
const shownSpaces = restrictedAllowRoomIds
.map(roomId => cli.getRoom(roomId))
.filter(room => room?.isSpaceRoom())
.map((roomId) => cli.getRoom(roomId))
.filter((room) => room?.isSpaceRoom())
.slice(0, 4);
let moreText;
@ -139,9 +147,9 @@ const JoinRuleSettings = ({ room, promptUpgrade, aliasWarning, onError, beforeCh
setContent({
join_rule: JoinRule.Restricted,
allow: newAllowRoomIds.map(roomId => ({
"type": RestrictedAllowType.RoomMembership,
"room_id": roomId,
allow: newAllowRoomIds.map((roomId) => ({
type: RestrictedAllowType.RoomMembership,
room_id: roomId,
})),
});
};
@ -156,44 +164,60 @@ const JoinRuleSettings = ({ room, promptUpgrade, aliasWarning, onError, beforeCh
}
};
description = <div>
<span>
{ _t("Anyone in a space can find and join. <a>Edit which spaces can access here.</a>", {}, {
a: sub => <AccessibleButton
disabled={disabled}
onClick={onEditRestrictedClick}
kind="link_inline"
>
{ sub }
</AccessibleButton>,
}) }
</span>
description = (
<div>
<span>
{_t(
"Anyone in a space can find and join. <a>Edit which spaces can access here.</a>",
{},
{
a: (sub) => (
<AccessibleButton
disabled={disabled}
onClick={onEditRestrictedClick}
kind="link_inline"
>
{sub}
</AccessibleButton>
),
},
)}
</span>
<div className="mx_JoinRuleSettings_spacesWithAccess">
<h4>{ _t("Spaces with access") }</h4>
{ shownSpaces.map(room => {
return <span key={room.roomId}>
<RoomAvatar room={room} height={32} width={32} />
{ room.name }
</span>;
}) }
{ moreText && <span>{ moreText }</span> }
<div className="mx_JoinRuleSettings_spacesWithAccess">
<h4>{_t("Spaces with access")}</h4>
{shownSpaces.map((room) => {
return (
<span key={room.roomId}>
<RoomAvatar room={room} height={32} width={32} />
{room.name}
</span>
);
})}
{moreText && <span>{moreText}</span>}
</div>
</div>
</div>;
);
} else if (SpaceStore.instance.activeSpaceRoom) {
description = _t("Anyone in <spaceName/> can find and join. You can select other spaces too.", {}, {
spaceName: () => <b>{ SpaceStore.instance.activeSpaceRoom.name }</b>,
});
description = _t(
"Anyone in <spaceName/> can find and join. You can select other spaces too.",
{},
{
spaceName: () => <b>{SpaceStore.instance.activeSpaceRoom.name}</b>,
},
);
} else {
description = _t("Anyone in a space can find and join. You can select multiple spaces.");
}
definitions.splice(1, 0, {
value: JoinRule.Restricted,
label: <>
{ _t("Space members") }
{ upgradeRequiredPill }
</>,
label: (
<>
{_t("Space members")}
{upgradeRequiredPill}
</>
),
description,
// if there are 0 allowed spaces then render it as invite only instead
checked: joinRule === JoinRule.Restricted && !!restrictedAllowRoomIds?.length,
@ -215,24 +239,33 @@ const JoinRuleSettings = ({ room, promptUpgrade, aliasWarning, onError, beforeCh
let warning: JSX.Element;
const userId = cli.getUserId();
const unableToUpdateSomeParents = Array.from(SpaceStore.instance.getKnownParents(room.roomId))
.some(roomId => !cli.getRoom(roomId)?.currentState.maySendStateEvent(EventType.SpaceChild, userId));
const unableToUpdateSomeParents = Array.from(SpaceStore.instance.getKnownParents(room.roomId)).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. " +
"In those spaces, the old room will still be shown, " +
"but people will be prompted to join the new one.") }
</b>;
warning = (
<b>
{_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>
);
}
Modal.createDialog(RoomUpgradeWarningDialog, {
roomId: room.roomId,
targetVersion,
description: <>
{ _t("This upgrade will allow members of selected spaces " +
"access to this room without an invite.") }
{ warning }
</>,
description: (
<>
{_t(
"This upgrade will allow members of selected spaces " +
"access to this room without an invite.",
)}
{warning}
</>
),
doUpgrade: async (
opts: IFinishedOpts,
fn: (progressText: string, progress: number, total: number) => void,
@ -244,22 +277,30 @@ const JoinRuleSettings = ({ room, promptUpgrade, aliasWarning, onError, beforeCh
true,
true,
true,
progress => {
(progress) => {
const total = 2 + progress.updateSpacesTotal + progress.inviteUsersTotal;
if (!progress.roomUpgraded) {
fn(_t("Upgrading room"), 0, total);
} else if (!progress.roomSynced) {
fn(_t("Loading new room"), 1, total);
} else if (progress.inviteUsersProgress < progress.inviteUsersTotal) {
fn(_t("Sending invites... (%(progress)s out of %(count)s)", {
progress: progress.inviteUsersProgress,
count: progress.inviteUsersTotal,
}), 2 + progress.inviteUsersProgress, total);
fn(
_t("Sending invites... (%(progress)s out of %(count)s)", {
progress: progress.inviteUsersProgress,
count: progress.inviteUsersTotal,
}),
2 + progress.inviteUsersProgress,
total,
);
} else if (progress.updateSpacesProgress < progress.updateSpacesTotal) {
fn(_t("Updating spaces... (%(progress)s out of %(count)s)", {
progress: progress.updateSpacesProgress,
count: progress.updateSpacesTotal,
}), 2 + progress.inviteUsersProgress + progress.updateSpacesProgress, total);
fn(
_t("Updating spaces... (%(progress)s out of %(count)s)", {
progress: progress.updateSpacesProgress,
count: progress.updateSpacesTotal,
}),
2 + progress.inviteUsersProgress + progress.updateSpacesProgress,
total,
);
}
},
);
@ -298,9 +339,9 @@ const JoinRuleSettings = ({ room, promptUpgrade, aliasWarning, onError, beforeCh
// pre-set the accepted spaces with the currently viewed one as per the microcopy
if (joinRule === JoinRule.Restricted) {
newContent.allow = restrictedAllowRoomIds.map(roomId => ({
"type": RestrictedAllowType.RoomMembership,
"room_id": roomId,
newContent.allow = restrictedAllowRoomIds.map((roomId) => ({
type: RestrictedAllowType.RoomMembership,
room_id: roomId,
}));
}

View file

@ -30,10 +30,12 @@ export const KeyboardKey: React.FC<IKeyboardKeyProps> = ({ name, last }) => {
const icon = KEY_ICON[name];
const alternateName = ALTERNATE_KEY_NAME[name];
return <React.Fragment>
<kbd> { icon || (alternateName && _t(alternateName)) || name } </kbd>
{ !last && "+" }
</React.Fragment>;
return (
<React.Fragment>
<kbd> {icon || (alternateName && _t(alternateName)) || name} </kbd>
{!last && "+"}
</React.Fragment>
);
};
interface IKeyboardShortcutProps {
@ -41,7 +43,7 @@ interface IKeyboardShortcutProps {
className?: string;
}
export const KeyboardShortcut: React.FC<IKeyboardShortcutProps> = ({ value, className = 'mx_KeyboardShortcut' }) => {
export const KeyboardShortcut: React.FC<IKeyboardShortcutProps> = ({ value, className = "mx_KeyboardShortcut" }) => {
if (!value) return null;
const modifiersElement = [];
@ -59,8 +61,10 @@ export const KeyboardShortcut: React.FC<IKeyboardShortcutProps> = ({ value, clas
modifiersElement.push(<KeyboardKey key="shiftKey" name={Key.SHIFT} />);
}
return <div className={className}>
{ modifiersElement }
<KeyboardKey name={value.key} last />
</div>;
return (
<div className={className}>
{modifiersElement}
<KeyboardKey name={value.key} last />
</div>
);
};

View file

@ -68,9 +68,7 @@ export default class LayoutSwitcher extends React.Component<IProps, IState> {
return (
<div className="mx_SettingsTab_section mx_LayoutSwitcher">
<span className="mx_SettingsTab_subheading">
{ _t("Message layout") }
</span>
<span className="mx_SettingsTab_subheading">{_t("Message layout")}</span>
<div className="mx_LayoutSwitcher_RadioButtons">
<label className={ircClasses}>
@ -88,7 +86,7 @@ export default class LayoutSwitcher extends React.Component<IProps, IState> {
checked={this.state.layout === Layout.IRC}
onChange={this.onLayoutChange}
>
{ _t("IRC (Experimental)") }
{_t("IRC (Experimental)")}
</StyledRadioButton>
</label>
<label className={groupClasses}>
@ -106,7 +104,7 @@ export default class LayoutSwitcher extends React.Component<IProps, IState> {
checked={this.state.layout == Layout.Group}
onChange={this.onLayoutChange}
>
{ _t("Modern") }
{_t("Modern")}
</StyledRadioButton>
</label>
<label className={bubbleClasses}>
@ -124,7 +122,7 @@ export default class LayoutSwitcher extends React.Component<IProps, IState> {
checked={this.state.layout == Layout.Bubble}
onChange={this.onLayoutChange}
>
{ _t("Message bubbles") }
{_t("Message bubbles")}
</StyledRadioButton>
</label>
</div>

View file

@ -132,16 +132,16 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
};
this.settingWatchers = [
SettingsStore.watchSetting("notificationsEnabled", null, (...[,,,, value]) =>
SettingsStore.watchSetting("notificationsEnabled", null, (...[, , , , value]) =>
this.setState({ desktopNotifications: value as boolean }),
),
SettingsStore.watchSetting("deviceNotificationsEnabled", null, (...[,,,, value]) => {
SettingsStore.watchSetting("deviceNotificationsEnabled", null, (...[, , , , value]) => {
this.setState({ deviceNotificationsEnabled: value as boolean });
}),
SettingsStore.watchSetting("notificationBodyEnabled", null, (...[,,,, value]) =>
SettingsStore.watchSetting("notificationBodyEnabled", null, (...[, , , , value]) =>
this.setState({ desktopShowBody: value as boolean }),
),
SettingsStore.watchSetting("audioNotificationsEnabled", null, (...[,,,, value]) =>
SettingsStore.watchSetting("audioNotificationsEnabled", null, (...[, , , , value]) =>
this.setState({ audioNotifications: value as boolean }),
),
];
@ -162,7 +162,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
}
public componentWillUnmount() {
this.settingWatchers.forEach(watcher => SettingsStore.unwatchSetting(watcher));
this.settingWatchers.forEach((watcher) => SettingsStore.unwatchSetting(watcher));
}
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>): void {
@ -173,19 +173,20 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
private async refreshFromServer() {
try {
const newState = (await Promise.all([
this.refreshRules(),
this.refreshPushers(),
this.refreshThreepids(),
])).reduce((p, c) => Object.assign(c, p), {});
const newState = (
await Promise.all([this.refreshRules(), this.refreshPushers(), this.refreshThreepids()])
).reduce((p, c) => Object.assign(c, p), {});
this.setState<keyof Omit<IState,
"deviceNotificationsEnabled" |
"desktopNotifications" |
"desktopShowBody" |
"audioNotifications" |
"clearingNotifications"
>>({
this.setState<
keyof Omit<
IState,
| "deviceNotificationsEnabled"
| "desktopNotifications"
| "desktopShowBody"
| "audioNotifications"
| "clearingNotifications"
>
>({
...newState,
phase: Phase.Ready,
});
@ -251,7 +252,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
const rule: IAnnotatedPushRule = Object.assign(r, { kind });
const category = categories[rule.rule_id] ?? RuleClass.Other;
if (rule.rule_id[0] === '.') {
if (rule.rule_id[0] === ".") {
defaultRules[category].push(rule);
}
}
@ -278,7 +279,8 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
const vectorState = definition.ruleToVectorState(rule);
preparedNewState.vectorPushRules[category].push({
ruleId: rule.rule_id,
rule, vectorState,
rule,
vectorState,
description: _t(definition.description),
});
}
@ -317,8 +319,8 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
private showSaveError() {
Modal.createDialog(ErrorDialog, {
title: _t('Error saving notification preferences'),
description: _t('An error occurred whilst saving your notification preferences.'),
title: _t("Error saving notification preferences"),
description: _t("An error occurred whilst saving your notification preferences."),
});
}
@ -327,7 +329,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
try {
const masterRule = this.state.masterPushRule;
await MatrixClientPeg.get().setPushRuleEnabled('global', masterRule.kind, masterRule.rule_id, !checked);
await MatrixClientPeg.get().setPushRuleEnabled("global", masterRule.kind, masterRule.rule_id, !checked);
await this.refreshFromServer();
} catch (e) {
this.setState({ phase: Phase.Error });
@ -361,7 +363,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
append: true,
});
} else {
const pusher = this.state.pushers.find(p => p.kind === "email" && p.pushkey === email);
const pusher = this.state.pushers.find((p) => p.kind === "email" && p.pushkey === email);
pusher.kind = null; // flag for delete
await MatrixClientPeg.get().setPusher(pusher);
}
@ -397,14 +399,16 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
let enabled: boolean;
let actions: PushRuleAction[];
if (checkedState === VectorState.On) {
if (rule.actions.length !== 1) { // XXX: Magic number
if (rule.actions.length !== 1) {
// XXX: Magic number
actions = PushRuleVectorState.actionsFor(checkedState);
}
if (this.state.vectorKeywordRuleInfo.vectorState === VectorState.Off) {
enabled = true;
}
} else if (checkedState === VectorState.Loud) {
if (rule.actions.length !== 3) { // XXX: Magic number
if (rule.actions.length !== 3) {
// XXX: Magic number
actions = PushRuleVectorState.actionsFor(checkedState);
}
if (this.state.vectorKeywordRuleInfo.vectorState === VectorState.Off) {
@ -415,20 +419,20 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
}
if (actions) {
await cli.setPushRuleActions('global', rule.kind, rule.rule_id, actions);
await cli.setPushRuleActions("global", rule.kind, rule.rule_id, actions);
}
if (enabled !== undefined) {
await cli.setPushRuleEnabled('global', rule.kind, rule.rule_id, enabled);
await cli.setPushRuleEnabled("global", rule.kind, rule.rule_id, enabled);
}
}
} else {
const definition: VectorPushRuleDefinition = VectorPushRulesDefinitions[rule.ruleId];
const actions = definition.vectorStateToActions[checkedState];
if (!actions) {
await cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, false);
await cli.setPushRuleEnabled("global", rule.rule.kind, rule.rule.rule_id, false);
} else {
await cli.setPushRuleActions('global', rule.rule.kind, rule.rule.rule_id, actions);
await cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, true);
await cli.setPushRuleActions("global", rule.rule.kind, rule.rule.rule_id, actions);
await cli.setPushRuleEnabled("global", rule.rule.kind, rule.rule.rule_id, true);
}
}
@ -453,8 +457,8 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
private async setKeywords(keywords: string[], originalRules: IAnnotatedPushRule[]) {
try {
// De-duplicate and remove empties
keywords = Array.from(new Set(keywords)).filter(k => !!k);
const oldKeywords = Array.from(new Set(originalRules.map(r => r.pattern))).filter(k => !!k);
keywords = Array.from(new Set(keywords)).filter((k) => !!k);
const oldKeywords = Array.from(new Set(originalRules.map((r) => r.pattern))).filter((k) => !!k);
// Note: Technically because of the UI interaction (at the time of writing), the diff
// will only ever be +/-1 so we don't really have to worry about efficiently handling
@ -463,8 +467,8 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
const diff = arrayDiff(oldKeywords, keywords);
for (const word of diff.removed) {
for (const rule of originalRules.filter(r => r.pattern === word)) {
await MatrixClientPeg.get().deletePushRule('global', rule.kind, rule.rule_id);
for (const rule of originalRules.filter((r) => r.pattern === word)) {
await MatrixClientPeg.get().deletePushRule("global", rule.kind, rule.rule_id);
}
}
@ -481,12 +485,12 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
}
const kind = PushRuleKind.ContentSpecific;
for (const word of diff.added) {
await MatrixClientPeg.get().addPushRule('global', kind, word, {
await MatrixClientPeg.get().addPushRule("global", kind, word, {
actions: PushRuleVectorState.actionsFor(ruleVectorState),
pattern: word,
});
if (ruleVectorState === VectorState.Off) {
await MatrixClientPeg.get().setPushRuleEnabled('global', kind, word, false);
await MatrixClientPeg.get().setPushRuleEnabled("global", kind, word, false);
}
}
@ -502,99 +506,120 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
const originalRules = objectClone(this.state.vectorKeywordRuleInfo.rules);
// We add the keyword immediately as a sort of local echo effect
this.setState({
phase: Phase.Persisting,
vectorKeywordRuleInfo: {
...this.state.vectorKeywordRuleInfo,
rules: [
...this.state.vectorKeywordRuleInfo.rules,
this.setState(
{
phase: Phase.Persisting,
vectorKeywordRuleInfo: {
...this.state.vectorKeywordRuleInfo,
rules: [
...this.state.vectorKeywordRuleInfo.rules,
// XXX: Horrible assumption that we don't need the remaining fields
{ pattern: keyword } as IAnnotatedPushRule,
],
// XXX: Horrible assumption that we don't need the remaining fields
{ pattern: keyword } as IAnnotatedPushRule,
],
},
},
}, async () => {
await this.setKeywords(this.state.vectorKeywordRuleInfo.rules.map(r => r.pattern), originalRules);
});
async () => {
await this.setKeywords(
this.state.vectorKeywordRuleInfo.rules.map((r) => r.pattern),
originalRules,
);
},
);
};
private onKeywordRemove = (keyword: string) => {
const originalRules = objectClone(this.state.vectorKeywordRuleInfo.rules);
// We remove the keyword immediately as a sort of local echo effect
this.setState({
phase: Phase.Persisting,
vectorKeywordRuleInfo: {
...this.state.vectorKeywordRuleInfo,
rules: this.state.vectorKeywordRuleInfo.rules.filter(r => r.pattern !== keyword),
this.setState(
{
phase: Phase.Persisting,
vectorKeywordRuleInfo: {
...this.state.vectorKeywordRuleInfo,
rules: this.state.vectorKeywordRuleInfo.rules.filter((r) => r.pattern !== keyword),
},
},
}, async () => {
await this.setKeywords(this.state.vectorKeywordRuleInfo.rules.map(r => r.pattern), originalRules);
});
async () => {
await this.setKeywords(
this.state.vectorKeywordRuleInfo.rules.map((r) => r.pattern),
originalRules,
);
},
);
};
private renderTopSection() {
const masterSwitch = <LabelledToggleSwitch
data-testid='notif-master-switch'
value={!this.isInhibited}
label={_t("Enable notifications for this account")}
caption={_t("Turn off to disable notifications on all your devices and sessions")}
onChange={this.onMasterRuleChanged}
disabled={this.state.phase === Phase.Persisting}
/>;
const masterSwitch = (
<LabelledToggleSwitch
data-testid="notif-master-switch"
value={!this.isInhibited}
label={_t("Enable notifications for this account")}
caption={_t("Turn off to disable notifications on all your devices and sessions")}
onChange={this.onMasterRuleChanged}
disabled={this.state.phase === Phase.Persisting}
/>
);
// If all the rules are inhibited, don't show anything.
if (this.isInhibited) {
return masterSwitch;
}
const emailSwitches = (this.state.threepids || []).filter(t => t.medium === ThreepidMedium.Email)
.map(e => <LabelledToggleSwitch
data-testid='notif-email-switch'
key={e.address}
value={this.state.pushers.some(p => p.kind === "email" && p.pushkey === e.address)}
label={_t("Enable email notifications for %(email)s", { email: e.address })}
onChange={this.onEmailNotificationsChanged.bind(this, e.address)}
disabled={this.state.phase === Phase.Persisting}
/>);
return <>
{ masterSwitch }
<LabelledToggleSwitch
data-testid='notif-device-switch'
value={this.state.deviceNotificationsEnabled}
label={_t("Enable notifications for this device")}
onChange={checked => this.updateDeviceNotifications(checked)}
disabled={this.state.phase === Phase.Persisting}
/>
{ this.state.deviceNotificationsEnabled && (<>
const emailSwitches = (this.state.threepids || [])
.filter((t) => t.medium === ThreepidMedium.Email)
.map((e) => (
<LabelledToggleSwitch
data-testid='notif-setting-notificationsEnabled'
value={this.state.desktopNotifications}
onChange={this.onDesktopNotificationsChanged}
label={_t('Enable desktop notifications for this session')}
data-testid="notif-email-switch"
key={e.address}
value={this.state.pushers.some((p) => p.kind === "email" && p.pushkey === e.address)}
label={_t("Enable email notifications for %(email)s", { email: e.address })}
onChange={this.onEmailNotificationsChanged.bind(this, e.address)}
disabled={this.state.phase === Phase.Persisting}
/>
<LabelledToggleSwitch
data-testid='notif-setting-notificationBodyEnabled'
value={this.state.desktopShowBody}
onChange={this.onDesktopShowBodyChanged}
label={_t('Show message in desktop notification')}
disabled={this.state.phase === Phase.Persisting}
/>
<LabelledToggleSwitch
data-testid='notif-setting-audioNotificationsEnabled'
value={this.state.audioNotifications}
onChange={this.onAudioNotificationsChanged}
label={_t('Enable audible notifications for this session')}
disabled={this.state.phase === Phase.Persisting}
/>
</>) }
));
{ emailSwitches }
</>;
return (
<>
{masterSwitch}
<LabelledToggleSwitch
data-testid="notif-device-switch"
value={this.state.deviceNotificationsEnabled}
label={_t("Enable notifications for this device")}
onChange={(checked) => this.updateDeviceNotifications(checked)}
disabled={this.state.phase === Phase.Persisting}
/>
{this.state.deviceNotificationsEnabled && (
<>
<LabelledToggleSwitch
data-testid="notif-setting-notificationsEnabled"
value={this.state.desktopNotifications}
onChange={this.onDesktopNotificationsChanged}
label={_t("Enable desktop notifications for this session")}
disabled={this.state.phase === Phase.Persisting}
/>
<LabelledToggleSwitch
data-testid="notif-setting-notificationBodyEnabled"
value={this.state.desktopShowBody}
onChange={this.onDesktopShowBodyChanged}
label={_t("Show message in desktop notification")}
disabled={this.state.phase === Phase.Persisting}
/>
<LabelledToggleSwitch
data-testid="notif-setting-audioNotificationsEnabled"
value={this.state.audioNotifications}
onChange={this.onAudioNotificationsChanged}
label={_t("Enable audible notifications for this session")}
disabled={this.state.phase === Phase.Persisting}
/>
</>
)}
{emailSwitches}
</>
);
}
private renderCategory(category: RuleClass) {
@ -604,45 +629,55 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
let clearNotifsButton: JSX.Element;
if (
category === RuleClass.VectorOther
&& MatrixClientPeg.get().getRooms().some(r => r.getUnreadNotificationCount() > 0)
category === RuleClass.VectorOther &&
MatrixClientPeg.get()
.getRooms()
.some((r) => r.getUnreadNotificationCount() > 0)
) {
clearNotifsButton = <AccessibleButton
onClick={this.onClearNotificationsClicked}
disabled={this.state.clearingNotifications}
kind='danger'
className='mx_UserNotifSettings_clearNotifsButton'
data-testid="clear-notifications"
>{ _t("Clear notifications") }</AccessibleButton>;
clearNotifsButton = (
<AccessibleButton
onClick={this.onClearNotificationsClicked}
disabled={this.state.clearingNotifications}
kind="danger"
className="mx_UserNotifSettings_clearNotifsButton"
data-testid="clear-notifications"
>
{_t("Clear notifications")}
</AccessibleButton>
);
}
if (category === RuleClass.VectorOther && this.isInhibited) {
// only render the utility buttons (if needed)
if (clearNotifsButton) {
return <div className='mx_UserNotifSettings_floatingSection'>
<div>{ _t("Other") }</div>
{ clearNotifsButton }
</div>;
return (
<div className="mx_UserNotifSettings_floatingSection">
<div>{_t("Other")}</div>
{clearNotifsButton}
</div>
);
}
return null;
}
let keywordComposer: JSX.Element;
if (category === RuleClass.VectorMentions) {
keywordComposer = <TagComposer
tags={this.state.vectorKeywordRuleInfo?.rules.map(r => r.pattern)}
onAdd={this.onKeywordAdd}
onRemove={this.onKeywordRemove}
disabled={this.state.phase === Phase.Persisting}
label={_t("Keyword")}
placeholder={_t("New keyword")}
/>;
keywordComposer = (
<TagComposer
tags={this.state.vectorKeywordRuleInfo?.rules.map((r) => r.pattern)}
onAdd={this.onKeywordAdd}
onRemove={this.onKeywordRemove}
disabled={this.state.phase === Phase.Persisting}
label={_t("Keyword")}
placeholder={_t("New keyword")}
/>
);
}
const VectorStateToLabel = {
[VectorState.On]: _t('On'),
[VectorState.Off]: _t('Off'),
[VectorState.Loud]: _t('Noisy'),
[VectorState.On]: _t("On"),
[VectorState.Off]: _t("Off"),
[VectorState.Loud]: _t("Noisy"),
};
const makeRadio = (r: IVectorPushRule, s: VectorState) => (
@ -656,17 +691,18 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
/>
);
const fieldsetRows = this.state.vectorPushRules[category].map(r =>
const fieldsetRows = this.state.vectorPushRules[category].map((r) => (
<fieldset
key={category + r.ruleId}
data-testid={category + r.ruleId}
className='mx_UserNotifSettings_gridRowContainer'
className="mx_UserNotifSettings_gridRowContainer"
>
<legend className='mx_UserNotifSettings_gridRowLabel'>{ r.description }</legend>
{ makeRadio(r, VectorState.Off) }
{ makeRadio(r, VectorState.On) }
{ makeRadio(r, VectorState.Loud) }
</fieldset>);
<legend className="mx_UserNotifSettings_gridRowLabel">{r.description}</legend>
{makeRadio(r, VectorState.Off)}
{makeRadio(r, VectorState.On)}
{makeRadio(r, VectorState.Loud)}
</fieldset>
));
let sectionName: TranslatedString;
switch (category) {
@ -683,37 +719,43 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
throw new Error("Developer error: Unnamed notifications section: " + category);
}
return <>
<div data-testid={`notif-section-${category}`} className='mx_UserNotifSettings_grid'>
<span className='mx_UserNotifSettings_gridRowLabel mx_UserNotifSettings_gridRowHeading'>{ sectionName }</span>
<span className='mx_UserNotifSettings_gridColumnLabel'>{ VectorStateToLabel[VectorState.Off] }</span>
<span className='mx_UserNotifSettings_gridColumnLabel'>{ VectorStateToLabel[VectorState.On] }</span>
<span className='mx_UserNotifSettings_gridColumnLabel'>{ VectorStateToLabel[VectorState.Loud] }</span>
{ fieldsetRows }
</div>
{ clearNotifsButton }
{ keywordComposer }
</>;
return (
<>
<div data-testid={`notif-section-${category}`} className="mx_UserNotifSettings_grid">
<span className="mx_UserNotifSettings_gridRowLabel mx_UserNotifSettings_gridRowHeading">
{sectionName}
</span>
<span className="mx_UserNotifSettings_gridColumnLabel">{VectorStateToLabel[VectorState.Off]}</span>
<span className="mx_UserNotifSettings_gridColumnLabel">{VectorStateToLabel[VectorState.On]}</span>
<span className="mx_UserNotifSettings_gridColumnLabel">{VectorStateToLabel[VectorState.Loud]}</span>
{fieldsetRows}
</div>
{clearNotifsButton}
{keywordComposer}
</>
);
}
private renderTargets() {
if (this.isInhibited) return null; // no targets if there's no notifications
const rows = this.state.pushers.map(p => <tr key={p.kind+p.pushkey}>
<td>{ p.app_display_name }</td>
<td>{ p.device_display_name }</td>
</tr>);
const rows = this.state.pushers.map((p) => (
<tr key={p.kind + p.pushkey}>
<td>{p.app_display_name}</td>
<td>{p.device_display_name}</td>
</tr>
));
if (!rows.length) return null; // no targets to show
return <div className='mx_UserNotifSettings_floatingSection'>
<div>{ _t("Notification targets") }</div>
<table>
<tbody>
{ rows }
</tbody>
</table>
</div>;
return (
<div className="mx_UserNotifSettings_floatingSection">
<div>{_t("Notification targets")}</div>
<table>
<tbody>{rows}</tbody>
</table>
</div>
);
}
public render() {
@ -721,15 +763,17 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
// Ends up default centered
return <Spinner />;
} else if (this.state.phase === Phase.Error) {
return <p data-testid='error-message'>{ _t("There was an error loading your notification settings.") }</p>;
return <p data-testid="error-message">{_t("There was an error loading your notification settings.")}</p>;
}
return <div className='mx_UserNotifSettings'>
{ this.renderTopSection() }
{ this.renderCategory(RuleClass.VectorGlobal) }
{ this.renderCategory(RuleClass.VectorMentions) }
{ this.renderCategory(RuleClass.VectorOther) }
{ this.renderTargets() }
</div>;
return (
<div className="mx_UserNotifSettings">
{this.renderTopSection()}
{this.renderCategory(RuleClass.VectorGlobal)}
{this.renderCategory(RuleClass.VectorMentions)}
{this.renderCategory(RuleClass.VectorOther)}
{this.renderTargets()}
</div>
);
}
}

View file

@ -14,23 +14,23 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef } from 'react';
import React, { createRef } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import Field from "../elements/Field";
import { getHostingLink } from '../../../utils/HostingLink';
import { getHostingLink } from "../../../utils/HostingLink";
import { OwnProfileStore } from "../../../stores/OwnProfileStore";
import Modal from "../../../Modal";
import ErrorDialog from "../dialogs/ErrorDialog";
import { mediaFromMxc } from "../../../customisations/Media";
import AccessibleButton from '../elements/AccessibleButton';
import AvatarSetting from './AvatarSetting';
import ExternalLink from '../elements/ExternalLink';
import UserIdentifierCustomisations from '../../../customisations/UserIdentifier';
import AccessibleButton from "../elements/AccessibleButton";
import AvatarSetting from "./AvatarSetting";
import ExternalLink from "../elements/ExternalLink";
import UserIdentifierCustomisations from "../../../customisations/UserIdentifier";
import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds";
import PosthogTrackers from '../../../PosthogTrackers';
import PosthogTrackers from "../../../PosthogTrackers";
interface IState {
userId?: string;
@ -110,7 +110,8 @@ export default class ProfileSettings extends React.Component<{}, IState> {
if (this.state.avatarFile) {
logger.log(
`Uploading new avatar, ${this.state.avatarFile.name} of type ${this.state.avatarFile.type},` +
` (${this.state.avatarFile.size}) bytes`);
` (${this.state.avatarFile.size}) bytes`,
);
const { content_uri: uri } = await client.uploadContent(this.state.avatarFile);
await client.setAvatarUrl(uri);
newState.avatarUrl = mediaFromMxc(uri).getSquareThumbnailHttp(96);
@ -123,7 +124,7 @@ export default class ProfileSettings extends React.Component<{}, IState> {
logger.log("Failed to save profile", err);
Modal.createDialog(ErrorDialog, {
title: _t("Failed to save your profile"),
description: ((err && err.message) ? err.message : _t("The operation could not be completed")),
description: err && err.message ? err.message : _t("The operation could not be completed"),
});
}
@ -160,32 +161,32 @@ export default class ProfileSettings extends React.Component<{}, IState> {
};
public render(): JSX.Element {
const hostingSignupLink = getHostingLink('user-settings');
const hostingSignupLink = getHostingLink("user-settings");
let hostingSignup = null;
if (hostingSignupLink) {
hostingSignup = <span>
{ _t(
"<a>Upgrade</a> to your own domain", {},
{
a: sub => <ExternalLink href={hostingSignupLink} target="_blank" rel="noreferrer noopener">
{ sub }
</ExternalLink>,
},
) }
</span>;
hostingSignup = (
<span>
{_t(
"<a>Upgrade</a> to your own domain",
{},
{
a: (sub) => (
<ExternalLink href={hostingSignupLink} target="_blank" rel="noreferrer noopener">
{sub}
</ExternalLink>
),
},
)}
</span>
);
}
const userIdentifier = UserIdentifierCustomisations.getDisplayUserIdentifier(
this.state.userId, { withDisplayName: true },
);
const userIdentifier = UserIdentifierCustomisations.getDisplayUserIdentifier(this.state.userId, {
withDisplayName: true,
});
return (
<form
onSubmit={this.saveProfile}
autoComplete="off"
noValidate={true}
className="mx_ProfileSettings"
>
<form onSubmit={this.saveProfile} autoComplete="off" noValidate={true} className="mx_ProfileSettings">
<input
type="file"
ref={this.avatarUpload}
@ -199,7 +200,7 @@ export default class ProfileSettings extends React.Component<{}, IState> {
/>
<div className="mx_ProfileSettings_profile">
<div className="mx_ProfileSettings_profile_controls">
<span className="mx_SettingsTab_subheading">{ _t("Profile") }</span>
<span className="mx_SettingsTab_subheading">{_t("Profile")}</span>
<Field
label={_t("Display Name")}
type="text"
@ -208,10 +209,10 @@ export default class ProfileSettings extends React.Component<{}, IState> {
onChange={this.onDisplayNameChanged}
/>
<p>
{ userIdentifier && <span className="mx_ProfileSettings_profile_controls_userId">
{ userIdentifier }
</span> }
{ hostingSignup }
{userIdentifier && (
<span className="mx_ProfileSettings_profile_controls_userId">{userIdentifier}</span>
)}
{hostingSignup}
</p>
</div>
<AvatarSetting
@ -219,7 +220,8 @@ export default class ProfileSettings extends React.Component<{}, IState> {
avatarName={this.state.displayName || this.state.userId}
avatarAltText={_t("Profile picture")}
uploadAvatar={this.uploadAvatar}
removeAvatar={this.removeAvatar} />
removeAvatar={this.removeAvatar}
/>
</div>
<div className="mx_ProfileSettings_buttons">
<AccessibleButton
@ -227,14 +229,14 @@ export default class ProfileSettings extends React.Component<{}, IState> {
kind="link"
disabled={!this.state.enableProfileSave}
>
{ _t("Cancel") }
{_t("Cancel")}
</AccessibleButton>
<AccessibleButton
onClick={this.saveProfile}
kind="primary"
disabled={!this.state.enableProfileSave}
>
{ _t("Save") }
{_t("Save")}
</AccessibleButton>
</div>
</form>

View file

@ -15,21 +15,21 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ComponentType } from 'react';
import React, { ComponentType } from "react";
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
import { TrustInfo } from "matrix-js-sdk/src/crypto/backup";
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import { isSecureBackupRequired } from '../../../utils/WellKnownUtils';
import Spinner from '../elements/Spinner';
import AccessibleButton from '../elements/AccessibleButton';
import QuestionDialog from '../dialogs/QuestionDialog';
import RestoreKeyBackupDialog from '../dialogs/security/RestoreKeyBackupDialog';
import { accessSecretStorage } from '../../../SecurityManager';
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from "../../../languageHandler";
import Modal from "../../../Modal";
import { isSecureBackupRequired } from "../../../utils/WellKnownUtils";
import Spinner from "../elements/Spinner";
import AccessibleButton from "../elements/AccessibleButton";
import QuestionDialog from "../dialogs/QuestionDialog";
import RestoreKeyBackupDialog from "../dialogs/security/RestoreKeyBackupDialog";
import { accessSecretStorage } from "../../../SecurityManager";
interface IState {
loading: boolean;
@ -68,10 +68,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
this.checkKeyBackupStatus();
MatrixClientPeg.get().on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus);
MatrixClientPeg.get().on(
CryptoEvent.KeyBackupSessionsRemaining,
this.onKeyBackupSessionsRemaining,
);
MatrixClientPeg.get().on(CryptoEvent.KeyBackupSessionsRemaining, this.onKeyBackupSessionsRemaining);
}
public componentWillUnmount(): void {
@ -151,7 +148,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
const backupKeyStored = !!(await cli.isKeyBackupKeyStored());
const backupKeyFromCache = await cli.crypto.getSessionBackupPrivateKey();
const backupKeyCached = !!(backupKeyFromCache);
const backupKeyCached = !!backupKeyFromCache;
const backupKeyWellFormed = backupKeyFromCache instanceof Uint8Array;
const secretStorageKeyInAccount = await secretStorage.hasKey();
const secretStorageReady = await cli.isSecretStorageReady();
@ -168,32 +165,36 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
private startNewBackup = (): void => {
Modal.createDialogAsync(
import(
'../../../async-components/views/dialogs/security/CreateKeyBackupDialog'
) as unknown as Promise<ComponentType<{}>>,
import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog") as unknown as Promise<
ComponentType<{}>
>,
{
onFinished: () => {
this.loadBackupStatus();
},
}, null, /* priority = */ false, /* static = */ true,
},
null,
/* priority = */ false,
/* static = */ true,
);
};
private deleteBackup = (): void => {
Modal.createDialog(QuestionDialog, {
title: _t('Delete Backup'),
title: _t("Delete Backup"),
description: _t(
"Are you sure? You will lose your encrypted messages if your " +
"keys are not backed up properly.",
"Are you sure? You will lose your encrypted messages if your " + "keys are not backed up properly.",
),
button: _t('Delete Backup'),
button: _t("Delete Backup"),
danger: true,
onFinished: (proceed) => {
if (!proceed) return;
this.setState({ loading: true });
MatrixClientPeg.get().deleteKeyBackupVersion(this.state.backupInfo.version).then(() => {
this.loadBackupStatus();
});
MatrixClientPeg.get()
.deleteKeyBackupVersion(this.state.backupInfo.version)
.then(() => {
this.loadBackupStatus();
});
},
});
};
@ -205,7 +206,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
private resetSecretStorage = async (): Promise<void> => {
this.setState({ error: null });
try {
await accessSecretStorage(async () => { }, /* forceReset = */ true);
await accessSecretStorage(async () => {}, /* forceReset = */ true);
} catch (e) {
logger.error("Error resetting secret storage", e);
if (this.unmounted) return;
@ -234,31 +235,34 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
let extraDetails;
const actions = [];
if (error) {
statusDescription = (
<div className="error">
{ _t("Unable to load key backup status") }
</div>
);
statusDescription = <div className="error">{_t("Unable to load key backup status")}</div>;
} else if (loading) {
statusDescription = <Spinner />;
} else if (backupInfo) {
let restoreButtonCaption = _t("Restore from Backup");
if (MatrixClientPeg.get().getKeyBackupEnabled()) {
statusDescription = <p> { _t("This session is backing up your keys. ") }</p>;
statusDescription = <p> {_t("This session is backing up your keys. ")}</p>;
} else {
statusDescription = <>
<p>{ _t(
"This session is <b>not backing up your keys</b>, " +
"but you do have an existing backup you can restore from " +
"and add to going forward.", {},
{ b: sub => <b>{ sub }</b> },
) }</p>
<p>{ _t(
"Connect this session to key backup before signing out to avoid " +
"losing any keys that may only be on this session.",
) }</p>
</>;
statusDescription = (
<>
<p>
{_t(
"This session is <b>not backing up your keys</b>, " +
"but you do have an existing backup you can restore from " +
"and add to going forward.",
{},
{ b: (sub) => <b>{sub}</b> },
)}
</p>
<p>
{_t(
"Connect this session to key backup before signing out to avoid " +
"losing any keys that may only be on this session.",
)}
</p>
</>
);
restoreButtonCaption = _t("Connect this session to Key Backup");
}
@ -267,95 +271,110 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
// No upload status to show when backup disabled.
uploadStatus = "";
} else if (sessionsRemaining > 0) {
uploadStatus = <div>
{ _t("Backing up %(sessionsRemaining)s keys...", { sessionsRemaining }) } <br />
</div>;
uploadStatus = (
<div>
{_t("Backing up %(sessionsRemaining)s keys...", { sessionsRemaining })} <br />
</div>
);
} else {
uploadStatus = <div>
{ _t("All keys backed up") } <br />
</div>;
uploadStatus = (
<div>
{_t("All keys backed up")} <br />
</div>
);
}
let backupSigStatuses: React.ReactNode = backupSigStatus.sigs.map((sig, i) => {
const deviceName = sig.device ? (sig.device.getDisplayName() || sig.device.deviceId) : null;
const validity = sub =>
<span className={sig.valid ? 'mx_SecureBackupPanel_sigValid' : 'mx_SecureBackupPanel_sigInvalid'}>
{ sub }
</span>;
const verify = sub =>
<span className={sig.device && sig.deviceTrust.isVerified() ? 'mx_SecureBackupPanel_deviceVerified' : 'mx_SecureBackupPanel_deviceNotVerified'}>
{ sub }
</span>;
const device = sub => <span className="mx_SecureBackupPanel_deviceName">{ deviceName }</span>;
const fromThisDevice = (
sig.device &&
sig.device.getFingerprint() === MatrixClientPeg.get().getDeviceEd25519Key()
const deviceName = sig.device ? sig.device.getDisplayName() || sig.device.deviceId : null;
const validity = (sub) => (
<span className={sig.valid ? "mx_SecureBackupPanel_sigValid" : "mx_SecureBackupPanel_sigInvalid"}>
{sub}
</span>
);
const fromThisUser = (
sig.crossSigningId &&
sig.deviceId === MatrixClientPeg.get().getCrossSigningId()
const verify = (sub) => (
<span
className={
sig.device && sig.deviceTrust.isVerified()
? "mx_SecureBackupPanel_deviceVerified"
: "mx_SecureBackupPanel_deviceNotVerified"
}
>
{sub}
</span>
);
const device = (sub) => <span className="mx_SecureBackupPanel_deviceName">{deviceName}</span>;
const fromThisDevice =
sig.device && sig.device.getFingerprint() === MatrixClientPeg.get().getDeviceEd25519Key();
const fromThisUser = sig.crossSigningId && sig.deviceId === MatrixClientPeg.get().getCrossSigningId();
let sigStatus;
if (sig.valid && fromThisUser) {
sigStatus = _t(
"Backup has a <validity>valid</validity> signature from this user",
{}, { validity },
{},
{ validity },
);
} else if (!sig.valid && fromThisUser) {
sigStatus = _t(
"Backup has a <validity>invalid</validity> signature from this user",
{}, { validity },
{},
{ validity },
);
} else if (sig.crossSigningId) {
sigStatus = _t(
"Backup has a signature from <verify>unknown</verify> user with ID %(deviceId)s",
{ deviceId: sig.deviceId }, { verify },
{ deviceId: sig.deviceId },
{ verify },
);
} else if (!sig.device) {
sigStatus = _t(
"Backup has a signature from <verify>unknown</verify> session with ID %(deviceId)s",
{ deviceId: sig.deviceId }, { verify },
{ deviceId: sig.deviceId },
{ verify },
);
} else if (sig.valid && fromThisDevice) {
sigStatus = _t(
"Backup has a <validity>valid</validity> signature from this session",
{}, { validity },
{},
{ validity },
);
} else if (!sig.valid && fromThisDevice) {
// it can happen...
sigStatus = _t(
"Backup has an <validity>invalid</validity> signature from this session",
{}, { validity },
{},
{ validity },
);
} else if (sig.valid && sig.deviceTrust.isVerified()) {
sigStatus = _t(
"Backup has a <validity>valid</validity> signature from " +
"<verify>verified</verify> session <device></device>",
{}, { validity, verify, device },
"<verify>verified</verify> session <device></device>",
{},
{ validity, verify, device },
);
} else if (sig.valid && !sig.deviceTrust.isVerified()) {
sigStatus = _t(
"Backup has a <validity>valid</validity> signature from " +
"<verify>unverified</verify> session <device></device>",
{}, { validity, verify, device },
"<verify>unverified</verify> session <device></device>",
{},
{ validity, verify, device },
);
} else if (!sig.valid && sig.deviceTrust.isVerified()) {
sigStatus = _t(
"Backup has an <validity>invalid</validity> signature from " +
"<verify>verified</verify> session <device></device>",
{}, { validity, verify, device },
"<verify>verified</verify> session <device></device>",
{},
{ validity, verify, device },
);
} else if (!sig.valid && !sig.deviceTrust.isVerified()) {
sigStatus = _t(
"Backup has an <validity>invalid</validity> signature from " +
"<verify>unverified</verify> session <device></device>",
{}, { validity, verify, device },
"<verify>unverified</verify> session <device></device>",
{},
{ validity, verify, device },
);
}
return <div key={i}>
{ sigStatus }
</div>;
return <div key={i}>{sigStatus}</div>;
});
if (backupSigStatus.sigs.length === 0) {
backupSigStatuses = _t("Backup is not signed by any of your sessions");
@ -366,47 +385,56 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
trustedLocally = _t("This backup is trusted because it has been restored on this session");
}
extraDetailsTableRows = <>
<tr>
<td>{ _t("Backup version:") }</td>
<td>{ backupInfo.version }</td>
</tr>
<tr>
<td>{ _t("Algorithm:") }</td>
<td>{ backupInfo.algorithm }</td>
</tr>
</>;
extraDetailsTableRows = (
<>
<tr>
<td>{_t("Backup version:")}</td>
<td>{backupInfo.version}</td>
</tr>
<tr>
<td>{_t("Algorithm:")}</td>
<td>{backupInfo.algorithm}</td>
</tr>
</>
);
extraDetails = <>
{ uploadStatus }
<div>{ backupSigStatuses }</div>
<div>{ trustedLocally }</div>
</>;
extraDetails = (
<>
{uploadStatus}
<div>{backupSigStatuses}</div>
<div>{trustedLocally}</div>
</>
);
actions.push(
<AccessibleButton key="restore" kind="primary" onClick={this.restoreBackup}>
{ restoreButtonCaption }
{restoreButtonCaption}
</AccessibleButton>,
);
if (!isSecureBackupRequired()) {
actions.push(
<AccessibleButton key="delete" kind="danger" onClick={this.deleteBackup}>
{ _t("Delete Backup") }
{_t("Delete Backup")}
</AccessibleButton>,
);
}
} else {
statusDescription = <>
<p>{ _t(
"Your keys are <b>not being backed up from this session</b>.", {},
{ b: sub => <b>{ sub }</b> },
) }</p>
<p>{ _t("Back up your keys before signing out to avoid losing them.") }</p>
</>;
statusDescription = (
<>
<p>
{_t(
"Your keys are <b>not being backed up from this session</b>.",
{},
{ b: (sub) => <b>{sub}</b> },
)}
</p>
<p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
</>
);
actions.push(
<AccessibleButton key="setup" kind="primary" onClick={this.startNewBackup}>
{ _t("Set up") }
{_t("Set up")}
</AccessibleButton>,
);
}
@ -414,7 +442,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
if (secretStorageKeyInAccount) {
actions.push(
<AccessibleButton key="reset" kind="danger" onClick={this.resetSecretStorage}>
{ _t("Reset") }
{_t("Reset")}
</AccessibleButton>,
);
}
@ -431,48 +459,48 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
let actionRow;
if (actions.length) {
actionRow = <div className="mx_SecureBackupPanel_buttonRow">
{ actions }
</div>;
actionRow = <div className="mx_SecureBackupPanel_buttonRow">{actions}</div>;
}
return (
<div>
<p>{ _t(
"Back up your encryption keys with your account data in case you " +
"lose access to your sessions. Your keys will be secured with a " +
"unique Security Key.",
) }</p>
{ statusDescription }
<p>
{_t(
"Back up your encryption keys with your account data in case you " +
"lose access to your sessions. Your keys will be secured with a " +
"unique Security Key.",
)}
</p>
{statusDescription}
<details>
<summary>{ _t("Advanced") }</summary>
<table className="mx_SecureBackupPanel_statusList"><tbody>
<tr>
<td>{ _t("Backup key stored:") }</td>
<td>{
backupKeyStored === true ? _t("in secret storage") : _t("not stored")
}</td>
</tr>
<tr>
<td>{ _t("Backup key cached:") }</td>
<td>
{ backupKeyCached ? _t("cached locally") : _t("not found locally") }
{ backupKeyWellFormedText }
</td>
</tr>
<tr>
<td>{ _t("Secret storage public key:") }</td>
<td>{ secretStorageKeyInAccount ? _t("in account data") : _t("not found") }</td>
</tr>
<tr>
<td>{ _t("Secret storage:") }</td>
<td>{ secretStorageReady ? _t("ready") : _t("not ready") }</td>
</tr>
{ extraDetailsTableRows }
</tbody></table>
{ extraDetails }
<summary>{_t("Advanced")}</summary>
<table className="mx_SecureBackupPanel_statusList">
<tbody>
<tr>
<td>{_t("Backup key stored:")}</td>
<td>{backupKeyStored === true ? _t("in secret storage") : _t("not stored")}</td>
</tr>
<tr>
<td>{_t("Backup key cached:")}</td>
<td>
{backupKeyCached ? _t("cached locally") : _t("not found locally")}
{backupKeyWellFormedText}
</td>
</tr>
<tr>
<td>{_t("Secret storage public key:")}</td>
<td>{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}</td>
</tr>
<tr>
<td>{_t("Secret storage:")}</td>
<td>{secretStorageReady ? _t("ready") : _t("not ready")}</td>
</tr>
{extraDetailsTableRows}
</tbody>
</table>
{extraDetails}
</details>
{ actionRow }
{actionRow}
</div>
);
}

View file

@ -14,23 +14,23 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import url from 'url';
import React from 'react';
import url from "url";
import React from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import Modal from '../../../Modal';
import Modal from "../../../Modal";
import dis from "../../../dispatcher/dispatcher";
import { getThreepidsWithBindStatus } from '../../../boundThreepids';
import { getThreepidsWithBindStatus } from "../../../boundThreepids";
import IdentityAuthClient from "../../../IdentityAuthClient";
import { abbreviateUrl, unabbreviateUrl } from "../../../utils/UrlUtils";
import { getDefaultIdentityServerUrl, doesIdentityServerHaveTerms } from '../../../utils/IdentityServerUtils';
import { getDefaultIdentityServerUrl, doesIdentityServerHaveTerms } from "../../../utils/IdentityServerUtils";
import { timeout } from "../../../utils/promise";
import { ActionPayload } from '../../../dispatcher/payloads';
import InlineSpinner from '../elements/InlineSpinner';
import AccessibleButton from '../elements/AccessibleButton';
import Field from '../elements/Field';
import { ActionPayload } from "../../../dispatcher/payloads";
import InlineSpinner from "../elements/InlineSpinner";
import AccessibleButton from "../elements/AccessibleButton";
import Field from "../elements/Field";
import QuestionDialog from "../dialogs/QuestionDialog";
// We'll wait up to this long when checking for 3PID bindings on the IS.
@ -45,12 +45,12 @@ const REACHABILITY_TIMEOUT = 10000; // ms
async function checkIdentityServerUrl(u) {
const parsedUrl = url.parse(u);
if (parsedUrl.protocol !== 'https:') return _t("Identity server URL must be HTTPS");
if (parsedUrl.protocol !== "https:") return _t("Identity server URL must be HTTPS");
// XXX: duplicated logic from js-sdk but it's quite tied up in the validation logic in the
// js-sdk so probably as easy to duplicate it than to separate it out so we can reuse it
try {
const response = await fetch(u + '/_matrix/identity/api/v1');
const response = await fetch(u + "/_matrix/identity/api/v1");
if (response.ok) {
return null;
} else if (response.status < 200 || response.status >= 300) {
@ -85,7 +85,7 @@ export default class SetIdServer extends React.Component<IProps, IState> {
constructor(props) {
super(props);
let defaultIdServer = '';
let defaultIdServer = "";
if (!MatrixClientPeg.get().getIdentityServerUrl() && getDefaultIdentityServerUrl()) {
// If no identity server is configured but there's one in the config, prepopulate
// the field to help the user.
@ -129,12 +129,14 @@ export default class SetIdServer extends React.Component<IProps, IState> {
private getTooltip = () => {
if (this.state.checking) {
return <div>
<InlineSpinner />
{ _t("Checking server") }
</div>;
return (
<div>
<InlineSpinner />
{_t("Checking server")}
</div>
);
} else if (this.state.error) {
return <span className='warning'>{ this.state.error }</span>;
return <span className="warning">{this.state.error}</span>;
} else {
return null;
}
@ -153,7 +155,7 @@ export default class SetIdServer extends React.Component<IProps, IState> {
busy: false,
error: null,
currentClientIdServer: fullUrl,
idServer: '',
idServer: "",
});
};
@ -190,11 +192,11 @@ export default class SetIdServer extends React.Component<IProps, IState> {
const [confirmed] = await this.showServerChangeWarning({
title: _t("Change identity server"),
unboundMessage: _t(
"Disconnect from the identity server <current /> and " +
"connect to <new /> instead?", {},
"Disconnect from the identity server <current /> and " + "connect to <new /> instead?",
{},
{
current: sub => <b>{ abbreviateUrl(currentClientIdServer) }</b>,
new: sub => <b>{ abbreviateUrl(idServer) }</b>,
current: (sub) => <b>{abbreviateUrl(currentClientIdServer)}</b>,
new: (sub) => <b>{abbreviateUrl(idServer)}</b>,
},
),
button: _t("Continue"),
@ -224,11 +226,9 @@ export default class SetIdServer extends React.Component<IProps, IState> {
description: (
<div>
<span className="warning">
{ _t("The identity server you have chosen does not have any terms of service.") }
</span>
<span>
&nbsp;{ _t("Only continue if you trust the owner of the server.") }
{_t("The identity server you have chosen does not have any terms of service.")}
</span>
<span>&nbsp;{_t("Only continue if you trust the owner of the server.")}</span>
</div>
),
button: _t("Continue"),
@ -242,8 +242,9 @@ export default class SetIdServer extends React.Component<IProps, IState> {
const [confirmed] = await this.showServerChangeWarning({
title: _t("Disconnect identity server"),
unboundMessage: _t(
"Disconnect from the identity server <idserver />?", {},
{ idserver: sub => <b>{ abbreviateUrl(this.state.currentClientIdServer) }</b> },
"Disconnect from the identity server <idserver />?",
{},
{ idserver: (sub) => <b>{abbreviateUrl(this.state.currentClientIdServer)}</b> },
),
button: _t("Disconnect"),
});
@ -270,50 +271,70 @@ export default class SetIdServer extends React.Component<IProps, IState> {
currentServerReachable = false;
logger.warn(
`Unable to reach identity server at ${currentClientIdServer} to check ` +
`for 3PIDs during IS change flow`,
`for 3PIDs during IS change flow`,
);
logger.warn(e);
}
const boundThreepids = threepids.filter(tp => tp.bound);
const boundThreepids = threepids.filter((tp) => tp.bound);
let message;
let danger = false;
const messageElements = {
idserver: sub => <b>{ abbreviateUrl(currentClientIdServer) }</b>,
b: sub => <b>{ sub }</b>,
idserver: (sub) => <b>{abbreviateUrl(currentClientIdServer)}</b>,
b: (sub) => <b>{sub}</b>,
};
if (!currentServerReachable) {
message = <div>
<p>{ _t(
"You should <b>remove your personal data</b> from identity server " +
"<idserver /> before disconnecting. Unfortunately, identity server " +
"<idserver /> is currently offline or cannot be reached.",
{}, messageElements,
) }</p>
<p>{ _t("You should:") }</p>
<ul>
<li>{ _t(
"check your browser plugins for anything that might block " +
"the identity server (such as Privacy Badger)",
) }</li>
<li>{ _t("contact the administrators of identity server <idserver />", {}, {
idserver: messageElements.idserver,
}) }</li>
<li>{ _t("wait and try again later") }</li>
</ul>
</div>;
message = (
<div>
<p>
{_t(
"You should <b>remove your personal data</b> from identity server " +
"<idserver /> before disconnecting. Unfortunately, identity server " +
"<idserver /> is currently offline or cannot be reached.",
{},
messageElements,
)}
</p>
<p>{_t("You should:")}</p>
<ul>
<li>
{_t(
"check your browser plugins for anything that might block " +
"the identity server (such as Privacy Badger)",
)}
</li>
<li>
{_t(
"contact the administrators of identity server <idserver />",
{},
{
idserver: messageElements.idserver,
},
)}
</li>
<li>{_t("wait and try again later")}</li>
</ul>
</div>
);
danger = true;
button = _t("Disconnect anyway");
} else if (boundThreepids.length) {
message = <div>
<p>{ _t(
"You are still <b>sharing your personal data</b> on the identity " +
"server <idserver />.", {}, messageElements,
) }</p>
<p>{ _t(
"We recommend that you remove your email addresses and phone numbers " +
"from the identity server before disconnecting.",
) }</p>
</div>;
message = (
<div>
<p>
{_t(
"You are still <b>sharing your personal data</b> on the identity " + "server <idserver />.",
{},
messageElements,
)}
</p>
<p>
{_t(
"We recommend that you remove your email addresses and phone numbers " +
"from the identity server before disconnecting.",
)}
</p>
</div>
);
danger = true;
button = _t("Disconnect anyway");
} else {
@ -336,7 +357,7 @@ export default class SetIdServer extends React.Component<IProps, IState> {
base_url: null, // clear
});
let newFieldVal = '';
let newFieldVal = "";
if (getDefaultIdentityServerUrl()) {
// Prepopulate the client's default so the user at least has some idea of
// a valid value they might enter
@ -359,23 +380,24 @@ export default class SetIdServer extends React.Component<IProps, IState> {
sectionTitle = _t("Identity server (%(server)s)", { server: abbreviateUrl(idServerUrl) });
bodyText = _t(
"You are currently using <server></server> to discover and be discoverable by " +
"existing contacts you know. You can change your identity server below.",
"existing contacts you know. You can change your identity server below.",
{},
{ server: sub => <b>{ abbreviateUrl(idServerUrl) }</b> },
{ server: (sub) => <b>{abbreviateUrl(idServerUrl)}</b> },
);
if (this.props.missingTerms) {
bodyText = _t(
"If you don't want to use <server /> to discover and be discoverable by existing " +
"contacts you know, enter another identity server below.",
{}, { server: sub => <b>{ abbreviateUrl(idServerUrl) }</b> },
"contacts you know, enter another identity server below.",
{},
{ server: (sub) => <b>{abbreviateUrl(idServerUrl)}</b> },
);
}
} else {
sectionTitle = _t("Identity server");
bodyText = _t(
"You are not currently using an identity server. " +
"To discover and be discoverable by existing contacts you know, " +
"add one below.",
"To discover and be discoverable by existing contacts you know, " +
"add one below.",
);
}
@ -384,36 +406,34 @@ export default class SetIdServer extends React.Component<IProps, IState> {
let discoButtonContent: React.ReactNode = _t("Disconnect");
let discoBodyText = _t(
"Disconnecting from your identity server will mean you " +
"won't be discoverable by other users and you won't be " +
"able to invite others by email or phone.",
"won't be discoverable by other users and you won't be " +
"able to invite others by email or phone.",
);
if (this.props.missingTerms) {
discoBodyText = _t(
"Using an identity server is optional. If you choose not to " +
"use an identity server, you won't be discoverable by other users " +
"and you won't be able to invite others by email or phone.",
"use an identity server, you won't be discoverable by other users " +
"and you won't be able to invite others by email or phone.",
);
discoButtonContent = _t("Do not use an identity server");
}
if (this.state.disconnectBusy) {
discoButtonContent = <InlineSpinner />;
}
discoSection = <div>
<span className="mx_SettingsTab_subsectionText">{ discoBodyText }</span>
<AccessibleButton onClick={this.onDisconnectClicked} kind="danger_sm">
{ discoButtonContent }
</AccessibleButton>
</div>;
discoSection = (
<div>
<span className="mx_SettingsTab_subsectionText">{discoBodyText}</span>
<AccessibleButton onClick={this.onDisconnectClicked} kind="danger_sm">
{discoButtonContent}
</AccessibleButton>
</div>
);
}
return (
<form className="mx_SetIdServer" onSubmit={this.checkIdServer}>
<span className="mx_SettingsTab_subheading">
{ sectionTitle }
</span>
<span className="mx_SettingsTab_subsectionText">
{ bodyText }
</span>
<span className="mx_SettingsTab_subheading">{sectionTitle}</span>
<span className="mx_SettingsTab_subsectionText">{bodyText}</span>
<Field
label={_t("Enter a new identity server")}
type="text"
@ -431,8 +451,10 @@ export default class SetIdServer extends React.Component<IProps, IState> {
kind="primary_sm"
onClick={this.checkIdServer}
disabled={!this.idServerChangeEnabled()}
>{ _t("Change") }</AccessibleButton>
{ discoSection }
>
{_t("Change")}
</AccessibleButton>
{discoSection}
</form>
);
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../../../languageHandler";
@ -24,9 +24,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import { SettingLevel } from "../../../settings/SettingLevel";
import ToggleSwitch from "../elements/ToggleSwitch";
interface IProps {
}
interface IProps {}
interface IState {
currentManager: IntegrationManagerInstance;
@ -47,7 +45,7 @@ export default class SetIntegrationManager extends React.Component<IProps, IStat
private onProvisioningToggled = (): void => {
const current = this.state.provisioningEnabled;
SettingsStore.setValue("integrationProvisioning", null, SettingLevel.ACCOUNT, !current).catch(err => {
SettingsStore.setValue("integrationProvisioning", null, SettingLevel.ACCOUNT, !current).catch((err) => {
logger.error("Error changing integration manager provisioning");
logger.error(err);
@ -63,21 +61,20 @@ export default class SetIntegrationManager extends React.Component<IProps, IStat
if (currentManager) {
managerName = `(${currentManager.name})`;
bodyText = _t(
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, " +
"and sticker packs.",
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, " + "and sticker packs.",
{ serverName: currentManager.name },
{ b: sub => <b>{ sub }</b> },
{ b: (sub) => <b>{sub}</b> },
);
} else {
bodyText = _t("Use an integration manager to manage bots, widgets, and sticker packs.");
}
return (
<div className='mx_SetIntegrationManager'>
<div className="mx_SetIntegrationManager">
<div className="mx_SettingsFlag">
<div className="mx_SetIntegrationManager_heading_manager">
<span className="mx_SettingsTab_heading">{ _t("Manage integrations") }</span>
<span className="mx_SettingsTab_subheading">{ managerName }</span>
<span className="mx_SettingsTab_heading">{_t("Manage integrations")}</span>
<span className="mx_SettingsTab_subheading">{managerName}</span>
</div>
<ToggleSwitch
checked={this.state.provisioningEnabled}
@ -85,14 +82,12 @@ export default class SetIntegrationManager extends React.Component<IProps, IStat
onChange={this.onProvisioningToggled}
/>
</div>
<div className="mx_SettingsTab_subsectionText">{bodyText}</div>
<div className="mx_SettingsTab_subsectionText">
{ bodyText }
</div>
<div className="mx_SettingsTab_subsectionText">
{ _t(
{_t(
"Integration managers receive configuration data, and can modify widgets, " +
"send room invites, and set power levels on your behalf.",
) }
"send room invites, and set power levels on your behalf.",
)}
</div>
</div>
);

View file

@ -12,8 +12,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ReactNode, HTMLAttributes } from 'react';
import classNames from 'classnames';
import React, { ReactNode, HTMLAttributes } from "react";
import classNames from "classnames";
interface Props extends HTMLAttributes<HTMLFieldSetElement> {
// section title
@ -21,11 +21,12 @@ interface Props extends HTMLAttributes<HTMLFieldSetElement> {
description?: string | ReactNode;
}
const SettingsFieldset: React.FC<Props> = ({ legend, className, children, description, ...rest }) =>
<fieldset {...rest} className={classNames('mx_SettingsFieldset', className)}>
<legend className='mx_SettingsFieldset_legend'>{ legend }</legend>
{ description && <div className='mx_SettingsFieldset_description'>{ description }</div> }
{ children }
</fieldset>;
const SettingsFieldset: React.FC<Props> = ({ legend, className, children, description, ...rest }) => (
<fieldset {...rest} className={classNames("mx_SettingsFieldset", className)}>
<legend className="mx_SettingsFieldset_legend">{legend}</legend>
{description && <div className="mx_SettingsFieldset_description">{description}</div>}
{children}
</fieldset>
);
export default SettingsFieldset;

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import SpellCheckLanguagesDropdown from "../../../components/views/elements/SpellCheckLanguagesDropdown";
import AccessibleButton from "../../../components/views/elements/AccessibleButton";
@ -45,9 +45,9 @@ export class ExistingSpellCheckLanguage extends React.Component<ExistingSpellChe
render() {
return (
<div className="mx_ExistingSpellCheckLanguage">
<span className="mx_ExistingSpellCheckLanguage_language">{ this.props.language }</span>
<span className="mx_ExistingSpellCheckLanguage_language">{this.props.language}</span>
<AccessibleButton onClick={this.onRemove} kind="danger_sm">
{ _t("Remove") }
{_t("Remove")}
</AccessibleButton>
</div>
);
@ -93,19 +93,20 @@ export default class SpellCheckLanguages extends React.Component<SpellCheckLangu
const addButton = (
<AccessibleButton onClick={this.onAddClick} kind="primary">
{ _t("Add") }
{_t("Add")}
</AccessibleButton>
);
return (
<div className="mx_SpellCheckLanguages">
{ existingSpellCheckLanguages }
{existingSpellCheckLanguages}
<form onSubmit={this.onAddClick} noValidate={true}>
<SpellCheckLanguagesDropdown
className="mx_GeneralUserSettingsTab_spellCheckLanguageInput"
value={this.state.newLanguage}
onOptionChange={this.onNewLanguageChange} />
{ addButton }
onOptionChange={this.onNewLanguageChange}
/>
{addButton}
</form>
</div>
);

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../../../languageHandler";
@ -23,16 +23,15 @@ import { findHighContrastTheme, findNonHighContrastTheme, getOrderedThemes, isHi
import ThemeWatcher from "../../../settings/watchers/ThemeWatcher";
import AccessibleButton from "../elements/AccessibleButton";
import dis from "../../../dispatcher/dispatcher";
import { RecheckThemePayload } from '../../../dispatcher/payloads/RecheckThemePayload';
import { Action } from '../../../dispatcher/actions';
import StyledCheckbox from '../elements/StyledCheckbox';
import Field from '../elements/Field';
import { RecheckThemePayload } from "../../../dispatcher/payloads/RecheckThemePayload";
import { Action } from "../../../dispatcher/actions";
import StyledCheckbox from "../elements/StyledCheckbox";
import Field from "../elements/Field";
import StyledRadioGroup from "../elements/StyledRadioGroup";
import { SettingLevel } from "../../../settings/SettingLevel";
import PosthogTrackers from "../../../PosthogTrackers";
interface IProps {
}
interface IProps {}
interface IThemeState {
theme: string;
@ -68,9 +67,13 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
const themeChoice: string = SettingsStore.getValue("theme");
const systemThemeExplicit: boolean = SettingsStore.getValueAt(
SettingLevel.DEVICE, "use_system_theme", null, false, true);
const themeExplicit: string = SettingsStore.getValueAt(
SettingLevel.DEVICE, "theme", null, false, true);
SettingLevel.DEVICE,
"use_system_theme",
null,
false,
true,
);
const themeExplicit: string = SettingsStore.getValueAt(SettingLevel.DEVICE, "theme", null, false, true);
// If the user has enabled system theme matching, use that.
if (systemThemeExplicit) {
@ -102,7 +105,7 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
// doing getValue in the .catch will still return the value we failed to set,
// so remember what the value was before we tried to set it so we can revert
const oldTheme: string = SettingsStore.getValue('theme');
const oldTheme: string = SettingsStore.getValue("theme");
SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme).catch(() => {
dis.dispatch<RecheckThemePayload>({ action: Action.RecheckTheme });
this.setState({ theme: oldTheme });
@ -126,7 +129,7 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
private onAddCustomTheme = async (): Promise<void> => {
let currentThemes: string[] = SettingsStore.getValue("custom_themes");
if (!currentThemes) currentThemes = [];
currentThemes = currentThemes.map(c => c); // cheap clone
currentThemes = currentThemes.map((c) => c); // cheap clone
if (this.themeTimer) {
clearTimeout(this.themeTimer);
@ -136,7 +139,7 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
const r = await fetch(this.state.customThemeUrl);
// XXX: need some schema for this
const themeInfo = await r.json();
if (!themeInfo || typeof(themeInfo['name']) !== 'string' || typeof(themeInfo['colors']) !== 'object') {
if (!themeInfo || typeof themeInfo["name"] !== "string" || typeof themeInfo["colors"] !== "object") {
this.setState({ customThemeMessage: { text: _t("Invalid theme schema."), isError: true } });
return;
}
@ -161,19 +164,19 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
private renderHighContrastCheckbox(): React.ReactElement<HTMLDivElement> {
if (
!this.state.useSystemTheme && (
findHighContrastTheme(this.state.theme) ||
isHighContrastTheme(this.state.theme)
)
!this.state.useSystemTheme &&
(findHighContrastTheme(this.state.theme) || isHighContrastTheme(this.state.theme))
) {
return <div>
<StyledCheckbox
checked={isHighContrastTheme(this.state.theme)}
onChange={(e) => this.highContrastThemeChanged(e.target.checked)}
>
{ _t("Use high contrast") }
</StyledCheckbox>
</div>;
return (
<div>
<StyledCheckbox
checked={isHighContrastTheme(this.state.theme)}
onChange={(e) => this.highContrastThemeChanged(e.target.checked)}
>
{_t("Use high contrast")}
</StyledCheckbox>
</div>
);
}
}
@ -193,14 +196,16 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
const themeWatcher = new ThemeWatcher();
let systemThemeSection: JSX.Element;
if (themeWatcher.isSystemThemeSupported()) {
systemThemeSection = <div>
<StyledCheckbox
checked={this.state.useSystemTheme}
onChange={(e) => this.onUseSystemThemeChanged(e.target.checked)}
>
{ SettingsStore.getDisplayName("use_system_theme") }
</StyledCheckbox>
</div>;
systemThemeSection = (
<div>
<StyledCheckbox
checked={this.state.useSystemTheme}
onChange={(e) => this.onUseSystemThemeChanged(e.target.checked)}
>
{SettingsStore.getDisplayName("use_system_theme")}
</StyledCheckbox>
</div>
);
}
let customThemeForm: JSX.Element;
@ -208,18 +213,18 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
let messageElement = null;
if (this.state.customThemeMessage.text) {
if (this.state.customThemeMessage.isError) {
messageElement = <div className='text-error'>{ this.state.customThemeMessage.text }</div>;
messageElement = <div className="text-error">{this.state.customThemeMessage.text}</div>;
} else {
messageElement = <div className='text-success'>{ this.state.customThemeMessage.text }</div>;
messageElement = <div className="text-success">{this.state.customThemeMessage.text}</div>;
}
}
customThemeForm = (
<div className='mx_SettingsTab_section'>
<div className="mx_SettingsTab_section">
<form onSubmit={this.onAddCustomTheme}>
<Field
label={_t("Custom theme URL")}
type='text'
id='mx_GeneralUserSettingsTab_customThemeInput'
type="text"
id="mx_GeneralUserSettingsTab_customThemeInput"
autoComplete="off"
onChange={this.onCustomThemeChange}
value={this.state.customThemeUrl}
@ -230,9 +235,9 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
kind="primary_sm"
disabled={!this.state.customThemeUrl.trim()}
>
{ _t("Add theme") }
{_t("Add theme")}
</AccessibleButton>
{ messageElement }
{messageElement}
</form>
</div>
);
@ -241,12 +246,12 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
const orderedThemes = getOrderedThemes();
return (
<div className="mx_SettingsTab_section mx_ThemeChoicePanel">
<span className="mx_SettingsTab_subheading">{ _t("Theme") }</span>
{ systemThemeSection }
<span className="mx_SettingsTab_subheading">{_t("Theme")}</span>
{systemThemeSection}
<div className="mx_ThemeSelectors">
<StyledRadioGroup
name="theme"
definitions={orderedThemes.map(t => ({
definitions={orderedThemes.map((t) => ({
value: t.id,
label: t.name,
disabled: this.state.useSystemTheme,
@ -257,8 +262,8 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
outlined
/>
</div>
{ this.renderHighContrastCheckbox() }
{ customThemeForm }
{this.renderHighContrastCheckbox()}
{customThemeForm}
</div>
);
}

View file

@ -14,17 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import SettingsStore from '../../../settings/SettingsStore';
import { UIFeature } from '../../../settings/UIFeature';
import SettingsStore from "../../../settings/SettingsStore";
import { UIFeature } from "../../../settings/UIFeature";
interface IProps {
uiFeature: UIFeature;
}
const UiFeatureSettingWrapper: React.FC<IProps> = ({ children, uiFeature }) => {
const settingValue = SettingsStore.getValue(uiFeature);
return settingValue && children ? <>{ children }</> : null;
return settingValue && children ? <>{children}</> : null;
};
export default UiFeatureSettingWrapper;

View file

@ -33,25 +33,29 @@ function installUpdate() {
function getStatusText(status: UpdateCheckStatus, errorDetail?: string) {
switch (status) {
case UpdateCheckStatus.Error:
return _t('Error encountered (%(errorDetail)s).', { errorDetail });
return _t("Error encountered (%(errorDetail)s).", { errorDetail });
case UpdateCheckStatus.Checking:
return _t('Checking for an update...');
return _t("Checking for an update...");
case UpdateCheckStatus.NotAvailable:
return _t('No update available.');
return _t("No update available.");
case UpdateCheckStatus.Downloading:
return _t('Downloading update...');
return _t("Downloading update...");
case UpdateCheckStatus.Ready:
return _t("New version available. <a>Update now.</a>", {}, {
a: sub => <AccessibleButton kind="link_inline" onClick={installUpdate}>{ sub }</AccessibleButton>,
});
return _t(
"New version available. <a>Update now.</a>",
{},
{
a: (sub) => (
<AccessibleButton kind="link_inline" onClick={installUpdate}>
{sub}
</AccessibleButton>
),
},
);
}
}
const doneStatuses = [
UpdateCheckStatus.Ready,
UpdateCheckStatus.Error,
UpdateCheckStatus.NotAvailable,
];
const doneStatuses = [UpdateCheckStatus.Ready, UpdateCheckStatus.Error, UpdateCheckStatus.NotAvailable];
const UpdateCheckButton = () => {
const [state, setState] = useState<CheckUpdatesPayload>(null);
@ -71,18 +75,22 @@ const UpdateCheckButton = () => {
let suffix;
if (state) {
suffix = <span className="mx_UpdateCheckButton_summary">
{ getStatusText(state.status, state.detail) }
{ busy && <InlineSpinner /> }
</span>;
suffix = (
<span className="mx_UpdateCheckButton_summary">
{getStatusText(state.status, state.detail)}
{busy && <InlineSpinner />}
</span>
);
}
return <React.Fragment>
<AccessibleButton onClick={onCheckForUpdateClick} kind="primary" disabled={busy}>
{ _t("Check for update") }
</AccessibleButton>
{ suffix }
</React.Fragment>;
return (
<React.Fragment>
<AccessibleButton onClick={onCheckForUpdateClick} kind="primary" disabled={busy}>
{_t("Check for update")}
</AccessibleButton>
{suffix}
</React.Fragment>
);
};
export default UpdateCheckButton;

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
import { logger } from "matrix-js-sdk/src/logger";
@ -25,7 +25,7 @@ import Field from "../../elements/Field";
import AccessibleButton from "../../elements/AccessibleButton";
import * as Email from "../../../../email";
import AddThreepid from "../../../../AddThreepid";
import Modal from '../../../../Modal';
import Modal from "../../../../Modal";
import ErrorDialog from "../../dialogs/ErrorDialog";
/*
@ -76,15 +76,18 @@ export class ExistingEmailAddress extends React.Component<IExistingEmailAddressP
e.stopPropagation();
e.preventDefault();
MatrixClientPeg.get().deleteThreePid(this.props.email.medium, this.props.email.address).then(() => {
return this.props.onRemoved(this.props.email);
}).catch((err) => {
logger.error("Unable to remove contact information: " + err);
Modal.createDialog(ErrorDialog, {
title: _t("Unable to remove contact information"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
MatrixClientPeg.get()
.deleteThreePid(this.props.email.medium, this.props.email.address)
.then(() => {
return this.props.onRemoved(this.props.email);
})
.catch((err) => {
logger.error("Unable to remove contact information: " + err);
Modal.createDialog(ErrorDialog, {
title: _t("Unable to remove contact information"),
description: err && err.message ? err.message : _t("Operation failed"),
});
});
});
};
public render(): JSX.Element {
@ -92,21 +95,21 @@ export class ExistingEmailAddress extends React.Component<IExistingEmailAddressP
return (
<div className="mx_ExistingEmailAddress">
<span className="mx_ExistingEmailAddress_promptText">
{ _t("Remove %(email)s?", { email: this.props.email.address }) }
{_t("Remove %(email)s?", { email: this.props.email.address })}
</span>
<AccessibleButton
onClick={this.onActuallyRemove}
kind="danger_sm"
className="mx_ExistingEmailAddress_confirmBtn"
>
{ _t("Remove") }
{_t("Remove")}
</AccessibleButton>
<AccessibleButton
onClick={this.onDontRemove}
kind="link_sm"
className="mx_ExistingEmailAddress_confirmBtn"
>
{ _t("Cancel") }
{_t("Cancel")}
</AccessibleButton>
</div>
);
@ -114,9 +117,9 @@ export class ExistingEmailAddress extends React.Component<IExistingEmailAddressP
return (
<div className="mx_ExistingEmailAddress">
<span className="mx_ExistingEmailAddress_email">{ this.props.email.address }</span>
<span className="mx_ExistingEmailAddress_email">{this.props.email.address}</span>
<AccessibleButton onClick={this.onRemove} kind="danger_sm">
{ _t("Remove") }
{_t("Remove")}
</AccessibleButton>
</div>
);
@ -178,16 +181,18 @@ export default class EmailAddresses extends React.Component<IProps, IState> {
const task = new AddThreepid();
this.setState({ verifying: true, continueDisabled: true, addTask: task });
task.addEmailAddress(email).then(() => {
this.setState({ continueDisabled: false });
}).catch((err) => {
logger.error("Unable to add email address " + email + " " + err);
this.setState({ verifying: false, continueDisabled: false, addTask: null });
Modal.createDialog(ErrorDialog, {
title: _t("Unable to add email address"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
task.addEmailAddress(email)
.then(() => {
this.setState({ continueDisabled: false });
})
.catch((err) => {
logger.error("Unable to add email address " + email + " " + err);
this.setState({ verifying: false, continueDisabled: false, addTask: null });
Modal.createDialog(ErrorDialog, {
title: _t("Unable to add email address"),
description: err && err.message ? err.message : _t("Operation failed"),
});
});
});
};
private onContinueClick = (e: React.MouseEvent): void => {
@ -195,39 +200,40 @@ export default class EmailAddresses extends React.Component<IProps, IState> {
e.preventDefault();
this.setState({ continueDisabled: true });
this.state.addTask.checkEmailLinkClicked().then(([finished]) => {
let newEmailAddress = this.state.newEmailAddress;
if (finished) {
const email = this.state.newEmailAddress;
const emails = [
...this.props.emails,
{ address: email, medium: ThreepidMedium.Email },
];
this.props.onEmailsChange(emails);
newEmailAddress = "";
}
this.setState({
addTask: null,
continueDisabled: false,
verifying: false,
newEmailAddress,
this.state.addTask
.checkEmailLinkClicked()
.then(([finished]) => {
let newEmailAddress = this.state.newEmailAddress;
if (finished) {
const email = this.state.newEmailAddress;
const emails = [...this.props.emails, { address: email, medium: ThreepidMedium.Email }];
this.props.onEmailsChange(emails);
newEmailAddress = "";
}
this.setState({
addTask: null,
continueDisabled: false,
verifying: false,
newEmailAddress,
});
})
.catch((err) => {
this.setState({ continueDisabled: false });
if (err.errcode === "M_THREEPID_AUTH_FAILED") {
Modal.createDialog(ErrorDialog, {
title: _t("Your email address hasn't been verified yet"),
description: _t(
"Click the link in the email you received to verify " + "and then click continue again.",
),
});
} else {
logger.error("Unable to verify email address: ", err);
Modal.createDialog(ErrorDialog, {
title: _t("Unable to verify email address."),
description: err && err.message ? err.message : _t("Operation failed"),
});
}
});
}).catch((err) => {
this.setState({ continueDisabled: false });
if (err.errcode === 'M_THREEPID_AUTH_FAILED') {
Modal.createDialog(ErrorDialog, {
title: _t("Your email address hasn't been verified yet"),
description: _t("Click the link in the email you received to verify " +
"and then click continue again."),
});
} else {
logger.error("Unable to verify email address: ", err);
Modal.createDialog(ErrorDialog, {
title: _t("Unable to verify email address."),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
}
});
};
public render(): JSX.Element {
@ -237,19 +243,23 @@ export default class EmailAddresses extends React.Component<IProps, IState> {
let addButton = (
<AccessibleButton onClick={this.onAddClick} kind="primary">
{ _t("Add") }
{_t("Add")}
</AccessibleButton>
);
if (this.state.verifying) {
addButton = (
<div>
<div>{ _t("We've sent you an email to verify your address. Please follow the instructions there and then click the button below.") }</div>
<div>
{_t(
"We've sent you an email to verify your address. Please follow the instructions there and then click the button below.",
)}
</div>
<AccessibleButton
onClick={this.onContinueClick}
kind="primary"
disabled={this.state.continueDisabled}
>
{ _t("Continue") }
{_t("Continue")}
</AccessibleButton>
</div>
);
@ -257,13 +267,8 @@ export default class EmailAddresses extends React.Component<IProps, IState> {
return (
<div className="mx_EmailAddresses">
{ existingEmailElements }
<form
onSubmit={this.onAddClick}
autoComplete="off"
noValidate={true}
className="mx_EmailAddresses_new"
>
{existingEmailElements}
<form onSubmit={this.onAddClick} autoComplete="off" noValidate={true} className="mx_EmailAddresses_new">
<Field
type="text"
label={_t("Email Address")}
@ -272,7 +277,7 @@ export default class EmailAddresses extends React.Component<IProps, IState> {
value={this.state.newEmailAddress}
onChange={this.onChangeNewEmailAddress}
/>
{ addButton }
{addButton}
</form>
</div>
);

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
import { logger } from "matrix-js-sdk/src/logger";
@ -25,7 +25,7 @@ import Field from "../../elements/Field";
import AccessibleButton from "../../elements/AccessibleButton";
import AddThreepid from "../../../../AddThreepid";
import CountryDropdown from "../../auth/CountryDropdown";
import Modal from '../../../../Modal';
import Modal from "../../../../Modal";
import ErrorDialog from "../../dialogs/ErrorDialog";
import { PhoneNumberCountryDefinition } from "../../../../phonenumber";
@ -72,15 +72,18 @@ export class ExistingPhoneNumber extends React.Component<IExistingPhoneNumberPro
e.stopPropagation();
e.preventDefault();
MatrixClientPeg.get().deleteThreePid(this.props.msisdn.medium, this.props.msisdn.address).then(() => {
return this.props.onRemoved(this.props.msisdn);
}).catch((err) => {
logger.error("Unable to remove contact information: " + err);
Modal.createDialog(ErrorDialog, {
title: _t("Unable to remove contact information"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
MatrixClientPeg.get()
.deleteThreePid(this.props.msisdn.medium, this.props.msisdn.address)
.then(() => {
return this.props.onRemoved(this.props.msisdn);
})
.catch((err) => {
logger.error("Unable to remove contact information: " + err);
Modal.createDialog(ErrorDialog, {
title: _t("Unable to remove contact information"),
description: err && err.message ? err.message : _t("Operation failed"),
});
});
});
};
public render(): JSX.Element {
@ -88,21 +91,21 @@ export class ExistingPhoneNumber extends React.Component<IExistingPhoneNumberPro
return (
<div className="mx_ExistingPhoneNumber">
<span className="mx_ExistingPhoneNumber_promptText">
{ _t("Remove %(phone)s?", { phone: this.props.msisdn.address }) }
{_t("Remove %(phone)s?", { phone: this.props.msisdn.address })}
</span>
<AccessibleButton
onClick={this.onActuallyRemove}
kind="danger_sm"
className="mx_ExistingPhoneNumber_confirmBtn"
>
{ _t("Remove") }
{_t("Remove")}
</AccessibleButton>
<AccessibleButton
onClick={this.onDontRemove}
kind="link_sm"
className="mx_ExistingPhoneNumber_confirmBtn"
>
{ _t("Cancel") }
{_t("Cancel")}
</AccessibleButton>
</div>
);
@ -110,9 +113,9 @@ export class ExistingPhoneNumber extends React.Component<IExistingPhoneNumberPro
return (
<div className="mx_ExistingPhoneNumber">
<span className="mx_ExistingPhoneNumber_address">+{ this.props.msisdn.address }</span>
<span className="mx_ExistingPhoneNumber_address">+{this.props.msisdn.address}</span>
<AccessibleButton onClick={this.onRemove} kind="danger_sm">
{ _t("Remove") }
{_t("Remove")}
</AccessibleButton>
</div>
);
@ -180,16 +183,18 @@ export default class PhoneNumbers extends React.Component<IProps, IState> {
const task = new AddThreepid();
this.setState({ verifying: true, continueDisabled: true, addTask: task });
task.addMsisdn(phoneCountry, phoneNumber).then((response) => {
this.setState({ continueDisabled: false, verifyMsisdn: response.msisdn });
}).catch((err) => {
logger.error("Unable to add phone number " + phoneNumber + " " + err);
this.setState({ verifying: false, continueDisabled: false, addTask: null });
Modal.createDialog(ErrorDialog, {
title: _t("Error"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
task.addMsisdn(phoneCountry, phoneNumber)
.then((response) => {
this.setState({ continueDisabled: false, verifyMsisdn: response.msisdn });
})
.catch((err) => {
logger.error("Unable to add phone number " + phoneNumber + " " + err);
this.setState({ verifying: false, continueDisabled: false, addTask: null });
Modal.createDialog(ErrorDialog, {
title: _t("Error"),
description: err && err.message ? err.message : _t("Operation failed"),
});
});
});
};
private onContinueClick = (e: React.MouseEvent | React.FormEvent): void => {
@ -199,37 +204,37 @@ export default class PhoneNumbers extends React.Component<IProps, IState> {
this.setState({ continueDisabled: true });
const token = this.state.newPhoneNumberCode;
const address = this.state.verifyMsisdn;
this.state.addTask.haveMsisdnToken(token).then(([finished]) => {
let newPhoneNumber = this.state.newPhoneNumber;
if (finished) {
const msisdns = [
...this.props.msisdns,
{ address, medium: ThreepidMedium.Phone },
];
this.props.onMsisdnsChange(msisdns);
newPhoneNumber = "";
}
this.setState({
addTask: null,
continueDisabled: false,
verifying: false,
verifyMsisdn: "",
verifyError: null,
newPhoneNumber,
newPhoneNumberCode: "",
});
}).catch((err) => {
this.setState({ continueDisabled: false });
if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {
logger.error("Unable to verify phone number: " + err);
Modal.createDialog(ErrorDialog, {
title: _t("Unable to verify phone number."),
description: ((err && err.message) ? err.message : _t("Operation failed")),
this.state.addTask
.haveMsisdnToken(token)
.then(([finished]) => {
let newPhoneNumber = this.state.newPhoneNumber;
if (finished) {
const msisdns = [...this.props.msisdns, { address, medium: ThreepidMedium.Phone }];
this.props.onMsisdnsChange(msisdns);
newPhoneNumber = "";
}
this.setState({
addTask: null,
continueDisabled: false,
verifying: false,
verifyMsisdn: "",
verifyError: null,
newPhoneNumber,
newPhoneNumberCode: "",
});
} else {
this.setState({ verifyError: _t("Incorrect verification code") });
}
});
})
.catch((err) => {
this.setState({ continueDisabled: false });
if (err.errcode !== "M_THREEPID_AUTH_FAILED") {
logger.error("Unable to verify phone number: " + err);
Modal.createDialog(ErrorDialog, {
title: _t("Unable to verify phone number."),
description: err && err.message ? err.message : _t("Operation failed"),
});
} else {
this.setState({ verifyError: _t("Incorrect verification code") });
}
});
};
private onCountryChanged = (country: PhoneNumberCountryDefinition): void => {
@ -243,7 +248,7 @@ export default class PhoneNumbers extends React.Component<IProps, IState> {
let addVerifySection = (
<AccessibleButton onClick={this.onAddClick} kind="primary">
{ _t("Add") }
{_t("Add")}
</AccessibleButton>
);
if (this.state.verifying) {
@ -251,10 +256,13 @@ export default class PhoneNumbers extends React.Component<IProps, IState> {
addVerifySection = (
<div>
<div>
{ _t("A text message has been sent to +%(msisdn)s. " +
"Please enter the verification code it contains.", { msisdn: msisdn }) }
{_t(
"A text message has been sent to +%(msisdn)s. " +
"Please enter the verification code it contains.",
{ msisdn: msisdn },
)}
<br />
{ this.state.verifyError }
{this.state.verifyError}
</div>
<form onSubmit={this.onContinueClick} autoComplete="off" noValidate={true}>
<Field
@ -270,24 +278,27 @@ export default class PhoneNumbers extends React.Component<IProps, IState> {
kind="primary"
disabled={this.state.continueDisabled || this.state.newPhoneNumberCode.length === 0}
>
{ _t("Continue") }
{_t("Continue")}
</AccessibleButton>
</form>
</div>
);
}
const phoneCountry = <CountryDropdown onOptionChange={this.onCountryChanged}
className="mx_PhoneNumbers_country"
value={this.state.phoneCountry}
disabled={this.state.verifying}
isSmall={true}
showPrefix={true}
/>;
const phoneCountry = (
<CountryDropdown
onOptionChange={this.onCountryChanged}
className="mx_PhoneNumbers_country"
value={this.state.phoneCountry}
disabled={this.state.verifying}
isSmall={true}
showPrefix={true}
/>
);
return (
<div className="mx_PhoneNumbers">
{ existingPhoneElements }
{existingPhoneElements}
<form onSubmit={this.onAddClick} autoComplete="off" noValidate={true} className="mx_PhoneNumbers_new">
<div className="mx_PhoneNumbers_input">
<Field
@ -301,7 +312,7 @@ export default class PhoneNumbers extends React.Component<IProps, IState> {
/>
</div>
</form>
{ addVerifySection }
{addVerifySection}
</div>
);
}

View file

@ -14,20 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useState } from 'react';
import { LocalNotificationSettings } from 'matrix-js-sdk/src/@types/local_notifications';
import React, { useState } from "react";
import { LocalNotificationSettings } from "matrix-js-sdk/src/@types/local_notifications";
import { _t } from '../../../../languageHandler';
import Spinner from '../../elements/Spinner';
import SettingsSubsection from '../shared/SettingsSubsection';
import { SettingsSubsectionHeading } from '../shared/SettingsSubsectionHeading';
import DeviceDetails from './DeviceDetails';
import { DeviceExpandDetailsButton } from './DeviceExpandDetailsButton';
import DeviceTile from './DeviceTile';
import { DeviceVerificationStatusCard } from './DeviceVerificationStatusCard';
import { ExtendedDevice } from './types';
import { KebabContextMenu } from '../../context_menus/KebabContextMenu';
import { IconizedContextMenuOption } from '../../context_menus/IconizedContextMenu';
import { _t } from "../../../../languageHandler";
import Spinner from "../../elements/Spinner";
import SettingsSubsection from "../shared/SettingsSubsection";
import { SettingsSubsectionHeading } from "../shared/SettingsSubsectionHeading";
import DeviceDetails from "./DeviceDetails";
import { DeviceExpandDetailsButton } from "./DeviceExpandDetailsButton";
import DeviceTile from "./DeviceTile";
import { DeviceVerificationStatusCard } from "./DeviceVerificationStatusCard";
import { ExtendedDevice } from "./types";
import { KebabContextMenu } from "../../context_menus/KebabContextMenu";
import { IconizedContextMenuOption } from "../../context_menus/IconizedContextMenu";
interface Props {
device?: ExtendedDevice;
@ -41,9 +41,9 @@ interface Props {
saveDeviceName: (deviceName: string) => Promise<void>;
}
type CurrentDeviceSectionHeadingProps =
Pick<Props, 'onSignOutCurrentDevice' | 'signOutAllOtherSessions'>
& { disabled?: boolean };
type CurrentDeviceSectionHeadingProps = Pick<Props, "onSignOutCurrentDevice" | "signOutAllOtherSessions"> & {
disabled?: boolean;
};
const CurrentDeviceSectionHeading: React.FC<CurrentDeviceSectionHeadingProps> = ({
onSignOutCurrentDevice,
@ -53,30 +53,31 @@ const CurrentDeviceSectionHeading: React.FC<CurrentDeviceSectionHeadingProps> =
const menuOptions = [
<IconizedContextMenuOption
key="sign-out"
label={_t('Sign out')}
label={_t("Sign out")}
onClick={onSignOutCurrentDevice}
isDestructive
/>,
...(signOutAllOtherSessions
? [
<IconizedContextMenuOption
key="sign-out-all-others"
label={_t('Sign out all other sessions')}
onClick={signOutAllOtherSessions}
isDestructive
/>,
]
: []
),
<IconizedContextMenuOption
key="sign-out-all-others"
label={_t("Sign out all other sessions")}
onClick={signOutAllOtherSessions}
isDestructive
/>,
]
: []),
];
return <SettingsSubsectionHeading heading={_t('Current session')}>
<KebabContextMenu
disabled={disabled}
title={_t('Options')}
options={menuOptions}
data-testid='current-session-menu'
/>
</SettingsSubsectionHeading>;
return (
<SettingsSubsectionHeading heading={_t("Current session")}>
<KebabContextMenu
disabled={disabled}
title={_t("Options")}
options={menuOptions}
data-testid="current-session-menu"
/>
</SettingsSubsectionHeading>
);
};
const CurrentDeviceSection: React.FC<Props> = ({
@ -92,43 +93,45 @@ const CurrentDeviceSection: React.FC<Props> = ({
}) => {
const [isExpanded, setIsExpanded] = useState(false);
return <SettingsSubsection
data-testid='current-session-section'
heading={<CurrentDeviceSectionHeading
onSignOutCurrentDevice={onSignOutCurrentDevice}
signOutAllOtherSessions={signOutAllOtherSessions}
disabled={isLoading || !device || isSigningOut}
/>}
>
{ /* only show big spinner on first load */ }
{ isLoading && !device && <Spinner /> }
{ !!device && <>
<DeviceTile
device={device}
onClick={() => setIsExpanded(!isExpanded)}
>
<DeviceExpandDetailsButton
data-testid='current-session-toggle-details'
isExpanded={isExpanded}
onClick={() => setIsExpanded(!isExpanded)}
/>
</DeviceTile>
{ isExpanded &&
<DeviceDetails
device={device}
localNotificationSettings={localNotificationSettings}
setPushNotifications={setPushNotifications}
isSigningOut={isSigningOut}
onVerifyDevice={onVerifyCurrentDevice}
onSignOutDevice={onSignOutCurrentDevice}
saveDeviceName={saveDeviceName}
return (
<SettingsSubsection
data-testid="current-session-section"
heading={
<CurrentDeviceSectionHeading
onSignOutCurrentDevice={onSignOutCurrentDevice}
signOutAllOtherSessions={signOutAllOtherSessions}
disabled={isLoading || !device || isSigningOut}
/>
}
<br />
<DeviceVerificationStatusCard device={device} onVerifyDevice={onVerifyCurrentDevice} />
</>
}
</SettingsSubsection>;
>
{/* only show big spinner on first load */}
{isLoading && !device && <Spinner />}
{!!device && (
<>
<DeviceTile device={device} onClick={() => setIsExpanded(!isExpanded)}>
<DeviceExpandDetailsButton
data-testid="current-session-toggle-details"
isExpanded={isExpanded}
onClick={() => setIsExpanded(!isExpanded)}
/>
</DeviceTile>
{isExpanded && (
<DeviceDetails
device={device}
localNotificationSettings={localNotificationSettings}
setPushNotifications={setPushNotifications}
isSigningOut={isSigningOut}
onVerifyDevice={onVerifyCurrentDevice}
onSignOutDevice={onSignOutCurrentDevice}
saveDeviceName={saveDeviceName}
/>
)}
<br />
<DeviceVerificationStatusCard device={device} onVerifyDevice={onVerifyCurrentDevice} />
</>
)}
</SettingsSubsection>
);
};
export default CurrentDeviceSection;

View file

@ -14,35 +14,32 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { FormEvent, useEffect, useState } from 'react';
import React, { FormEvent, useEffect, useState } from "react";
import { _t } from '../../../../languageHandler';
import AccessibleButton from '../../elements/AccessibleButton';
import Field from '../../elements/Field';
import LearnMore from '../../elements/LearnMore';
import Spinner from '../../elements/Spinner';
import { Caption } from '../../typography/Caption';
import Heading from '../../typography/Heading';
import { ExtendedDevice } from './types';
import { _t } from "../../../../languageHandler";
import AccessibleButton from "../../elements/AccessibleButton";
import Field from "../../elements/Field";
import LearnMore from "../../elements/LearnMore";
import Spinner from "../../elements/Spinner";
import { Caption } from "../../typography/Caption";
import Heading from "../../typography/Heading";
import { ExtendedDevice } from "./types";
interface Props {
device: ExtendedDevice;
saveDeviceName: (deviceName: string) => Promise<void>;
}
const DeviceNameEditor: React.FC<Props & { stopEditing: () => void }> = ({
device, saveDeviceName, stopEditing,
}) => {
const [deviceName, setDeviceName] = useState(device.display_name || '');
const DeviceNameEditor: React.FC<Props & { stopEditing: () => void }> = ({ device, saveDeviceName, stopEditing }) => {
const [deviceName, setDeviceName] = useState(device.display_name || "");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
setDeviceName(device.display_name || '');
setDeviceName(device.display_name || "");
}, [device.display_name]);
const onInputChange = (event: React.ChangeEvent<HTMLInputElement>): void =>
setDeviceName(event.target.value);
const onInputChange = (event: React.ChangeEvent<HTMLInputElement>): void => setDeviceName(event.target.value);
const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
setIsLoading(true);
@ -52,7 +49,7 @@ const DeviceNameEditor: React.FC<Props & { stopEditing: () => void }> = ({
await saveDeviceName(deviceName);
stopEditing();
} catch (error) {
setError(_t('Failed to set display name'));
setError(_t("Failed to set display name"));
setIsLoading(false);
}
};
@ -60,102 +57,92 @@ const DeviceNameEditor: React.FC<Props & { stopEditing: () => void }> = ({
const headingId = `device-rename-${device.device_id}`;
const descriptionId = `device-rename-description-${device.device_id}`;
return <form
aria-disabled={isLoading}
className="mx_DeviceDetailHeading_renameForm"
onSubmit={onSubmit}
method="post"
>
<p
id={headingId}
className="mx_DeviceDetailHeading_renameFormHeading"
>
{ _t('Rename session') }
</p>
<div>
<Field
data-testid='device-rename-input'
type="text"
value={deviceName}
autoComplete="off"
onChange={onInputChange}
autoFocus
disabled={isLoading}
aria-labelledby={headingId}
aria-describedby={descriptionId}
className="mx_DeviceDetailHeading_renameFormInput"
maxLength={100}
/>
<Caption
id={descriptionId}
>
{ _t('Please be aware that session names are also visible to people you communicate with.') }
<LearnMore
title={_t('Renaming sessions')}
description={<>
<p>
{ _t(`Other users in direct messages and rooms that you join ` +
`are able to view a full list of your sessions.`,
) }
</p>
<p>
{ _t(`This provides them with confidence that they are really speaking to you, ` +
`but it also means they can see the session name you enter here.`,
) }
</p>
</>}
return (
<form aria-disabled={isLoading} className="mx_DeviceDetailHeading_renameForm" onSubmit={onSubmit} method="post">
<p id={headingId} className="mx_DeviceDetailHeading_renameFormHeading">
{_t("Rename session")}
</p>
<div>
<Field
data-testid="device-rename-input"
type="text"
value={deviceName}
autoComplete="off"
onChange={onInputChange}
autoFocus
disabled={isLoading}
aria-labelledby={headingId}
aria-describedby={descriptionId}
className="mx_DeviceDetailHeading_renameFormInput"
maxLength={100}
/>
{ !!error &&
<span
data-testid="device-rename-error"
className='mx_DeviceDetailHeading_renameFormError'>
{ error }
</span>
}
</Caption>
</div>
<div className="mx_DeviceDetailHeading_renameFormButtons">
<AccessibleButton
onClick={onSubmit}
kind="primary"
data-testid='device-rename-submit-cta'
disabled={isLoading}
>
{ _t('Save') }
</AccessibleButton>
<AccessibleButton
onClick={stopEditing}
kind="secondary"
data-testid='device-rename-cancel-cta'
disabled={isLoading}
>
{ _t('Cancel') }
</AccessibleButton>
{ isLoading && <Spinner w={16} h={16} /> }
</div>
</form>;
<Caption id={descriptionId}>
{_t("Please be aware that session names are also visible to people you communicate with.")}
<LearnMore
title={_t("Renaming sessions")}
description={
<>
<p>
{_t(
`Other users in direct messages and rooms that you join ` +
`are able to view a full list of your sessions.`,
)}
</p>
<p>
{_t(
`This provides them with confidence that they are really speaking to you, ` +
`but it also means they can see the session name you enter here.`,
)}
</p>
</>
}
/>
{!!error && (
<span data-testid="device-rename-error" className="mx_DeviceDetailHeading_renameFormError">
{error}
</span>
)}
</Caption>
</div>
<div className="mx_DeviceDetailHeading_renameFormButtons">
<AccessibleButton
onClick={onSubmit}
kind="primary"
data-testid="device-rename-submit-cta"
disabled={isLoading}
>
{_t("Save")}
</AccessibleButton>
<AccessibleButton
onClick={stopEditing}
kind="secondary"
data-testid="device-rename-cancel-cta"
disabled={isLoading}
>
{_t("Cancel")}
</AccessibleButton>
{isLoading && <Spinner w={16} h={16} />}
</div>
</form>
);
};
export const DeviceDetailHeading: React.FC<Props> = ({
device, saveDeviceName,
}) => {
export const DeviceDetailHeading: React.FC<Props> = ({ device, saveDeviceName }) => {
const [isEditing, setIsEditing] = useState(false);
return isEditing
? <DeviceNameEditor
device={device}
saveDeviceName={saveDeviceName}
stopEditing={() => setIsEditing(false)}
/>
: <div className='mx_DeviceDetailHeading' data-testid='device-detail-heading'>
<Heading size='h3'>{ device.display_name || device.device_id }</Heading>
return isEditing ? (
<DeviceNameEditor device={device} saveDeviceName={saveDeviceName} stopEditing={() => setIsEditing(false)} />
) : (
<div className="mx_DeviceDetailHeading" data-testid="device-detail-heading">
<Heading size="h3">{device.display_name || device.device_id}</Heading>
<AccessibleButton
kind='link_inline'
kind="link_inline"
onClick={() => setIsEditing(true)}
className='mx_DeviceDetailHeading_renameCta'
data-testid='device-heading-rename-cta'
className="mx_DeviceDetailHeading_renameCta"
data-testid="device-heading-rename-cta"
>
{ _t('Rename') }
{_t("Rename")}
</AccessibleButton>
</div>;
</div>
);
};

View file

@ -14,19 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { IPusher } from 'matrix-js-sdk/src/@types/PushRules';
import { PUSHER_ENABLED } from 'matrix-js-sdk/src/@types/event';
import { LocalNotificationSettings } from 'matrix-js-sdk/src/@types/local_notifications';
import React from "react";
import { IPusher } from "matrix-js-sdk/src/@types/PushRules";
import { PUSHER_ENABLED } from "matrix-js-sdk/src/@types/event";
import { LocalNotificationSettings } from "matrix-js-sdk/src/@types/local_notifications";
import { formatDate } from '../../../../DateUtils';
import { _t } from '../../../../languageHandler';
import AccessibleButton from '../../elements/AccessibleButton';
import Spinner from '../../elements/Spinner';
import ToggleSwitch from '../../elements/ToggleSwitch';
import { DeviceDetailHeading } from './DeviceDetailHeading';
import { DeviceVerificationStatusCard } from './DeviceVerificationStatusCard';
import { ExtendedDevice } from './types';
import { formatDate } from "../../../../DateUtils";
import { _t } from "../../../../languageHandler";
import AccessibleButton from "../../elements/AccessibleButton";
import Spinner from "../../elements/Spinner";
import ToggleSwitch from "../../elements/ToggleSwitch";
import { DeviceDetailHeading } from "./DeviceDetailHeading";
import { DeviceVerificationStatusCard } from "./DeviceVerificationStatusCard";
import { ExtendedDevice } from "./types";
interface Props {
device: ExtendedDevice;
@ -43,7 +43,7 @@ interface Props {
interface MetadataTable {
id: string;
heading?: string;
values: { label: string, value?: string | React.ReactNode }[];
values: { label: string; value?: string | React.ReactNode }[];
}
const DeviceDetails: React.FC<Props> = ({
@ -59,40 +59,43 @@ const DeviceDetails: React.FC<Props> = ({
}) => {
const metadata: MetadataTable[] = [
{
id: 'session',
id: "session",
values: [
{ label: _t('Session ID'), value: device.device_id },
{ label: _t("Session ID"), value: device.device_id },
{
label: _t('Last activity'),
label: _t("Last activity"),
value: device.last_seen_ts && formatDate(new Date(device.last_seen_ts)),
},
],
},
{
id: 'application',
heading: _t('Application'),
id: "application",
heading: _t("Application"),
values: [
{ label: _t('Name'), value: device.appName },
{ label: _t('Version'), value: device.appVersion },
{ label: _t('URL'), value: device.url },
{ label: _t("Name"), value: device.appName },
{ label: _t("Version"), value: device.appVersion },
{ label: _t("URL"), value: device.url },
],
},
{
id: 'device',
heading: _t('Device'),
id: "device",
heading: _t("Device"),
values: [
{ label: _t('Model'), value: device.deviceModel },
{ label: _t('Operating system'), value: device.deviceOperatingSystem },
{ label: _t('Browser'), value: device.client },
{ label: _t('IP address'), value: device.last_seen_ip },
{ label: _t("Model"), value: device.deviceModel },
{ label: _t("Operating system"), value: device.deviceOperatingSystem },
{ label: _t("Browser"), value: device.client },
{ label: _t("IP address"), value: device.last_seen_ip },
],
},
].map(section =>
// filter out falsy values
({ ...section, values: section.values.filter(row => !!row.value) }))
.filter(section =>
// then filter out sections with no values
section.values.length,
]
.map((section) =>
// filter out falsy values
({ ...section, values: section.values.filter((row) => !!row.value) }),
)
.filter(
(section) =>
// then filter out sections with no values
section.values.length,
);
const showPushNotificationSection = !!pusher || !!localNotificationSettings;
@ -109,75 +112,75 @@ const DeviceDetails: React.FC<Props> = ({
return false;
}
return <div className='mx_DeviceDetails' data-testid={`device-detail-${device.device_id}`}>
<section className='mx_DeviceDetails_section'>
<DeviceDetailHeading
device={device}
saveDeviceName={saveDeviceName}
/>
<DeviceVerificationStatusCard
device={device}
onVerifyDevice={onVerifyDevice}
/>
</section>
<section className='mx_DeviceDetails_section'>
<p className='mx_DeviceDetails_sectionHeading'>{ _t('Session details') }</p>
{ metadata.map(({ heading, values, id }, index) => <table
className='mx_DeviceDetails_metadataTable'
key={index}
data-testid={`device-detail-metadata-${id}`}
>
{ heading &&
<thead>
<tr><th>{ heading }</th></tr>
</thead>
}
<tbody>
{ values.map(({ label, value }) => <tr key={label}>
<td className='mxDeviceDetails_metadataLabel'>{ label }</td>
<td className='mxDeviceDetails_metadataValue'>{ value }</td>
</tr>) }
</tbody>
</table>,
) }
</section>
{ showPushNotificationSection && (
<section
className='mx_DeviceDetails_section mx_DeviceDetails_pushNotifications'
data-testid='device-detail-push-notification'
>
<ToggleSwitch
// For backwards compatibility, if `enabled` is missing
// default to `true`
checked={isPushNotificationsEnabled(pusher, localNotificationSettings)}
disabled={isCheckboxDisabled(pusher, localNotificationSettings)}
onChange={checked => setPushNotifications?.(device.device_id, checked)}
title={_t("Toggle push notifications on this session.")}
data-testid='device-detail-push-notification-checkbox'
/>
<p className='mx_DeviceDetails_sectionHeading'>
{ _t('Push notifications') }
<small className='mx_DeviceDetails_sectionSubheading'>
{ _t('Receive push notifications on this session.') }
</small>
</p>
return (
<div className="mx_DeviceDetails" data-testid={`device-detail-${device.device_id}`}>
<section className="mx_DeviceDetails_section">
<DeviceDetailHeading device={device} saveDeviceName={saveDeviceName} />
<DeviceVerificationStatusCard device={device} onVerifyDevice={onVerifyDevice} />
</section>
) }
<section className='mx_DeviceDetails_section'>
<AccessibleButton
onClick={onSignOutDevice}
kind='danger_inline'
disabled={isSigningOut}
data-testid='device-detail-sign-out-cta'
>
<span className='mx_DeviceDetails_signOutButtonContent'>
{ _t('Sign out of this session') }
{ isSigningOut && <Spinner w={16} h={16} /> }
</span>
</AccessibleButton>
</section>
</div>;
<section className="mx_DeviceDetails_section">
<p className="mx_DeviceDetails_sectionHeading">{_t("Session details")}</p>
{metadata.map(({ heading, values, id }, index) => (
<table
className="mx_DeviceDetails_metadataTable"
key={index}
data-testid={`device-detail-metadata-${id}`}
>
{heading && (
<thead>
<tr>
<th>{heading}</th>
</tr>
</thead>
)}
<tbody>
{values.map(({ label, value }) => (
<tr key={label}>
<td className="mxDeviceDetails_metadataLabel">{label}</td>
<td className="mxDeviceDetails_metadataValue">{value}</td>
</tr>
))}
</tbody>
</table>
))}
</section>
{showPushNotificationSection && (
<section
className="mx_DeviceDetails_section mx_DeviceDetails_pushNotifications"
data-testid="device-detail-push-notification"
>
<ToggleSwitch
// For backwards compatibility, if `enabled` is missing
// default to `true`
checked={isPushNotificationsEnabled(pusher, localNotificationSettings)}
disabled={isCheckboxDisabled(pusher, localNotificationSettings)}
onChange={(checked) => setPushNotifications?.(device.device_id, checked)}
title={_t("Toggle push notifications on this session.")}
data-testid="device-detail-push-notification-checkbox"
/>
<p className="mx_DeviceDetails_sectionHeading">
{_t("Push notifications")}
<small className="mx_DeviceDetails_sectionSubheading">
{_t("Receive push notifications on this session.")}
</small>
</p>
</section>
)}
<section className="mx_DeviceDetails_section">
<AccessibleButton
onClick={onSignOutDevice}
kind="danger_inline"
disabled={isSigningOut}
data-testid="device-detail-sign-out-cta"
>
<span className="mx_DeviceDetails_signOutButtonContent">
{_t("Sign out of this session")}
{isSigningOut && <Spinner w={16} h={16} />}
</span>
</AccessibleButton>
</section>
</div>
);
};
export default DeviceDetails;

View file

@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import classNames from 'classnames';
import React from 'react';
import classNames from "classnames";
import React from "react";
import { Icon as CaretIcon } from '../../../../../res/img/feather-customised/dropdown-arrow.svg';
import { _t } from '../../../../languageHandler';
import AccessibleTooltipButton from '../../elements/AccessibleTooltipButton';
import { Icon as CaretIcon } from "../../../../../res/img/feather-customised/dropdown-arrow.svg";
import { _t } from "../../../../languageHandler";
import AccessibleTooltipButton from "../../elements/AccessibleTooltipButton";
interface Props extends React.ComponentProps<typeof AccessibleTooltipButton> {
isExpanded: boolean;
@ -27,17 +27,19 @@ interface Props extends React.ComponentProps<typeof AccessibleTooltipButton> {
}
export const DeviceExpandDetailsButton: React.FC<Props> = ({ isExpanded, onClick, ...rest }) => {
const label = isExpanded ? _t('Hide details') : _t('Show details');
return <AccessibleTooltipButton
{...rest}
aria-label={label}
title={label}
kind='icon'
className={classNames('mx_DeviceExpandDetailsButton', {
mx_DeviceExpandDetailsButton_expanded: isExpanded,
})}
onClick={onClick}
>
<CaretIcon className='mx_DeviceExpandDetailsButton_icon' />
</AccessibleTooltipButton>;
const label = isExpanded ? _t("Hide details") : _t("Show details");
return (
<AccessibleTooltipButton
{...rest}
aria-label={label}
title={label}
kind="icon"
className={classNames("mx_DeviceExpandDetailsButton", {
mx_DeviceExpandDetailsButton_expanded: isExpanded,
})}
onClick={onClick}
>
<CaretIcon className="mx_DeviceExpandDetailsButton_icon" />
</AccessibleTooltipButton>
);
};

View file

@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import classNames from 'classnames';
import React from 'react';
import classNames from "classnames";
import React from "react";
import { Icon as VerifiedIcon } from '../../../../../res/img/e2e/verified.svg';
import { Icon as UnverifiedIcon } from '../../../../../res/img/e2e/warning.svg';
import { Icon as InactiveIcon } from '../../../../../res/img/element-icons/settings/inactive.svg';
import { DeviceSecurityVariation } from './types';
import { Icon as VerifiedIcon } from "../../../../../res/img/e2e/verified.svg";
import { Icon as UnverifiedIcon } from "../../../../../res/img/e2e/warning.svg";
import { Icon as InactiveIcon } from "../../../../../res/img/element-icons/settings/inactive.svg";
import { DeviceSecurityVariation } from "./types";
interface Props {
variation: DeviceSecurityVariation;
heading: string;
@ -37,22 +37,24 @@ const VariationIcon: Record<DeviceSecurityVariation, React.FC<React.SVGProps<SVG
const DeviceSecurityIcon: React.FC<{ variation: DeviceSecurityVariation }> = ({ variation }) => {
const Icon = VariationIcon[variation];
return <div className={classNames('mx_DeviceSecurityCard_icon', variation)}>
<Icon height={16} width={16} />
</div>;
return (
<div className={classNames("mx_DeviceSecurityCard_icon", variation)}>
<Icon height={16} width={16} />
</div>
);
};
const DeviceSecurityCard: React.FC<Props> = ({ variation, heading, description, children }) => {
return <div className='mx_DeviceSecurityCard'>
<DeviceSecurityIcon variation={variation} />
<div className='mx_DeviceSecurityCard_content'>
<p className='mx_DeviceSecurityCard_heading'>{ heading }</p>
<p className='mx_DeviceSecurityCard_description'>{ description }</p>
{ !!children && <div className='mx_DeviceSecurityCard_actions'>
{ children }
</div> }
return (
<div className="mx_DeviceSecurityCard">
<DeviceSecurityIcon variation={variation} />
<div className="mx_DeviceSecurityCard_content">
<p className="mx_DeviceSecurityCard_heading">{heading}</p>
<p className="mx_DeviceSecurityCard_description">{description}</p>
{!!children && <div className="mx_DeviceSecurityCard_actions">{children}</div>}
</div>
</div>
</div>;
);
};
export default DeviceSecurityCard;

View file

@ -14,83 +14,98 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { _t } from "../../../../languageHandler";
import LearnMore, { LearnMoreProps } from "../../elements/LearnMore";
import { DeviceSecurityVariation } from "./types";
interface Props extends Omit<LearnMoreProps, 'title' | 'description'> {
interface Props extends Omit<LearnMoreProps, "title" | "description"> {
variation: DeviceSecurityVariation;
}
const securityCardContent: Record<DeviceSecurityVariation, {
title: string;
description: React.ReactNode | string;
}> = {
[DeviceSecurityVariation.Verified]: {
title: _t('Verified sessions'),
description: <>
<p>{ _t('Verified sessions are anywhere you are using this account after entering your passphrase or confirming your identity with another verified session.') }
</p>
<p>
{ _t(
`This means that you have all the keys needed to unlock your encrypted messages ` +
`and confirm to other users that you trust this session.`,
)
}
</p>
</>,
},
[DeviceSecurityVariation.Unverified]: {
title: _t('Unverified sessions'),
description: <>
<p>{ _t('Unverified sessions are sessions that have logged in with your credentials but have not been cross-verified.') }
</p>
<p>
{ _t(
`You should make especially certain that you recognise these sessions ` +
`as they could represent an unauthorised use of your account.`,
)
}
</p>
</>,
},
// unverifiable uses single-session case
// because it is only ever displayed on a single session detail
[DeviceSecurityVariation.Unverifiable]: {
title: _t('Unverified session'),
description: <>
<p>{ _t(`This session doesn't support encryption, so it can't be verified.`) }
</p>
<p>
{ _t(
`You won't be able to participate in rooms where encryption is enabled when using this session.`,
)
}
</p><p>
{ _t(
`For best security and privacy, it is recommended to use Matrix clients that support encryption.`,
)
}
</p>
</>,
},
[DeviceSecurityVariation.Inactive]: {
title: _t('Inactive sessions'),
description: <>
<p>{ _t('Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.') }
</p>
<p>
{ _t(
`Removing inactive sessions improves security and performance, ` +
`and makes it easier for you to identify if a new session is suspicious.`,
)
}
</p>
</>,
},
};
const securityCardContent: Record<
DeviceSecurityVariation,
{
title: string;
description: React.ReactNode | string;
}
> = {
[DeviceSecurityVariation.Verified]: {
title: _t("Verified sessions"),
description: (
<>
<p>
{_t(
"Verified sessions are anywhere you are using this account after entering your passphrase or confirming your identity with another verified session.",
)}
</p>
<p>
{_t(
`This means that you have all the keys needed to unlock your encrypted messages ` +
`and confirm to other users that you trust this session.`,
)}
</p>
</>
),
},
[DeviceSecurityVariation.Unverified]: {
title: _t("Unverified sessions"),
description: (
<>
<p>
{_t(
"Unverified sessions are sessions that have logged in with your credentials but have not been cross-verified.",
)}
</p>
<p>
{_t(
`You should make especially certain that you recognise these sessions ` +
`as they could represent an unauthorised use of your account.`,
)}
</p>
</>
),
},
// unverifiable uses single-session case
// because it is only ever displayed on a single session detail
[DeviceSecurityVariation.Unverifiable]: {
title: _t("Unverified session"),
description: (
<>
<p>{_t(`This session doesn't support encryption, so it can't be verified.`)}</p>
<p>
{_t(
`You won't be able to participate in rooms where encryption is enabled when using this session.`,
)}
</p>
<p>
{_t(
`For best security and privacy, it is recommended to use Matrix clients that support encryption.`,
)}
</p>
</>
),
},
[DeviceSecurityVariation.Inactive]: {
title: _t("Inactive sessions"),
description: (
<>
<p>
{_t(
"Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.",
)}
</p>
<p>
{_t(
`Removing inactive sessions improves security and performance, ` +
`and makes it easier for you to identify if a new session is suspicious.`,
)}
</p>
</>
),
},
};
/**
* LearnMore with content for device security warnings

View file

@ -17,7 +17,7 @@ limitations under the License.
import React, { Fragment } from "react";
import classNames from "classnames";
import { Icon as InactiveIcon } from '../../../../../res/img/element-icons/settings/inactive.svg';
import { Icon as InactiveIcon } from "../../../../../res/img/element-icons/settings/inactive.svg";
import { _t } from "../../../../languageHandler";
import { formatDate, formatRelativeTime } from "../../../../DateUtils";
import Heading from "../../typography/Heading";
@ -33,9 +33,7 @@ export interface DeviceTileProps {
}
const DeviceTileName: React.FC<{ device: ExtendedDevice }> = ({ device }) => {
return <Heading size='h4'>
{ device.display_name || device.device_id }
</Heading>;
return <Heading size="h4">{device.display_name || device.device_id}</Heading>;
};
const MS_DAY = 24 * 60 * 60 * 1000;
@ -50,76 +48,66 @@ const formatLastActivity = (timestamp: number, now = new Date().getTime()): stri
return formatRelativeTime(new Date(timestamp));
};
const getInactiveMetadata = (device: ExtendedDevice): { id: string, value: React.ReactNode } | undefined => {
const getInactiveMetadata = (device: ExtendedDevice): { id: string; value: React.ReactNode } | undefined => {
const isInactive = isDeviceInactive(device);
if (!isInactive) {
return undefined;
}
return { id: 'inactive', value: (
<>
<InactiveIcon className="mx_DeviceTile_inactiveIcon" />
{
_t('Inactive for %(inactiveAgeDays)s+ days', { inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS }) +
` (${formatLastActivity(device.last_seen_ts)})`
}
</>),
return {
id: "inactive",
value: (
<>
<InactiveIcon className="mx_DeviceTile_inactiveIcon" />
{_t("Inactive for %(inactiveAgeDays)s+ days", { inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS }) +
` (${formatLastActivity(device.last_seen_ts)})`}
</>
),
};
};
const DeviceMetadata: React.FC<{ value: string | React.ReactNode, id: string }> = ({ value, id }) => (
value ? <span data-testid={`device-metadata-${id}`}>{ value }</span> : null
);
const DeviceMetadata: React.FC<{ value: string | React.ReactNode; id: string }> = ({ value, id }) =>
value ? <span data-testid={`device-metadata-${id}`}>{value}</span> : null;
const DeviceTile: React.FC<DeviceTileProps> = ({
device,
children,
isSelected,
onClick,
}) => {
const DeviceTile: React.FC<DeviceTileProps> = ({ device, children, isSelected, onClick }) => {
const inactive = getInactiveMetadata(device);
const lastActivity = device.last_seen_ts && `${_t('Last activity')} ${formatLastActivity(device.last_seen_ts)}`;
const verificationStatus = device.isVerified ? _t('Verified') : _t('Unverified');
const lastActivity = device.last_seen_ts && `${_t("Last activity")} ${formatLastActivity(device.last_seen_ts)}`;
const verificationStatus = device.isVerified ? _t("Verified") : _t("Unverified");
// if device is inactive, don't display last activity or verificationStatus
const metadata = inactive
? [inactive, { id: 'lastSeenIp', value: device.last_seen_ip }]
? [inactive, { id: "lastSeenIp", value: device.last_seen_ip }]
: [
{ id: 'isVerified', value: verificationStatus },
{ id: 'lastActivity', value: lastActivity },
{ id: 'lastSeenIp', value: device.last_seen_ip },
{ id: 'deviceId', value: device.device_id },
];
{ id: "isVerified", value: verificationStatus },
{ id: "lastActivity", value: lastActivity },
{ id: "lastSeenIp", value: device.last_seen_ip },
{ id: "deviceId", value: device.device_id },
];
return <div
className={classNames(
"mx_DeviceTile",
{ "mx_DeviceTile_interactive": !!onClick },
)}
data-testid={`device-tile-${device.device_id}`}
onClick={onClick}
>
<DeviceTypeIcon
isVerified={device.isVerified}
isSelected={isSelected}
deviceType={device.deviceType}
/>
<div className="mx_DeviceTile_info">
<DeviceTileName device={device} />
<div className="mx_DeviceTile_metadata">
{ metadata.map(({ id, value }, index) =>
!!value
? <Fragment key={id}>
{ !!index && ' · ' }
<DeviceMetadata id={id} value={value} />
</Fragment>
: null,
) }
return (
<div
className={classNames("mx_DeviceTile", { mx_DeviceTile_interactive: !!onClick })}
data-testid={`device-tile-${device.device_id}`}
onClick={onClick}
>
<DeviceTypeIcon isVerified={device.isVerified} isSelected={isSelected} deviceType={device.deviceType} />
<div className="mx_DeviceTile_info">
<DeviceTileName device={device} />
<div className="mx_DeviceTile_metadata">
{metadata.map(({ id, value }, index) =>
!!value ? (
<Fragment key={id}>
{!!index && " · "}
<DeviceMetadata id={id} value={value} />
</Fragment>
) : null,
)}
</div>
</div>
<div className="mx_DeviceTile_actions" onClick={preventDefaultWrapper(() => {})}>
{children}
</div>
</div>
<div className="mx_DeviceTile_actions" onClick={preventDefaultWrapper(() => {})}>
{ children }
</div>
</div>;
);
};
export default DeviceTile;

View file

@ -14,21 +14,21 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import classNames from 'classnames';
import React from "react";
import classNames from "classnames";
import { Icon as UnknownDeviceIcon } from '../../../../../res/img/element-icons/settings/unknown-device.svg';
import { Icon as DesktopIcon } from '../../../../../res/img/element-icons/settings/desktop.svg';
import { Icon as WebIcon } from '../../../../../res/img/element-icons/settings/web.svg';
import { Icon as MobileIcon } from '../../../../../res/img/element-icons/settings/mobile.svg';
import { Icon as VerifiedIcon } from '../../../../../res/img/e2e/verified.svg';
import { Icon as UnverifiedIcon } from '../../../../../res/img/e2e/warning.svg';
import { _t } from '../../../../languageHandler';
import { ExtendedDevice } from './types';
import { DeviceType } from '../../../../utils/device/parseUserAgent';
import { Icon as UnknownDeviceIcon } from "../../../../../res/img/element-icons/settings/unknown-device.svg";
import { Icon as DesktopIcon } from "../../../../../res/img/element-icons/settings/desktop.svg";
import { Icon as WebIcon } from "../../../../../res/img/element-icons/settings/web.svg";
import { Icon as MobileIcon } from "../../../../../res/img/element-icons/settings/mobile.svg";
import { Icon as VerifiedIcon } from "../../../../../res/img/e2e/verified.svg";
import { Icon as UnverifiedIcon } from "../../../../../res/img/e2e/warning.svg";
import { _t } from "../../../../languageHandler";
import { ExtendedDevice } from "./types";
import { DeviceType } from "../../../../utils/device/parseUserAgent";
interface Props {
isVerified?: ExtendedDevice['isVerified'];
isVerified?: ExtendedDevice["isVerified"];
isSelected?: boolean;
deviceType?: DeviceType;
}
@ -40,44 +40,37 @@ const deviceTypeIcon: Record<DeviceType, React.FC<React.SVGProps<SVGSVGElement>>
[DeviceType.Unknown]: UnknownDeviceIcon,
};
const deviceTypeLabel: Record<DeviceType, string> = {
[DeviceType.Desktop]: _t('Desktop session'),
[DeviceType.Mobile]: _t('Mobile session'),
[DeviceType.Web]: _t('Web session'),
[DeviceType.Unknown]: _t('Unknown session type'),
[DeviceType.Desktop]: _t("Desktop session"),
[DeviceType.Mobile]: _t("Mobile session"),
[DeviceType.Web]: _t("Web session"),
[DeviceType.Unknown]: _t("Unknown session type"),
};
export const DeviceTypeIcon: React.FC<Props> = ({
isVerified,
isSelected,
deviceType,
}) => {
export const DeviceTypeIcon: React.FC<Props> = ({ isVerified, isSelected, deviceType }) => {
const Icon = deviceTypeIcon[deviceType] || deviceTypeIcon[DeviceType.Unknown];
const label = deviceTypeLabel[deviceType] || deviceTypeLabel[DeviceType.Unknown];
return (
<div className={classNames('mx_DeviceTypeIcon', {
mx_DeviceTypeIcon_selected: isSelected,
})}
<div
className={classNames("mx_DeviceTypeIcon", {
mx_DeviceTypeIcon_selected: isSelected,
})}
>
<div className='mx_DeviceTypeIcon_deviceIconWrapper'>
<Icon
className='mx_DeviceTypeIcon_deviceIcon'
role='img'
aria-label={label}
/>
<div className="mx_DeviceTypeIcon_deviceIconWrapper">
<Icon className="mx_DeviceTypeIcon_deviceIcon" role="img" aria-label={label} />
</div>
{
isVerified
? <VerifiedIcon
className={classNames('mx_DeviceTypeIcon_verificationIcon', 'verified')}
role='img'
aria-label={_t('Verified')}
/>
: <UnverifiedIcon
className={classNames('mx_DeviceTypeIcon_verificationIcon', 'unverified')}
role='img'
aria-label={_t('Unverified')}
/>
}
</div>);
{isVerified ? (
<VerifiedIcon
className={classNames("mx_DeviceTypeIcon_verificationIcon", "verified")}
role="img"
aria-label={_t("Verified")}
/>
) : (
<UnverifiedIcon
className={classNames("mx_DeviceTypeIcon_verificationIcon", "unverified")}
role="img"
aria-label={_t("Unverified")}
/>
)}
</div>
);
};

View file

@ -14,23 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { _t } from '../../../../languageHandler';
import AccessibleButton from '../../elements/AccessibleButton';
import DeviceSecurityCard from './DeviceSecurityCard';
import { DeviceSecurityLearnMore } from './DeviceSecurityLearnMore';
import {
DeviceSecurityVariation,
ExtendedDevice,
} from './types';
import { _t } from "../../../../languageHandler";
import AccessibleButton from "../../elements/AccessibleButton";
import DeviceSecurityCard from "./DeviceSecurityCard";
import { DeviceSecurityLearnMore } from "./DeviceSecurityLearnMore";
import { DeviceSecurityVariation, ExtendedDevice } from "./types";
interface Props {
device: ExtendedDevice;
onVerifyDevice?: () => void;
}
const getCardProps = (device: ExtendedDevice): {
const getCardProps = (
device: ExtendedDevice,
): {
variation: DeviceSecurityVariation;
heading: string;
description: React.ReactNode;
@ -38,52 +37,55 @@ const getCardProps = (device: ExtendedDevice): {
if (device.isVerified) {
return {
variation: DeviceSecurityVariation.Verified,
heading: _t('Verified session'),
description: <>
{ _t('This session is ready for secure messaging.') }
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Verified} />
</>,
heading: _t("Verified session"),
description: (
<>
{_t("This session is ready for secure messaging.")}
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Verified} />
</>
),
};
}
if (device.isVerified === null) {
return {
variation: DeviceSecurityVariation.Unverified,
heading: _t('Unverified session'),
description: <>
{ _t(`This session doesn't support encryption and thus can't be verified.`) }
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Unverifiable} />
</>,
heading: _t("Unverified session"),
description: (
<>
{_t(`This session doesn't support encryption and thus can't be verified.`)}
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Unverifiable} />
</>
),
};
}
return {
variation: DeviceSecurityVariation.Unverified,
heading: _t('Unverified session'),
description: <>
{ _t('Verify or sign out from this session for best security and reliability.') }
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Unverified} />
</>,
heading: _t("Unverified session"),
description: (
<>
{_t("Verify or sign out from this session for best security and reliability.")}
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Unverified} />
</>
),
};
};
export const DeviceVerificationStatusCard: React.FC<Props> = ({
device,
onVerifyDevice,
}) => {
export const DeviceVerificationStatusCard: React.FC<Props> = ({ device, onVerifyDevice }) => {
const securityCardProps = getCardProps(device);
return <DeviceSecurityCard
{...securityCardProps}
>
{ /* check for explicit false to exclude unverifiable devices */ }
{ device.isVerified === false && !!onVerifyDevice &&
<AccessibleButton
kind='primary'
onClick={onVerifyDevice}
data-testid={`verification-status-button-${device.device_id}`}
>
{ _t('Verify session') }
</AccessibleButton>
}
</DeviceSecurityCard>;
return (
<DeviceSecurityCard {...securityCardProps}>
{/* check for explicit false to exclude unverifiable devices */}
{device.isVerified === false && !!onVerifyDevice && (
<AccessibleButton
kind="primary"
onClick={onVerifyDevice}
data-testid={`verification-status-button-${device.device_id}`}
>
{_t("Verify session")}
</AccessibleButton>
)}
</DeviceSecurityCard>
);
};

View file

@ -14,122 +14,117 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ForwardedRef, forwardRef } from 'react';
import { IPusher } from 'matrix-js-sdk/src/@types/PushRules';
import { PUSHER_DEVICE_ID } from 'matrix-js-sdk/src/@types/event';
import { LocalNotificationSettings } from 'matrix-js-sdk/src/@types/local_notifications';
import React, { ForwardedRef, forwardRef } from "react";
import { IPusher } from "matrix-js-sdk/src/@types/PushRules";
import { PUSHER_DEVICE_ID } from "matrix-js-sdk/src/@types/event";
import { LocalNotificationSettings } from "matrix-js-sdk/src/@types/local_notifications";
import { _t } from '../../../../languageHandler';
import AccessibleButton from '../../elements/AccessibleButton';
import { FilterDropdown, FilterDropdownOption } from '../../elements/FilterDropdown';
import DeviceDetails from './DeviceDetails';
import { DeviceExpandDetailsButton } from './DeviceExpandDetailsButton';
import DeviceSecurityCard from './DeviceSecurityCard';
import {
filterDevicesBySecurityRecommendation,
FilterVariation,
INACTIVE_DEVICE_AGE_DAYS,
} from './filter';
import SelectableDeviceTile from './SelectableDeviceTile';
import {
DevicesDictionary,
DeviceSecurityVariation,
ExtendedDevice,
} from './types';
import { DevicesState } from './useOwnDevices';
import FilteredDeviceListHeader from './FilteredDeviceListHeader';
import Spinner from '../../elements/Spinner';
import { DeviceSecurityLearnMore } from './DeviceSecurityLearnMore';
import { _t } from "../../../../languageHandler";
import AccessibleButton from "../../elements/AccessibleButton";
import { FilterDropdown, FilterDropdownOption } from "../../elements/FilterDropdown";
import DeviceDetails from "./DeviceDetails";
import { DeviceExpandDetailsButton } from "./DeviceExpandDetailsButton";
import DeviceSecurityCard from "./DeviceSecurityCard";
import { filterDevicesBySecurityRecommendation, FilterVariation, INACTIVE_DEVICE_AGE_DAYS } from "./filter";
import SelectableDeviceTile from "./SelectableDeviceTile";
import { DevicesDictionary, DeviceSecurityVariation, ExtendedDevice } from "./types";
import { DevicesState } from "./useOwnDevices";
import FilteredDeviceListHeader from "./FilteredDeviceListHeader";
import Spinner from "../../elements/Spinner";
import { DeviceSecurityLearnMore } from "./DeviceSecurityLearnMore";
interface Props {
devices: DevicesDictionary;
pushers: IPusher[];
localNotificationSettings: Map<string, LocalNotificationSettings>;
expandedDeviceIds: ExtendedDevice['device_id'][];
signingOutDeviceIds: ExtendedDevice['device_id'][];
selectedDeviceIds: ExtendedDevice['device_id'][];
expandedDeviceIds: ExtendedDevice["device_id"][];
signingOutDeviceIds: ExtendedDevice["device_id"][];
selectedDeviceIds: ExtendedDevice["device_id"][];
filter?: FilterVariation;
onFilterChange: (filter: FilterVariation | undefined) => void;
onDeviceExpandToggle: (deviceId: ExtendedDevice['device_id']) => void;
onSignOutDevices: (deviceIds: ExtendedDevice['device_id'][]) => void;
saveDeviceName: DevicesState['saveDeviceName'];
onRequestDeviceVerification?: (deviceId: ExtendedDevice['device_id']) => void;
onDeviceExpandToggle: (deviceId: ExtendedDevice["device_id"]) => void;
onSignOutDevices: (deviceIds: ExtendedDevice["device_id"][]) => void;
saveDeviceName: DevicesState["saveDeviceName"];
onRequestDeviceVerification?: (deviceId: ExtendedDevice["device_id"]) => void;
setPushNotifications: (deviceId: string, enabled: boolean) => Promise<void>;
setSelectedDeviceIds: (deviceIds: ExtendedDevice['device_id'][]) => void;
setSelectedDeviceIds: (deviceIds: ExtendedDevice["device_id"][]) => void;
supportsMSC3881?: boolean | undefined;
}
const isDeviceSelected = (
deviceId: ExtendedDevice['device_id'],
selectedDeviceIds: ExtendedDevice['device_id'][],
) => selectedDeviceIds.includes(deviceId);
const isDeviceSelected = (deviceId: ExtendedDevice["device_id"], selectedDeviceIds: ExtendedDevice["device_id"][]) =>
selectedDeviceIds.includes(deviceId);
// devices without timestamp metadata should be sorted last
const sortDevicesByLatestActivityThenDisplayName = (left: ExtendedDevice, right: ExtendedDevice) =>
(right.last_seen_ts || 0) - (left.last_seen_ts || 0)
|| ((left.display_name || left.device_id).localeCompare(right.display_name || right.device_id));
(right.last_seen_ts || 0) - (left.last_seen_ts || 0) ||
(left.display_name || left.device_id).localeCompare(right.display_name || right.device_id);
const getFilteredSortedDevices = (devices: DevicesDictionary, filter?: FilterVariation) =>
filterDevicesBySecurityRecommendation(Object.values(devices), filter ? [filter] : [])
.sort(sortDevicesByLatestActivityThenDisplayName);
filterDevicesBySecurityRecommendation(Object.values(devices), filter ? [filter] : []).sort(
sortDevicesByLatestActivityThenDisplayName,
);
const ALL_FILTER_ID = 'ALL';
const ALL_FILTER_ID = "ALL";
type DeviceFilterKey = FilterVariation | typeof ALL_FILTER_ID;
const securityCardContent: Record<DeviceSecurityVariation, {
title: string;
description: string;
}> = {
[DeviceSecurityVariation.Verified]: {
title: _t('Verified sessions'),
description: _t('For best security, sign out from any session that you don\'t recognize or use anymore.'),
},
[DeviceSecurityVariation.Unverified]: {
title: _t('Unverified sessions'),
description: _t(
`Verify your sessions for enhanced secure messaging or ` +
`sign out from those you don't recognize or use anymore.`,
),
},
[DeviceSecurityVariation.Unverifiable]: {
title: _t('Unverified session'),
description: _t(
`This session doesn't support encryption and thus can't be verified.`,
),
},
[DeviceSecurityVariation.Inactive]: {
title: _t('Inactive sessions'),
description: _t(
`Consider signing out from old sessions ` +
`(%(inactiveAgeDays)s days or older) you don't use anymore.`,
{ inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS },
),
},
};
const securityCardContent: Record<
DeviceSecurityVariation,
{
title: string;
description: string;
}
> = {
[DeviceSecurityVariation.Verified]: {
title: _t("Verified sessions"),
description: _t("For best security, sign out from any session that you don't recognize or use anymore."),
},
[DeviceSecurityVariation.Unverified]: {
title: _t("Unverified sessions"),
description: _t(
`Verify your sessions for enhanced secure messaging or ` +
`sign out from those you don't recognize or use anymore.`,
),
},
[DeviceSecurityVariation.Unverifiable]: {
title: _t("Unverified session"),
description: _t(`This session doesn't support encryption and thus can't be verified.`),
},
[DeviceSecurityVariation.Inactive]: {
title: _t("Inactive sessions"),
description: _t(
`Consider signing out from old sessions ` + `(%(inactiveAgeDays)s days or older) you don't use anymore.`,
{ inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS },
),
},
};
const isSecurityVariation = (filter?: DeviceFilterKey): filter is FilterVariation =>
!!filter && ([
DeviceSecurityVariation.Inactive,
DeviceSecurityVariation.Unverified,
DeviceSecurityVariation.Verified,
] as string[]).includes(filter);
!!filter &&
(
[
DeviceSecurityVariation.Inactive,
DeviceSecurityVariation.Unverified,
DeviceSecurityVariation.Verified,
] as string[]
).includes(filter);
const FilterSecurityCard: React.FC<{ filter?: DeviceFilterKey }> = ({ filter }) => {
if (isSecurityVariation(filter)) {
const { title, description } = securityCardContent[filter];
return <div className='mx_FilteredDeviceList_securityCard'>
<DeviceSecurityCard
variation={filter}
heading={title}
description={<span>
{ description }
<DeviceSecurityLearnMore
variation={filter}
/>
</span>}
/>
</div>
;
return (
<div className="mx_FilteredDeviceList_securityCard">
<DeviceSecurityCard
variation={filter}
heading={title}
description={
<span>
{description}
<DeviceSecurityLearnMore variation={filter} />
</span>
}
/>
</div>
);
}
return null;
@ -138,34 +133,35 @@ const FilterSecurityCard: React.FC<{ filter?: DeviceFilterKey }> = ({ filter })
const getNoResultsMessage = (filter?: FilterVariation): string => {
switch (filter) {
case DeviceSecurityVariation.Verified:
return _t('No verified sessions found.');
return _t("No verified sessions found.");
case DeviceSecurityVariation.Unverified:
return _t('No unverified sessions found.');
return _t("No unverified sessions found.");
case DeviceSecurityVariation.Inactive:
return _t('No inactive sessions found.');
return _t("No inactive sessions found.");
default:
return _t('No sessions found.');
return _t("No sessions found.");
}
};
interface NoResultsProps { filter?: FilterVariation, clearFilter: () => void}
const NoResults: React.FC<NoResultsProps> = ({ filter, clearFilter }) =>
<div className='mx_FilteredDeviceList_noResults'>
{ getNoResultsMessage(filter) }
interface NoResultsProps {
filter?: FilterVariation;
clearFilter: () => void;
}
const NoResults: React.FC<NoResultsProps> = ({ filter, clearFilter }) => (
<div className="mx_FilteredDeviceList_noResults">
{getNoResultsMessage(filter)}
{
/* No clear filter button when filter is falsy (ie 'All') */
!!filter &&
<>
&nbsp;
<AccessibleButton
kind='link_inline'
onClick={clearFilter}
data-testid='devices-clear-filter-btn'
>
{ _t('Show all') }
</AccessibleButton>
</>
!!filter && (
<>
&nbsp;
<AccessibleButton kind="link_inline" onClick={clearFilter} data-testid="devices-clear-filter-btn">
{_t("Show all")}
</AccessibleButton>
</>
)
}
</div>;
</div>
);
const DeviceListItem: React.FC<{
device: ExtendedDevice;
@ -195,96 +191,96 @@ const DeviceListItem: React.FC<{
setPushNotifications,
toggleSelected,
supportsMSC3881,
}) => <li className='mx_FilteredDeviceList_listItem'>
<SelectableDeviceTile
isSelected={isSelected}
onSelect={toggleSelected}
onClick={onDeviceExpandToggle}
device={device}
>
{ isSigningOut && <Spinner w={16} h={16} /> }
<DeviceExpandDetailsButton
isExpanded={isExpanded}
}) => (
<li className="mx_FilteredDeviceList_listItem">
<SelectableDeviceTile
isSelected={isSelected}
onSelect={toggleSelected}
onClick={onDeviceExpandToggle}
/>
</SelectableDeviceTile>
{
isExpanded &&
<DeviceDetails
device={device}
pusher={pusher}
localNotificationSettings={localNotificationSettings}
isSigningOut={isSigningOut}
onVerifyDevice={onRequestDeviceVerification}
onSignOutDevice={onSignOutDevice}
saveDeviceName={saveDeviceName}
setPushNotifications={setPushNotifications}
supportsMSC3881={supportsMSC3881}
/>
}
</li>;
>
{isSigningOut && <Spinner w={16} h={16} />}
<DeviceExpandDetailsButton isExpanded={isExpanded} onClick={onDeviceExpandToggle} />
</SelectableDeviceTile>
{isExpanded && (
<DeviceDetails
device={device}
pusher={pusher}
localNotificationSettings={localNotificationSettings}
isSigningOut={isSigningOut}
onVerifyDevice={onRequestDeviceVerification}
onSignOutDevice={onSignOutDevice}
saveDeviceName={saveDeviceName}
setPushNotifications={setPushNotifications}
supportsMSC3881={supportsMSC3881}
/>
)}
</li>
);
/**
* Filtered list of devices
* Sorted by latest activity descending
*/
export const FilteredDeviceList =
forwardRef(({
devices,
pushers,
localNotificationSettings,
filter,
expandedDeviceIds,
signingOutDeviceIds,
selectedDeviceIds,
onFilterChange,
onDeviceExpandToggle,
saveDeviceName,
onSignOutDevices,
onRequestDeviceVerification,
setPushNotifications,
setSelectedDeviceIds,
supportsMSC3881,
}: Props, ref: ForwardedRef<HTMLDivElement>) => {
export const FilteredDeviceList = forwardRef(
(
{
devices,
pushers,
localNotificationSettings,
filter,
expandedDeviceIds,
signingOutDeviceIds,
selectedDeviceIds,
onFilterChange,
onDeviceExpandToggle,
saveDeviceName,
onSignOutDevices,
onRequestDeviceVerification,
setPushNotifications,
setSelectedDeviceIds,
supportsMSC3881,
}: Props,
ref: ForwardedRef<HTMLDivElement>,
) => {
const sortedDevices = getFilteredSortedDevices(devices, filter);
function getPusherForDevice(device: ExtendedDevice): IPusher | undefined {
return pushers.find(pusher => pusher[PUSHER_DEVICE_ID.name] === device.device_id);
return pushers.find((pusher) => pusher[PUSHER_DEVICE_ID.name] === device.device_id);
}
const toggleSelection = (deviceId: ExtendedDevice['device_id']): void => {
const toggleSelection = (deviceId: ExtendedDevice["device_id"]): void => {
if (isDeviceSelected(deviceId, selectedDeviceIds)) {
// remove from selection
setSelectedDeviceIds(selectedDeviceIds.filter(id => id !== deviceId));
setSelectedDeviceIds(selectedDeviceIds.filter((id) => id !== deviceId));
} else {
setSelectedDeviceIds([...selectedDeviceIds, deviceId]);
}
};
const options: FilterDropdownOption<DeviceFilterKey>[] = [
{ id: ALL_FILTER_ID, label: _t('All') },
{ id: ALL_FILTER_ID, label: _t("All") },
{
id: DeviceSecurityVariation.Verified,
label: _t('Verified'),
description: _t('Ready for secure messaging'),
label: _t("Verified"),
description: _t("Ready for secure messaging"),
},
{
id: DeviceSecurityVariation.Unverified,
label: _t('Unverified'),
description: _t('Not ready for secure messaging'),
label: _t("Unverified"),
description: _t("Not ready for secure messaging"),
},
{
id: DeviceSecurityVariation.Inactive,
label: _t('Inactive'),
description: _t(
'Inactive for %(inactiveAgeDays)s days or longer',
{ inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS },
),
label: _t("Inactive"),
description: _t("Inactive for %(inactiveAgeDays)s days or longer", {
inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS,
}),
},
];
const onFilterOptionChange = (filterId: DeviceFilterKey) => {
onFilterChange(filterId === ALL_FILTER_ID ? undefined : filterId as FilterVariation);
onFilterChange(filterId === ALL_FILTER_ID ? undefined : (filterId as FilterVariation));
};
const isAllSelected = selectedDeviceIds.length >= sortedDevices.length;
@ -292,77 +288,82 @@ export const FilteredDeviceList =
if (isAllSelected) {
setSelectedDeviceIds([]);
} else {
setSelectedDeviceIds(sortedDevices.map(device => device.device_id));
setSelectedDeviceIds(sortedDevices.map((device) => device.device_id));
}
};
const isSigningOut = !!signingOutDeviceIds.length;
return <div className='mx_FilteredDeviceList' ref={ref}>
<FilteredDeviceListHeader
selectedDeviceCount={selectedDeviceIds.length}
isAllSelected={isAllSelected}
toggleSelectAll={toggleSelectAll}
>
{ selectedDeviceIds.length
? <>
<AccessibleButton
data-testid='sign-out-selection-cta'
kind='danger_inline'
disabled={isSigningOut}
onClick={() => onSignOutDevices(selectedDeviceIds)}
className='mx_FilteredDeviceList_headerButton'
>
{ isSigningOut && <Spinner w={16} h={16} /> }
{ _t('Sign out') }
</AccessibleButton>
<AccessibleButton
data-testid='cancel-selection-cta'
kind='content_inline'
disabled={isSigningOut}
onClick={() => setSelectedDeviceIds([])}
className='mx_FilteredDeviceList_headerButton'
>
{ _t('Cancel') }
</AccessibleButton>
</>
: <FilterDropdown<DeviceFilterKey>
id='device-list-filter'
label={_t('Filter devices')}
value={filter || ALL_FILTER_ID}
onOptionChange={onFilterOptionChange}
options={options}
selectedLabel={_t('Show')}
/>
}
</FilteredDeviceListHeader>
{ !!sortedDevices.length
? <FilterSecurityCard filter={filter} />
: <NoResults filter={filter} clearFilter={() => onFilterChange(undefined)} />
}
<ol className='mx_FilteredDeviceList_list'>
{ sortedDevices.map((device) => <DeviceListItem
key={device.device_id}
device={device}
pusher={getPusherForDevice(device)}
localNotificationSettings={localNotificationSettings.get(device.device_id)}
isExpanded={expandedDeviceIds.includes(device.device_id)}
isSigningOut={signingOutDeviceIds.includes(device.device_id)}
isSelected={isDeviceSelected(device.device_id, selectedDeviceIds)}
onDeviceExpandToggle={() => onDeviceExpandToggle(device.device_id)}
onSignOutDevice={() => onSignOutDevices([device.device_id])}
saveDeviceName={(deviceName: string) => saveDeviceName(device.device_id, deviceName)}
onRequestDeviceVerification={
onRequestDeviceVerification
? () => onRequestDeviceVerification(device.device_id)
: undefined
}
setPushNotifications={setPushNotifications}
toggleSelected={() => toggleSelection(device.device_id)}
supportsMSC3881={supportsMSC3881}
/>,
) }
</ol>
</div>;
});
return (
<div className="mx_FilteredDeviceList" ref={ref}>
<FilteredDeviceListHeader
selectedDeviceCount={selectedDeviceIds.length}
isAllSelected={isAllSelected}
toggleSelectAll={toggleSelectAll}
>
{selectedDeviceIds.length ? (
<>
<AccessibleButton
data-testid="sign-out-selection-cta"
kind="danger_inline"
disabled={isSigningOut}
onClick={() => onSignOutDevices(selectedDeviceIds)}
className="mx_FilteredDeviceList_headerButton"
>
{isSigningOut && <Spinner w={16} h={16} />}
{_t("Sign out")}
</AccessibleButton>
<AccessibleButton
data-testid="cancel-selection-cta"
kind="content_inline"
disabled={isSigningOut}
onClick={() => setSelectedDeviceIds([])}
className="mx_FilteredDeviceList_headerButton"
>
{_t("Cancel")}
</AccessibleButton>
</>
) : (
<FilterDropdown<DeviceFilterKey>
id="device-list-filter"
label={_t("Filter devices")}
value={filter || ALL_FILTER_ID}
onOptionChange={onFilterOptionChange}
options={options}
selectedLabel={_t("Show")}
/>
)}
</FilteredDeviceListHeader>
{!!sortedDevices.length ? (
<FilterSecurityCard filter={filter} />
) : (
<NoResults filter={filter} clearFilter={() => onFilterChange(undefined)} />
)}
<ol className="mx_FilteredDeviceList_list">
{sortedDevices.map((device) => (
<DeviceListItem
key={device.device_id}
device={device}
pusher={getPusherForDevice(device)}
localNotificationSettings={localNotificationSettings.get(device.device_id)}
isExpanded={expandedDeviceIds.includes(device.device_id)}
isSigningOut={signingOutDeviceIds.includes(device.device_id)}
isSelected={isDeviceSelected(device.device_id, selectedDeviceIds)}
onDeviceExpandToggle={() => onDeviceExpandToggle(device.device_id)}
onSignOutDevice={() => onSignOutDevices([device.device_id])}
saveDeviceName={(deviceName: string) => saveDeviceName(device.device_id, deviceName)}
onRequestDeviceVerification={
onRequestDeviceVerification
? () => onRequestDeviceVerification(device.device_id)
: undefined
}
setPushNotifications={setPushNotifications}
toggleSelected={() => toggleSelection(device.device_id)}
supportsMSC3881={supportsMSC3881}
/>
))}
</ol>
</div>
);
},
);

View file

@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { HTMLProps } from 'react';
import React, { HTMLProps } from "react";
import { _t } from '../../../../languageHandler';
import StyledCheckbox, { CheckboxStyle } from '../../elements/StyledCheckbox';
import { Alignment } from '../../elements/Tooltip';
import TooltipTarget from '../../elements/TooltipTarget';
import { _t } from "../../../../languageHandler";
import StyledCheckbox, { CheckboxStyle } from "../../elements/StyledCheckbox";
import { Alignment } from "../../elements/Tooltip";
import TooltipTarget from "../../elements/TooltipTarget";
interface Props extends Omit<HTMLProps<HTMLDivElement>, 'className'> {
interface Props extends Omit<HTMLProps<HTMLDivElement>, "className"> {
selectedDeviceCount: number;
isAllSelected: boolean;
toggleSelectAll: () => void;
@ -35,29 +35,27 @@ const FilteredDeviceListHeader: React.FC<Props> = ({
children,
...rest
}) => {
const checkboxLabel = isAllSelected ? _t('Deselect all') : _t('Select all');
return <div className='mx_FilteredDeviceListHeader' {...rest}>
<TooltipTarget
label={checkboxLabel}
alignment={Alignment.Top}
>
<StyledCheckbox
kind={CheckboxStyle.Solid}
checked={isAllSelected}
onChange={toggleSelectAll}
id='device-select-all-checkbox'
data-testid='device-select-all-checkbox'
aria-label={checkboxLabel}
/>
</TooltipTarget>
<span className='mx_FilteredDeviceListHeader_label'>
{ selectedDeviceCount > 0
? _t('%(selectedDeviceCount)s sessions selected', { selectedDeviceCount })
: _t('Sessions')
}
</span>
{ children }
</div>;
const checkboxLabel = isAllSelected ? _t("Deselect all") : _t("Select all");
return (
<div className="mx_FilteredDeviceListHeader" {...rest}>
<TooltipTarget label={checkboxLabel} alignment={Alignment.Top}>
<StyledCheckbox
kind={CheckboxStyle.Solid}
checked={isAllSelected}
onChange={toggleSelectAll}
id="device-select-all-checkbox"
data-testid="device-select-all-checkbox"
aria-label={checkboxLabel}
/>
</TooltipTarget>
<span className="mx_FilteredDeviceListHeader_label">
{selectedDeviceCount > 0
? _t("%(selectedDeviceCount)s sessions selected", { selectedDeviceCount })
: _t("Sessions")}
</span>
{children}
</div>
);
};
export default FilteredDeviceListHeader;

View file

@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import type { IServerVersions } from 'matrix-js-sdk/src/matrix';
import { _t } from '../../../../languageHandler';
import AccessibleButton from '../../elements/AccessibleButton';
import SettingsSubsection from '../shared/SettingsSubsection';
import SettingsStore from '../../../../settings/SettingsStore';
import type { IServerVersions } from "matrix-js-sdk/src/matrix";
import { _t } from "../../../../languageHandler";
import AccessibleButton from "../../elements/AccessibleButton";
import SettingsSubsection from "../shared/SettingsSubsection";
import SettingsStore from "../../../../settings/SettingsStore";
interface IProps {
onShowQr: () => void;
@ -33,31 +33,32 @@ export default class LoginWithQRSection extends React.Component<IProps> {
}
public render(): JSX.Element | null {
const msc3882Supported = !!this.props.versions?.unstable_features?.['org.matrix.msc3882'];
const msc3886Supported = !!this.props.versions?.unstable_features?.['org.matrix.msc3886'];
const msc3882Supported = !!this.props.versions?.unstable_features?.["org.matrix.msc3882"];
const msc3886Supported = !!this.props.versions?.unstable_features?.["org.matrix.msc3886"];
// Needs to be enabled as a feature + server support MSC3886 or have a default rendezvous server configured:
const offerShowQr = SettingsStore.getValue("feature_qr_signin_reciprocate_show") &&
msc3882Supported && msc3886Supported; // We don't support configuration of a fallback at the moment so we just check the MSCs
const offerShowQr =
SettingsStore.getValue("feature_qr_signin_reciprocate_show") && msc3882Supported && msc3886Supported; // We don't support configuration of a fallback at the moment so we just check the MSCs
// don't show anything if no method is available
if (!offerShowQr) {
return null;
}
return <SettingsSubsection
heading={_t('Sign in with QR code')}
>
<div className="mx_LoginWithQRSection">
<p className="mx_SettingsTab_subsectionText">{
_t("You can use this device to sign in a new device with a QR code. You will need to " +
"scan the QR code shown on this device with your device that's signed out.")
}</p>
<AccessibleButton
onClick={this.props.onShowQr}
kind="primary"
>{ _t("Show QR code") }</AccessibleButton>
</div>
</SettingsSubsection>;
return (
<SettingsSubsection heading={_t("Sign in with QR code")}>
<div className="mx_LoginWithQRSection">
<p className="mx_SettingsTab_subsectionText">
{_t(
"You can use this device to sign in a new device with a QR code. You will need to " +
"scan the QR code shown on this device with your device that's signed out.",
)}
</p>
<AccessibleButton onClick={this.props.onShowQr} kind="primary">
{_t("Show QR code")}
</AccessibleButton>
</div>
</SettingsSubsection>
);
}
}

View file

@ -14,46 +14,35 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { _t } from '../../../../languageHandler';
import AccessibleButton from '../../elements/AccessibleButton';
import SettingsSubsection from '../shared/SettingsSubsection';
import DeviceSecurityCard from './DeviceSecurityCard';
import { DeviceSecurityLearnMore } from './DeviceSecurityLearnMore';
import { filterDevicesBySecurityRecommendation, FilterVariation, INACTIVE_DEVICE_AGE_DAYS } from './filter';
import {
DeviceSecurityVariation,
ExtendedDevice,
DevicesDictionary,
} from './types';
import { _t } from "../../../../languageHandler";
import AccessibleButton from "../../elements/AccessibleButton";
import SettingsSubsection from "../shared/SettingsSubsection";
import DeviceSecurityCard from "./DeviceSecurityCard";
import { DeviceSecurityLearnMore } from "./DeviceSecurityLearnMore";
import { filterDevicesBySecurityRecommendation, FilterVariation, INACTIVE_DEVICE_AGE_DAYS } from "./filter";
import { DeviceSecurityVariation, ExtendedDevice, DevicesDictionary } from "./types";
interface Props {
devices: DevicesDictionary;
currentDeviceId: ExtendedDevice['device_id'];
currentDeviceId: ExtendedDevice["device_id"];
goToFilteredList: (filter: FilterVariation) => void;
}
const SecurityRecommendations: React.FC<Props> = ({
devices,
currentDeviceId,
goToFilteredList,
}) => {
const SecurityRecommendations: React.FC<Props> = ({ devices, currentDeviceId, goToFilteredList }) => {
const devicesArray = Object.values<ExtendedDevice>(devices);
const unverifiedDevicesCount = filterDevicesBySecurityRecommendation(
devicesArray,
[DeviceSecurityVariation.Unverified],
)
const unverifiedDevicesCount = filterDevicesBySecurityRecommendation(devicesArray, [
DeviceSecurityVariation.Unverified,
])
// filter out the current device
// as unverfied warning and actions
// will be shown in current session section
.filter((device) => device.device_id !== currentDeviceId)
.length;
const inactiveDevicesCount = filterDevicesBySecurityRecommendation(
devicesArray,
[DeviceSecurityVariation.Inactive],
).length;
.filter((device) => device.device_id !== currentDeviceId).length;
const inactiveDevicesCount = filterDevicesBySecurityRecommendation(devicesArray, [
DeviceSecurityVariation.Inactive,
]).length;
if (!(unverifiedDevicesCount | inactiveDevicesCount)) {
return null;
@ -61,61 +50,64 @@ const SecurityRecommendations: React.FC<Props> = ({
const inactiveAgeDays = INACTIVE_DEVICE_AGE_DAYS;
return <SettingsSubsection
heading={_t('Security recommendations')}
description={_t('Improve your account security by following these recommendations')}
data-testid='security-recommendations-section'
>
{
!!unverifiedDevicesCount &&
<DeviceSecurityCard
variation={DeviceSecurityVariation.Unverified}
heading={_t('Unverified sessions')}
description={<>
{ _t(
`Verify your sessions for enhanced secure messaging` +
` or sign out from those you don't recognize or use anymore.`,
) }
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Unverified} />
</>}
>
<AccessibleButton
kind='link_inline'
onClick={() => goToFilteredList(DeviceSecurityVariation.Unverified)}
data-testid='unverified-devices-cta'
>
{ _t('View all') + ` (${unverifiedDevicesCount})` }
</AccessibleButton>
</DeviceSecurityCard>
}
{
!!inactiveDevicesCount &&
<>
{ !!unverifiedDevicesCount && <div className='mx_SecurityRecommendations_spacing' /> }
return (
<SettingsSubsection
heading={_t("Security recommendations")}
description={_t("Improve your account security by following these recommendations")}
data-testid="security-recommendations-section"
>
{!!unverifiedDevicesCount && (
<DeviceSecurityCard
variation={DeviceSecurityVariation.Inactive}
heading={_t('Inactive sessions')}
description={<>
{ _t(
`Consider signing out from old sessions ` +
`(%(inactiveAgeDays)s days or older) you don't use anymore`,
{ inactiveAgeDays },
) }
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Inactive} />
</>
variation={DeviceSecurityVariation.Unverified}
heading={_t("Unverified sessions")}
description={
<>
{_t(
`Verify your sessions for enhanced secure messaging` +
` or sign out from those you don't recognize or use anymore.`,
)}
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Unverified} />
</>
}
>
<AccessibleButton
kind='link_inline'
onClick={() => goToFilteredList(DeviceSecurityVariation.Inactive)}
data-testid='inactive-devices-cta'
kind="link_inline"
onClick={() => goToFilteredList(DeviceSecurityVariation.Unverified)}
data-testid="unverified-devices-cta"
>
{ _t('View all') + ` (${inactiveDevicesCount})` }
{_t("View all") + ` (${unverifiedDevicesCount})`}
</AccessibleButton>
</DeviceSecurityCard>
</>
}
</SettingsSubsection>;
)}
{!!inactiveDevicesCount && (
<>
{!!unverifiedDevicesCount && <div className="mx_SecurityRecommendations_spacing" />}
<DeviceSecurityCard
variation={DeviceSecurityVariation.Inactive}
heading={_t("Inactive sessions")}
description={
<>
{_t(
`Consider signing out from old sessions ` +
`(%(inactiveAgeDays)s days or older) you don't use anymore`,
{ inactiveAgeDays },
)}
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Inactive} />
</>
}
>
<AccessibleButton
kind="link_inline"
onClick={() => goToFilteredList(DeviceSecurityVariation.Inactive)}
data-testid="inactive-devices-cta"
>
{_t("View all") + ` (${inactiveDevicesCount})`}
</AccessibleButton>
</DeviceSecurityCard>
</>
)}
</SettingsSubsection>
);
};
export default SecurityRecommendations;

View file

@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import StyledCheckbox, { CheckboxStyle } from '../../elements/StyledCheckbox';
import DeviceTile, { DeviceTileProps } from './DeviceTile';
import StyledCheckbox, { CheckboxStyle } from "../../elements/StyledCheckbox";
import DeviceTile, { DeviceTileProps } from "./DeviceTile";
interface Props extends DeviceTileProps {
isSelected: boolean;
@ -25,26 +25,22 @@ interface Props extends DeviceTileProps {
onClick?: () => void;
}
const SelectableDeviceTile: React.FC<Props> = ({
children,
device,
isSelected,
onSelect,
onClick,
}) => {
return <div className='mx_SelectableDeviceTile'>
<StyledCheckbox
kind={CheckboxStyle.Solid}
checked={isSelected}
onChange={onSelect}
className='mx_SelectableDeviceTile_checkbox'
id={`device-tile-checkbox-${device.device_id}`}
data-testid={`device-tile-checkbox-${device.device_id}`}
/>
<DeviceTile device={device} onClick={onClick} isSelected={isSelected}>
{ children }
</DeviceTile>
</div>;
const SelectableDeviceTile: React.FC<Props> = ({ children, device, isSelected, onSelect, onClick }) => {
return (
<div className="mx_SelectableDeviceTile">
<StyledCheckbox
kind={CheckboxStyle.Solid}
checked={isSelected}
onChange={onSelect}
className="mx_SelectableDeviceTile_checkbox"
id={`device-tile-checkbox-${device.device_id}`}
data-testid={`device-tile-checkbox-${device.device_id}`}
/>
<DeviceTile device={device} onClick={onClick} isSelected={isSelected}>
{children}
</DeviceTile>
</div>
);
};
export default SelectableDeviceTile;

View file

@ -23,14 +23,16 @@ import { InteractiveAuthCallback } from "../../../structures/InteractiveAuth";
import { SSOAuthEntry } from "../../auth/InteractiveAuthEntryComponents";
import InteractiveAuthDialog from "../../dialogs/InteractiveAuthDialog";
const makeDeleteRequest = (
matrixClient: MatrixClient, deviceIds: string[],
) => async (auth?: IAuthData): Promise<void> => {
await matrixClient.deleteMultipleDevices(deviceIds, auth);
};
const makeDeleteRequest =
(matrixClient: MatrixClient, deviceIds: string[]) =>
async (auth?: IAuthData): Promise<void> => {
await matrixClient.deleteMultipleDevices(deviceIds, auth);
};
export const deleteDevicesWithInteractiveAuth = async (
matrixClient: MatrixClient, deviceIds: string[], onFinished?: InteractiveAuthCallback,
matrixClient: MatrixClient,
deviceIds: string[],
onFinished?: InteractiveAuthCallback,
) => {
if (!deviceIds.length) {
return;

View file

@ -19,19 +19,20 @@ import { ExtendedDevice, DeviceSecurityVariation } from "./types";
type DeviceFilterCondition = (device: ExtendedDevice) => boolean;
const MS_DAY = 24 * 60 * 60 * 1000;
export const INACTIVE_DEVICE_AGE_MS = 7.776e+9; // 90 days
export const INACTIVE_DEVICE_AGE_MS = 7.776e9; // 90 days
export const INACTIVE_DEVICE_AGE_DAYS = INACTIVE_DEVICE_AGE_MS / MS_DAY;
export type FilterVariation = DeviceSecurityVariation.Verified
export type FilterVariation =
| DeviceSecurityVariation.Verified
| DeviceSecurityVariation.Inactive
| DeviceSecurityVariation.Unverified;
export const isDeviceInactive: DeviceFilterCondition = device =>
export const isDeviceInactive: DeviceFilterCondition = (device) =>
!!device.last_seen_ts && device.last_seen_ts < Date.now() - INACTIVE_DEVICE_AGE_MS;
const filters: Record<FilterVariation, DeviceFilterCondition> = {
[DeviceSecurityVariation.Verified]: device => !!device.isVerified,
[DeviceSecurityVariation.Unverified]: device => !device.isVerified,
[DeviceSecurityVariation.Verified]: (device) => !!device.isVerified,
[DeviceSecurityVariation.Unverified]: (device) => !device.isVerified,
[DeviceSecurityVariation.Inactive]: isDeviceInactive,
};
@ -39,9 +40,9 @@ export const filterDevicesBySecurityRecommendation = (
devices: ExtendedDevice[],
securityVariations: FilterVariation[],
) => {
const activeFilters = securityVariations.map(variation => filters[variation]);
const activeFilters = securityVariations.map((variation) => filters[variation]);
if (!activeFilters.length) {
return devices;
}
return devices.filter(device => activeFilters.every(filter => filter(device)));
return devices.filter((device) => activeFilters.every((filter) => filter(device)));
};

View file

@ -26,13 +26,13 @@ export type ExtendedDeviceAppInfo = {
url?: string;
};
export type ExtendedDevice = DeviceWithVerification & ExtendedDeviceAppInfo & ExtendedDeviceInformation;
export type DevicesDictionary = Record<ExtendedDevice['device_id'], ExtendedDevice>;
export type DevicesDictionary = Record<ExtendedDevice["device_id"], ExtendedDevice>;
export enum DeviceSecurityVariation {
Verified = 'Verified',
Unverified = 'Unverified',
Inactive = 'Inactive',
Verified = "Verified",
Unverified = "Unverified",
Inactive = "Inactive",
// sessions that do not support encryption
// eg a session that logged in via api to get an access token
Unverifiable = 'Unverifiable'
Unverifiable = "Unverifiable",
}

View file

@ -48,18 +48,13 @@ const isDeviceVerified = (
try {
const userId = matrixClient.getUserId();
if (!userId) {
throw new Error('No user id');
throw new Error("No user id");
}
const deviceInfo = matrixClient.getStoredDevice(userId, device.device_id);
if (!deviceInfo) {
throw new Error('No device info available');
throw new Error("No device info available");
}
return crossSigningInfo.checkDeviceTrust(
crossSigningInfo,
deviceInfo,
false,
true,
).isCrossSigningVerified();
return crossSigningInfo.checkDeviceTrust(crossSigningInfo, deviceInfo, false, true).isCrossSigningVerified();
} catch (error) {
logger.error("Error getting device cross-signing info", error);
return null;
@ -79,27 +74,30 @@ const parseDeviceExtendedInformation = (matrixClient: MatrixClient, device: IMyD
const fetchDevicesWithVerification = async (
matrixClient: MatrixClient,
userId: string,
): Promise<DevicesState['devices']> => {
): Promise<DevicesState["devices"]> => {
const { devices } = await matrixClient.getDevices();
const crossSigningInfo = matrixClient.getStoredCrossSigningForUser(userId);
const devicesDict = devices.reduce((acc, device: IMyDevice) => ({
...acc,
[device.device_id]: {
...device,
isVerified: isDeviceVerified(matrixClient, crossSigningInfo, device),
...parseDeviceExtendedInformation(matrixClient, device),
...parseUserAgent(device[UNSTABLE_MSC3852_LAST_SEEN_UA.name]),
},
}), {});
const devicesDict = devices.reduce(
(acc, device: IMyDevice) => ({
...acc,
[device.device_id]: {
...device,
isVerified: isDeviceVerified(matrixClient, crossSigningInfo, device),
...parseDeviceExtendedInformation(matrixClient, device),
...parseUserAgent(device[UNSTABLE_MSC3852_LAST_SEEN_UA.name]),
},
}),
{},
);
return devicesDict;
};
export enum OwnDevicesError {
Unsupported = 'Unsupported',
Default = 'Default',
Unsupported = "Unsupported",
Default = "Default",
}
export type DevicesState = {
devices: DevicesDictionary;
@ -108,10 +106,10 @@ export type DevicesState = {
currentDeviceId: string;
isLoadingDeviceList: boolean;
// not provided when current session cannot request verification
requestDeviceVerification?: (deviceId: ExtendedDevice['device_id']) => Promise<VerificationRequest>;
requestDeviceVerification?: (deviceId: ExtendedDevice["device_id"]) => Promise<VerificationRequest>;
refreshDevices: () => Promise<void>;
saveDeviceName: (deviceId: ExtendedDevice['device_id'], deviceName: string) => Promise<void>;
setPushNotifications: (deviceId: ExtendedDevice['device_id'], enabled: boolean) => Promise<void>;
saveDeviceName: (deviceId: ExtendedDevice["device_id"], deviceName: string) => Promise<void>;
setPushNotifications: (deviceId: ExtendedDevice["device_id"], enabled: boolean) => Promise<void>;
error?: OwnDevicesError;
supportsMSC3881?: boolean | undefined;
};
@ -121,17 +119,18 @@ export const useOwnDevices = (): DevicesState => {
const currentDeviceId = matrixClient.getDeviceId();
const userId = matrixClient.getUserId();
const [devices, setDevices] = useState<DevicesState['devices']>({});
const [pushers, setPushers] = useState<DevicesState['pushers']>([]);
const [localNotificationSettings, setLocalNotificationSettings]
= useState<DevicesState['localNotificationSettings']>(new Map<string, LocalNotificationSettings>());
const [devices, setDevices] = useState<DevicesState["devices"]>({});
const [pushers, setPushers] = useState<DevicesState["pushers"]>([]);
const [localNotificationSettings, setLocalNotificationSettings] = useState<
DevicesState["localNotificationSettings"]
>(new Map<string, LocalNotificationSettings>());
const [isLoadingDeviceList, setIsLoadingDeviceList] = useState(true);
const [supportsMSC3881, setSupportsMSC3881] = useState(true); // optimisticly saying yes!
const [error, setError] = useState<OwnDevicesError>();
useEffect(() => {
matrixClient.doesServerSupportUnstableFeature("org.matrix.msc3881").then(hasSupport => {
matrixClient.doesServerSupportUnstableFeature("org.matrix.msc3881").then((hasSupport) => {
setSupportsMSC3881(hasSupport);
});
}, [matrixClient]);
@ -142,7 +141,7 @@ export const useOwnDevices = (): DevicesState => {
// realistically we should never hit this
// but it satisfies types
if (!userId) {
throw new Error('Cannot fetch devices without user id');
throw new Error("Cannot fetch devices without user id");
}
const devices = await fetchDevicesWithVerification(matrixClient, userId);
setDevices(devices);
@ -155,10 +154,7 @@ export const useOwnDevices = (): DevicesState => {
const eventType = `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`;
const event = matrixClient.getAccountData(eventType);
if (event) {
notificationSettings.set(
deviceId,
event.getContent(),
);
notificationSettings.set(deviceId, event.getContent());
}
});
setLocalNotificationSettings(notificationSettings);
@ -198,17 +194,15 @@ export const useOwnDevices = (): DevicesState => {
const isCurrentDeviceVerified = !!devices[currentDeviceId]?.isVerified;
const requestDeviceVerification = isCurrentDeviceVerified && userId
? async (deviceId: ExtendedDevice['device_id']) => {
return await matrixClient.requestVerification(
userId,
[deviceId],
);
}
: undefined;
const requestDeviceVerification =
isCurrentDeviceVerified && userId
? async (deviceId: ExtendedDevice["device_id"]) => {
return await matrixClient.requestVerification(userId, [deviceId]);
}
: undefined;
const saveDeviceName = useCallback(
async (deviceId: ExtendedDevice['device_id'], deviceName: string): Promise<void> => {
async (deviceId: ExtendedDevice["device_id"], deviceName: string): Promise<void> => {
const device = devices[deviceId];
// no change
@ -217,21 +211,20 @@ export const useOwnDevices = (): DevicesState => {
}
try {
await matrixClient.setDeviceDetails(
deviceId,
{ display_name: deviceName },
);
await matrixClient.setDeviceDetails(deviceId, { display_name: deviceName });
await refreshDevices();
} catch (error) {
logger.error("Error setting session display name", error);
throw new Error(_t("Failed to set display name"));
}
}, [matrixClient, devices, refreshDevices]);
},
[matrixClient, devices, refreshDevices],
);
const setPushNotifications = useCallback(
async (deviceId: ExtendedDevice['device_id'], enabled: boolean): Promise<void> => {
async (deviceId: ExtendedDevice["device_id"], enabled: boolean): Promise<void> => {
try {
const pusher = pushers.find(pusher => pusher[PUSHER_DEVICE_ID.name] === deviceId);
const pusher = pushers.find((pusher) => pusher[PUSHER_DEVICE_ID.name] === deviceId);
if (pusher) {
await matrixClient.setPusher({
...pusher,
@ -248,7 +241,8 @@ export const useOwnDevices = (): DevicesState => {
} finally {
await refreshDevices();
}
}, [matrixClient, pushers, localNotificationSettings, refreshDevices],
},
[matrixClient, pushers, localNotificationSettings, refreshDevices],
);
return {

View file

@ -15,14 +15,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { IThreepid } from "matrix-js-sdk/src/@types/threepids";
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../../../../languageHandler";
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
import Modal from '../../../../Modal';
import AddThreepid from '../../../../AddThreepid';
import Modal from "../../../../Modal";
import AddThreepid from "../../../../AddThreepid";
import ErrorDialog from "../../dialogs/ErrorDialog";
import AccessibleButton from "../../elements/AccessibleButton";
@ -106,7 +106,7 @@ export class EmailAddress extends React.Component<IEmailAddressProps, IEmailAddr
});
Modal.createDialog(ErrorDialog, {
title: errorTitle,
description: ((err && err.message) ? err.message : _t("Operation failed")),
description: err && err.message ? err.message : _t("Operation failed"),
});
}
}
@ -141,7 +141,7 @@ export class EmailAddress extends React.Component<IEmailAddressProps, IEmailAddr
});
Modal.createDialog(ErrorDialog, {
title: errorTitle,
description: ((err && err.message) ? err.message : _t("Operation failed")),
description: err && err.message ? err.message : _t("Operation failed"),
});
}
}
@ -180,17 +180,18 @@ export class EmailAddress extends React.Component<IEmailAddressProps, IEmailAddr
});
} catch (err) {
this.setState({ continueDisabled: false });
if (err.errcode === 'M_THREEPID_AUTH_FAILED') {
if (err.errcode === "M_THREEPID_AUTH_FAILED") {
Modal.createDialog(ErrorDialog, {
title: _t("Your email address hasn't been verified yet"),
description: _t("Click the link in the email you received to verify " +
"and then click continue again."),
description: _t(
"Click the link in the email you received to verify " + "and then click continue again.",
),
});
} else {
logger.error("Unable to verify email address: " + err);
Modal.createDialog(ErrorDialog, {
title: _t("Unable to verify email address."),
description: ((err && err.message) ? err.message : _t("Operation failed")),
description: err && err.message ? err.message : _t("Operation failed"),
});
}
}
@ -202,39 +203,45 @@ export class EmailAddress extends React.Component<IEmailAddressProps, IEmailAddr
let status;
if (verifying) {
status = <span>
{ _t("Verify the link in your inbox") }
status = (
<span>
{_t("Verify the link in your inbox")}
<AccessibleButton
className="mx_ExistingEmailAddress_confirmBtn"
kind="primary_sm"
onClick={this.onContinueClick}
disabled={this.state.continueDisabled}
>
{_t("Complete")}
</AccessibleButton>
</span>
);
} else if (bound) {
status = (
<AccessibleButton
className="mx_ExistingEmailAddress_confirmBtn"
kind="danger_sm"
onClick={this.onRevokeClick}
>
{_t("Revoke")}
</AccessibleButton>
);
} else {
status = (
<AccessibleButton
className="mx_ExistingEmailAddress_confirmBtn"
kind="primary_sm"
onClick={this.onContinueClick}
disabled={this.state.continueDisabled}
onClick={this.onShareClick}
>
{ _t("Complete") }
{_t("Share")}
</AccessibleButton>
</span>;
} else if (bound) {
status = <AccessibleButton
className="mx_ExistingEmailAddress_confirmBtn"
kind="danger_sm"
onClick={this.onRevokeClick}
>
{ _t("Revoke") }
</AccessibleButton>;
} else {
status = <AccessibleButton
className="mx_ExistingEmailAddress_confirmBtn"
kind="primary_sm"
onClick={this.onShareClick}
>
{ _t("Share") }
</AccessibleButton>;
);
}
return (
<div className="mx_ExistingEmailAddress">
<span className="mx_ExistingEmailAddress_email">{ address }</span>
{ status }
<span className="mx_ExistingEmailAddress_email">{address}</span>
{status}
</div>
);
}
@ -251,15 +258,13 @@ export default class EmailAddresses extends React.Component<IProps> {
return <EmailAddress email={e} key={e.address} />;
});
} else {
content = <span className="mx_SettingsTab_subsectionText">
{ _t("Discovery options will appear once you have added an email above.") }
</span>;
content = (
<span className="mx_SettingsTab_subsectionText">
{_t("Discovery options will appear once you have added an email above.")}
</span>
);
}
return (
<div className="mx_EmailAddresses">
{ content }
</div>
);
return <div className="mx_EmailAddresses">{content}</div>;
}
}

View file

@ -15,14 +15,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { IThreepid } from "matrix-js-sdk/src/@types/threepids";
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../../../../languageHandler";
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
import Modal from '../../../../Modal';
import AddThreepid from '../../../../AddThreepid';
import Modal from "../../../../Modal";
import AddThreepid from "../../../../AddThreepid";
import ErrorDialog from "../../dialogs/ErrorDialog";
import Field from "../../elements/Field";
import AccessibleButton from "../../elements/AccessibleButton";
@ -106,7 +106,7 @@ export class PhoneNumber extends React.Component<IPhoneNumberProps, IPhoneNumber
});
Modal.createDialog(ErrorDialog, {
title: errorTitle,
description: ((err && err.message) ? err.message : _t("Operation failed")),
description: err && err.message ? err.message : _t("Operation failed"),
});
}
}
@ -145,7 +145,7 @@ export class PhoneNumber extends React.Component<IPhoneNumberProps, IPhoneNumber
});
Modal.createDialog(ErrorDialog, {
title: errorTitle,
description: ((err && err.message) ? err.message : _t("Operation failed")),
description: err && err.message ? err.message : _t("Operation failed"),
});
}
}
@ -193,11 +193,11 @@ export class PhoneNumber extends React.Component<IPhoneNumberProps, IPhoneNumber
});
} catch (err) {
this.setState({ continueDisabled: false });
if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {
if (err.errcode !== "M_THREEPID_AUTH_FAILED") {
logger.error("Unable to verify phone number: " + err);
Modal.createDialog(ErrorDialog, {
title: _t("Unable to verify phone number."),
description: ((err && err.message) ? err.message : _t("Operation failed")),
description: err && err.message ? err.message : _t("Operation failed"),
});
} else {
this.setState({ verifyError: _t("Incorrect verification code") });
@ -211,45 +211,51 @@ export class PhoneNumber extends React.Component<IPhoneNumberProps, IPhoneNumber
let status;
if (verifying) {
status = <span className="mx_ExistingPhoneNumber_verification">
<span>
{ _t("Please enter verification code sent via text.") }
<br />
{ this.state.verifyError }
status = (
<span className="mx_ExistingPhoneNumber_verification">
<span>
{_t("Please enter verification code sent via text.")}
<br />
{this.state.verifyError}
</span>
<form onSubmit={this.onContinueClick} autoComplete="off" noValidate={true}>
<Field
type="text"
label={_t("Verification code")}
autoComplete="off"
disabled={this.state.continueDisabled}
value={this.state.verificationCode}
onChange={this.onVerificationCodeChange}
/>
</form>
</span>
<form onSubmit={this.onContinueClick} autoComplete="off" noValidate={true}>
<Field
type="text"
label={_t("Verification code")}
autoComplete="off"
disabled={this.state.continueDisabled}
value={this.state.verificationCode}
onChange={this.onVerificationCodeChange}
/>
</form>
</span>;
);
} else if (bound) {
status = <AccessibleButton
className="mx_ExistingPhoneNumber_confirmBtn"
kind="danger_sm"
onClick={this.onRevokeClick}
>
{ _t("Revoke") }
</AccessibleButton>;
status = (
<AccessibleButton
className="mx_ExistingPhoneNumber_confirmBtn"
kind="danger_sm"
onClick={this.onRevokeClick}
>
{_t("Revoke")}
</AccessibleButton>
);
} else {
status = <AccessibleButton
className="mx_ExistingPhoneNumber_confirmBtn"
kind="primary_sm"
onClick={this.onShareClick}
>
{ _t("Share") }
</AccessibleButton>;
status = (
<AccessibleButton
className="mx_ExistingPhoneNumber_confirmBtn"
kind="primary_sm"
onClick={this.onShareClick}
>
{_t("Share")}
</AccessibleButton>
);
}
return (
<div className="mx_ExistingPhoneNumber">
<span className="mx_ExistingPhoneNumber_address">+{ address }</span>
{ status }
<span className="mx_ExistingPhoneNumber_address">+{address}</span>
{status}
</div>
);
}
@ -267,15 +273,13 @@ export default class PhoneNumbers extends React.Component<IProps> {
return <PhoneNumber msisdn={e} key={e.address} />;
});
} else {
content = <span className="mx_SettingsTab_subsectionText">
{ _t("Discovery options will appear once you have added a phone number above.") }
</span>;
content = (
<span className="mx_SettingsTab_subsectionText">
{_t("Discovery options will appear once you have added a phone number above.")}
</span>
);
}
return (
<div className="mx_PhoneNumbers">
{ content }
</div>
);
return <div className="mx_PhoneNumbers">{content}</div>;
}
}

View file

@ -26,16 +26,9 @@ export interface SettingsSubsectionProps extends HTMLAttributes<HTMLDivElement>
const SettingsSubsection: React.FC<SettingsSubsectionProps> = ({ heading, description, children, ...rest }) => (
<div {...rest} className="mx_SettingsSubsection">
{ typeof heading === 'string'
? <SettingsSubsectionHeading heading={heading} />
: <>
{ heading }
</>
}
{ !!description && <div className="mx_SettingsSubsection_description">{ description }</div> }
<div className="mx_SettingsSubsection_content">
{ children }
</div>
{typeof heading === "string" ? <SettingsSubsectionHeading heading={heading} /> : <>{heading}</>}
{!!description && <div className="mx_SettingsSubsection_description">{description}</div>}
<div className="mx_SettingsSubsection_content">{children}</div>
</div>
);

View file

@ -25,7 +25,9 @@ export interface SettingsSubsectionHeadingProps extends HTMLAttributes<HTMLDivEl
export const SettingsSubsectionHeading: React.FC<SettingsSubsectionHeadingProps> = ({ heading, children, ...rest }) => (
<div {...rest} className="mx_SettingsSubsectionHeading">
<Heading className="mx_SettingsSubsectionHeading_heading" size='h3'>{ heading }</Heading>
{ children }
<Heading className="mx_SettingsSubsectionHeading_heading" size="h3">
{heading}
</Heading>
{children}
</div>
);

View file

@ -24,10 +24,8 @@ export interface SettingsTabProps {
const SettingsTab: React.FC<SettingsTabProps> = ({ heading, children }) => (
<div className="mx_SettingsTab">
<Heading size='h2'>{ heading }</Heading>
<div className="mx_SettingsTab_sections">
{ children }
</div>
<Heading size="h2">{heading}</Heading>
<div className="mx_SettingsTab_sections">{children}</div>
</div>
);

View file

@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { EventType } from 'matrix-js-sdk/src/@types/event';
import React from "react";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { _t } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
@ -23,7 +23,7 @@ import AccessibleButton from "../../../elements/AccessibleButton";
import RoomUpgradeDialog from "../../../dialogs/RoomUpgradeDialog";
import Modal from "../../../../../Modal";
import dis from "../../../../../dispatcher/dispatcher";
import { Action } from '../../../../../dispatcher/actions';
import { Action } from "../../../../../dispatcher/actions";
import CopyableText from "../../../elements/CopyableText";
import { ViewRoomPayload } from "../../../../../dispatcher/payloads/ViewRoomPayload";
@ -100,30 +100,31 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
const isSpace = room.isSpaceRoom();
let unfederatableSection;
const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, '');
if (createEvent && createEvent.getContent()['m.federate'] === false) {
unfederatableSection = <div>{ _t('This room is not accessible by remote Matrix servers') }</div>;
const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, "");
if (createEvent && createEvent.getContent()["m.federate"] === false) {
unfederatableSection = <div>{_t("This room is not accessible by remote Matrix servers")}</div>;
}
let roomUpgradeButton;
if (this.state.upgradeRecommendation && this.state.upgradeRecommendation.needsUpgrade && !this.state.upgraded) {
roomUpgradeButton = (
<div>
<p className='mx_SettingsTab_warningText'>
{ _t(
<p className="mx_SettingsTab_warningText">
{_t(
"<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members " +
"to the new version of the room.</i> We'll post a link to the new room in the old " +
"version of the room - room members will have to click this link to join the new room.",
{}, {
"b": (sub) => <b>{ sub }</b>,
"i": (sub) => <i>{ sub }</i>,
"to the new version of the room.</i> We'll post a link to the new room in the old " +
"version of the room - room members will have to click this link to join the new room.",
{},
{
b: (sub) => <b>{sub}</b>,
i: (sub) => <i>{sub}</i>,
},
) }
)}
</p>
<AccessibleButton onClick={this.upgradeRoom} kind='primary'>
{ isSpace
<AccessibleButton onClick={this.upgradeRoom} kind="primary">
{isSpace
? _t("Upgrade this space to the recommended room version")
: _t("Upgrade this room to the recommended room version") }
: _t("Upgrade this room to the recommended room version")}
</AccessibleButton>
</div>
);
@ -139,35 +140,33 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
}
oldRoomLink = (
<AccessibleButton element='a' onClick={this.onOldRoomClicked}>
{ copy }
<AccessibleButton element="a" onClick={this.onOldRoomClicked}>
{copy}
</AccessibleButton>
);
}
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{ _t("Advanced") }</div>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<span className='mx_SettingsTab_subheading'>
{ room?.isSpaceRoom() ? _t("Space information") : _t("Room information") }
<div className="mx_SettingsTab_heading">{_t("Advanced")}</div>
<div className="mx_SettingsTab_section mx_SettingsTab_subsectionText">
<span className="mx_SettingsTab_subheading">
{room?.isSpaceRoom() ? _t("Space information") : _t("Room information")}
</span>
<div>
<span>{ _t("Internal room ID") }</span>
<CopyableText getTextToCopy={() => this.props.roomId}>
{ this.props.roomId }
</CopyableText>
<span>{_t("Internal room ID")}</span>
<CopyableText getTextToCopy={() => this.props.roomId}>{this.props.roomId}</CopyableText>
</div>
{ unfederatableSection }
{unfederatableSection}
</div>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<span className='mx_SettingsTab_subheading'>{ _t("Room version") }</span>
<div className="mx_SettingsTab_section mx_SettingsTab_subsectionText">
<span className="mx_SettingsTab_subheading">{_t("Room version")}</span>
<div>
<span>{ _t("Room version:") }</span>&nbsp;
{ room.getVersion() }
<span>{_t("Room version:")}</span>&nbsp;
{room.getVersion()}
</div>
{ oldRoomLink }
{ roomUpgradeButton }
{oldRoomLink}
{roomUpgradeButton}
</div>
</div>
);

View file

@ -46,7 +46,7 @@ export default class BridgeSettingsTab extends React.Component<IProps> {
const client = MatrixClientPeg.get();
const roomState = client.getRoom(roomId).currentState;
return BRIDGE_EVENT_TYPES.map(typeName => roomState.getStateEvents(typeName)).flat(1);
return BRIDGE_EVENT_TYPES.map((typeName) => roomState.getStateEvents(typeName)).flat(1);
}
render() {
@ -58,38 +58,52 @@ export default class BridgeSettingsTab extends React.Component<IProps> {
let content: JSX.Element;
if (bridgeEvents.length > 0) {
content = <div>
<p>{ _t(
"This room is bridging messages to the following platforms. " +
"<a>Learn more.</a>", {},
{
// TODO: We don't have this link yet: this will prevent the translators
// having to re-translate the string when we do.
a: sub => <a href={BRIDGES_LINK} target="_blank" rel="noreferrer noopener">{ sub }</a>,
},
) }</p>
<ul className="mx_RoomSettingsDialog_BridgeList">
{ bridgeEvents.map((event) => this.renderBridgeCard(event, room)) }
</ul>
</div>;
content = (
<div>
<p>
{_t(
"This room is bridging messages to the following platforms. " + "<a>Learn more.</a>",
{},
{
// TODO: We don't have this link yet: this will prevent the translators
// having to re-translate the string when we do.
a: (sub) => (
<a href={BRIDGES_LINK} target="_blank" rel="noreferrer noopener">
{sub}
</a>
),
},
)}
</p>
<ul className="mx_RoomSettingsDialog_BridgeList">
{bridgeEvents.map((event) => this.renderBridgeCard(event, room))}
</ul>
</div>
);
} else {
content = <p>{ _t(
"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
// having to re-translate the string when we do.
a: sub => <a href={BRIDGES_LINK} target="_blank" rel="noreferrer noopener">{ sub }</a>,
},
) }</p>;
content = (
<p>
{_t(
"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
// having to re-translate the string when we do.
a: (sub) => (
<a href={BRIDGES_LINK} target="_blank" rel="noreferrer noopener">
{sub}
</a>
),
},
)}
</p>
);
}
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{ _t("Bridges") }</div>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
{ content }
</div>
<div className="mx_SettingsTab_heading">{_t("Bridges")}</div>
<div className="mx_SettingsTab_section mx_SettingsTab_subsectionText">{content}</div>
</div>
);
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ContextType } from 'react';
import React, { ContextType } from "react";
import { _t } from "../../../../../languageHandler";
import RoomProfileSettings from "../../../room_settings/RoomProfileSettings";
@ -49,7 +49,7 @@ export default class GeneralRoomSettingsTab extends React.Component<IProps, ISta
private onLeaveClick = (ev: ButtonEvent): void => {
dis.dispatch({
action: 'leave_room',
action: "leave_room",
room_id: this.props.roomId,
});
@ -62,41 +62,43 @@ export default class GeneralRoomSettingsTab extends React.Component<IProps, ISta
const canSetAliases = true; // Previously, we arbitrarily only allowed admins to do this
const canSetCanonical = room.currentState.mayClientSendStateEvent("m.room.canonical_alias", client);
const canonicalAliasEv = room.currentState.getStateEvents("m.room.canonical_alias", '');
const canonicalAliasEv = room.currentState.getStateEvents("m.room.canonical_alias", "");
const urlPreviewSettings = SettingsStore.getValue(UIFeature.URLPreviews) ?
<UrlPreviewSettings room={room} /> :
null;
const urlPreviewSettings = SettingsStore.getValue(UIFeature.URLPreviews) ? (
<UrlPreviewSettings room={room} />
) : null;
let leaveSection;
if (room.getMyMembership() === "join") {
leaveSection = <>
<span className='mx_SettingsTab_subheading'>{ _t("Leave room") }</span>
<div className='mx_SettingsTab_section'>
<AccessibleButton kind='danger' onClick={this.onLeaveClick}>
{ _t('Leave room') }
</AccessibleButton>
</div>
</>;
leaveSection = (
<>
<span className="mx_SettingsTab_subheading">{_t("Leave room")}</span>
<div className="mx_SettingsTab_section">
<AccessibleButton kind="danger" onClick={this.onLeaveClick}>
{_t("Leave room")}
</AccessibleButton>
</div>
</>
);
}
return (
<div className="mx_SettingsTab mx_GeneralRoomSettingsTab">
<div className="mx_SettingsTab_heading">{ _t("General") }</div>
<div className='mx_SettingsTab_section mx_GeneralRoomSettingsTab_profileSection'>
<div className="mx_SettingsTab_heading">{_t("General")}</div>
<div className="mx_SettingsTab_section mx_GeneralRoomSettingsTab_profileSection">
<RoomProfileSettings roomId={this.props.roomId} />
</div>
<div className="mx_SettingsTab_heading">{ _t("Room Addresses") }</div>
<div className="mx_SettingsTab_heading">{_t("Room Addresses")}</div>
<AliasSettings
roomId={this.props.roomId}
canSetCanonicalAlias={canSetCanonical}
canSetAliases={canSetAliases}
canonicalAliasEvent={canonicalAliasEv}
/>
<div className="mx_SettingsTab_heading">{ _t("Other") }</div>
{ urlPreviewSettings }
{ leaveSection }
<div className="mx_SettingsTab_heading">{_t("Other")}</div>
{urlPreviewSettings}
{leaveSection}
</div>
);
}

View file

@ -14,20 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef } from 'react';
import React, { createRef } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import AccessibleButton, { ButtonEvent } from "../../../elements/AccessibleButton";
import Notifier from "../../../../../Notifier";
import SettingsStore from '../../../../../settings/SettingsStore';
import SettingsStore from "../../../../../settings/SettingsStore";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import { RoomEchoChamber } from "../../../../../stores/local-echo/RoomEchoChamber";
import { EchoChamber } from '../../../../../stores/local-echo/EchoChamber';
import { EchoChamber } from "../../../../../stores/local-echo/EchoChamber";
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
import StyledRadioGroup from "../../../elements/StyledRadioGroup";
import { RoomNotifState } from '../../../../../RoomNotifs';
import { RoomNotifState } from "../../../../../RoomNotifs";
import defaultDispatcher from "../../../../../dispatcher/dispatcher";
import { Action } from "../../../../../dispatcher/actions";
import { UserTab } from "../../../dialogs/UserTab";
@ -95,9 +95,7 @@ export default class NotificationsSettingsTab extends React.Component<IProps, IS
try {
await this.saveSound();
} catch (ex) {
logger.error(
`Unable to save notification sound for ${this.props.roomId}`,
);
logger.error(`Unable to save notification sound for ${this.props.roomId}`);
logger.error(ex);
}
};
@ -115,23 +113,16 @@ export default class NotificationsSettingsTab extends React.Component<IProps, IS
type = "audio/ogg";
}
const { content_uri: url } = await MatrixClientPeg.get().uploadContent(
this.state.uploadedFile, {
type,
},
);
const { content_uri: url } = await MatrixClientPeg.get().uploadContent(this.state.uploadedFile, {
type,
});
await SettingsStore.setValue(
"notificationSound",
this.props.roomId,
SettingLevel.ROOM_ACCOUNT,
{
name: this.state.uploadedFile.name,
type: type,
size: this.state.uploadedFile.size,
url,
},
);
await SettingsStore.setValue("notificationSound", this.props.roomId, SettingLevel.ROOM_ACCOUNT, {
name: this.state.uploadedFile.name,
type: type,
size: this.state.uploadedFile.size,
url,
});
this.setState({
uploadedFile: null,
@ -142,12 +133,7 @@ export default class NotificationsSettingsTab extends React.Component<IProps, IS
private clearSound = (e: React.MouseEvent): void => {
e.stopPropagation();
e.preventDefault();
SettingsStore.setValue(
"notificationSound",
this.props.roomId,
SettingLevel.ROOM_ACCOUNT,
null,
);
SettingsStore.setValue("notificationSound", this.props.roomId, SettingLevel.ROOM_ACCOUNT, null);
this.setState({
currentSound: "default",
@ -174,14 +160,16 @@ export default class NotificationsSettingsTab extends React.Component<IProps, IS
if (this.state.uploadedFile) {
currentUploadedFile = (
<div>
<span>{ _t("Uploaded sound") }: <code>{ this.state.uploadedFile.name }</code></span>
<span>
{_t("Uploaded sound")}: <code>{this.state.uploadedFile.name}</code>
</span>
</div>
);
}
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{ _t("Notifications") }</div>
<div className="mx_SettingsTab_heading">{_t("Notifications")}</div>
<div className="mx_SettingsTab_section mx_NotificationSettingsTab_notificationsSection">
<StyledRadioGroup
@ -190,54 +178,77 @@ export default class NotificationsSettingsTab extends React.Component<IProps, IS
{
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_inline"
onClick={this.onOpenSettingsClick}
>
{ sub }
</AccessibleButton>,
}) }
</div>
</>,
}, {
label: (
<>
{_t("Default")}
<div className="mx_NotificationSettingsTab_microCopy">
{_t(
"Get notifications as set up in your <a>settings</a>",
{},
{
a: (sub) => (
<AccessibleButton
kind="link_inline"
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>
</>,
}, {
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_inline"
onClick={this.onOpenSettingsClick}
>
{ sub }
</AccessibleButton>,
}) }
</div>
</>,
}, {
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_inline"
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>
</>,
label: (
<>
{_t("Off")}
<div className="mx_NotificationSettingsTab_microCopy">
{_t("You won't get any notifications")}
</div>
</>
),
},
]}
onChange={this.onRoomNotificationChange}
@ -245,18 +256,25 @@ export default class NotificationsSettingsTab extends React.Component<IProps, IS
/>
</div>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<span className='mx_SettingsTab_subheading'>{ _t("Sounds") }</span>
<div className="mx_SettingsTab_section mx_SettingsTab_subsectionText">
<span className="mx_SettingsTab_subheading">{_t("Sounds")}</span>
<div>
<div className="mx_SettingsTab_subsectionText">
<span>{ _t("Notification sound") }: <code>{ this.state.currentSound }</code></span>
<span>
{_t("Notification sound")}: <code>{this.state.currentSound}</code>
</span>
</div>
<AccessibleButton className="mx_NotificationSound_resetSound" disabled={this.state.currentSound == "default"} onClick={this.clearSound} kind="primary">
{ _t("Reset") }
<AccessibleButton
className="mx_NotificationSound_resetSound"
disabled={this.state.currentSound == "default"}
onClick={this.clearSound}
kind="primary"
>
{_t("Reset")}
</AccessibleButton>
</div>
<div>
<h3>{ _t("Set a new custom sound") }</h3>
<h3>{_t("Set a new custom sound")}</h3>
<div className="mx_SettingsFlag">
<form autoComplete="off" noValidate={true}>
<input
@ -269,15 +287,24 @@ export default class NotificationsSettingsTab extends React.Component<IProps, IS
/>
</form>
{ currentUploadedFile }
{currentUploadedFile}
</div>
<AccessibleButton className="mx_NotificationSound_browse" onClick={this.triggerUploader} kind="primary">
{ _t("Browse") }
<AccessibleButton
className="mx_NotificationSound_browse"
onClick={this.triggerUploader}
kind="primary"
>
{_t("Browse")}
</AccessibleButton>
<AccessibleButton className="mx_NotificationSound_save" disabled={this.state.uploadedFile == null} onClick={this.onClickSaveSound} kind="primary">
{ _t("Save") }
<AccessibleButton
className="mx_NotificationSound_save"
disabled={this.state.uploadedFile == null}
onClick={this.onClickSaveSound}
kind="primary"
>
{_t("Save")}
</AccessibleButton>
<br />
</div>

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { RoomState, RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
@ -26,11 +26,11 @@ import { _t, _td } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import AccessibleButton from "../../../elements/AccessibleButton";
import Modal from "../../../../../Modal";
import ErrorDialog from '../../../dialogs/ErrorDialog';
import ErrorDialog from "../../../dialogs/ErrorDialog";
import PowerSelector from "../../../elements/PowerSelector";
import SettingsFieldset from '../../SettingsFieldset';
import SettingsFieldset from "../../SettingsFieldset";
import SettingsStore from "../../../../../settings/SettingsStore";
import { VoiceBroadcastInfoEventType } from '../../../../../voice-broadcast';
import { VoiceBroadcastInfoEventType } from "../../../../../voice-broadcast";
import { ElementCall } from "../../../../../models/Call";
import SdkConfig, { DEFAULTS } from "../../../../../SdkConfig";
import { AddPrivilegedUsers } from "../../AddPrivilegedUsers";
@ -88,13 +88,15 @@ interface IBannedUserProps {
export class BannedUser extends React.Component<IBannedUserProps> {
private onUnbanClick = (e) => {
MatrixClientPeg.get().unban(this.props.member.roomId, this.props.member.userId).catch((err) => {
logger.error("Failed to unban: " + err);
Modal.createDialog(ErrorDialog, {
title: _t('Error'),
description: _t('Failed to unban'),
MatrixClientPeg.get()
.unban(this.props.member.roomId, this.props.member.userId)
.catch((err) => {
logger.error("Failed to unban: " + err);
Modal.createDialog(ErrorDialog, {
title: _t("Error"),
description: _t("Failed to unban"),
});
});
});
};
render() {
@ -102,11 +104,12 @@ export class BannedUser extends React.Component<IBannedUserProps> {
if (this.props.canUnban) {
unbanButton = (
<AccessibleButton className='mx_RolesRoomSettingsTab_unbanBtn'
kind='danger_sm'
<AccessibleButton
className="mx_RolesRoomSettingsTab_unbanBtn"
kind="danger_sm"
onClick={this.onUnbanClick}
>
{ _t('Unban') }
{_t("Unban")}
</AccessibleButton>
);
}
@ -114,10 +117,10 @@ export class BannedUser extends React.Component<IBannedUserProps> {
const userId = this.props.member.name === this.props.member.userId ? null : this.props.member.userId;
return (
<li>
{ unbanButton }
{unbanButton}
<span title={_t("Banned by %(displayName)s", { displayName: this.props.by })}>
<strong>{ this.props.member.name }</strong> { userId }
{ this.props.reason ? " " + _t('Reason') + ": " + this.props.reason : "" }
<strong>{this.props.member.name}</strong> {userId}
{this.props.reason ? " " + _t("Reason") + ": " + this.props.reason : ""}
</span>
</li>
);
@ -145,14 +148,18 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
this.onThisRoomMembership();
};
private onThisRoomMembership = throttle(() => {
this.forceUpdate();
}, 200, { leading: true, trailing: true });
private onThisRoomMembership = throttle(
() => {
this.forceUpdate();
},
200,
{ leading: true, trailing: true },
);
private populateDefaultPlEvents(eventsSection: Record<string, number>, stateLevel: number, eventsLevel: number) {
for (const desiredEvent of Object.keys(plEventsToShow)) {
if (!(desiredEvent in eventsSection)) {
eventsSection[desiredEvent] = (plEventsToShow[desiredEvent].isState ? stateLevel : eventsLevel);
eventsSection[desiredEvent] = plEventsToShow[desiredEvent].isState ? stateLevel : eventsLevel;
}
}
}
@ -160,8 +167,8 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
private onPowerLevelsChanged = (value: number, powerLevelKey: string) => {
const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId);
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, '');
let plContent = plEvent ? (plEvent.getContent() || {}) : {};
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
let plContent = plEvent ? plEvent.getContent() || {} : {};
// Clone the power levels just in case
plContent = Object.assign({}, plContent);
@ -173,7 +180,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
plContent["events"] = Object.assign({}, plContent["events"] || {});
plContent["events"][powerLevelKey.slice(eventsLevelPrefix.length)] = value;
} else {
const keyPath = powerLevelKey.split('.');
const keyPath = powerLevelKey.split(".");
let parentObj;
let currentObj = plContent;
for (const key of keyPath) {
@ -186,14 +193,14 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
parentObj[keyPath[keyPath.length - 1]] = value;
}
client.sendStateEvent(this.props.roomId, EventType.RoomPowerLevels, plContent).catch(e => {
client.sendStateEvent(this.props.roomId, EventType.RoomPowerLevels, plContent).catch((e) => {
logger.error(e);
Modal.createDialog(ErrorDialog, {
title: _t('Error changing power level requirement'),
title: _t("Error changing power level requirement"),
description: _t(
"An error occurred changing the room's power level requirements. Ensure you have sufficient " +
"permissions and try again.",
"permissions and try again.",
),
});
});
@ -202,24 +209,24 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
private onUserPowerLevelChanged = (value: number, powerLevelKey: string) => {
const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId);
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, '');
let plContent = plEvent ? (plEvent.getContent() || {}) : {};
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
let plContent = plEvent ? plEvent.getContent() || {} : {};
// Clone the power levels just in case
plContent = Object.assign({}, plContent);
// powerLevelKey should be a user ID
if (!plContent['users']) plContent['users'] = {};
plContent['users'][powerLevelKey] = value;
if (!plContent["users"]) plContent["users"] = {};
plContent["users"][powerLevelKey] = value;
client.sendStateEvent(this.props.roomId, EventType.RoomPowerLevels, plContent).catch(e => {
client.sendStateEvent(this.props.roomId, EventType.RoomPowerLevels, plContent).catch((e) => {
logger.error(e);
Modal.createDialog(ErrorDialog, {
title: _t('Error changing power level'),
title: _t("Error changing power level"),
description: _t(
"An error occurred changing the user's power level. Ensure you have sufficient " +
"permissions and try again.",
"permissions and try again.",
),
});
});
@ -230,8 +237,8 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
const room = client.getRoom(this.props.roomId);
const isSpaceRoom = room.isSpaceRoom();
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, '');
const plContent = plEvent ? (plEvent.getContent() || {}) : {};
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
const plContent = plEvent ? plEvent.getContent() || {} : {};
const canChangeLevels = room.currentState.mayClientSendStateEvent(EventType.RoomPowerLevels, client);
const plEventsToLabels = {
@ -267,37 +274,37 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
const powerLevelDescriptors: Record<string, IPowerLevelDescriptor> = {
"users_default": {
desc: _t('Default role'),
desc: _t("Default role"),
defaultValue: 0,
},
"events_default": {
desc: _t('Send messages'),
desc: _t("Send messages"),
defaultValue: 0,
hideForSpace: true,
},
"invite": {
desc: _t('Invite users'),
desc: _t("Invite users"),
defaultValue: 0,
},
"state_default": {
desc: _t('Change settings'),
desc: _t("Change settings"),
defaultValue: 50,
},
"kick": {
desc: _t('Remove users'),
desc: _t("Remove users"),
defaultValue: 50,
},
"ban": {
desc: _t('Ban users'),
desc: _t("Ban users"),
defaultValue: 50,
},
"redact": {
desc: _t('Remove messages sent by others'),
desc: _t("Remove messages sent by others"),
defaultValue: 50,
hideForSpace: true,
},
"notifications.room": {
desc: _t('Notify everyone'),
desc: _t("Notify everyone"),
defaultValue: 50,
hideForSpace: true,
},
@ -322,7 +329,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
parseIntWithDefault(plContent.events_default, powerLevelDescriptors.events_default.defaultValue),
);
let privilegedUsersSection = <div>{ _t('No users have specific privileges in this room') }</div>;
let privilegedUsersSection = <div>{_t("No users have specific privileges in this room")}</div>;
let mutedUsersSection;
if (Object.keys(userLevels).length) {
const privilegedUsers: JSX.Element[] = [];
@ -332,7 +339,8 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
if (!Number.isInteger(userLevels[user])) return;
const isMe = user === client.getUserId();
const canChange = canChangeLevels && (userLevels[user] < currentUserLevel || isMe);
if (userLevels[user] > defaultUserLevel) { // privileged
if (userLevels[user] > defaultUserLevel) {
// privileged
privilegedUsers.push(
<PowerSelector
value={userLevels[user]}
@ -343,7 +351,8 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
onChange={this.onUserPowerLevelChanged}
/>,
);
} else if (userLevels[user] < defaultUserLevel) { // muted
} else if (userLevels[user] < defaultUserLevel) {
// muted
mutedUsers.push(
<PowerSelector
value={userLevels[user]}
@ -367,16 +376,12 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
mutedUsers.sort(comparator);
if (privilegedUsers.length) {
privilegedUsersSection =
<SettingsFieldset legend={_t('Privileged Users')}>
{ privilegedUsers }
</SettingsFieldset>;
privilegedUsersSection = (
<SettingsFieldset legend={_t("Privileged Users")}>{privilegedUsers}</SettingsFieldset>
);
}
if (mutedUsers.length) {
mutedUsersSection =
<SettingsFieldset legend={_t('Muted Users')}>
{ mutedUsers }
</SettingsFieldset>;
mutedUsersSection = <SettingsFieldset legend={_t("Muted Users")}>{mutedUsers}</SettingsFieldset>;
}
}
@ -384,10 +389,10 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
let bannedUsersSection;
if (banned.length) {
const canBanUsers = currentUserLevel >= banLevel;
bannedUsersSection =
<SettingsFieldset legend={_t('Banned users')}>
bannedUsersSection = (
<SettingsFieldset legend={_t("Banned users")}>
<ul>
{ banned.map((member) => {
{banned.map((member) => {
const banEvent = member.events.member.getContent();
const sender = room.getMember(member.events.member.getSender());
let bannedBy = member.events.member.getSender(); // start by falling back to mxid
@ -401,93 +406,98 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
by={bannedBy}
/>
);
}) }
})}
</ul>
</SettingsFieldset>;
</SettingsFieldset>
);
}
const powerSelectors = Object.keys(powerLevelDescriptors).map((key, index) => {
const descriptor = powerLevelDescriptors[key];
if (isSpaceRoom && descriptor.hideForSpace) {
return null;
}
const keyPath = key.split('.');
let currentObj = plContent;
for (const prop of keyPath) {
if (currentObj === undefined) {
break;
const powerSelectors = Object.keys(powerLevelDescriptors)
.map((key, index) => {
const descriptor = powerLevelDescriptors[key];
if (isSpaceRoom && descriptor.hideForSpace) {
return null;
}
currentObj = currentObj[prop];
}
const value = parseIntWithDefault(currentObj, descriptor.defaultValue);
return <div key={index} className="">
<PowerSelector
label={descriptor.desc}
value={value}
usersDefault={defaultUserLevel}
disabled={!canChangeLevels || currentUserLevel < value}
powerLevelKey={key} // Will be sent as the second parameter to `onChange`
onChange={this.onPowerLevelsChanged}
/>
</div>;
}).filter(Boolean);
const keyPath = key.split(".");
let currentObj = plContent;
for (const prop of keyPath) {
if (currentObj === undefined) {
break;
}
currentObj = currentObj[prop];
}
const value = parseIntWithDefault(currentObj, descriptor.defaultValue);
return (
<div key={index} className="">
<PowerSelector
label={descriptor.desc}
value={value}
usersDefault={defaultUserLevel}
disabled={!canChangeLevels || currentUserLevel < value}
powerLevelKey={key} // Will be sent as the second parameter to `onChange`
onChange={this.onPowerLevelsChanged}
/>
</div>
);
})
.filter(Boolean);
// hide the power level selector for enabling E2EE if it the room is already encrypted
if (client.isRoomEncrypted(this.props.roomId)) {
delete eventsLevels[EventType.RoomEncryption];
}
const eventPowerSelectors = Object.keys(eventsLevels).map((eventType, i) => {
if (isSpaceRoom && plEventsToShow[eventType]?.hideForSpace) {
return null;
} else if (!isSpaceRoom && plEventsToShow[eventType]?.hideForRoom) {
return null;
}
const eventPowerSelectors = Object.keys(eventsLevels)
.map((eventType, i) => {
if (isSpaceRoom && plEventsToShow[eventType]?.hideForSpace) {
return null;
} else if (!isSpaceRoom && plEventsToShow[eventType]?.hideForRoom) {
return null;
}
let label = plEventsToLabels[eventType];
if (label) {
const brand = SdkConfig.get("element_call").brand ?? DEFAULTS.element_call.brand;
label = _t(label, { brand });
} else {
label = _t("Send %(eventType)s events", { eventType });
}
return (
<div className="" key={eventType}>
<PowerSelector
label={label}
value={eventsLevels[eventType]}
usersDefault={defaultUserLevel}
disabled={!canChangeLevels || currentUserLevel < eventsLevels[eventType]}
powerLevelKey={"event_levels_" + eventType}
onChange={this.onPowerLevelsChanged}
/>
</div>
);
}).filter(Boolean);
let label = plEventsToLabels[eventType];
if (label) {
const brand = SdkConfig.get("element_call").brand ?? DEFAULTS.element_call.brand;
label = _t(label, { brand });
} else {
label = _t("Send %(eventType)s events", { eventType });
}
return (
<div className="" key={eventType}>
<PowerSelector
label={label}
value={eventsLevels[eventType]}
usersDefault={defaultUserLevel}
disabled={!canChangeLevels || currentUserLevel < eventsLevels[eventType]}
powerLevelKey={"event_levels_" + eventType}
onChange={this.onPowerLevelsChanged}
/>
</div>
);
})
.filter(Boolean);
return (
<div className="mx_SettingsTab mx_RolesRoomSettingsTab">
<div className="mx_SettingsTab_heading">{ _t("Roles & Permissions") }</div>
{ privilegedUsersSection }
{
(canChangeLevels && room !== null) && (
<AddPrivilegedUsers room={room} defaultUserLevel={defaultUserLevel} />
)
}
{ mutedUsersSection }
{ bannedUsersSection }
<div className="mx_SettingsTab_heading">{_t("Roles & Permissions")}</div>
{privilegedUsersSection}
{canChangeLevels && room !== null && (
<AddPrivilegedUsers room={room} defaultUserLevel={defaultUserLevel} />
)}
{mutedUsersSection}
{bannedUsersSection}
<SettingsFieldset
legend={_t("Permissions")}
description={
isSpaceRoom
? _t('Select the roles required to change various parts of the space')
: _t('Select the roles required to change various parts of the room')
? _t("Select the roles required to change various parts of the space")
: _t("Select the roles required to change various parts of the room")
}
>
{ powerSelectors }
{ eventPowerSelectors }
{powerSelectors}
{eventPowerSelectors}
</SettingsFieldset>
</div>
);

View file

@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { GuestAccess, HistoryVisibility, JoinRule } from "matrix-js-sdk/src/@types/partials";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { EventType } from 'matrix-js-sdk/src/@types/event';
import { EventType } from "matrix-js-sdk/src/@types/event";
import { logger } from "matrix-js-sdk/src/logger";
import { Icon as WarningIcon } from "../../../../../../res/img/warning.svg";
@ -26,18 +26,18 @@ import { _t } from "../../../../../languageHandler";
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
import Modal from "../../../../../Modal";
import QuestionDialog from "../../../dialogs/QuestionDialog";
import StyledRadioGroup from '../../../elements/StyledRadioGroup';
import StyledRadioGroup from "../../../elements/StyledRadioGroup";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import SettingsStore from "../../../../../settings/SettingsStore";
import { UIFeature } from "../../../../../settings/UIFeature";
import AccessibleButton from "../../../elements/AccessibleButton";
import SettingsFlag from '../../../elements/SettingsFlag';
import createRoom, { IOpts } from '../../../../../createRoom';
import CreateRoomDialog from '../../../dialogs/CreateRoomDialog';
import SettingsFlag from "../../../elements/SettingsFlag";
import createRoom, { IOpts } from "../../../../../createRoom";
import CreateRoomDialog from "../../../dialogs/CreateRoomDialog";
import JoinRuleSettings from "../../JoinRuleSettings";
import ErrorDialog from "../../../dialogs/ErrorDialog";
import SettingsFieldset from '../../SettingsFieldset';
import ExternalLink from '../../../elements/ExternalLink';
import SettingsFieldset from "../../SettingsFieldset";
import ExternalLink from "../../../elements/ExternalLink";
import PosthogTrackers from "../../../../../PosthogTrackers";
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
@ -66,12 +66,12 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
this.state = {
guestAccess: this.pullContentPropertyFromEvent<GuestAccess>(
state.getStateEvents(EventType.RoomGuestAccess, ""),
'guest_access',
"guest_access",
GuestAccess.Forbidden,
),
history: this.pullContentPropertyFromEvent<HistoryVisibility>(
state.getStateEvents(EventType.RoomHistoryVisibility, ""),
'history_visibility',
"history_visibility",
HistoryVisibility.Shared,
),
hasAliases: false, // async loaded in componentDidMount
@ -82,7 +82,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
componentDidMount() {
this.context.on(RoomStateEvent.Events, this.onStateEvent);
this.hasAliases().then(hasAliases => this.setState({ hasAliases }));
this.hasAliases().then((hasAliases) => this.setState({ hasAliases }));
}
private pullContentPropertyFromEvent<T>(event: MatrixEvent, key: string, defaultValue: T): T {
@ -106,31 +106,45 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
private onEncryptionChange = async () => {
if (this.context.getRoom(this.props.roomId)?.getJoinRule() === JoinRule.Public) {
const dialog = Modal.createDialog(QuestionDialog, {
title: _t('Are you sure you want to add encryption to this public room?'),
description: <div>
<p> { _t(
"<b>It's not recommended to add encryption to public rooms.</b> " +
"Anyone can find and join public rooms, so anyone can read messages in them. " +
"You'll get none of the benefits of encryption, and you won't be able to turn it " +
"off later. Encrypting messages in a public room will make receiving and sending " +
"messages slower.",
null,
{ "b": (sub) => <b>{ sub }</b> },
) } </p>
<p> { _t(
"To avoid these issues, create a <a>new encrypted room</a> for " +
"the conversation you plan to have.",
null,
{
"a": (sub) => <AccessibleButton kind='link_inline'
onClick={() => {
dialog.close();
this.createNewRoom(false, true);
}}> { sub } </AccessibleButton>,
},
) } </p>
</div>,
title: _t("Are you sure you want to add encryption to this public room?"),
description: (
<div>
<p>
{" "}
{_t(
"<b>It's not recommended to add encryption to public rooms.</b> " +
"Anyone can find and join public rooms, so anyone can read messages in them. " +
"You'll get none of the benefits of encryption, and you won't be able to turn it " +
"off later. Encrypting messages in a public room will make receiving and sending " +
"messages slower.",
null,
{ b: (sub) => <b>{sub}</b> },
)}{" "}
</p>
<p>
{" "}
{_t(
"To avoid these issues, create a <a>new encrypted room</a> for " +
"the conversation you plan to have.",
null,
{
a: (sub) => (
<AccessibleButton
kind="link_inline"
onClick={() => {
dialog.close();
this.createNewRoom(false, true);
}}
>
{" "}
{sub}{" "}
</AccessibleButton>
),
},
)}{" "}
</p>
</div>
),
});
const { finished } = dialog;
@ -139,16 +153,14 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
}
Modal.createDialog(QuestionDialog, {
title: _t('Enable encryption?'),
title: _t("Enable encryption?"),
description: _t(
"Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted " +
"room cannot be seen by the server, only by the participants of the room. Enabling encryption " +
"may prevent many bots and bridges from working correctly. <a>Learn more about encryption.</a>",
"room cannot be seen by the server, only by the participants of the room. Enabling encryption " +
"may prevent many bots and bridges from working correctly. <a>Learn more about encryption.</a>",
{},
{
a: sub => <ExternalLink
href="https://element.io/help#encryption"
>{ sub }</ExternalLink>,
a: (sub) => <ExternalLink href="https://element.io/help#encryption">{sub}</ExternalLink>,
},
),
onFinished: (confirm) => {
@ -159,13 +171,12 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
const beforeEncrypted = this.state.encrypted;
this.setState({ encrypted: true });
this.context.sendStateEvent(
this.props.roomId, EventType.RoomEncryption,
{ algorithm: "m.megolm.v1.aes-sha2" },
).catch((e) => {
logger.error(e);
this.setState({ encrypted: beforeEncrypted });
});
this.context
.sendStateEvent(this.props.roomId, EventType.RoomEncryption, { algorithm: "m.megolm.v1.aes-sha2" })
.catch((e) => {
logger.error(e);
this.setState({ encrypted: beforeEncrypted });
});
},
});
};
@ -177,19 +188,23 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
this.setState({ guestAccess });
this.context.sendStateEvent(this.props.roomId, EventType.RoomGuestAccess, {
guest_access: guestAccess,
}, "").catch((e) => {
logger.error(e);
this.setState({ guestAccess: beforeGuestAccess });
});
this.context
.sendStateEvent(
this.props.roomId,
EventType.RoomGuestAccess,
{
guest_access: guestAccess,
},
"",
)
.catch((e) => {
logger.error(e);
this.setState({ guestAccess: beforeGuestAccess });
});
};
private createNewRoom = async (defaultPublic: boolean, defaultEncrypted: boolean) => {
const modal = Modal.createDialog<[boolean, IOpts]>(
CreateRoomDialog,
{ defaultPublic, defaultEncrypted },
);
const modal = Modal.createDialog<[boolean, IOpts]>(CreateRoomDialog, { defaultPublic, defaultEncrypted });
PosthogTrackers.trackInteraction("WebRoomSettingsSecurityTabCreateNewRoomButton");
@ -205,12 +220,19 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
if (beforeHistory === history) return;
this.setState({ history: history });
this.context.sendStateEvent(this.props.roomId, EventType.RoomHistoryVisibility, {
history_visibility: history,
}, "").catch((e) => {
logger.error(e);
this.setState({ history: beforeHistory });
});
this.context
.sendStateEvent(
this.props.roomId,
EventType.RoomHistoryVisibility,
{
history_visibility: history,
},
"",
)
.catch((e) => {
logger.error(e);
this.setState({ history: beforeHistory });
});
};
private updateBlacklistDevicesFlag = (checked: boolean) => {
@ -231,11 +253,9 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
let aliasWarning = null;
if (room.getJoinRule() === JoinRule.Public && !this.state.hasAliases) {
aliasWarning = (
<div className='mx_SecurityRoomSettingsTab_warning'>
<div className="mx_SecurityRoomSettingsTab_warning">
<WarningIcon width={15} height={15} />
<span>
{ _t("To link to this room, please add an address.") }
</span>
<span>{_t("To link to this room, please add an address.")}</span>
</div>
);
}
@ -243,16 +263,18 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
roomName: room?.name,
});
return <SettingsFieldset legend={_t("Access")} description={description}>
<JoinRuleSettings
room={room}
beforeChange={this.onBeforeJoinRuleChange}
onError={this.onJoinRuleChangeError}
closeSettingsFn={this.props.closeSettingsFn}
promptUpgrade={true}
aliasWarning={aliasWarning}
/>
</SettingsFieldset>;
return (
<SettingsFieldset legend={_t("Access")} description={description}>
<JoinRuleSettings
room={room}
beforeChange={this.onBeforeJoinRuleChange}
onError={this.onJoinRuleChangeError}
closeSettingsFn={this.props.closeSettingsFn}
promptUpgrade={true}
aliasWarning={aliasWarning}
/>
</SettingsFieldset>
);
}
private onJoinRuleChangeError = (error: Error) => {
@ -266,29 +288,43 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
if (this.state.encrypted && joinRule === JoinRule.Public) {
const dialog = Modal.createDialog(QuestionDialog, {
title: _t("Are you sure you want to make this encrypted room public?"),
description: <div>
<p> { _t(
"<b>It's not recommended to make encrypted rooms public.</b> " +
"It will mean anyone can find and join the room, so anyone can read messages. " +
"You'll get none of the benefits of encryption. Encrypting messages in a public " +
"room will make receiving and sending messages slower.",
null,
{ "b": (sub) => <b>{ sub }</b> },
) } </p>
<p> { _t(
"To avoid these issues, create a <a>new public room</a> for the conversation " +
"you plan to have.",
null,
{
"a": (sub) => <AccessibleButton
kind='link_inline'
onClick={() => {
dialog.close();
this.createNewRoom(true, false);
}}> { sub } </AccessibleButton>,
},
) } </p>
</div>,
description: (
<div>
<p>
{" "}
{_t(
"<b>It's not recommended to make encrypted rooms public.</b> " +
"It will mean anyone can find and join the room, so anyone can read messages. " +
"You'll get none of the benefits of encryption. Encrypting messages in a public " +
"room will make receiving and sending messages slower.",
null,
{ b: (sub) => <b>{sub}</b> },
)}{" "}
</p>
<p>
{" "}
{_t(
"To avoid these issues, create a <a>new public room</a> for the conversation " +
"you plan to have.",
null,
{
a: (sub) => (
<AccessibleButton
kind="link_inline"
onClick={() => {
dialog.close();
this.createNewRoom(true, false);
}}
>
{" "}
{sub}{" "}
</AccessibleButton>
),
},
)}{" "}
</p>
</div>
),
});
const { finished } = dialog;
@ -312,15 +348,15 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
const options = [
{
value: HistoryVisibility.Shared,
label: _t('Members only (since the point in time of selecting this option)'),
label: _t("Members only (since the point in time of selecting this option)"),
},
{
value: HistoryVisibility.Invited,
label: _t('Members only (since they were invited)'),
label: _t("Members only (since they were invited)"),
},
{
value: HistoryVisibility.Joined,
label: _t('Members only (since they joined)'),
label: _t("Members only (since they joined)"),
},
];
@ -332,18 +368,22 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
});
}
const description = _t('Changes to who can read history will only apply to future messages in this room. ' +
'The visibility of existing history will be unchanged.');
const description = _t(
"Changes to who can read history will only apply to future messages in this room. " +
"The visibility of existing history will be unchanged.",
);
return (<SettingsFieldset legend={_t("Who can read history?")} description={description}>
<StyledRadioGroup
name="historyVis"
value={history}
onChange={this.onHistoryRadioToggle}
disabled={!canChangeHistory}
definitions={options}
/>
</SettingsFieldset>);
return (
<SettingsFieldset legend={_t("Who can read history?")} description={description}>
<StyledRadioGroup
name="historyVis"
value={history}
onChange={this.onHistoryRadioToggle}
disabled={!canChangeHistory}
definitions={options}
/>
</SettingsFieldset>
);
}
private toggleAdvancedSection = () => {
@ -356,18 +396,22 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
const state = client.getRoom(this.props.roomId).currentState;
const canSetGuestAccess = state.mayClientSendStateEvent(EventType.RoomGuestAccess, client);
return <>
<LabelledToggleSwitch
value={guestAccess === GuestAccess.CanJoin}
onChange={this.onGuestAccessChange}
disabled={!canSetGuestAccess}
label={_t("Enable guest access")}
/>
<p>
{ _t("People with supported clients will be able to join " +
"the room without having a registered account.") }
</p>
</>;
return (
<>
<LabelledToggleSwitch
value={guestAccess === GuestAccess.CanJoin}
onChange={this.onGuestAccessChange}
disabled={!canSetGuestAccess}
label={_t("Enable guest access")}
/>
<p>
{_t(
"People with supported clients will be able to join " +
"the room without having a registered account.",
)}
</p>
</>
);
}
render() {
@ -379,12 +423,14 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
let encryptionSettings = null;
if (isEncrypted && SettingsStore.isEnabled("blacklistUnverifiedDevices")) {
encryptionSettings = <SettingsFlag
name="blacklistUnverifiedDevices"
level={SettingLevel.ROOM_DEVICE}
onChange={this.updateBlacklistDevicesFlag}
roomId={this.props.roomId}
/>;
encryptionSettings = (
<SettingsFlag
name="blacklistUnverifiedDevices"
level={SettingLevel.ROOM_DEVICE}
onChange={this.updateBlacklistDevicesFlag}
roomId={this.props.roomId}
/>
);
}
const historySection = this.renderHistory();
@ -398,31 +444,34 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
kind="link"
className="mx_SettingsTab_showAdvanced"
>
{ this.state.showAdvancedSection ? _t("Hide advanced") : _t("Show advanced") }
{this.state.showAdvancedSection ? _t("Hide advanced") : _t("Show advanced")}
</AccessibleButton>
{ this.state.showAdvancedSection && this.renderAdvanced() }
{this.state.showAdvancedSection && this.renderAdvanced()}
</div>
);
}
return (
<div className="mx_SettingsTab mx_SecurityRoomSettingsTab">
<div className="mx_SettingsTab_heading">{ _t("Security & Privacy") }</div>
<div className="mx_SettingsTab_heading">{_t("Security & Privacy")}</div>
<SettingsFieldset legend={_t("Encryption")} description={_t("Once enabled, encryption cannot be disabled.")}>
<SettingsFieldset
legend={_t("Encryption")}
description={_t("Once enabled, encryption cannot be disabled.")}
>
<LabelledToggleSwitch
value={isEncrypted}
onChange={this.onEncryptionChange}
label={_t("Encrypted")}
disabled={!canEnableEncryption}
/>
{ encryptionSettings }
{encryptionSettings}
</SettingsFieldset>
{ this.renderJoinRule() }
{this.renderJoinRule()}
{ advanced }
{ historySection }
{advanced}
{historySection}
</div>
);
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useMemo, useState } from "react";
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import { EventType } from "matrix-js-sdk/src/@types/event";
@ -34,56 +34,63 @@ interface ElementCallSwitchProps {
const ElementCallSwitch: React.FC<ElementCallSwitchProps> = ({ roomId }) => {
const room = useMemo(() => MatrixClientPeg.get().getRoom(roomId), [roomId]);
const isPublic = useMemo(() => room.getJoinRule() === JoinRule.Public, [room]);
const [content, events, maySend] = useRoomState(room, useCallback((state) => {
const content = state?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent();
return [
content ?? {},
content?.["events"] ?? {},
state?.maySendStateEvent(EventType.RoomPowerLevels, MatrixClientPeg.get().getUserId()),
];
}, []));
const [content, events, maySend] = useRoomState(
room,
useCallback((state) => {
const content = state?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent();
return [
content ?? {},
content?.["events"] ?? {},
state?.maySendStateEvent(EventType.RoomPowerLevels, MatrixClientPeg.get().getUserId()),
];
}, []),
);
const [elementCallEnabled, setElementCallEnabled] = useState<boolean>(() => {
return events[ElementCall.MEMBER_EVENT_TYPE.name] === 0;
});
const onChange = useCallback((enabled: boolean): void => {
setElementCallEnabled(enabled);
const onChange = useCallback(
(enabled: boolean): void => {
setElementCallEnabled(enabled);
if (enabled) {
const userLevel = events[EventType.RoomMessage] ?? content.users_default ?? 0;
const moderatorLevel = content.kick ?? 50;
if (enabled) {
const userLevel = events[EventType.RoomMessage] ?? content.users_default ?? 0;
const moderatorLevel = content.kick ?? 50;
events[ElementCall.CALL_EVENT_TYPE.name] = isPublic ? moderatorLevel : userLevel;
events[ElementCall.MEMBER_EVENT_TYPE.name] = userLevel;
} else {
const adminLevel = events[EventType.RoomPowerLevels] ?? content.state_default ?? 100;
events[ElementCall.CALL_EVENT_TYPE.name] = isPublic ? moderatorLevel : userLevel;
events[ElementCall.MEMBER_EVENT_TYPE.name] = userLevel;
} else {
const adminLevel = events[EventType.RoomPowerLevels] ?? content.state_default ?? 100;
events[ElementCall.CALL_EVENT_TYPE.name] = adminLevel;
events[ElementCall.MEMBER_EVENT_TYPE.name] = adminLevel;
}
events[ElementCall.CALL_EVENT_TYPE.name] = adminLevel;
events[ElementCall.MEMBER_EVENT_TYPE.name] = adminLevel;
}
MatrixClientPeg.get().sendStateEvent(roomId, EventType.RoomPowerLevels, {
"events": events,
...content,
});
}, [roomId, content, events, isPublic]);
MatrixClientPeg.get().sendStateEvent(roomId, EventType.RoomPowerLevels, {
events: events,
...content,
});
},
[roomId, content, events, isPublic],
);
const brand = SdkConfig.get("element_call").brand ?? DEFAULTS.element_call.brand;
return <LabelledToggleSwitch
data-testid="element-call-switch"
label={_t("Enable %(brand)s as an additional calling option in this room", { brand })}
caption={_t(
"%(brand)s is end-to-end encrypted, " +
"but is currently limited to smaller numbers of users.",
{ brand },
)}
value={elementCallEnabled}
onChange={onChange}
disabled={!maySend}
tooltip={_t("You do not have sufficient permissions to change this.")}
/>;
return (
<LabelledToggleSwitch
data-testid="element-call-switch"
label={_t("Enable %(brand)s as an additional calling option in this room", { brand })}
caption={_t(
"%(brand)s is end-to-end encrypted, " + "but is currently limited to smaller numbers of users.",
{ brand },
)}
value={elementCallEnabled}
onChange={onChange}
disabled={!maySend}
tooltip={_t("You do not have sufficient permissions to change this.")}
/>
);
};
interface Props {
@ -91,9 +98,11 @@ interface Props {
}
export const VoipRoomSettingsTab: React.FC<Props> = ({ roomId }) => {
return <SettingsTab heading={_t("Voice & Video")}>
<SettingsSubsection heading={_t("Call type")}>
<ElementCallSwitch roomId={roomId} />
</SettingsSubsection>
</SettingsTab>;
return (
<SettingsTab heading={_t("Voice & Video")}>
<SettingsSubsection heading={_t("Call type")}>
<ElementCallSwitch roomId={roomId} />
</SettingsSubsection>
</SettingsTab>
);
};

View file

@ -15,25 +15,24 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { _t } from "../../../../../languageHandler";
import SdkConfig from "../../../../../SdkConfig";
import { MatrixClientPeg } from '../../../../../MatrixClientPeg';
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import SettingsStore from "../../../../../settings/SettingsStore";
import SettingsFlag from '../../../elements/SettingsFlag';
import Field from '../../../elements/Field';
import SettingsFlag from "../../../elements/SettingsFlag";
import Field from "../../../elements/Field";
import AccessibleButton from "../../../elements/AccessibleButton";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import { UIFeature } from "../../../../../settings/UIFeature";
import { Layout } from "../../../../../settings/enums/Layout";
import LayoutSwitcher from "../../LayoutSwitcher";
import FontScalingPanel from '../../FontScalingPanel';
import ThemeChoicePanel from '../../ThemeChoicePanel';
import FontScalingPanel from "../../FontScalingPanel";
import ThemeChoicePanel from "../../ThemeChoicePanel";
import ImageSizePanel from "../../ImageSizePanel";
interface IProps {
}
interface IProps {}
interface IState {
useSystemFont: boolean;
@ -91,12 +90,11 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
if (!SettingsStore.getValue(UIFeature.AdvancedSettings)) return null;
const brand = SdkConfig.get().brand;
const toggle = <AccessibleButton
kind="link"
onClick={() => this.setState({ showAdvanced: !this.state.showAdvanced })}
>
{ this.state.showAdvanced ? _t("Hide advanced") : _t("Show advanced") }
</AccessibleButton>;
const toggle = (
<AccessibleButton kind="link" onClick={() => this.setState({ showAdvanced: !this.state.showAdvanced })}>
{this.state.showAdvanced ? _t("Hide advanced") : _t("Show advanced")}
</AccessibleButton>
);
let advanced: React.ReactNode;
@ -105,36 +103,40 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
"Set the name of a font installed on your system & %(brand)s will attempt to use it.",
{ brand },
);
advanced = <>
<SettingsFlag name="useCompactLayout" level={SettingLevel.DEVICE} useCheckbox={true} />
advanced = (
<>
<SettingsFlag name="useCompactLayout" level={SettingLevel.DEVICE} useCheckbox={true} />
<SettingsFlag
name="useSystemFont"
level={SettingLevel.DEVICE}
useCheckbox={true}
onChange={(checked) => this.setState({ useSystemFont: checked })}
/>
<Field
className="mx_AppearanceUserSettingsTab_systemFont"
label={SettingsStore.getDisplayName("systemFont")}
onChange={(value) => {
this.setState({
systemFont: value.target.value,
});
<SettingsFlag
name="useSystemFont"
level={SettingLevel.DEVICE}
useCheckbox={true}
onChange={(checked) => this.setState({ useSystemFont: checked })}
/>
<Field
className="mx_AppearanceUserSettingsTab_systemFont"
label={SettingsStore.getDisplayName("systemFont")}
onChange={(value) => {
this.setState({
systemFont: value.target.value,
});
SettingsStore.setValue("systemFont", null, SettingLevel.DEVICE, value.target.value);
}}
tooltipContent={tooltipContent}
forceTooltipVisible={true}
disabled={!this.state.useSystemFont}
value={this.state.systemFont}
/>
</>;
SettingsStore.setValue("systemFont", null, SettingLevel.DEVICE, value.target.value);
}}
tooltipContent={tooltipContent}
forceTooltipVisible={true}
disabled={!this.state.useSystemFont}
value={this.state.systemFont}
/>
</>
);
}
return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_Advanced">
{ toggle }
{ advanced }
</div>;
return (
<div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_Advanced">
{toggle}
{advanced}
</div>
);
}
render() {
@ -142,9 +144,9 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
return (
<div className="mx_SettingsTab mx_AppearanceUserSettingsTab">
<div className="mx_SettingsTab_heading">{ _t("Customise your appearance") }</div>
<div className="mx_SettingsTab_heading">{_t("Customise your appearance")}</div>
<div className="mx_SettingsTab_subsectionText">
{ _t("Appearance Settings only affect this %(brand)s session.", { brand }) }
{_t("Appearance Settings only affect this %(brand)s session.", { brand })}
</div>
<ThemeChoicePanel />
<LayoutSwitcher
@ -155,7 +157,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
onLayoutChanged={this.onLayoutChanged}
/>
<FontScalingPanel />
{ this.renderAdvancedSection() }
{this.renderAdvancedSection()}
<ImageSizePanel />
</div>
);

View file

@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types";
import { IThreepid } from "matrix-js-sdk/src/@types/threepids";
import { logger } from "matrix-js-sdk/src/logger";
@ -36,7 +36,7 @@ import dis from "../../../../../dispatcher/dispatcher";
import { Policies, Service, startTermsFlow } from "../../../../../Terms";
import IdentityAuthClient from "../../../../../IdentityAuthClient";
import { abbreviateUrl } from "../../../../../utils/UrlUtils";
import { getThreepidsWithBindStatus } from '../../../../../boundThreepids';
import { getThreepidsWithBindStatus } from "../../../../../boundThreepids";
import Spinner from "../../../elements/Spinner";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import { UIFeature } from "../../../../../settings/UIFeature";
@ -64,7 +64,8 @@ interface IState {
haveIdServer: boolean;
serverSupportsSeparateAddAndBind: boolean;
idServerHasUnsignedTerms: boolean;
requiredPolicyInfo: { // This object is passed along to a component for handling
requiredPolicyInfo: {
// This object is passed along to a component for handling
hasTerms: boolean;
policiesAndServices: {
service: Service;
@ -93,11 +94,12 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()),
serverSupportsSeparateAddAndBind: null,
idServerHasUnsignedTerms: false,
requiredPolicyInfo: { // This object is passed along to a component for handling
requiredPolicyInfo: {
// This object is passed along to a component for handling
hasTerms: false,
policiesAndServices: null, // From the startTermsFlow callback
agreedUrls: null, // From the startTermsFlow callback
resolve: null, // Promise resolve function for startTermsFlow callback
agreedUrls: null, // From the startTermsFlow callback
resolve: null, // Promise resolve function for startTermsFlow callback
},
emails: [],
msisdns: [],
@ -132,7 +134,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
}
private onAction = (payload: ActionPayload): void => {
if (payload.action === 'id_server_changed') {
if (payload.action === "id_server_changed") {
this.setState({ haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()) });
this.getThreepidState();
}
@ -152,12 +154,12 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
const serverSupportsSeparateAddAndBind = await cli.doesServerSupportSeparateAddAndBind();
const capabilities = await cli.getCapabilities(); // this is cached
const changePasswordCap = capabilities['m.change_password'];
const changePasswordCap = capabilities["m.change_password"];
// You can change your password so long as the capability isn't explicitly disabled. The implicit
// behaviour is you can change your password when the capability is missing or has not-false as
// the enabled flag value.
const canChangePassword = !changePasswordCap || changePasswordCap['enabled'] !== false;
const canChangePassword = !changePasswordCap || changePasswordCap["enabled"] !== false;
this.setState({ serverSupportsSeparateAddAndBind, canChangePassword });
}
@ -176,14 +178,13 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
} catch (e) {
const idServerUrl = MatrixClientPeg.get().getIdentityServerUrl();
logger.warn(
`Unable to reach identity server at ${idServerUrl} to check ` +
`for 3PIDs bindings in Settings`,
`Unable to reach identity server at ${idServerUrl} to check ` + `for 3PIDs bindings in Settings`,
);
logger.warn(e);
}
this.setState({
emails: threepids.filter((a) => a.medium === 'email'),
msisdns: threepids.filter((a) => a.medium === 'msisdn'),
emails: threepids.filter((a) => a.medium === "email"),
msisdns: threepids.filter((a) => a.medium === "msisdn"),
loading3pids: false,
});
}
@ -200,23 +201,22 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
const authClient = new IdentityAuthClient();
try {
const idAccessToken = await authClient.getAccessToken({ check: false });
await startTermsFlow([new Service(
SERVICE_TYPES.IS,
idServerUrl,
idAccessToken,
)], (policiesAndServices, agreedUrls, extraClassNames) => {
return new Promise((resolve, reject) => {
this.setState({
idServerName: abbreviateUrl(idServerUrl),
requiredPolicyInfo: {
hasTerms: true,
policiesAndServices,
agreedUrls,
resolve,
},
await startTermsFlow(
[new Service(SERVICE_TYPES.IS, idServerUrl, idAccessToken)],
(policiesAndServices, agreedUrls, extraClassNames) => {
return new Promise((resolve, reject) => {
this.setState({
idServerName: abbreviateUrl(idServerUrl),
requiredPolicyInfo: {
hasTerms: true,
policiesAndServices,
agreedUrls,
resolve,
},
});
});
});
});
},
);
// User accepted all terms
this.setState({
requiredPolicyInfo: {
@ -225,10 +225,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
},
});
} catch (e) {
logger.warn(
`Unable to reach identity server at ${idServerUrl} to check ` +
`for terms in Settings`,
);
logger.warn(`Unable to reach identity server at ${idServerUrl} to check ` + `for terms in Settings`);
logger.warn(e);
}
}
@ -273,9 +270,8 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
private onPasswordChanged = ({ didLogoutOutOtherDevices }: { didLogoutOutOtherDevices: boolean }): void => {
let description = _t("Your password was successfully changed.");
if (didLogoutOutOtherDevices) {
description += " " + _t(
"You will not receive push notifications on other devices until you sign back in to them.",
);
description +=
" " + _t("You will not receive push notifications on other devices until you sign back in to them.");
}
// TODO: Figure out a design that doesn't involve replacing the current dialog
Modal.createDialog(ErrorDialog, {
@ -307,7 +303,8 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
rowClassName=""
buttonKind="primary"
onError={this.onPasswordChangeError}
onFinished={this.onPasswordChanged} />
onFinished={this.onPasswordChanged}
/>
);
let threepidSection = null;
@ -317,28 +314,29 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
// validate 3PID ownership even if we're just adding to the homeserver only.
// For newer homeservers with separate 3PID add and bind methods (MSC2290),
// there is no such concern, so we can always show the HS account 3PIDs.
if (SettingsStore.getValue(UIFeature.ThirdPartyID) &&
if (
SettingsStore.getValue(UIFeature.ThirdPartyID) &&
(this.state.haveIdServer || this.state.serverSupportsSeparateAddAndBind === true)
) {
const emails = this.state.loading3pids
? <Spinner />
: <AccountEmailAddresses
emails={this.state.emails}
onEmailsChange={this.onEmailsChange}
/>;
const msisdns = this.state.loading3pids
? <Spinner />
: <AccountPhoneNumbers
msisdns={this.state.msisdns}
onMsisdnsChange={this.onMsisdnsChange}
/>;
threepidSection = <div>
<span className="mx_SettingsTab_subheading">{ _t("Email addresses") }</span>
{ emails }
const emails = this.state.loading3pids ? (
<Spinner />
) : (
<AccountEmailAddresses emails={this.state.emails} onEmailsChange={this.onEmailsChange} />
);
const msisdns = this.state.loading3pids ? (
<Spinner />
) : (
<AccountPhoneNumbers msisdns={this.state.msisdns} onMsisdnsChange={this.onMsisdnsChange} />
);
threepidSection = (
<div>
<span className="mx_SettingsTab_subheading">{_t("Email addresses")}</span>
{emails}
<span className="mx_SettingsTab_subheading">{ _t("Phone numbers") }</span>
{ msisdns }
</div>;
<span className="mx_SettingsTab_subheading">{_t("Phone numbers")}</span>
{msisdns}
</div>
);
} else if (this.state.serverSupportsSeparateAddAndBind === null) {
threepidSection = <Spinner />;
}
@ -352,12 +350,10 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
return (
<div className="mx_SettingsTab_section mx_GeneralUserSettingsTab_accountSection">
<span className="mx_SettingsTab_subheading">{ _t("Account") }</span>
<p className="mx_SettingsTab_subsectionText">
{ passwordChangeText }
</p>
{ passwordChangeForm }
{ threepidSection }
<span className="mx_SettingsTab_subheading">{_t("Account")}</span>
<p className="mx_SettingsTab_subsectionText">{passwordChangeText}</p>
{passwordChangeForm}
{threepidSection}
</div>
);
}
@ -366,7 +362,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
// TODO: Convert to new-styled Field
return (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Language and region") }</span>
<span className="mx_SettingsTab_subheading">{_t("Language and region")}</span>
<LanguageDropdown
className="mx_GeneralUserSettingsTab_languageInput"
onOptionChange={this.onLanguageChange}
@ -380,29 +376,30 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
return (
<div className="mx_SettingsTab_section mx_SettingsTab_section_spellcheck">
<span className="mx_SettingsTab_subheading">
{ _t("Spell check") }
<ToggleSwitch
checked={this.state.spellCheckEnabled}
onChange={this.onSpellCheckEnabledChange}
/>
{_t("Spell check")}
<ToggleSwitch checked={this.state.spellCheckEnabled} onChange={this.onSpellCheckEnabledChange} />
</span>
{ (this.state.spellCheckEnabled && !IS_MAC) && <SpellCheckSettings
languages={this.state.spellCheckLanguages}
onLanguagesChange={this.onSpellCheckLanguagesChange}
/> }
{this.state.spellCheckEnabled && !IS_MAC && (
<SpellCheckSettings
languages={this.state.spellCheckLanguages}
onLanguagesChange={this.onSpellCheckLanguagesChange}
/>
)}
</div>
);
}
private renderDiscoverySection(): JSX.Element {
if (this.state.requiredPolicyInfo.hasTerms) {
const intro = <span className="mx_SettingsTab_subsectionText">
{ _t(
"Agree to the identity server (%(serverName)s) Terms of Service to " +
"allow yourself to be discoverable by email address or phone number.",
{ serverName: this.state.idServerName },
) }
</span>;
const intro = (
<span className="mx_SettingsTab_subsectionText">
{_t(
"Agree to the identity server (%(serverName)s) Terms of Service to " +
"allow yourself to be discoverable by email address or phone number.",
{ serverName: this.state.idServerName },
)}
</span>
);
return (
<div>
<InlineTermsAgreement
@ -411,7 +408,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
onFinished={this.state.requiredPolicyInfo.resolve}
introElement={intro}
/>
{ /* has its own heading as it includes the current identity server */ }
{/* has its own heading as it includes the current identity server */}
<SetIdServer missingTerms={true} />
</div>
);
@ -420,18 +417,20 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
const emails = this.state.loading3pids ? <Spinner /> : <DiscoveryEmailAddresses emails={this.state.emails} />;
const msisdns = this.state.loading3pids ? <Spinner /> : <DiscoveryPhoneNumbers msisdns={this.state.msisdns} />;
const threepidSection = this.state.haveIdServer ? <div className='mx_GeneralUserSettingsTab_discovery'>
<span className="mx_SettingsTab_subheading">{ _t("Email addresses") }</span>
{ emails }
const threepidSection = this.state.haveIdServer ? (
<div className="mx_GeneralUserSettingsTab_discovery">
<span className="mx_SettingsTab_subheading">{_t("Email addresses")}</span>
{emails}
<span className="mx_SettingsTab_subheading">{ _t("Phone numbers") }</span>
{ msisdns }
</div> : null;
<span className="mx_SettingsTab_subheading">{_t("Phone numbers")}</span>
{msisdns}
</div>
) : null;
return (
<div className="mx_SettingsTab_section">
{ threepidSection }
{ /* has its own heading as it includes the current identity server */ }
{threepidSection}
{/* has its own heading as it includes the current identity server */}
<SetIdServer missingTerms={false} />
</div>
);
@ -441,12 +440,12 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
// TODO: Improve warning text for account deactivation
return (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Account management") }</span>
<span className="mx_SettingsTab_subheading">{_t("Account management")}</span>
<span className="mx_SettingsTab_subsectionText">
{ _t("Deactivating your account is a permanent action — be careful!") }
{_t("Deactivating your account is a permanent action — be careful!")}
</span>
<AccessibleButton onClick={this.onDeactivateClicked} kind="danger">
{ _t("Deactivate Account") }
{_t("Deactivate Account")}
</AccessibleButton>
</div>
);
@ -457,7 +456,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
return (
<div className="mx_SettingsTab_section">
{ /* has its own heading as it includes the current integration manager */ }
{/* has its own heading as it includes the current integration manager */}
<SetIntegrationManager />
</div>
);
@ -467,42 +466,48 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
const plaf = PlatformPeg.get();
const supportsMultiLanguageSpellCheck = plaf.supportsSpellCheckSettings();
const discoWarning = this.state.requiredPolicyInfo.hasTerms
? <img
className='mx_GeneralUserSettingsTab_warningIcon'
const discoWarning = this.state.requiredPolicyInfo.hasTerms ? (
<img
className="mx_GeneralUserSettingsTab_warningIcon"
src={require("../../../../../../res/img/feather-customised/warning-triangle.svg").default}
width="18"
height="18"
alt={_t("Warning")}
/>
: null;
) : null;
let accountManagementSection;
if (SettingsStore.getValue(UIFeature.Deactivate)) {
accountManagementSection = <>
<div className="mx_SettingsTab_heading">{ _t("Deactivate account") }</div>
{ this.renderManagementSection() }
</>;
accountManagementSection = (
<>
<div className="mx_SettingsTab_heading">{_t("Deactivate account")}</div>
{this.renderManagementSection()}
</>
);
}
let discoverySection;
if (SettingsStore.getValue(UIFeature.IdentityServer)) {
discoverySection = <>
<div className="mx_SettingsTab_heading">{ discoWarning } { _t("Discovery") }</div>
{ this.renderDiscoverySection() }
</>;
discoverySection = (
<>
<div className="mx_SettingsTab_heading">
{discoWarning} {_t("Discovery")}
</div>
{this.renderDiscoverySection()}
</>
);
}
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{ _t("General") }</div>
{ this.renderProfileSection() }
{ this.renderAccountSection() }
{ this.renderLanguageSection() }
{ supportsMultiLanguageSpellCheck ? this.renderSpellCheckSection() : null }
{ discoverySection }
{ this.renderIntegrationManagerSection() /* Has its own title */ }
{ accountManagementSection }
<div className="mx_SettingsTab_heading">{_t("General")}</div>
{this.renderProfileSection()}
{this.renderAccountSection()}
{this.renderLanguageSection()}
{supportsMultiLanguageSpellCheck ? this.renderSpellCheckSection() : null}
{discoverySection}
{this.renderIntegrationManagerSection() /* Has its own title */}
{accountManagementSection}
</div>
);
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { logger } from "matrix-js-sdk/src/logger";
import AccessibleButton from "../../../elements/AccessibleButton";
@ -25,7 +25,7 @@ import createRoom from "../../../../../createRoom";
import Modal from "../../../../../Modal";
import PlatformPeg from "../../../../../PlatformPeg";
import UpdateCheckButton from "../../UpdateCheckButton";
import BugReportDialog from '../../../dialogs/BugReportDialog';
import BugReportDialog from "../../../dialogs/BugReportDialog";
import { OpenToTabPayload } from "../../../../../dispatcher/payloads/OpenToTabPayload";
import { Action } from "../../../../../dispatcher/actions";
import { UserTab } from "../../../dialogs/UserTab";
@ -52,21 +52,27 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
}
componentDidMount(): void {
PlatformPeg.get().getAppVersion().then((ver) => this.setState({ appVersion: ver })).catch((e) => {
logger.error("Error getting vector version: ", e);
});
PlatformPeg.get().canSelfUpdate().then((v) => this.setState({ canUpdate: v })).catch((e) => {
logger.error("Error getting self updatability: ", e);
});
PlatformPeg.get()
.getAppVersion()
.then((ver) => this.setState({ appVersion: ver }))
.catch((e) => {
logger.error("Error getting vector version: ", e);
});
PlatformPeg.get()
.canSelfUpdate()
.then((v) => this.setState({ canUpdate: v }))
.catch((e) => {
logger.error("Error getting self updatability: ", e);
});
}
private getVersionInfo(): { appVersion: string, olmVersion: string } {
private getVersionInfo(): { appVersion: string; olmVersion: string } {
const brand = SdkConfig.get().brand;
const appVersion = this.state.appVersion || 'unknown';
const appVersion = this.state.appVersion || "unknown";
const olmVersionTuple = MatrixClientPeg.get().olmVersion;
const olmVersion = olmVersionTuple
? `${olmVersionTuple[0]}.${olmVersionTuple[1]}.${olmVersionTuple[2]}`
: '<not-enabled>';
: "<not-enabled>";
return {
appVersion: `${_t("%(brand)s version:", { brand })} ${appVersion}`,
@ -81,9 +87,11 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
// stopping in the middle of the logs.
logger.log("Clear cache & reload clicked");
MatrixClientPeg.get().stopClient();
MatrixClientPeg.get().store.deleteAllData().then(() => {
PlatformPeg.get().reload();
});
MatrixClientPeg.get()
.store.deleteAllData()
.then(() => {
PlatformPeg.get().reload();
});
};
private onBugReport = (e) => {
@ -104,17 +112,19 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
const legalLinks = [];
for (const tocEntry of tocLinks) {
legalLinks.push(<div key={tocEntry.url}>
<a href={tocEntry.url} rel="noreferrer noopener" target="_blank">{ tocEntry.text }</a>
</div>);
legalLinks.push(
<div key={tocEntry.url}>
<a href={tocEntry.url} rel="noreferrer noopener" target="_blank">
{tocEntry.text}
</a>
</div>,
);
}
return (
<div className='mx_SettingsTab_section'>
<span className='mx_SettingsTab_subheading'>{ _t("Legal") }</span>
<div className='mx_SettingsTab_subsectionText'>
{ legalLinks }
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Legal")}</span>
<div className="mx_SettingsTab_subsectionText">{legalLinks}</div>
</div>
);
}
@ -123,43 +133,61 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
// Note: This is not translated because it is legal text.
// Also, &nbsp; is ugly but necessary.
return (
<div className='mx_SettingsTab_section'>
<span className='mx_SettingsTab_subheading'>{ _t("Credits") }</span>
<ul className='mx_SettingsTab_subsectionText'>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Credits")}</span>
<ul className="mx_SettingsTab_subsectionText">
<li>
The <a href="themes/element/img/backgrounds/lake.jpg" rel="noreferrer noopener" target="_blank">
The{" "}
<a href="themes/element/img/backgrounds/lake.jpg" rel="noreferrer noopener" target="_blank">
default cover photo
</a> is ©&nbsp;
</a>{" "}
is ©&nbsp;
<a href="https://www.flickr.com/golan" rel="noreferrer noopener" target="_blank">
Jesús Roncero
</a> used under the terms of&nbsp;
<a href="https://creativecommons.org/licenses/by-sa/4.0/" rel="noreferrer noopener" target="_blank">
CC-BY-SA 4.0
</a>.
</li>
<li>
The <a
href="https://github.com/matrix-org/twemoji-colr"
</a>{" "}
used under the terms of&nbsp;
<a
href="https://creativecommons.org/licenses/by-sa/4.0/"
rel="noreferrer noopener"
target="_blank"
>
twemoji-colr
</a> font is ©&nbsp;
<a href="https://mozilla.org" rel="noreferrer noopener" target="_blank">
Mozilla Foundation
</a> used under the terms of&nbsp;
<a href="https://www.apache.org/licenses/LICENSE-2.0" rel="noreferrer noopener" target="_blank">Apache 2.0</a>.
CC-BY-SA 4.0
</a>
.
</li>
<li>
The <a href="https://twemoji.twitter.com/" rel="noreferrer noopener" target="_blank">
The{" "}
<a href="https://github.com/matrix-org/twemoji-colr" rel="noreferrer noopener" target="_blank">
twemoji-colr
</a>{" "}
font is ©&nbsp;
<a href="https://mozilla.org" rel="noreferrer noopener" target="_blank">
Mozilla Foundation
</a>{" "}
used under the terms of&nbsp;
<a href="https://www.apache.org/licenses/LICENSE-2.0" rel="noreferrer noopener" target="_blank">
Apache 2.0
</a>
.
</li>
<li>
The{" "}
<a href="https://twemoji.twitter.com/" rel="noreferrer noopener" target="_blank">
Twemoji
</a> emoji art is ©&nbsp;
</a>{" "}
emoji art is ©&nbsp;
<a href="https://twemoji.twitter.com/" rel="noreferrer noopener" target="_blank">
Twitter, Inc and other contributors
</a> used under the terms of&nbsp;
<a href="https://creativecommons.org/licenses/by/4.0/" rel="noreferrer noopener" target="_blank">
</a>{" "}
used under the terms of&nbsp;
<a
href="https://creativecommons.org/licenses/by/4.0/"
rel="noreferrer noopener"
target="_blank"
>
CC-BY 4.0
</a>.
</a>
.
</li>
</ul>
</div>
@ -182,42 +210,38 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
const brand = SdkConfig.get().brand;
let faqText = _t(
'For help with using %(brand)s, click <a>here</a>.',
"For help with using %(brand)s, click <a>here</a>.",
{
brand,
},
{
'a': (sub) => <a
href="https://element.io/help"
rel="noreferrer noopener"
target="_blank"
>
{ sub }
</a>,
a: (sub) => (
<a href="https://element.io/help" rel="noreferrer noopener" target="_blank">
{sub}
</a>
),
},
);
if (SdkConfig.get("welcome_user_id") && getCurrentLanguage().startsWith('en')) {
if (SdkConfig.get("welcome_user_id") && getCurrentLanguage().startsWith("en")) {
faqText = (
<div>
{ _t(
'For help with using %(brand)s, click <a>here</a> or start a chat with our ' +
'bot using the button below.',
{_t(
"For help with using %(brand)s, click <a>here</a> or start a chat with our " +
"bot using the button below.",
{
brand,
},
{
'a': (sub) => <a
href="https://element.io/help"
rel='noreferrer noopener'
target='_blank'
>
{ sub }
</a>,
a: (sub) => (
<a href="https://element.io/help" rel="noreferrer noopener" target="_blank">
{sub}
</a>
),
},
) }
)}
<div>
<AccessibleButton onClick={this.onStartBotChat} kind='primary'>
{ _t("Chat with %(brand)s Bot", { brand }) }
<AccessibleButton onClick={this.onStartBotChat} kind="primary">
{_t("Chat with %(brand)s Bot", { brand })}
</AccessibleButton>
</div>
</div>
@ -233,33 +257,40 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
if (SdkConfig.get().bug_report_endpoint_url) {
bugReportingSection = (
<div className="mx_SettingsTab_section">
<span className='mx_SettingsTab_subheading'>{ _t('Bug reporting') }</span>
<div className='mx_SettingsTab_subsectionText'>
{ _t(
<span className="mx_SettingsTab_subheading">{_t("Bug reporting")}</span>
<div className="mx_SettingsTab_subsectionText">
{_t(
"If you've submitted a bug via GitHub, debug logs can help " +
"us track down the problem. ",
) }
{ _t("Debug logs contain application " +
"usage data including your username, the IDs or aliases of " +
"the rooms you have visited, which UI elements you " +
"last interacted with, and the usernames of other users. " +
"They do not contain messages.",
) }
"us track down the problem. ",
)}
{_t(
"Debug logs contain application " +
"usage data including your username, the IDs or aliases of " +
"the rooms you have visited, which UI elements you " +
"last interacted with, and the usernames of other users. " +
"They do not contain messages.",
)}
</div>
<AccessibleButton onClick={this.onBugReport} kind='primary'>
{ _t("Submit debug logs") }
<AccessibleButton onClick={this.onBugReport} kind="primary">
{_t("Submit debug logs")}
</AccessibleButton>
<div className='mx_SettingsTab_subsectionText'>
{ _t(
<div className="mx_SettingsTab_subsectionText">
{_t(
"To report a Matrix-related security issue, please read the Matrix.org " +
"<a>Security Disclosure Policy</a>.", {},
"<a>Security Disclosure Policy</a>.",
{},
{
a: sub => <a href="https://matrix.org/security-disclosure-policy/"
rel="noreferrer noopener"
target="_blank"
>{ sub }</a>,
a: (sub) => (
<a
href="https://matrix.org/security-disclosure-policy/"
rel="noreferrer noopener"
target="_blank"
>
{sub}
</a>
),
},
) }
)}
</div>
</div>
);
@ -269,44 +300,52 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
return (
<div className="mx_SettingsTab mx_HelpUserSettingsTab">
<div className="mx_SettingsTab_heading">{ _t("Help & About") }</div>
{ bugReportingSection }
<div className='mx_SettingsTab_section'>
<span className='mx_SettingsTab_subheading'>{ _t("FAQ") }</span>
<div className='mx_SettingsTab_subsectionText'>
{ faqText }
</div>
<div className="mx_SettingsTab_heading">{_t("Help & About")}</div>
{bugReportingSection}
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("FAQ")}</span>
<div className="mx_SettingsTab_subsectionText">{faqText}</div>
<AccessibleButton kind="primary" onClick={this.onKeyboardShortcutsClicked}>
{ _t("Keyboard Shortcuts") }
{_t("Keyboard Shortcuts")}
</AccessibleButton>
</div>
<div className='mx_SettingsTab_section'>
<span className='mx_SettingsTab_subheading'>{ _t("Versions") }</span>
<div className='mx_SettingsTab_subsectionText'>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Versions")}</span>
<div className="mx_SettingsTab_subsectionText">
<CopyableText getTextToCopy={this.getVersionTextToCopy}>
{ appVersion }<br />
{ olmVersion }<br />
{appVersion}
<br />
{olmVersion}
<br />
</CopyableText>
{ updateButton }
{updateButton}
</div>
</div>
{ this.renderLegal() }
{ this.renderCredits() }
<div className='mx_SettingsTab_section'>
<span className='mx_SettingsTab_subheading'>{ _t("Advanced") }</span>
<div className='mx_SettingsTab_subsectionText'>
<div>{ _t("Homeserver is") } <code>{ MatrixClientPeg.get().getHomeserverUrl() }</code></div>
<div>{ _t("Identity server is") } <code>{ MatrixClientPeg.get().getIdentityServerUrl() }</code></div>
{this.renderLegal()}
{this.renderCredits()}
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Advanced")}</span>
<div className="mx_SettingsTab_subsectionText">
<div>
{_t("Homeserver is")} <code>{MatrixClientPeg.get().getHomeserverUrl()}</code>
</div>
<div>
{_t("Identity server is")} <code>{MatrixClientPeg.get().getIdentityServerUrl()}</code>
</div>
<details>
<summary>{ _t("Access Token") }</summary>
<b>{ _t("Your access token gives full access to your account."
+ " Do not share it with anyone.") }</b>
<summary>{_t("Access Token")}</summary>
<b>
{_t(
"Your access token gives full access to your account." +
" Do not share it with anyone.",
)}
</b>
<CopyableText getTextToCopy={() => MatrixClientPeg.get().getAccessToken()}>
{ MatrixClientPeg.get().getAccessToken() }
{MatrixClientPeg.get().getAccessToken()}
</CopyableText>
</details>
<AccessibleButton onClick={this.onClearCacheAndReload} kind='danger'>
{ _t("Clear cache and reload") }
<AccessibleButton onClick={this.onClearCacheAndReload} kind="danger">
{_t("Clear cache and reload")}
</AccessibleButton>
</div>
</div>

View file

@ -17,15 +17,12 @@ limitations under the License.
import React from "react";
import {
ICategory,
CATEGORIES,
CategoryName,
} from "../../../../../accessibility/KeyboardShortcuts";
import { ICategory, CATEGORIES, CategoryName } from "../../../../../accessibility/KeyboardShortcuts";
import SdkConfig from "../../../../../SdkConfig";
import { _t } from "../../../../../languageHandler";
import {
getKeyboardShortcutDisplayName, getKeyboardShortcutValue,
getKeyboardShortcutDisplayName,
getKeyboardShortcutValue,
} from "../../../../../accessibility/KeyboardShortcutUtils";
import { KeyboardShortcut } from "../../KeyboardShortcut";
@ -34,18 +31,21 @@ interface IKeyboardShortcutRowProps {
}
// Filter out the labs section if labs aren't enabled.
const visibleCategories = Object.entries(CATEGORIES).filter(([categoryName]) =>
categoryName !== CategoryName.LABS || SdkConfig.get("show_labs_settings"));
const visibleCategories = Object.entries(CATEGORIES).filter(
([categoryName]) => categoryName !== CategoryName.LABS || SdkConfig.get("show_labs_settings"),
);
const KeyboardShortcutRow: React.FC<IKeyboardShortcutRowProps> = ({ name }) => {
const displayName = getKeyboardShortcutDisplayName(name);
const value = getKeyboardShortcutValue(name);
if (!displayName || !value) return null;
return <div className="mx_KeyboardShortcut_shortcutRow">
{ displayName }
<KeyboardShortcut value={value} />
</div>;
return (
<div className="mx_KeyboardShortcut_shortcutRow">
{displayName}
<KeyboardShortcut value={value} />
</div>
);
};
interface IKeyboardShortcutSectionProps {
@ -56,21 +56,28 @@ interface IKeyboardShortcutSectionProps {
const KeyboardShortcutSection: React.FC<IKeyboardShortcutSectionProps> = ({ categoryName, category }) => {
if (!category.categoryLabel) return null;
return <div className="mx_SettingsTab_section" key={categoryName}>
<div className="mx_SettingsTab_subheading">{ _t(category.categoryLabel) }</div>
<div> { category.settingNames.map((shortcutName) => {
return <KeyboardShortcutRow key={shortcutName} name={shortcutName} />;
}) } </div>
</div>;
return (
<div className="mx_SettingsTab_section" key={categoryName}>
<div className="mx_SettingsTab_subheading">{_t(category.categoryLabel)}</div>
<div>
{" "}
{category.settingNames.map((shortcutName) => {
return <KeyboardShortcutRow key={shortcutName} name={shortcutName} />;
})}{" "}
</div>
</div>
);
};
const KeyboardUserSettingsTab: React.FC = () => {
return <div className="mx_SettingsTab mx_KeyboardUserSettingsTab">
<div className="mx_SettingsTab_heading">{ _t("Keyboard") }</div>
{ visibleCategories.map(([categoryName, category]: [CategoryName, ICategory]) => {
return <KeyboardShortcutSection key={categoryName} categoryName={categoryName} category={category} />;
}) }
</div>;
return (
<div className="mx_SettingsTab mx_KeyboardUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Keyboard")}</div>
{visibleCategories.map(([categoryName, category]: [CategoryName, ICategory]) => {
return <KeyboardShortcutSection key={categoryName} categoryName={categoryName} category={category} />;
})}
</div>
);
};
export default KeyboardUserSettingsTab;

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { sortBy } from "lodash";
import { _t } from "../../../../../languageHandler";
@ -22,8 +22,8 @@ import SettingsStore from "../../../../../settings/SettingsStore";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import SdkConfig from "../../../../../SdkConfig";
import BetaCard from "../../../beta/BetaCard";
import SettingsFlag from '../../../elements/SettingsFlag';
import { MatrixClientPeg } from '../../../../../MatrixClientPeg';
import SettingsFlag from "../../../elements/SettingsFlag";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import { LabGroup, labGroupNames } from "../../../../../settings/Settings";
import { EnhancedMap } from "../../../../../utils/maps";
@ -54,123 +54,130 @@ export default class LabsUserSettingsTab extends React.Component<{}, IState> {
public render(): JSX.Element {
const features = SettingsStore.getFeatureSettingNames();
const [labs, betas] = features.reduce((arr, f) => {
arr[SettingsStore.getBetaInfo(f) ? 1 : 0].push(f);
return arr;
}, [[], []] as [string[], string[]]);
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) {
betaSection = <div
data-testid="labs-beta-section"
className="mx_SettingsTab_section"
>
{ betas.map(f => <BetaCard key={f} featureId={f} />) }
</div>;
betaSection = (
<div data-testid="labs-beta-section" className="mx_SettingsTab_section">
{betas.map((f) => (
<BetaCard key={f} featureId={f} />
))}
</div>
);
}
let labsSections;
if (SdkConfig.get("show_labs_settings")) {
const groups = new EnhancedMap<LabGroup, JSX.Element[]>();
labs.forEach(f => {
groups.getOrCreate(SettingsStore.getLabGroup(f), []).push(
<SettingsFlag level={SettingLevel.DEVICE} name={f} key={f} />,
);
labs.forEach((f) => {
groups
.getOrCreate(SettingsStore.getLabGroup(f), [])
.push(<SettingsFlag level={SettingLevel.DEVICE} name={f} key={f} />);
});
groups.getOrCreate(LabGroup.Experimental, []).push(
<SettingsFlag
key="lowBandwidth"
name="lowBandwidth"
level={SettingLevel.DEVICE}
/>,
);
groups
.getOrCreate(LabGroup.Experimental, [])
.push(<SettingsFlag key="lowBandwidth" name="lowBandwidth" level={SettingLevel.DEVICE} />);
groups.getOrCreate(LabGroup.Analytics, []).push(
<SettingsFlag
key="automaticErrorReporting"
name="automaticErrorReporting"
level={SettingLevel.DEVICE}
/>,
<SettingsFlag
key="automaticDecryptionErrorReporting"
name="automaticDecryptionErrorReporting"
level={SettingLevel.DEVICE}
/>,
);
if (this.state.showJumpToDate) {
groups.getOrCreate(LabGroup.Messaging, []).push(
groups
.getOrCreate(LabGroup.Analytics, [])
.push(
<SettingsFlag
key="feature_jump_to_date"
name="feature_jump_to_date"
key="automaticErrorReporting"
name="automaticErrorReporting"
level={SettingLevel.DEVICE}
/>,
<SettingsFlag
key="automaticDecryptionErrorReporting"
name="automaticDecryptionErrorReporting"
level={SettingLevel.DEVICE}
/>,
);
if (this.state.showJumpToDate) {
groups
.getOrCreate(LabGroup.Messaging, [])
.push(
<SettingsFlag
key="feature_jump_to_date"
name="feature_jump_to_date"
level={SettingLevel.DEVICE}
/>,
);
}
if (this.state.showExploringPublicSpaces) {
groups.getOrCreate(LabGroup.Spaces, []).push(
<SettingsFlag
key="feature_exploring_public_spaces"
name="feature_exploring_public_spaces"
level={SettingLevel.DEVICE}
/>,
);
groups
.getOrCreate(LabGroup.Spaces, [])
.push(
<SettingsFlag
key="feature_exploring_public_spaces"
name="feature_exploring_public_spaces"
level={SettingLevel.DEVICE}
/>,
);
}
labsSections = <>
{ sortBy(Array.from(groups.entries()), "0").map(([group, flags]) => (
<div
className="mx_SettingsTab_section"
key={group}
data-testid={`labs-group-${group}`}
>
<span className="mx_SettingsTab_subheading">{ _t(labGroupNames[group]) }</span>
{ flags }
</div>
)) }
</>;
labsSections = (
<>
{sortBy(Array.from(groups.entries()), "0").map(([group, flags]) => (
<div className="mx_SettingsTab_section" key={group} data-testid={`labs-group-${group}`}>
<span className="mx_SettingsTab_subheading">{_t(labGroupNames[group])}</span>
{flags}
</div>
))}
</>
);
}
return (
<div className="mx_SettingsTab mx_LabsUserSettingsTab">
<div className="mx_SettingsTab_heading">{ _t("Upcoming features") }</div>
<div className='mx_SettingsTab_subsectionText'>
{
_t(
"What's next for %(brand)s? "
+ "Labs are the best way to get things early, "
+ "test out new features and help shape them before they actually launch.",
{ brand: SdkConfig.get("brand") },
)
}
<div className="mx_SettingsTab_heading">{_t("Upcoming features")}</div>
<div className="mx_SettingsTab_subsectionText">
{_t(
"What's next for %(brand)s? " +
"Labs are the best way to get things early, " +
"test out new features and help shape them before they actually launch.",
{ brand: SdkConfig.get("brand") },
)}
</div>
{ betaSection }
{ labsSections && <>
<div className="mx_SettingsTab_heading">{ _t("Early previews") }</div>
<div className='mx_SettingsTab_subsectionText'>
{
_t(
"Feeling experimental? "
+ "Try out our latest ideas in development. "
+ "These features are not finalised; "
+ "they may be unstable, may change, or may be dropped altogether. "
+ "<a>Learn more</a>.",
{betaSection}
{labsSections && (
<>
<div className="mx_SettingsTab_heading">{_t("Early previews")}</div>
<div className="mx_SettingsTab_subsectionText">
{_t(
"Feeling experimental? " +
"Try out our latest ideas in development. " +
"These features are not finalised; " +
"they may be unstable, may change, or may be dropped altogether. " +
"<a>Learn more</a>.",
{},
{
'a': (sub) => {
return <a
href="https://github.com/vector-im/element-web/blob/develop/docs/labs.md"
rel='noreferrer noopener'
target='_blank'
>{ sub }</a>;
a: (sub) => {
return (
<a
href="https://github.com/vector-im/element-web/blob/develop/docs/labs.md"
rel="noreferrer noopener"
target="_blank"
>
{sub}
</a>
);
},
})
}
</div>
{ labsSections }
</> }
},
)}
</div>
{labsSections}
</>
)}
</div>
);
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../../../../../languageHandler";
@ -72,8 +72,8 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
logger.error(e);
Modal.createDialog(ErrorDialog, {
title: _t('Error adding ignored user/server'),
description: _t('Something went wrong. Please try again or view your console for hints.'),
title: _t("Error adding ignored user/server"),
description: _t("Something went wrong. Please try again or view your console for hints."),
});
} finally {
this.setState({ busy: false });
@ -93,8 +93,8 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
logger.error(e);
Modal.createDialog(ErrorDialog, {
title: _t('Error subscribing to list'),
description: _t('Please verify the room ID or address and try again.'),
title: _t("Error subscribing to list"),
description: _t("Please verify the room ID or address and try again."),
});
} finally {
this.setState({ busy: false });
@ -110,8 +110,8 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
logger.error(e);
Modal.createDialog(ErrorDialog, {
title: _t('Error removing ignored user/server'),
description: _t('Something went wrong. Please try again or view your console for hints.'),
title: _t("Error removing ignored user/server"),
description: _t("Something went wrong. Please try again or view your console for hints."),
});
} finally {
this.setState({ busy: false });
@ -127,8 +127,8 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
logger.error(e);
Modal.createDialog(ErrorDialog, {
title: _t('Error unsubscribing from list'),
description: _t('Please try again or view your console for hints.'),
title: _t("Error unsubscribing from list"),
description: _t("Please try again or view your console for hints."),
});
} finally {
this.setState({ busy: false });
@ -140,23 +140,27 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
const name = room ? room.name : list.roomId;
const renderRules = (rules: ListRule[]) => {
if (rules.length === 0) return <i>{ _t("None") }</i>;
if (rules.length === 0) return <i>{_t("None")}</i>;
const tiles = [];
for (const rule of rules) {
tiles.push(<li key={rule.kind + rule.entity}><code>{ rule.entity }</code></li>);
tiles.push(
<li key={rule.kind + rule.entity}>
<code>{rule.entity}</code>
</li>,
);
}
return <ul>{ tiles }</ul>;
return <ul>{tiles}</ul>;
};
Modal.createDialog(QuestionDialog, {
title: _t("Ban list rules - %(roomName)s", { roomName: name }),
description: (
<div>
<h3>{ _t("Server rules") }</h3>
{ renderRules(list.serverRules) }
<h3>{ _t("User rules") }</h3>
{ renderRules(list.userRules) }
<h3>{_t("Server rules")}</h3>
{renderRules(list.serverRules)}
<h3>{_t("User rules")}</h3>
{renderRules(list.userRules)}
</div>
),
button: _t("Close"),
@ -167,7 +171,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
private renderPersonalBanListRules() {
const list = Mjolnir.sharedInstance().getPersonalList();
const rules = list ? [...list.userRules, ...list.serverRules] : [];
if (!list || rules.length <= 0) return <i>{ _t("You have not ignored anyone.") }</i>;
if (!list || rules.length <= 0) return <i>{_t("You have not ignored anyone.")}</i>;
const tiles = [];
for (const rule of rules) {
@ -178,32 +182,39 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
onClick={() => this.removePersonalRule(rule)}
disabled={this.state.busy}
>
{ _t("Remove") }
</AccessibleButton>&nbsp;
<code>{ rule.entity }</code>
{_t("Remove")}
</AccessibleButton>
&nbsp;
<code>{rule.entity}</code>
</li>,
);
}
return (
<div>
<p>{ _t("You are currently ignoring:") }</p>
<ul>{ tiles }</ul>
<p>{_t("You are currently ignoring:")}</p>
<ul>{tiles}</ul>
</div>
);
}
private renderSubscribedBanLists() {
const personalList = Mjolnir.sharedInstance().getPersonalList();
const lists = Mjolnir.sharedInstance().lists.filter(b => {
return personalList? personalList.roomId !== b.roomId : true;
const lists = Mjolnir.sharedInstance().lists.filter((b) => {
return personalList ? personalList.roomId !== b.roomId : true;
});
if (!lists || lists.length <= 0) return <i>{ _t("You are not subscribed to any lists") }</i>;
if (!lists || lists.length <= 0) return <i>{_t("You are not subscribed to any lists")}</i>;
const tiles = [];
for (const list of lists) {
const room = MatrixClientPeg.get().getRoom(list.roomId);
const name = room ? <span>{ room.name } (<code>{ list.roomId }</code>)</span> : <code>list.roomId</code>;
const name = room ? (
<span>
{room.name} (<code>{list.roomId}</code>)
</span>
) : (
<code>list.roomId</code>
);
tiles.push(
<li key={list.roomId} className="mx_MjolnirUserSettingsTab_listItem">
<AccessibleButton
@ -211,24 +222,26 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
onClick={() => this.unsubscribeFromList(list)}
disabled={this.state.busy}
>
{ _t("Unsubscribe") }
</AccessibleButton>&nbsp;
{_t("Unsubscribe")}
</AccessibleButton>
&nbsp;
<AccessibleButton
kind="primary_sm"
onClick={() => this.viewListRules(list)}
disabled={this.state.busy}
>
{ _t("View rules") }
</AccessibleButton>&nbsp;
{ name }
{_t("View rules")}
</AccessibleButton>
&nbsp;
{name}
</li>,
);
}
return (
<div>
<p>{ _t("You are currently subscribed to:") }</p>
<ul>{ tiles }</ul>
<p>{_t("You are currently subscribed to:")}</p>
<ul>{tiles}</ul>
</div>
);
}
@ -238,38 +251,39 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
return (
<div className="mx_SettingsTab mx_MjolnirUserSettingsTab">
<div className="mx_SettingsTab_heading">{ _t("Ignored users") }</div>
<div className="mx_SettingsTab_heading">{_t("Ignored users")}</div>
<div className="mx_SettingsTab_section">
<div className='mx_SettingsTab_subsectionText'>
<span className='warning'>{ _t("⚠ These settings are meant for advanced users.") }</span><br />
<div className="mx_SettingsTab_subsectionText">
<span className="warning">{_t("⚠ These settings are meant for advanced users.")}</span>
<br />
{ _t(
<br />
{_t(
"Add users and servers you want to ignore here. Use asterisks " +
"to have %(brand)s match any characters. For example, <code>@bot:*</code> " +
"would ignore all users that have the name 'bot' on any server.",
{ brand }, { code: (s) => <code>{ s }</code> },
) }<br />
"to have %(brand)s match any characters. For example, <code>@bot:*</code> " +
"would ignore all users that have the name 'bot' on any server.",
{ brand },
{ code: (s) => <code>{s}</code> },
)}
<br />
{ _t(
<br />
{_t(
"Ignoring people is done through ban lists which contain rules for " +
"who to ban. Subscribing to a ban list means the users/servers blocked by " +
"that list will be hidden from you.",
) }
"who to ban. Subscribing to a ban list means the users/servers blocked by " +
"that list will be hidden from you.",
)}
</div>
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Personal ban list") }</span>
<div className='mx_SettingsTab_subsectionText'>
{ _t(
<span className="mx_SettingsTab_subheading">{_t("Personal ban list")}</span>
<div className="mx_SettingsTab_subsectionText">
{_t(
"Your personal ban list holds all the users/servers you personally don't " +
"want to see messages from. After ignoring your first user/server, a new room " +
"will show up in your room list named 'My Ban List' - stay in this room to keep " +
"the ban list in effect.",
) }
</div>
<div>
{ this.renderPersonalBanListRules() }
"want to see messages from. After ignoring your first user/server, a new room " +
"will show up in your room list named 'My Ban List' - stay in this room to keep " +
"the ban list in effect.",
)}
</div>
<div>{this.renderPersonalBanListRules()}</div>
<div>
<form onSubmit={this.onAddPersonalRule} autoComplete="off">
<Field
@ -285,23 +299,19 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
onClick={this.onAddPersonalRule}
disabled={this.state.busy}
>
{ _t("Ignore") }
{_t("Ignore")}
</AccessibleButton>
</form>
</div>
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Subscribed lists") }</span>
<div className='mx_SettingsTab_subsectionText'>
<span className='warning'>{ _t("Subscribing to a ban list will cause you to join it!") }</span>
<span className="mx_SettingsTab_subheading">{_t("Subscribed lists")}</span>
<div className="mx_SettingsTab_subsectionText">
<span className="warning">{_t("Subscribing to a ban list will cause you to join it!")}</span>
&nbsp;
<span>{ _t(
"If this isn't what you want, please use a different tool to ignore users.",
) }</span>
</div>
<div>
{ this.renderSubscribedBanLists() }
<span>{_t("If this isn't what you want, please use a different tool to ignore users.")}</span>
</div>
<div>{this.renderSubscribedBanLists()}</div>
<div>
<form onSubmit={this.onSubscribeList} autoComplete="off">
<Field
@ -316,7 +326,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
onClick={this.onSubscribeList}
disabled={this.state.busy}
>
{ _t("Subscribe") }
{_t("Subscribe")}
</AccessibleButton>
</form>
</div>

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { _t } from "../../../../../languageHandler";
import Notifications from "../../Notifications";
@ -23,7 +23,7 @@ export default class NotificationUserSettingsTab extends React.Component {
render() {
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{ _t("Notifications") }</div>
<div className="mx_SettingsTab_heading">{_t("Notifications")}</div>
<div className="mx_SettingsTab_section mx_SettingsTab_subsectionText">
<Notifications />
</div>

View file

@ -15,14 +15,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { _t } from "../../../../../languageHandler";
import { UseCase } from "../../../../../settings/enums/UseCase";
import SettingsStore from "../../../../../settings/SettingsStore";
import Field from "../../../elements/Field";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import SettingsFlag from '../../../elements/SettingsFlag';
import SettingsFlag from "../../../elements/SettingsFlag";
import AccessibleButton from "../../../elements/AccessibleButton";
import dis from "../../../../../dispatcher/dispatcher";
import { UserTab } from "../../../dialogs/UserTab";
@ -44,18 +44,11 @@ interface IState {
}
export default class PreferencesUserSettingsTab extends React.Component<IProps, IState> {
private static ROOM_LIST_SETTINGS = [
'breadcrumbs',
"FTUE.userOnboardingButton",
];
private static ROOM_LIST_SETTINGS = ["breadcrumbs", "FTUE.userOnboardingButton"];
private static SPACES_SETTINGS = [
"Spaces.allRoomsInHome",
];
private static SPACES_SETTINGS = ["Spaces.allRoomsInHome"];
private static KEYBINDINGS_SETTINGS = [
'ctrlFForSearch',
];
private static KEYBINDINGS_SETTINGS = ["ctrlFForSearch"];
private static PRESENCE_SETTINGS = [
"sendTypingNotifications",
@ -63,49 +56,41 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
];
private static COMPOSER_SETTINGS = [
'MessageComposerInput.autoReplaceEmoji',
'MessageComposerInput.useMarkdown',
'MessageComposerInput.suggestEmoji',
'MessageComposerInput.ctrlEnterToSend',
'MessageComposerInput.surroundWith',
'MessageComposerInput.showStickersButton',
'MessageComposerInput.insertTrailingColon',
"MessageComposerInput.autoReplaceEmoji",
"MessageComposerInput.useMarkdown",
"MessageComposerInput.suggestEmoji",
"MessageComposerInput.ctrlEnterToSend",
"MessageComposerInput.surroundWith",
"MessageComposerInput.showStickersButton",
"MessageComposerInput.insertTrailingColon",
];
private static TIME_SETTINGS = [
'showTwelveHourTimestamps',
'alwaysShowTimestamps',
];
private static TIME_SETTINGS = ["showTwelveHourTimestamps", "alwaysShowTimestamps"];
private static CODE_BLOCKS_SETTINGS = [
'enableSyntaxHighlightLanguageDetection',
'expandCodeByDefault',
'showCodeLineNumbers',
"enableSyntaxHighlightLanguageDetection",
"expandCodeByDefault",
"showCodeLineNumbers",
];
private static IMAGES_AND_VIDEOS_SETTINGS = [
'urlPreviewsEnabled',
'autoplayGifs',
'autoplayVideo',
'showImages',
];
private static IMAGES_AND_VIDEOS_SETTINGS = ["urlPreviewsEnabled", "autoplayGifs", "autoplayVideo", "showImages"];
private static TIMELINE_SETTINGS = [
'showTypingNotifications',
'showRedactions',
'showReadReceipts',
'showJoinLeaves',
'showDisplaynameChanges',
'showChatEffects',
'showAvatarChanges',
'Pill.shouldShowPillAvatar',
'TextualBody.enableBigEmoji',
'scrollToBottomOnMessageSent',
'useOnlyCurrentProfiles',
"showTypingNotifications",
"showRedactions",
"showReadReceipts",
"showJoinLeaves",
"showDisplaynameChanges",
"showChatEffects",
"showAvatarChanges",
"Pill.shouldShowPillAvatar",
"TextualBody.enableBigEmoji",
"scrollToBottomOnMessageSent",
"useOnlyCurrentProfiles",
];
private static GENERAL_SETTINGS = [
'promptBeforeInviteUnknownUsers',
"promptBeforeInviteUnknownUsers",
// Start automatically after startup (electron-only)
// Autocomplete delay (niche text box)
];
@ -115,12 +100,15 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
this.state = {
disablingReadReceiptsSupported: false,
autocompleteDelay:
SettingsStore.getValueAt(SettingLevel.DEVICE, 'autocompleteDelay').toString(10),
readMarkerInViewThresholdMs:
SettingsStore.getValueAt(SettingLevel.DEVICE, 'readMarkerInViewThresholdMs').toString(10),
readMarkerOutOfViewThresholdMs:
SettingsStore.getValueAt(SettingLevel.DEVICE, 'readMarkerOutOfViewThresholdMs').toString(10),
autocompleteDelay: SettingsStore.getValueAt(SettingLevel.DEVICE, "autocompleteDelay").toString(10),
readMarkerInViewThresholdMs: SettingsStore.getValueAt(
SettingLevel.DEVICE,
"readMarkerInViewThresholdMs",
).toString(10),
readMarkerOutOfViewThresholdMs: SettingsStore.getValueAt(
SettingLevel.DEVICE,
"readMarkerOutOfViewThresholdMs",
).toString(10),
};
}
@ -128,10 +116,9 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
const cli = MatrixClientPeg.get();
this.setState({
disablingReadReceiptsSupported: (
(await cli.doesServerSupportUnstableFeature("org.matrix.msc2285.stable"))
|| (await cli.isVersionSupported("v1.4"))
),
disablingReadReceiptsSupported:
(await cli.doesServerSupportUnstableFeature("org.matrix.msc2285.stable")) ||
(await cli.isVersionSupported("v1.4")),
});
}
@ -150,11 +137,8 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
SettingsStore.setValue("readMarkerOutOfViewThresholdMs", null, SettingLevel.DEVICE, e.target.value);
};
private renderGroup(
settingIds: string[],
level = SettingLevel.ACCOUNT,
): React.ReactNodeArray {
return settingIds.map(i => {
private renderGroup(settingIds: string[], level = SettingLevel.ACCOUNT): React.ReactNodeArray {
return settingIds.map((i) => {
const disabled = !SettingsStore.isEnabled(i);
return <SettingsFlag key={i} name={i} level={level} disabled={disabled} />;
});
@ -171,90 +155,95 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
const useCase = SettingsStore.getValue<UseCase | null>("FTUE.useCaseSelection");
const roomListSettings = PreferencesUserSettingsTab.ROOM_LIST_SETTINGS
// Only show the breadcrumbs setting if breadcrumbs v2 is disabled
.filter(it => it !== "breadcrumbs" || !SettingsStore.getValue("feature_breadcrumbs_v2"))
.filter((it) => it !== "breadcrumbs" || !SettingsStore.getValue("feature_breadcrumbs_v2"))
// Only show the user onboarding setting if the user should see the user onboarding page
.filter(it => it !== "FTUE.userOnboardingButton" || showUserOnboardingPage(useCase));
.filter((it) => it !== "FTUE.userOnboardingButton" || showUserOnboardingPage(useCase));
return (
<div className="mx_SettingsTab mx_PreferencesUserSettingsTab">
<div className="mx_SettingsTab_heading">{ _t("Preferences") }</div>
<div className="mx_SettingsTab_heading">{_t("Preferences")}</div>
{ roomListSettings.length > 0 &&
{roomListSettings.length > 0 && (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Room list") }</span>
{ this.renderGroup(roomListSettings) }
<span className="mx_SettingsTab_subheading">{_t("Room list")}</span>
{this.renderGroup(roomListSettings)}
</div>
}
)}
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Spaces") }</span>
{ this.renderGroup(PreferencesUserSettingsTab.SPACES_SETTINGS, SettingLevel.ACCOUNT) }
<span className="mx_SettingsTab_subheading">{_t("Spaces")}</span>
{this.renderGroup(PreferencesUserSettingsTab.SPACES_SETTINGS, SettingLevel.ACCOUNT)}
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Keyboard shortcuts") }</span>
<span className="mx_SettingsTab_subheading">{_t("Keyboard shortcuts")}</span>
<div className="mx_SettingsFlag">
{ _t("To view all keyboard shortcuts, <a>click here</a>.", {}, {
a: sub => <AccessibleButton kind="link_inline" onClick={this.onKeyboardShortcutsClicked}>
{ sub }
</AccessibleButton>,
}) }
{_t(
"To view all keyboard shortcuts, <a>click here</a>.",
{},
{
a: (sub) => (
<AccessibleButton kind="link_inline" onClick={this.onKeyboardShortcutsClicked}>
{sub}
</AccessibleButton>
),
},
)}
</div>
{ this.renderGroup(PreferencesUserSettingsTab.KEYBINDINGS_SETTINGS) }
{this.renderGroup(PreferencesUserSettingsTab.KEYBINDINGS_SETTINGS)}
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Displaying time") }</span>
{ this.renderGroup(PreferencesUserSettingsTab.TIME_SETTINGS) }
<span className="mx_SettingsTab_subheading">{_t("Displaying time")}</span>
{this.renderGroup(PreferencesUserSettingsTab.TIME_SETTINGS)}
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Presence") }</span>
<span className="mx_SettingsTab_subheading">{_t("Presence")}</span>
<span className="mx_SettingsTab_subsectionText">
{ _t("Share your activity and status with others.") }
{_t("Share your activity and status with others.")}
</span>
<SettingsFlag
disabled={
!this.state.disablingReadReceiptsSupported
&& SettingsStore.getValue("sendReadReceipts") // Make sure the feature can always be enabled
!this.state.disablingReadReceiptsSupported && SettingsStore.getValue("sendReadReceipts") // Make sure the feature can always be enabled
}
disabledDescription={_t("Your server doesn't support disabling sending read receipts.")}
name="sendReadReceipts"
level={SettingLevel.ACCOUNT}
/>
{ this.renderGroup(PreferencesUserSettingsTab.PRESENCE_SETTINGS) }
{this.renderGroup(PreferencesUserSettingsTab.PRESENCE_SETTINGS)}
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Composer") }</span>
{ this.renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS) }
<span className="mx_SettingsTab_subheading">{_t("Composer")}</span>
{this.renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)}
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Code blocks") }</span>
{ this.renderGroup(PreferencesUserSettingsTab.CODE_BLOCKS_SETTINGS) }
<span className="mx_SettingsTab_subheading">{_t("Code blocks")}</span>
{this.renderGroup(PreferencesUserSettingsTab.CODE_BLOCKS_SETTINGS)}
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Images, GIFs and videos") }</span>
{ this.renderGroup(PreferencesUserSettingsTab.IMAGES_AND_VIDEOS_SETTINGS) }
<span className="mx_SettingsTab_subheading">{_t("Images, GIFs and videos")}</span>
{this.renderGroup(PreferencesUserSettingsTab.IMAGES_AND_VIDEOS_SETTINGS)}
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Timeline") }</span>
{ this.renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS) }
<span className="mx_SettingsTab_subheading">{_t("Timeline")}</span>
{this.renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)}
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("General") }</span>
{ this.renderGroup(PreferencesUserSettingsTab.GENERAL_SETTINGS) }
<span className="mx_SettingsTab_subheading">{_t("General")}</span>
{this.renderGroup(PreferencesUserSettingsTab.GENERAL_SETTINGS)}
<SettingsFlag name="Electron.showTrayIcon" level={SettingLevel.PLATFORM} hideIfCannotSet />
<SettingsFlag
name="Electron.enableHardwareAcceleration"
level={SettingLevel.PLATFORM}
hideIfCannotSet
label={_t('Enable hardware acceleration (restart %(appName)s to take effect)', {
label={_t("Enable hardware acceleration (restart %(appName)s to take effect)", {
appName: SdkConfig.get().brand,
})}
/>
@ -263,20 +252,23 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
<SettingsFlag name="Electron.warnBeforeExit" level={SettingLevel.PLATFORM} hideIfCannotSet />
<Field
label={_t('Autocomplete delay (ms)')}
type='number'
label={_t("Autocomplete delay (ms)")}
type="number"
value={this.state.autocompleteDelay}
onChange={this.onAutocompleteDelayChange} />
onChange={this.onAutocompleteDelayChange}
/>
<Field
label={_t('Read Marker lifetime (ms)')}
type='number'
label={_t("Read Marker lifetime (ms)")}
type="number"
value={this.state.readMarkerInViewThresholdMs}
onChange={this.onReadMarkerInViewThresholdMs} />
onChange={this.onReadMarkerInViewThresholdMs}
/>
<Field
label={_t('Read Marker off-screen lifetime (ms)')}
type='number'
label={_t("Read Marker off-screen lifetime (ms)")}
type="number"
value={this.state.readMarkerOutOfViewThresholdMs}
onChange={this.onReadMarkerOutOfViewThresholdMs} />
onChange={this.onReadMarkerOutOfViewThresholdMs}
/>
</div>
</div>
);

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { sleep } from "matrix-js-sdk/src/utils";
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { logger } from "matrix-js-sdk/src/logger";
@ -38,9 +38,9 @@ import InlineSpinner from "../../../elements/InlineSpinner";
import { PosthogAnalytics } from "../../../../../PosthogAnalytics";
import { showDialog as showAnalyticsLearnMoreDialog } from "../../../dialogs/AnalyticsLearnMoreDialog";
import { privateShouldBeEncrypted } from "../../../../../utils/rooms";
import LoginWithQR, { Mode } from '../../../auth/LoginWithQR';
import LoginWithQRSection from '../../devices/LoginWithQRSection';
import type { IServerVersions } from 'matrix-js-sdk/src/matrix';
import LoginWithQR, { Mode } from "../../../auth/LoginWithQR";
import LoginWithQRSection from "../../devices/LoginWithQRSection";
import type { IServerVersions } from "matrix-js-sdk/src/matrix";
interface IIgnoredUserProps {
userId: string;
@ -56,11 +56,16 @@ export class IgnoredUser extends React.Component<IIgnoredUserProps> {
public render(): JSX.Element {
const id = `mx_SecurityUserSettingsTab_ignoredUser_${this.props.userId}`;
return (
<div className='mx_SecurityUserSettingsTab_ignoredUser'>
<AccessibleButton onClick={this.onUnignoreClicked} kind='primary_sm' aria-describedby={id} disabled={this.props.inProgress}>
{ _t('Unignore') }
<div className="mx_SecurityUserSettingsTab_ignoredUser">
<AccessibleButton
onClick={this.onUnignoreClicked}
kind="primary_sm"
aria-describedby={id}
disabled={this.props.inProgress}
>
{_t("Unignore")}
</AccessibleButton>
<span id={id}>{ this.props.userId }</span>
<span id={id}>{this.props.userId}</span>
</div>
);
}
@ -86,7 +91,7 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
super(props);
// Get rooms we're invited to
const invitedRoomIds = new Set(this.getInvitedRooms().map(room => room.roomId));
const invitedRoomIds = new Set(this.getInvitedRooms().map((room) => room.roomId));
this.state = {
ignoredUserIds: MatrixClientPeg.get().getIgnoredUsers(),
@ -100,7 +105,7 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
private onAction = ({ action }: ActionPayload) => {
if (action === "ignore_state_changed") {
const ignoredUserIds = MatrixClientPeg.get().getIgnoredUsers();
const newWaitingUnignored = this.state.waitingUnignored.filter(e => ignoredUserIds.includes(e));
const newWaitingUnignored = this.state.waitingUnignored.filter((e) => ignoredUserIds.includes(e));
this.setState({ ignoredUserIds, waitingUnignored: newWaitingUnignored });
}
};
@ -108,7 +113,9 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
public componentDidMount(): void {
this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on(RoomEvent.MyMembership, this.onMyMembership);
MatrixClientPeg.get().getVersions().then(versions => this.setState({ versions }));
MatrixClientPeg.get()
.getVersions()
.then((versions) => this.setState({ versions }));
}
public componentWillUnmount(): void {
@ -148,7 +155,7 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
private onUserUnignored = async (userId: string): Promise<void> => {
const { ignoredUserIds, waitingUnignored } = this.state;
const currentlyIgnoredUserIds = ignoredUserIds.filter(e => !waitingUnignored.includes(e));
const currentlyIgnoredUserIds = ignoredUserIds.filter((e) => !waitingUnignored.includes(e));
const index = currentlyIgnoredUserIds.indexOf(userId);
if (index !== -1) {
@ -159,9 +166,11 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
};
private getInvitedRooms = (): Room[] => {
return MatrixClientPeg.get().getRooms().filter((r) => {
return r.hasMembershipState(MatrixClientPeg.get().getUserId(), "invite");
});
return MatrixClientPeg.get()
.getRooms()
.filter((r) => {
return r.hasMembershipState(MatrixClientPeg.get().getUserId(), "invite");
});
};
private manageInvites = async (accept: boolean): Promise<void> => {
@ -179,23 +188,26 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
const roomId = invitedRoomIdsValues[i];
// Accept/reject invite
await action(roomId).then(() => {
// No error, update invited rooms button
this.removeInvitedRoom(roomId);
}, async (e) => {
// Action failure
if (e.errcode === "M_LIMIT_EXCEEDED") {
// Add a delay between each invite change in order to avoid rate
// limiting by the server.
await sleep(e.retry_after_ms || 2500);
await action(roomId).then(
() => {
// No error, update invited rooms button
this.removeInvitedRoom(roomId);
},
async (e) => {
// Action failure
if (e.errcode === "M_LIMIT_EXCEEDED") {
// Add a delay between each invite change in order to avoid rate
// limiting by the server.
await sleep(e.retry_after_ms || 2500);
// Redo last action
i--;
} else {
// Print out error with joining/leaving room
logger.warn(e);
}
});
// Redo last action
i--;
} else {
// Print out error with joining/leaving room
logger.warn(e);
}
},
);
}
this.setState({
@ -215,24 +227,22 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
const { waitingUnignored, ignoredUserIds } = this.state;
const userIds = !ignoredUserIds?.length
? _t('You have no ignored users.')
? _t("You have no ignored users.")
: ignoredUserIds.map((u) => {
return (
<IgnoredUser
userId={u}
onUnignored={this.onUserUnignored}
key={u}
inProgress={waitingUnignored.includes(u)}
/>
);
});
return (
<IgnoredUser
userId={u}
onUnignored={this.onUserUnignored}
key={u}
inProgress={waitingUnignored.includes(u)}
/>
);
});
return (
<div className='mx_SettingsTab_section'>
<span className='mx_SettingsTab_subheading'>{ _t('Ignored users') }</span>
<div className='mx_SettingsTab_subsectionText'>
{ userIds }
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Ignored users")}</span>
<div className="mx_SettingsTab_subsectionText">{userIds}</div>
</div>
);
}
@ -245,15 +255,23 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
}
return (
<div className='mx_SettingsTab_section mx_SecurityUserSettingsTab_bulkOptions'>
<span className='mx_SettingsTab_subheading'>{ _t('Bulk options') }</span>
<AccessibleButton onClick={this.onAcceptAllInvitesClicked} kind='primary' disabled={this.state.managingInvites}>
{ _t("Accept all %(invitedRooms)s invites", { invitedRooms: invitedRoomIds.size }) }
<div className="mx_SettingsTab_section mx_SecurityUserSettingsTab_bulkOptions">
<span className="mx_SettingsTab_subheading">{_t("Bulk options")}</span>
<AccessibleButton
onClick={this.onAcceptAllInvitesClicked}
kind="primary"
disabled={this.state.managingInvites}
>
{_t("Accept all %(invitedRooms)s invites", { invitedRooms: invitedRoomIds.size })}
</AccessibleButton>
<AccessibleButton onClick={this.onRejectAllInvitesClicked} kind='danger' disabled={this.state.managingInvites}>
{ _t("Reject all %(invitedRooms)s invites", { invitedRooms: invitedRoomIds.size }) }
<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 /> }
{this.state.managingInvites ? <InlineSpinner /> : <div />}
</div>
);
}
@ -268,9 +286,9 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
public render(): JSX.Element {
const secureBackup = (
<div className='mx_SettingsTab_section'>
<span className="mx_SettingsTab_subheading">{ _t("Secure Backup") }</span>
<div className='mx_SettingsTab_subsectionText'>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Secure Backup")}</span>
<div className="mx_SettingsTab_subsectionText">
<SecureBackupPanel />
</div>
</div>
@ -278,7 +296,7 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
const eventIndex = (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Message search") }</span>
<span className="mx_SettingsTab_subheading">{_t("Message search")}</span>
<EventIndexPanel />
</div>
);
@ -288,9 +306,9 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
// in having advanced details here once all flows are implemented, we
// can remove this.
const crossSigning = (
<div className='mx_SettingsTab_section'>
<span className="mx_SettingsTab_subheading">{ _t("Cross-signing") }</span>
<div className='mx_SettingsTab_subsectionText'>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Cross-signing")}</span>
<div className="mx_SettingsTab_subsectionText">
<CrossSigningPanel />
</div>
</div>
@ -298,10 +316,14 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
let warning;
if (!privateShouldBeEncrypted()) {
warning = <div className="mx_SecurityUserSettingsTab_warning">
{ _t("Your server admin has disabled end-to-end encryption by default " +
"in private rooms & Direct Messages.") }
</div>;
warning = (
<div className="mx_SecurityUserSettingsTab_warning">
{_t(
"Your server admin has disabled end-to-end encryption by default " +
"in private rooms & Direct Messages.",
)}
</div>
);
}
let privacySection;
@ -312,35 +334,32 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
hasCancel: false,
});
};
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">
<p>
{ _t("Share anonymous data to help us identify issues. Nothing personal. " +
"No third parties.") }
</p>
<AccessibleButton
kind="link"
onClick={onClickAnalyticsLearnMore}
>
{ _t("Learn more") }
</AccessibleButton>
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">
<p>
{_t(
"Share anonymous data to help us identify issues. Nothing personal. " +
"No third parties.",
)}
</p>
<AccessibleButton kind="link" onClick={onClickAnalyticsLearnMore}>
{_t("Learn more")}
</AccessibleButton>
</div>
{PosthogAnalytics.instance.isEnabled() && (
<SettingsFlag name="pseudonymousAnalyticsOptIn" level={SettingLevel.ACCOUNT} />
)}
</div>
{ PosthogAnalytics.instance.isEnabled() && (
<SettingsFlag
name="pseudonymousAnalyticsOptIn"
level={SettingLevel.ACCOUNT} />
) }
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Sessions") }</span>
<SettingsFlag
name="deviceClientInformationOptIn"
level={SettingLevel.ACCOUNT} />
</div>
</React.Fragment>;
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Sessions")}</span>
<SettingsFlag name="deviceClientInformationOptIn" level={SettingLevel.ACCOUNT} />
</div>
</React.Fragment>
);
}
let advancedSection;
@ -350,62 +369,66 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
const e2ePanel = isE2eAdvancedPanelPossible() ? <E2eAdvancedPanel /> : null;
// only show the section if there's something to show
if (ignoreUsersPanel || invitesPanel || e2ePanel) {
advancedSection = <>
<div className="mx_SettingsTab_heading">{ _t("Advanced") }</div>
<div className="mx_SettingsTab_section">
{ ignoreUsersPanel }
{ invitesPanel }
{ e2ePanel }
</div>
</>;
advancedSection = (
<>
<div className="mx_SettingsTab_heading">{_t("Advanced")}</div>
<div className="mx_SettingsTab_section">
{ignoreUsersPanel}
{invitesPanel}
{e2ePanel}
</div>
</>
);
}
}
const useNewSessionManager = SettingsStore.getValue("feature_new_device_manager");
const showQrCodeEnabled = SettingsStore.getValue("feature_qr_signin_reciprocate_show");
const devicesSection = useNewSessionManager
? null
: <>
<div className="mx_SettingsTab_heading">{ _t("Where you're signed in") }</div>
<div
className="mx_SettingsTab_section"
data-testid="devices-section"
>
const devicesSection = useNewSessionManager ? null : (
<>
<div className="mx_SettingsTab_heading">{_t("Where you're signed in")}</div>
<div className="mx_SettingsTab_section" data-testid="devices-section">
<span className="mx_SettingsTab_subsectionText">
{ _t(
{_t(
"Manage your signed-in devices below. " +
"A device's name is visible to people you communicate with.",
) }
"A device's name is visible to people you communicate with.",
)}
</span>
<DevicesPanel />
</div>
{ showQrCodeEnabled ?
{showQrCodeEnabled ? (
<LoginWithQRSection onShowQr={this.onShowQRClicked} versions={this.state.versions} />
: null
}
</>;
) : null}
</>
);
const client = MatrixClientPeg.get();
if (showQrCodeEnabled && this.state.showLoginWithQR) {
return <div className="mx_SettingsTab mx_SecurityUserSettingsTab">
<LoginWithQR onFinished={this.onLoginWithQRFinished} mode={this.state.showLoginWithQR} client={client} />
</div>;
return (
<div className="mx_SettingsTab mx_SecurityUserSettingsTab">
<LoginWithQR
onFinished={this.onLoginWithQRFinished}
mode={this.state.showLoginWithQR}
client={client}
/>
</div>
);
}
return (
<div className="mx_SettingsTab mx_SecurityUserSettingsTab">
{ warning }
{ devicesSection }
<div className="mx_SettingsTab_heading">{ _t("Encryption") }</div>
{warning}
{devicesSection}
<div className="mx_SettingsTab_heading">{_t("Encryption")}</div>
<div className="mx_SettingsTab_section">
{ secureBackup }
{ eventIndex }
{ crossSigning }
{secureBackup}
{eventIndex}
{crossSigning}
<CryptographyPanel />
</div>
{ privacySection }
{ advancedSection }
{privacySection}
{advancedSection}
</div>
);
}

View file

@ -14,42 +14,44 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
import { MatrixClient } from 'matrix-js-sdk/src/client';
import { logger } from 'matrix-js-sdk/src/logger';
import React, { useCallback, useContext, useEffect, useRef, useState } from "react";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../../../../../languageHandler";
import MatrixClientContext from '../../../../../contexts/MatrixClientContext';
import Modal from '../../../../../Modal';
import SettingsSubsection from '../../shared/SettingsSubsection';
import SetupEncryptionDialog from '../../../dialogs/security/SetupEncryptionDialog';
import VerificationRequestDialog from '../../../dialogs/VerificationRequestDialog';
import LogoutDialog from '../../../dialogs/LogoutDialog';
import { useOwnDevices } from '../../devices/useOwnDevices';
import { FilteredDeviceList } from '../../devices/FilteredDeviceList';
import CurrentDeviceSection from '../../devices/CurrentDeviceSection';
import SecurityRecommendations from '../../devices/SecurityRecommendations';
import { ExtendedDevice } from '../../devices/types';
import { deleteDevicesWithInteractiveAuth } from '../../devices/deleteDevices';
import SettingsTab from '../SettingsTab';
import LoginWithQRSection from '../../devices/LoginWithQRSection';
import LoginWithQR, { Mode } from '../../../auth/LoginWithQR';
import SettingsStore from '../../../../../settings/SettingsStore';
import { useAsyncMemo } from '../../../../../hooks/useAsyncMemo';
import QuestionDialog from '../../../dialogs/QuestionDialog';
import { FilterVariation } from '../../devices/filter';
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
import Modal from "../../../../../Modal";
import SettingsSubsection from "../../shared/SettingsSubsection";
import SetupEncryptionDialog from "../../../dialogs/security/SetupEncryptionDialog";
import VerificationRequestDialog from "../../../dialogs/VerificationRequestDialog";
import LogoutDialog from "../../../dialogs/LogoutDialog";
import { useOwnDevices } from "../../devices/useOwnDevices";
import { FilteredDeviceList } from "../../devices/FilteredDeviceList";
import CurrentDeviceSection from "../../devices/CurrentDeviceSection";
import SecurityRecommendations from "../../devices/SecurityRecommendations";
import { ExtendedDevice } from "../../devices/types";
import { deleteDevicesWithInteractiveAuth } from "../../devices/deleteDevices";
import SettingsTab from "../SettingsTab";
import LoginWithQRSection from "../../devices/LoginWithQRSection";
import LoginWithQR, { Mode } from "../../../auth/LoginWithQR";
import SettingsStore from "../../../../../settings/SettingsStore";
import { useAsyncMemo } from "../../../../../hooks/useAsyncMemo";
import QuestionDialog from "../../../dialogs/QuestionDialog";
import { FilterVariation } from "../../devices/filter";
const confirmSignOut = async (sessionsToSignOutCount: number): Promise<boolean> => {
const { finished } = Modal.createDialog(QuestionDialog, {
title: _t("Sign out"),
description: (
<div>
<p>{ _t("Are you sure you want to sign out of %(count)s sessions?", {
count: sessionsToSignOutCount,
}) }</p>
<p>
{_t("Are you sure you want to sign out of %(count)s sessions?", {
count: sessionsToSignOutCount,
})}
</p>
</div>
),
cancelButton: _t('Cancel'),
cancelButton: _t("Cancel"),
button: _t("Sign out"),
});
const [confirmed] = await finished;
@ -61,11 +63,11 @@ const useSignOut = (
matrixClient: MatrixClient,
onSignoutResolvedCallback: () => Promise<void>,
): {
onSignOutCurrentDevice: () => void;
onSignOutOtherDevices: (deviceIds: ExtendedDevice['device_id'][]) => Promise<void>;
signingOutDeviceIds: ExtendedDevice['device_id'][];
} => {
const [signingOutDeviceIds, setSigningOutDeviceIds] = useState<ExtendedDevice['device_id'][]>([]);
onSignOutCurrentDevice: () => void;
onSignOutOtherDevices: (deviceIds: ExtendedDevice["device_id"][]) => Promise<void>;
signingOutDeviceIds: ExtendedDevice["device_id"][];
} => {
const [signingOutDeviceIds, setSigningOutDeviceIds] = useState<ExtendedDevice["device_id"][]>([]);
const onSignOutCurrentDevice = () => {
Modal.createDialog(
@ -77,7 +79,7 @@ const useSignOut = (
);
};
const onSignOutOtherDevices = async (deviceIds: ExtendedDevice['device_id'][]) => {
const onSignOutOtherDevices = async (deviceIds: ExtendedDevice["device_id"][]) => {
if (!deviceIds.length) {
return;
}
@ -88,19 +90,15 @@ const useSignOut = (
try {
setSigningOutDeviceIds([...signingOutDeviceIds, ...deviceIds]);
await deleteDevicesWithInteractiveAuth(
matrixClient,
deviceIds,
async (success) => {
if (success) {
await onSignoutResolvedCallback();
}
setSigningOutDeviceIds(signingOutDeviceIds.filter(deviceId => !deviceIds.includes(deviceId)));
},
);
await deleteDevicesWithInteractiveAuth(matrixClient, deviceIds, async (success) => {
if (success) {
await onSignoutResolvedCallback();
}
setSigningOutDeviceIds(signingOutDeviceIds.filter((deviceId) => !deviceIds.includes(deviceId)));
});
} catch (error) {
logger.error("Error deleting sessions", error);
setSigningOutDeviceIds(signingOutDeviceIds.filter(deviceId => !deviceIds.includes(deviceId)));
setSigningOutDeviceIds(signingOutDeviceIds.filter((deviceId) => !deviceIds.includes(deviceId)));
}
};
@ -125,19 +123,19 @@ const SessionManagerTab: React.FC = () => {
supportsMSC3881,
} = useOwnDevices();
const [filter, setFilter] = useState<FilterVariation>();
const [expandedDeviceIds, setExpandedDeviceIds] = useState<ExtendedDevice['device_id'][]>([]);
const [selectedDeviceIds, setSelectedDeviceIds] = useState<ExtendedDevice['device_id'][]>([]);
const [expandedDeviceIds, setExpandedDeviceIds] = useState<ExtendedDevice["device_id"][]>([]);
const [selectedDeviceIds, setSelectedDeviceIds] = useState<ExtendedDevice["device_id"][]>([]);
const filteredDeviceListRef = useRef<HTMLDivElement>(null);
const scrollIntoViewTimeoutRef = useRef<number>();
const matrixClient = useContext(MatrixClientContext);
const userId = matrixClient.getUserId();
const currentUserMember = userId && matrixClient.getUser(userId) || undefined;
const currentUserMember = (userId && matrixClient.getUser(userId)) || undefined;
const clientVersions = useAsyncMemo(() => matrixClient.getVersions(), [matrixClient]);
const onDeviceExpandToggle = (deviceId: ExtendedDevice['device_id']): void => {
const onDeviceExpandToggle = (deviceId: ExtendedDevice["device_id"]): void => {
if (expandedDeviceIds.includes(deviceId)) {
setExpandedDeviceIds(expandedDeviceIds.filter(id => id !== deviceId));
setExpandedDeviceIds(expandedDeviceIds.filter((id) => id !== deviceId));
} else {
setExpandedDeviceIds([...expandedDeviceIds, deviceId]);
}
@ -147,63 +145,68 @@ const SessionManagerTab: React.FC = () => {
setFilter(filter);
clearTimeout(scrollIntoViewTimeoutRef.current);
// wait a tick for the filtered section to rerender with different height
scrollIntoViewTimeoutRef.current =
window.setTimeout(() => filteredDeviceListRef.current?.scrollIntoView({
scrollIntoViewTimeoutRef.current = window.setTimeout(() =>
filteredDeviceListRef.current?.scrollIntoView({
// align element to top of scrollbox
block: 'start',
inline: 'nearest',
behavior: 'smooth',
}));
block: "start",
inline: "nearest",
behavior: "smooth",
}),
);
};
const { [currentDeviceId]: currentDevice, ...otherDevices } = devices;
const shouldShowOtherSessions = Object.keys(otherDevices).length > 0;
const onVerifyCurrentDevice = () => {
Modal.createDialog(
SetupEncryptionDialog as unknown as React.ComponentType,
{ onFinished: refreshDevices },
);
Modal.createDialog(SetupEncryptionDialog as unknown as React.ComponentType, { onFinished: refreshDevices });
};
const onTriggerDeviceVerification = useCallback((deviceId: ExtendedDevice['device_id']) => {
if (!requestDeviceVerification) {
return;
}
const verificationRequestPromise = requestDeviceVerification(deviceId);
Modal.createDialog(VerificationRequestDialog, {
verificationRequestPromise,
member: currentUserMember,
onFinished: async () => {
const request = await verificationRequestPromise;
request.cancel();
await refreshDevices();
},
});
}, [requestDeviceVerification, refreshDevices, currentUserMember]);
const onTriggerDeviceVerification = useCallback(
(deviceId: ExtendedDevice["device_id"]) => {
if (!requestDeviceVerification) {
return;
}
const verificationRequestPromise = requestDeviceVerification(deviceId);
Modal.createDialog(VerificationRequestDialog, {
verificationRequestPromise,
member: currentUserMember,
onFinished: async () => {
const request = await verificationRequestPromise;
request.cancel();
await refreshDevices();
},
});
},
[requestDeviceVerification, refreshDevices, currentUserMember],
);
const onSignoutResolvedCallback = async () => {
await refreshDevices();
setSelectedDeviceIds([]);
};
const {
onSignOutCurrentDevice,
onSignOutOtherDevices,
signingOutDeviceIds,
} = useSignOut(matrixClient, onSignoutResolvedCallback);
const { onSignOutCurrentDevice, onSignOutOtherDevices, signingOutDeviceIds } = useSignOut(
matrixClient,
onSignoutResolvedCallback,
);
useEffect(() => () => {
clearTimeout(scrollIntoViewTimeoutRef.current);
}, [scrollIntoViewTimeoutRef]);
useEffect(
() => () => {
clearTimeout(scrollIntoViewTimeoutRef.current);
},
[scrollIntoViewTimeoutRef],
);
// clear selection when filter changes
useEffect(() => {
setSelectedDeviceIds([]);
}, [filter, setSelectedDeviceIds]);
const signOutAllOtherSessions = shouldShowOtherSessions ? () => {
onSignOutOtherDevices(Object.keys(otherDevices));
}: undefined;
const signOutAllOtherSessions = shouldShowOtherSessions
? () => {
onSignOutOtherDevices(Object.keys(otherDevices));
}
: undefined;
const [signInWithQrMode, setSignInWithQrMode] = useState<Mode | null>();
@ -218,65 +221,61 @@ const SessionManagerTab: React.FC = () => {
}, [setSignInWithQrMode]);
if (showQrCodeEnabled && signInWithQrMode) {
return <LoginWithQR
mode={signInWithQrMode}
onFinished={onQrFinish}
client={matrixClient}
/>;
return <LoginWithQR mode={signInWithQrMode} onFinished={onQrFinish} client={matrixClient} />;
}
return <SettingsTab heading={_t('Sessions')}>
<SecurityRecommendations
devices={devices}
goToFilteredList={onGoToFilteredList}
currentDeviceId={currentDeviceId}
/>
<CurrentDeviceSection
device={currentDevice}
localNotificationSettings={localNotificationSettings.get(currentDeviceId)}
setPushNotifications={setPushNotifications}
isSigningOut={signingOutDeviceIds.includes(currentDeviceId)}
isLoading={isLoadingDeviceList}
saveDeviceName={(deviceName) => saveDeviceName(currentDeviceId, deviceName)}
onVerifyCurrentDevice={onVerifyCurrentDevice}
onSignOutCurrentDevice={onSignOutCurrentDevice}
signOutAllOtherSessions={signOutAllOtherSessions}
/>
{
shouldShowOtherSessions &&
<SettingsSubsection
heading={_t('Other sessions')}
description={_t(
`For best security, verify your sessions and sign out ` +
`from any session that you don't recognize or use anymore.`,
)}
data-testid='other-sessions-section'
>
<FilteredDeviceList
devices={otherDevices}
pushers={pushers}
localNotificationSettings={localNotificationSettings}
filter={filter}
expandedDeviceIds={expandedDeviceIds}
signingOutDeviceIds={signingOutDeviceIds}
selectedDeviceIds={selectedDeviceIds}
setSelectedDeviceIds={setSelectedDeviceIds}
onFilterChange={setFilter}
onDeviceExpandToggle={onDeviceExpandToggle}
onRequestDeviceVerification={requestDeviceVerification ? onTriggerDeviceVerification : undefined}
onSignOutDevices={onSignOutOtherDevices}
saveDeviceName={saveDeviceName}
setPushNotifications={setPushNotifications}
ref={filteredDeviceListRef}
supportsMSC3881={supportsMSC3881}
/>
</SettingsSubsection>
}
{ showQrCodeEnabled ?
<LoginWithQRSection onShowQr={onShowQrClicked} versions={clientVersions} />
: null
}
</SettingsTab>;
return (
<SettingsTab heading={_t("Sessions")}>
<SecurityRecommendations
devices={devices}
goToFilteredList={onGoToFilteredList}
currentDeviceId={currentDeviceId}
/>
<CurrentDeviceSection
device={currentDevice}
localNotificationSettings={localNotificationSettings.get(currentDeviceId)}
setPushNotifications={setPushNotifications}
isSigningOut={signingOutDeviceIds.includes(currentDeviceId)}
isLoading={isLoadingDeviceList}
saveDeviceName={(deviceName) => saveDeviceName(currentDeviceId, deviceName)}
onVerifyCurrentDevice={onVerifyCurrentDevice}
onSignOutCurrentDevice={onSignOutCurrentDevice}
signOutAllOtherSessions={signOutAllOtherSessions}
/>
{shouldShowOtherSessions && (
<SettingsSubsection
heading={_t("Other sessions")}
description={_t(
`For best security, verify your sessions and sign out ` +
`from any session that you don't recognize or use anymore.`,
)}
data-testid="other-sessions-section"
>
<FilteredDeviceList
devices={otherDevices}
pushers={pushers}
localNotificationSettings={localNotificationSettings}
filter={filter}
expandedDeviceIds={expandedDeviceIds}
signingOutDeviceIds={signingOutDeviceIds}
selectedDeviceIds={selectedDeviceIds}
setSelectedDeviceIds={setSelectedDeviceIds}
onFilterChange={setFilter}
onDeviceExpandToggle={onDeviceExpandToggle}
onRequestDeviceVerification={
requestDeviceVerification ? onTriggerDeviceVerification : undefined
}
onSignOutDevices={onSignOutOtherDevices}
saveDeviceName={saveDeviceName}
setPushNotifications={setPushNotifications}
ref={filteredDeviceListRef}
supportsMSC3881={supportsMSC3881}
/>
</SettingsSubsection>
)}
{showQrCodeEnabled ? <LoginWithQRSection onShowQr={onShowQrClicked} versions={clientVersions} /> : null}
</SettingsTab>
);
};
export default SessionManagerTab;

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ChangeEvent } from 'react';
import React, { ChangeEvent } from "react";
import { _t } from "../../../../../languageHandler";
import SettingsStore from "../../../../../settings/SettingsStore";
@ -26,22 +26,20 @@ import PosthogTrackers from "../../../../../PosthogTrackers";
type InteractionName = "WebSettingsSidebarTabSpacesCheckbox" | "WebQuickSettingsPinToSidebarCheckbox";
export const onMetaSpaceChangeFactory = (
metaSpace: MetaSpace,
interactionName: InteractionName,
) => (e: ChangeEvent<HTMLInputElement>) => {
const currentValue = SettingsStore.getValue("Spaces.enabledMetaSpaces");
SettingsStore.setValue("Spaces.enabledMetaSpaces", null, SettingLevel.ACCOUNT, {
...currentValue,
[metaSpace]: e.target.checked,
});
export const onMetaSpaceChangeFactory =
(metaSpace: MetaSpace, interactionName: InteractionName) => (e: ChangeEvent<HTMLInputElement>) => {
const currentValue = SettingsStore.getValue("Spaces.enabledMetaSpaces");
SettingsStore.setValue("Spaces.enabledMetaSpaces", null, SettingLevel.ACCOUNT, {
...currentValue,
[metaSpace]: e.target.checked,
});
PosthogTrackers.trackInteraction(
interactionName,
e,
[MetaSpace.Home, null, MetaSpace.Favourites, MetaSpace.People, MetaSpace.Orphans].indexOf(metaSpace),
);
};
PosthogTrackers.trackInteraction(
interactionName,
e,
[MetaSpace.Home, null, MetaSpace.Favourites, MetaSpace.People, MetaSpace.Orphans].indexOf(metaSpace),
);
};
const SidebarUserSettingsTab = () => {
const {
@ -54,12 +52,14 @@ const SidebarUserSettingsTab = () => {
return (
<div className="mx_SettingsTab mx_SidebarUserSettingsTab">
<div className="mx_SettingsTab_heading">{ _t("Sidebar") }</div>
<div className="mx_SettingsTab_heading">{_t("Sidebar")}</div>
<div className="mx_SettingsTab_section">
<div className="mx_SettingsTab_subheading">{ _t("Spaces to show") }</div>
<div className="mx_SettingsTab_subheading">{_t("Spaces to show")}</div>
<div className="mx_SettingsTab_subsectionText">
{ _t("Spaces are ways to group rooms and people. " +
"Alongside the spaces you're in, you can use some pre-built ones too.") }
{_t(
"Spaces are ways to group rooms and people. " +
"Alongside the spaces you're in, you can use some pre-built ones too.",
)}
</div>
<StyledCheckbox
@ -68,30 +68,25 @@ const SidebarUserSettingsTab = () => {
className="mx_SidebarUserSettingsTab_homeCheckbox"
disabled={homeEnabled}
>
{ _t("Home") }
{_t("Home")}
</StyledCheckbox>
<div className="mx_SidebarUserSettingsTab_checkboxMicrocopy">
{ _t("Home is useful for getting an overview of everything.") }
{_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,
);
onChange={(e) => {
SettingsStore.setValue("Spaces.allRoomsInHome", null, SettingLevel.ACCOUNT, e.target.checked);
PosthogTrackers.trackInteraction("WebSettingsSidebarTabSpacesCheckbox", e, 1);
}}
className="mx_SidebarUserSettingsTab_homeAllRoomsCheckbox"
>
{ _t("Show all rooms") }
{_t("Show all rooms")}
</StyledCheckbox>
<div className="mx_SidebarUserSettingsTab_checkboxMicrocopy">
{ _t("Show all your rooms in Home, even if they're in a space.") }
{_t("Show all your rooms in Home, even if they're in a space.")}
</div>
<StyledCheckbox
@ -99,10 +94,10 @@ const SidebarUserSettingsTab = () => {
onChange={onMetaSpaceChangeFactory(MetaSpace.Favourites, "WebSettingsSidebarTabSpacesCheckbox")}
className="mx_SidebarUserSettingsTab_favouritesCheckbox"
>
{ _t("Favourites") }
{_t("Favourites")}
</StyledCheckbox>
<div className="mx_SidebarUserSettingsTab_checkboxMicrocopy">
{ _t("Group all your favourite rooms and people in one place.") }
{_t("Group all your favourite rooms and people in one place.")}
</div>
<StyledCheckbox
@ -110,10 +105,10 @@ const SidebarUserSettingsTab = () => {
onChange={onMetaSpaceChangeFactory(MetaSpace.People, "WebSettingsSidebarTabSpacesCheckbox")}
className="mx_SidebarUserSettingsTab_peopleCheckbox"
>
{ _t("People") }
{_t("People")}
</StyledCheckbox>
<div className="mx_SidebarUserSettingsTab_checkboxMicrocopy">
{ _t("Group all your people in one place.") }
{_t("Group all your people in one place.")}
</div>
<StyledCheckbox
@ -121,10 +116,10 @@ const SidebarUserSettingsTab = () => {
onChange={onMetaSpaceChangeFactory(MetaSpace.Orphans, "WebSettingsSidebarTabSpacesCheckbox")}
className="mx_SidebarUserSettingsTab_orphansCheckbox"
>
{ _t("Rooms outside of a space") }
{_t("Rooms outside of a space")}
</StyledCheckbox>
<div className="mx_SidebarUserSettingsTab_checkboxMicrocopy">
{ _t("Group all your rooms that aren't part of a space in one place.") }
{_t("Group all your rooms that aren't part of a space in one place.")}
</div>
</div>
</div>

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { _t } from "../../../../../languageHandler";
import MediaDeviceHandler, { IMediaDevices, MediaDeviceKindEnum } from "../../../../../MediaDeviceHandler";
@ -23,9 +23,9 @@ import Field from "../../../elements/Field";
import AccessibleButton from "../../../elements/AccessibleButton";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import SettingsFlag from '../../../elements/SettingsFlag';
import SettingsFlag from "../../../elements/SettingsFlag";
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
import { requestMediaPermissions } from '../../../../../utils/media/requestMediaPermissions';
import { requestMediaPermissions } from "../../../../../utils/media/requestMediaPermissions";
interface IState {
mediaDevices: IMediaDevices;
@ -96,7 +96,11 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
private renderDeviceOptions(devices: Array<MediaDeviceInfo>, category: MediaDeviceKindEnum): Array<JSX.Element> {
return devices.map((d) => {
return (<option key={`${category}-${d.deviceId}`} value={d.deviceId}>{ d.label }</option>);
return (
<option key={`${category}-${d.deviceId}`} value={d.deviceId}>
{d.label}
</option>
);
});
}
@ -112,7 +116,7 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
value={this.state[kind] || defaultDevice}
onChange={(e) => this.setDevice(e.target.value, kind)}
>
{ this.renderDeviceOptions(devices, kind) }
{this.renderDeviceOptions(devices, kind)}
</Field>
);
}
@ -124,36 +128,33 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
let webcamDropdown = null;
if (!this.state.mediaDevices) {
requestButton = (
<div className='mx_VoiceUserSettingsTab_missingMediaPermissions'>
<p>{ _t("Missing media permissions, click the button below to request.") }</p>
<div className="mx_VoiceUserSettingsTab_missingMediaPermissions">
<p>{_t("Missing media permissions, click the button below to request.")}</p>
<AccessibleButton onClick={this.requestMediaPermissions} kind="primary">
{ _t("Request media permissions") }
{_t("Request media permissions")}
</AccessibleButton>
</div>
);
} else if (this.state.mediaDevices) {
speakerDropdown = (
this.renderDropdown(MediaDeviceKindEnum.AudioOutput, _t("Audio Output")) ||
<p>{ _t('No Audio Outputs detected') }</p>
speakerDropdown = this.renderDropdown(MediaDeviceKindEnum.AudioOutput, _t("Audio Output")) || (
<p>{_t("No Audio Outputs detected")}</p>
);
microphoneDropdown = (
this.renderDropdown(MediaDeviceKindEnum.AudioInput, _t("Microphone")) ||
<p>{ _t('No Microphones detected') }</p>
microphoneDropdown = this.renderDropdown(MediaDeviceKindEnum.AudioInput, _t("Microphone")) || (
<p>{_t("No Microphones detected")}</p>
);
webcamDropdown = (
this.renderDropdown(MediaDeviceKindEnum.VideoInput, _t("Camera")) ||
<p>{ _t('No Webcams detected') }</p>
webcamDropdown = this.renderDropdown(MediaDeviceKindEnum.VideoInput, _t("Camera")) || (
<p>{_t("No Webcams detected")}</p>
);
}
return (
<div className="mx_SettingsTab mx_VoiceUserSettingsTab">
<div className="mx_SettingsTab_heading">{ _t("Voice & Video") }</div>
{ requestButton }
<div className="mx_SettingsTab_heading">{_t("Voice & Video")}</div>
{requestButton}
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Voice settings") }</span>
{ speakerDropdown }
{ microphoneDropdown }
<span className="mx_SettingsTab_subheading">{_t("Voice settings")}</span>
{speakerDropdown}
{microphoneDropdown}
<LabelledToggleSwitch
value={this.state.audioAutoGainControl}
onChange={async (v) => {
@ -161,18 +162,18 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
this.setState({ audioAutoGainControl: MediaDeviceHandler.getAudioAutoGainControl() });
}}
label={_t("Automatically adjust the microphone volume")}
data-testid='voice-auto-gain'
data-testid="voice-auto-gain"
/>
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Video settings") }</span>
{ webcamDropdown }
<SettingsFlag name='VideoView.flipVideoHorizontally' level={SettingLevel.ACCOUNT} />
<span className="mx_SettingsTab_subheading">{_t("Video settings")}</span>
{webcamDropdown}
<SettingsFlag name="VideoView.flipVideoHorizontally" level={SettingLevel.ACCOUNT} />
</div>
<div className="mx_SettingsTab_heading">{ _t("Advanced") }</div>
<div className="mx_SettingsTab_heading">{_t("Advanced")}</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Voice processing") }</span>
<span className="mx_SettingsTab_subheading">{_t("Voice processing")}</span>
<div className="mx_SettingsTab_section">
<LabelledToggleSwitch
value={this.state.audioNoiseSuppression}
@ -181,7 +182,7 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
this.setState({ audioNoiseSuppression: MediaDeviceHandler.getAudioNoiseSuppression() });
}}
label={_t("Noise suppression")}
data-testid='voice-noise-suppression'
data-testid="voice-noise-suppression"
/>
<LabelledToggleSwitch
value={this.state.audioEchoCancellation}
@ -190,18 +191,18 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
this.setState({ audioEchoCancellation: MediaDeviceHandler.getAudioEchoCancellation() });
}}
label={_t("Echo cancellation")}
data-testid='voice-echo-cancellation'
data-testid="voice-echo-cancellation"
/>
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Connection") }</span>
<span className="mx_SettingsTab_subheading">{_t("Connection")}</span>
<SettingsFlag
name='webRtcAllowPeerToPeer'
name="webRtcAllowPeerToPeer"
level={SettingLevel.DEVICE}
onChange={this.changeWebRtcMethod}
/>
<SettingsFlag
name='fallbackICEServerAllowed'
name="fallbackICEServerAllowed"
level={SettingLevel.DEVICE}
onChange={this.changeFallbackICEServerAllowed}
/>