Apply prettier formatting
This commit is contained in:
parent
1cac306093
commit
526645c791
1576 changed files with 65385 additions and 62478 deletions
|
@ -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!);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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() },
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
{ _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> {_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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 &&
|
||||
<>
|
||||
|
||||
<AccessibleButton
|
||||
kind='link_inline'
|
||||
onClick={clearFilter}
|
||||
data-testid='devices-clear-filter-btn'
|
||||
>
|
||||
{ _t('Show all') }
|
||||
</AccessibleButton>
|
||||
</>
|
||||
!!filter && (
|
||||
<>
|
||||
|
||||
<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>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)));
|
||||
};
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
@ -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>
|
||||
{ room.getVersion() }
|
||||
<span>{_t("Room version:")}</span>
|
||||
{room.getVersion()}
|
||||
</div>
|
||||
{ oldRoomLink }
|
||||
{ roomUpgradeButton }
|
||||
{oldRoomLink}
|
||||
{roomUpgradeButton}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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, 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 ©
|
||||
</a>{" "}
|
||||
is ©
|
||||
<a href="https://www.flickr.com/golan" rel="noreferrer noopener" target="_blank">
|
||||
Jesús Roncero
|
||||
</a> used under the terms of
|
||||
<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
|
||||
<a
|
||||
href="https://creativecommons.org/licenses/by-sa/4.0/"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
twemoji-colr
|
||||
</a> font is ©
|
||||
<a href="https://mozilla.org" rel="noreferrer noopener" target="_blank">
|
||||
Mozilla Foundation
|
||||
</a> used under the terms of
|
||||
<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 ©
|
||||
<a href="https://mozilla.org" rel="noreferrer noopener" target="_blank">
|
||||
Mozilla Foundation
|
||||
</a>{" "}
|
||||
used under the terms of
|
||||
<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 ©
|
||||
</a>{" "}
|
||||
emoji art is ©
|
||||
<a href="https://twemoji.twitter.com/" rel="noreferrer noopener" target="_blank">
|
||||
Twitter, Inc and other contributors
|
||||
</a> used under the terms of
|
||||
<a href="https://creativecommons.org/licenses/by/4.0/" rel="noreferrer noopener" target="_blank">
|
||||
</a>{" "}
|
||||
used under the terms of
|
||||
<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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
<code>{ rule.entity }</code>
|
||||
{_t("Remove")}
|
||||
</AccessibleButton>
|
||||
|
||||
<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>
|
||||
{_t("Unsubscribe")}
|
||||
</AccessibleButton>
|
||||
|
||||
<AccessibleButton
|
||||
kind="primary_sm"
|
||||
onClick={() => this.viewListRules(list)}
|
||||
disabled={this.state.busy}
|
||||
>
|
||||
{ _t("View rules") }
|
||||
</AccessibleButton>
|
||||
{ name }
|
||||
{_t("View rules")}
|
||||
</AccessibleButton>
|
||||
|
||||
{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>
|
||||
|
||||
<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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue