Merge branch 'develop' into germain-gg/facepile-offset
This commit is contained in:
commit
224f34c211
163 changed files with 15302 additions and 13240 deletions
|
@ -119,7 +119,7 @@ const FileDropTarget: React.FC<IProps> = ({ parent, onFileDrop }) => {
|
|||
className="mx_FileDropTarget_image"
|
||||
alt=""
|
||||
/>
|
||||
{_t("Drop file here to upload")}
|
||||
{_t("room|drop_file_prompt")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -231,7 +231,7 @@ class FilePanel extends React.Component<IProps, IState> {
|
|||
<BaseCard className="mx_FilePanel mx_RoomView_messageListWrapper" onClose={this.props.onClose}>
|
||||
<div className="mx_RoomView_empty">
|
||||
{_t(
|
||||
"You must <a>register</a> to use this functionality",
|
||||
"file_panel|guest_note",
|
||||
{},
|
||||
{
|
||||
a: (sub) => (
|
||||
|
@ -247,7 +247,7 @@ class FilePanel extends React.Component<IProps, IState> {
|
|||
} else if (this.noRoom) {
|
||||
return (
|
||||
<BaseCard className="mx_FilePanel mx_RoomView_messageListWrapper" onClose={this.props.onClose}>
|
||||
<div className="mx_RoomView_empty">{_t("You must join the room to see its files")}</div>
|
||||
<div className="mx_RoomView_empty">{_t("file_panel|peek_note")}</div>
|
||||
</BaseCard>
|
||||
);
|
||||
}
|
||||
|
@ -256,8 +256,8 @@ class FilePanel extends React.Component<IProps, IState> {
|
|||
|
||||
const emptyState = (
|
||||
<div className="mx_RightPanel_empty mx_FilePanel_empty">
|
||||
<h2>{_t("No files visible in this room")}</h2>
|
||||
<p>{_t("Attach files from chat or just drag and drop them anywhere in a room.")}</p>
|
||||
<h2>{_t("file_panel|empty_heading")}</h2>
|
||||
<p>{_t("file_panel|empty_description")}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -515,12 +515,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
const normalFontSize = "15px";
|
||||
|
||||
const waitText = _t("Wait!");
|
||||
const scamText = _t(
|
||||
"If someone told you to copy/paste something here, there is a high likelihood you're being scammed!",
|
||||
);
|
||||
const devText = _t(
|
||||
"If you know what you're doing, Element is open-source, be sure to check out our GitHub (https://github.com/vector-im/element-web/) and contribute!",
|
||||
);
|
||||
const scamText = _t("console_scam_warning");
|
||||
const devText = _t("console_dev_note");
|
||||
|
||||
global.mx_rage_logger.bypassRageshake(
|
||||
"log",
|
||||
|
@ -1611,8 +1607,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("Signed Out"),
|
||||
description: _t("For security, this session has been signed out. Please sign in again."),
|
||||
title: _t("auth|session_logged_out_title"),
|
||||
description: _t("auth|session_logged_out_description"),
|
||||
});
|
||||
|
||||
dis.dispatch({
|
||||
|
@ -1623,19 +1619,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
Modal.createDialog(
|
||||
QuestionDialog,
|
||||
{
|
||||
title: _t("Terms and Conditions"),
|
||||
title: _t("terms|tac_title"),
|
||||
description: (
|
||||
<div>
|
||||
<p>
|
||||
{" "}
|
||||
{_t(
|
||||
"To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.",
|
||||
{ homeserverDomain: cli.getDomain() },
|
||||
)}
|
||||
</p>
|
||||
<p> {_t("terms|tac_description", { homeserverDomain: cli.getDomain() })}</p>
|
||||
</div>
|
||||
),
|
||||
button: _t("Review terms and conditions"),
|
||||
button: _t("terms|tac_button"),
|
||||
cancelButton: _t("action|dismiss"),
|
||||
onFinished: (confirmed) => {
|
||||
if (confirmed) {
|
||||
|
@ -1676,11 +1666,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
switch (type) {
|
||||
case "CRYPTO_WARNING_OLD_VERSION_DETECTED":
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("Old cryptography data detected"),
|
||||
description: _t(
|
||||
"Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.",
|
||||
{ brand: SdkConfig.get().brand },
|
||||
),
|
||||
title: _t("encryption|old_version_detected_title"),
|
||||
description: _t("encryption|old_version_detected_description", {
|
||||
brand: SdkConfig.get().brand,
|
||||
}),
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
@ -1736,7 +1725,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
} else if (request.pending) {
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key: "verifreq_" + request.transactionId,
|
||||
title: _t("Verification requested"),
|
||||
title: _t("encryption|verification_requested_toast_title"),
|
||||
icon: "verification",
|
||||
props: { request },
|
||||
component: VerificationRequestToast,
|
||||
|
|
|
@ -1232,9 +1232,9 @@ class CreationGrouper extends BaseGrouper {
|
|||
const roomId = ev.getRoomId();
|
||||
const creator = ev.sender?.name ?? ev.getSender();
|
||||
if (roomId && DMRoomMap.shared().getUserIdForRoomId(roomId)) {
|
||||
summaryText = _t("%(creator)s created this DM.", { creator });
|
||||
summaryText = _t("timeline|creation_summary_dm", { creator });
|
||||
} else {
|
||||
summaryText = _t("%(creator)s created and configured the room.", { creator });
|
||||
summaryText = _t("timeline|creation_summary_room", { creator });
|
||||
}
|
||||
|
||||
ret.push(<NewRoomIntro key="newroomintro" />);
|
||||
|
|
|
@ -58,8 +58,8 @@ export default class NotificationPanel extends React.PureComponent<IProps, IStat
|
|||
public render(): React.ReactNode {
|
||||
const emptyState = (
|
||||
<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
|
||||
<h2>{_t("You're all caught up")}</h2>
|
||||
<p>{_t("You have no visible notifications.")}</p>
|
||||
<h2>{_t("notif_panel|empty_heading")}</h2>
|
||||
<p>{_t("notif_panel|empty_description")}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -2335,7 +2335,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
className="mx_RoomView_auxPanel_hiddenHighlights"
|
||||
onClick={this.onHiddenHighlightsClick}
|
||||
>
|
||||
{_t("You have %(count)s unread notifications in a prior version of this room.", {
|
||||
{_t("room|unread_notifications_predecessor", {
|
||||
count: hiddenHighlightCount,
|
||||
})}
|
||||
</AccessibleButton>
|
||||
|
|
|
@ -245,9 +245,7 @@ const Tile: React.FC<ITileProps> = ({
|
|||
|
||||
let suggestedSection: ReactElement | undefined;
|
||||
if (suggested && (!joinedRoom || hasPermissions)) {
|
||||
suggestedSection = (
|
||||
<InfoTooltip tooltip={_t("This room is suggested as a good one to join")}>{_t("Suggested")}</InfoTooltip>
|
||||
);
|
||||
suggestedSection = <InfoTooltip tooltip={_t("space|suggested_tooltip")}>{_t("space|suggested")}</InfoTooltip>;
|
||||
}
|
||||
|
||||
const content = (
|
||||
|
@ -670,14 +668,14 @@ const ManageButtons: React.FC<IManageButtonsProps> = ({ hierarchy, selected, set
|
|||
if (!selectedRelations.length) {
|
||||
Button = AccessibleTooltipButton;
|
||||
props = {
|
||||
tooltip: _t("Select a room below first"),
|
||||
tooltip: _t("space|select_room_below"),
|
||||
alignment: Alignment.Top,
|
||||
};
|
||||
}
|
||||
|
||||
let buttonText = _t("Saving…");
|
||||
if (!saving) {
|
||||
buttonText = selectionAllSuggested ? _t("Mark as not suggested") : _t("Mark as suggested");
|
||||
buttonText = selectionAllSuggested ? _t("space|unmark_suggested") : _t("space|mark_suggested");
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -707,7 +705,7 @@ const ManageButtons: React.FC<IManageButtonsProps> = ({ hierarchy, selected, set
|
|||
hierarchy.removeRelation(parentId, childId);
|
||||
}
|
||||
} catch (e) {
|
||||
setError(_t("Failed to remove some rooms. Try again later"));
|
||||
setError(_t("space|failed_remove_rooms"));
|
||||
}
|
||||
setRemoving(false);
|
||||
setSelected(new Map());
|
||||
|
@ -788,13 +786,13 @@ const SpaceHierarchy: React.FC<IProps> = ({ space, initialText = "", showRoom, a
|
|||
const [error, setError] = useState("");
|
||||
let errorText = error;
|
||||
if (!error && hierarchyError) {
|
||||
errorText = _t("Failed to load list of rooms.");
|
||||
errorText = _t("space|failed_load_rooms");
|
||||
}
|
||||
|
||||
const loaderRef = useIntersectionObserver(loadMore);
|
||||
|
||||
if (!loading && hierarchy!.noSupport) {
|
||||
return <p>{_t("Your server does not support showing space hierarchies.")}</p>;
|
||||
return <p>{_t("space|incompatible_server_hierarchy")}</p>;
|
||||
}
|
||||
|
||||
const onKeyDown = (ev: KeyboardEvent, state: IState): void => {
|
||||
|
|
|
@ -487,7 +487,7 @@ const validateEmailRules = withValidation({
|
|||
{
|
||||
key: "email",
|
||||
test: ({ value }) => !value || Email.looksValid(value),
|
||||
invalid: () => _t("Doesn't look like a valid email address"),
|
||||
invalid: () => _t("auth|email_field_label_invalid"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -509,7 +509,7 @@ const SpaceSetupPrivateInvite: React.FC<{
|
|||
name={name}
|
||||
type="text"
|
||||
label={_t("Email address")}
|
||||
placeholder={_t("Email")}
|
||||
placeholder={_t("auth|email_field_label")}
|
||||
value={emailAddresses[i]}
|
||||
onChange={(ev: React.ChangeEvent<HTMLInputElement>) => setEmailAddress(i, ev.target.value)}
|
||||
ref={fieldRefs[i]}
|
||||
|
|
|
@ -354,7 +354,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
/>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconLock"
|
||||
label={_t("Security & Privacy")}
|
||||
label={_t("room_settings|security|title")}
|
||||
onClick={(e) => this.onSettingsOpen(e, UserTab.Security)}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
|
@ -410,11 +410,16 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
<RovingAccessibleTooltipButton
|
||||
className="mx_UserMenu_contextMenu_themeButton"
|
||||
onClick={this.onSwitchThemeClick}
|
||||
title={this.state.isDarkTheme ? _t("Switch to light mode") : _t("Switch to dark mode")}
|
||||
title={
|
||||
this.state.isDarkTheme
|
||||
? _t("user_menu|switch_theme_light")
|
||||
: _t("user_menu|switch_theme_dark")
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={require("../../../res/img/element-icons/roomlist/dark-light-mode.svg").default}
|
||||
alt={_t("Switch theme")}
|
||||
role="presentation"
|
||||
alt=""
|
||||
width={16}
|
||||
/>
|
||||
</RovingAccessibleTooltipButton>
|
||||
|
|
|
@ -79,19 +79,21 @@ export default class ViewSource extends React.Component<IProps, IState> {
|
|||
<>
|
||||
<details open className="mx_ViewSource_details">
|
||||
<summary>
|
||||
<span className="mx_ViewSource_heading">{_t("Decrypted event source")}</span>
|
||||
<span className="mx_ViewSource_heading">
|
||||
{_t("devtools|view_source_decrypted_event_source")}
|
||||
</span>
|
||||
</summary>
|
||||
{decryptedEventSource ? (
|
||||
<CopyableText getTextToCopy={copyDecryptedFunc}>
|
||||
<SyntaxHighlight language="json">{stringify(decryptedEventSource)}</SyntaxHighlight>
|
||||
</CopyableText>
|
||||
) : (
|
||||
<div>{_t("Decrypted source unavailable")}</div>
|
||||
<div>{_t("devtools|view_source_decrypted_event_source_unavailable")}</div>
|
||||
)}
|
||||
</details>
|
||||
<details className="mx_ViewSource_details">
|
||||
<summary>
|
||||
<span className="mx_ViewSource_heading">{_t("Original event source")}</span>
|
||||
<span className="mx_ViewSource_heading">{_t("devtools|original_event_source")}</span>
|
||||
</summary>
|
||||
<CopyableText getTextToCopy={copyOriginalFunc}>
|
||||
<SyntaxHighlight language="json">{stringify(originalEventSource)}</SyntaxHighlight>
|
||||
|
@ -102,7 +104,7 @@ export default class ViewSource extends React.Component<IProps, IState> {
|
|||
} else {
|
||||
return (
|
||||
<>
|
||||
<div className="mx_ViewSource_heading">{_t("Original event source")}</div>
|
||||
<div className="mx_ViewSource_heading">{_t("devtools|original_event_source")}</div>
|
||||
<CopyableText getTextToCopy={copyOriginalFunc}>
|
||||
<SyntaxHighlight language="json">{stringify(originalEventSource)}</SyntaxHighlight>
|
||||
</CopyableText>
|
||||
|
|
|
@ -396,7 +396,7 @@ export default class ForgotPassword extends React.Component<Props, State> {
|
|||
<PassphraseField
|
||||
name="reset_password"
|
||||
type="password"
|
||||
label={_td("New Password")}
|
||||
label={_td("auth|change_password_new_label")}
|
||||
value={this.state.password}
|
||||
minScore={PASSWORD_MIN_SCORE}
|
||||
fieldRef={(field) => (this.fieldPassword = field)}
|
||||
|
|
|
@ -591,7 +591,7 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
const signIn = (
|
||||
<span className="mx_AuthBody_changeFlow">
|
||||
{_t(
|
||||
"auth|sign_in_instead",
|
||||
"auth|sign_in_instead_prompt",
|
||||
{},
|
||||
{
|
||||
a: (sub) => (
|
||||
|
@ -682,7 +682,7 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
title={_t("Create account")}
|
||||
serverPicker={
|
||||
<ServerPicker
|
||||
title={_t("auth|server_picker_title")}
|
||||
title={_t("auth|server_picker_title_registration")}
|
||||
dialogTitle={_t("auth|server_picker_dialog_title")}
|
||||
serverConfig={this.props.serverConfig}
|
||||
onServerConfigChange={
|
||||
|
|
|
@ -53,7 +53,7 @@ export const CheckEmail: React.FC<CheckEmailProps> = ({
|
|||
return (
|
||||
<>
|
||||
<EMailPromptIcon className="mx_AuthBody_emailPromptIcon--shifted" />
|
||||
<h1>{_t("Check your email to continue")}</h1>
|
||||
<h1>{_t("auth|uia|email_auth_header")}</h1>
|
||||
<div className="mx_AuthBody_text">
|
||||
<p>{_t("auth|check_email_explainer", { email: email }, { b: (t) => <b>{t}</b> })}</p>
|
||||
<div className="mx_AuthBody_did-not-receive">
|
||||
|
|
|
@ -40,9 +40,9 @@ interface IProps extends Omit<IInputProps, "onValidate" | "element"> {
|
|||
|
||||
class EmailField extends PureComponent<IProps> {
|
||||
public static defaultProps = {
|
||||
label: _td("Email"),
|
||||
labelRequired: _td("Enter email address"),
|
||||
labelInvalid: _td("Doesn't look like a valid email address"),
|
||||
label: _td("auth|email_field_label"),
|
||||
labelRequired: _td("auth|email_field_label_required"),
|
||||
labelInvalid: _td("auth|email_field_label_invalid"),
|
||||
};
|
||||
|
||||
public readonly validate = withValidation({
|
||||
|
|
|
@ -169,7 +169,7 @@ export class PasswordAuthEntry extends React.Component<IAuthEntryProps, IPasswor
|
|||
|
||||
return (
|
||||
<div>
|
||||
<p>{_t("Confirm your identity by entering your account password below.")}</p>
|
||||
<p>{_t("auth|uia|password_prompt")}</p>
|
||||
<form onSubmit={this.onSubmit} className="mx_InteractiveAuthEntryComponents_passwordSection">
|
||||
<Field
|
||||
className={passwordBoxClass}
|
||||
|
@ -219,9 +219,7 @@ export class RecaptchaAuthEntry extends React.Component<IRecaptchaAuthEntryProps
|
|||
|
||||
let sitePublicKey: string | undefined;
|
||||
if (!this.props.stageParams || !this.props.stageParams.public_key) {
|
||||
errorText = _t(
|
||||
"Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.",
|
||||
);
|
||||
errorText = _t("auth|uia|recaptcha_missing_params");
|
||||
} else {
|
||||
sitePublicKey = this.props.stageParams.public_key;
|
||||
}
|
||||
|
@ -352,7 +350,7 @@ export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITerms
|
|||
if (allChecked) {
|
||||
this.props.submitAuthDict({ type: AuthType.Terms });
|
||||
} else {
|
||||
this.setState({ errorText: _t("Please review and accept all of the homeserver's policies") });
|
||||
this.setState({ errorText: _t("auth|uia|terms_invalid") });
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -403,7 +401,7 @@ export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITerms
|
|||
|
||||
return (
|
||||
<div className="mx_InteractiveAuthEntryComponents">
|
||||
<p>{_t("Please review and accept the policies of this homeserver:")}</p>
|
||||
<p>{_t("auth|uia|terms")}</p>
|
||||
{checkboxes}
|
||||
{errorSection}
|
||||
{submitButton}
|
||||
|
@ -474,19 +472,19 @@ export class EmailIdentityAuthEntry extends React.Component<
|
|||
return (
|
||||
<div className="mx_InteractiveAuthEntryComponents_emailWrapper">
|
||||
<AuthHeaderModifier
|
||||
title={_t("Check your email to continue")}
|
||||
icon={<img src={EmailPromptIcon} alt={_t("Unread email icon")} width={16} />}
|
||||
title={_t("auth|uia|email_auth_header")}
|
||||
icon={<img src={EmailPromptIcon} role="presentation" alt="" width={16} />}
|
||||
hideServerPicker={true}
|
||||
/>
|
||||
<p>
|
||||
{_t("To create your account, open the link in the email we just sent to %(emailAddress)s.", {
|
||||
{_t("auth|uia|email", {
|
||||
emailAddress: <b>{this.props.inputs.emailAddress}</b>,
|
||||
})}
|
||||
</p>
|
||||
{this.state.requesting ? (
|
||||
<p className="secondary">
|
||||
{_t(
|
||||
"Did not receive it? <a>Resend it</a>",
|
||||
"auth|uia|email_resend_prompt",
|
||||
{},
|
||||
{
|
||||
a: (text: string) => (
|
||||
|
@ -502,13 +500,15 @@ export class EmailIdentityAuthEntry extends React.Component<
|
|||
) : (
|
||||
<p className="secondary">
|
||||
{_t(
|
||||
"Did not receive it? <a>Resend it</a>",
|
||||
"auth|uia|email_resend_prompt",
|
||||
{},
|
||||
{
|
||||
a: (text: string) => (
|
||||
<AccessibleTooltipButton
|
||||
kind="link_inline"
|
||||
title={this.state.requested ? _t("Resent!") : _t("action|resend")}
|
||||
title={
|
||||
this.state.requested ? _t("auth|uia|email_resent") : _t("action|resend")
|
||||
}
|
||||
alignment={Alignment.Right}
|
||||
onHideTooltip={
|
||||
this.state.requested
|
||||
|
@ -642,7 +642,7 @@ export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsi
|
|||
});
|
||||
} else {
|
||||
this.setState({
|
||||
errorText: _t("Token incorrect"),
|
||||
errorText: _t("auth|uia|msisdn_token_incorrect"),
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
|
@ -670,8 +670,8 @@ export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsi
|
|||
}
|
||||
return (
|
||||
<div>
|
||||
<p>{_t("A text message has been sent to %(msisdn)s", { msisdn: <i>{this.msisdn}</i> })}</p>
|
||||
<p>{_t("Please enter the code it contains:")}</p>
|
||||
<p>{_t("auth|uia|msisdn", { msisdn: <i>{this.msisdn}</i> })}</p>
|
||||
<p>{_t("auth|uia|msisdn_token_prompt")}</p>
|
||||
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
|
||||
<form onSubmit={this.onFormSubmit}>
|
||||
<input
|
||||
|
@ -761,13 +761,13 @@ export class RegistrationTokenAuthEntry extends React.Component<IAuthEntryProps,
|
|||
|
||||
return (
|
||||
<div>
|
||||
<p>{_t("Enter a registration token provided by the homeserver administrator.")}</p>
|
||||
<p>{_t("auth|uia|registration_token_prompt")}</p>
|
||||
<form onSubmit={this.onSubmit} className="mx_InteractiveAuthEntryComponents_registrationTokenSection">
|
||||
<Field
|
||||
className={registrationTokenBoxClass}
|
||||
type="text"
|
||||
name="registrationTokenField"
|
||||
label={_t("Registration token")}
|
||||
label={_t("auth|uia|registration_token_label")}
|
||||
autoFocus={true}
|
||||
value={this.state.registrationToken}
|
||||
onChange={this.onRegistrationTokenFieldChange}
|
||||
|
@ -895,7 +895,7 @@ export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEn
|
|||
} else if (this.state.attemptFailed) {
|
||||
errorSection = (
|
||||
<div className="error" role="alert">
|
||||
{_t("Something went wrong in confirming your identity. Cancel and try again.")}
|
||||
{_t("auth|uia|sso_failed")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -972,7 +972,7 @@ export class FallbackAuthEntry extends React.Component<IAuthEntryProps> {
|
|||
return (
|
||||
<div>
|
||||
<AccessibleButton kind="link" inputRef={this.fallbackButton} onClick={this.onShowFallbackClick}>
|
||||
{_t("Start authentication")}
|
||||
{_t("auth|uia|fallback_button")}
|
||||
</AccessibleButton>
|
||||
{errorSection}
|
||||
</div>
|
||||
|
|
|
@ -169,7 +169,7 @@ export default class LoginWithQRFlow extends React.Component<IProps> {
|
|||
);
|
||||
break;
|
||||
case Phase.ShowingQR:
|
||||
title = _t("Sign in with QR code");
|
||||
title = _t("settings|sessions|sign_in_with_qr");
|
||||
if (this.props.code) {
|
||||
const code = (
|
||||
<div className="mx_LoginWithQR_qrWrapper">
|
||||
|
|
|
@ -37,9 +37,9 @@ interface IProps extends Omit<IInputProps, "onValidate" | "label" | "element"> {
|
|||
|
||||
class PassphraseConfirmField extends PureComponent<IProps> {
|
||||
public static defaultProps = {
|
||||
label: _td("Confirm password"),
|
||||
labelRequired: _td("Confirm password"),
|
||||
labelInvalid: _td("Passwords don't match"),
|
||||
label: _td("auth|change_password_confirm_label"),
|
||||
labelRequired: _td("auth|change_password_confirm_label"),
|
||||
labelInvalid: _td("auth|change_password_confirm_invalid"),
|
||||
};
|
||||
|
||||
private validate = withValidation({
|
||||
|
|
|
@ -46,9 +46,9 @@ interface IProps extends Omit<IInputProps, "onValidate" | "element"> {
|
|||
class PassphraseField extends PureComponent<IProps> {
|
||||
public static defaultProps = {
|
||||
label: _td("common|password"),
|
||||
labelEnterPassword: _td("Enter password"),
|
||||
labelStrongPassword: _td("Nice, strong password!"),
|
||||
labelAllowedButUnsafe: _td("Password is allowed, but unsafe"),
|
||||
labelEnterPassword: _td("auth|password_field_label"),
|
||||
labelStrongPassword: _td("auth|password_field_strong_label"),
|
||||
labelAllowedButUnsafe: _td("auth|password_field_weak_label"),
|
||||
};
|
||||
|
||||
public readonly validate = withValidation<this, ZxcvbnResult | null>({
|
||||
|
@ -91,7 +91,7 @@ class PassphraseField extends PureComponent<IProps> {
|
|||
return null;
|
||||
}
|
||||
const { feedback } = complexity;
|
||||
return feedback.warning || feedback.suggestions[0] || _t("Keep going…");
|
||||
return feedback.warning || feedback.suggestions[0] || _t("auth|password_field_keep_going_prompt");
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -214,7 +214,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
|||
test({ value, allowEmpty }) {
|
||||
return allowEmpty || !!value;
|
||||
},
|
||||
invalid: () => _t("Enter username"),
|
||||
invalid: () => _t("auth|username_field_required_invalid"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -236,12 +236,12 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
|||
test({ value, allowEmpty }): boolean {
|
||||
return allowEmpty || !!value;
|
||||
},
|
||||
invalid: (): string => _t("Enter phone number"),
|
||||
invalid: (): string => _t("auth|msisdn_field_required_invalid"),
|
||||
},
|
||||
{
|
||||
key: "number",
|
||||
test: ({ value }): boolean => !value || PHONE_NUMBER_REGEX.test(value),
|
||||
invalid: (): string => _t("That phone number doesn't look quite right, please check and try again"),
|
||||
invalid: (): string => _t("auth|msisdn_field_number_invalid"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -259,7 +259,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
|||
test({ value, allowEmpty }): boolean {
|
||||
return allowEmpty || !!value;
|
||||
},
|
||||
invalid: (): string => _t("Enter password"),
|
||||
invalid: (): string => _t("auth|password_field_label"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -341,7 +341,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
|||
autoComplete="tel-national"
|
||||
key="phone_input"
|
||||
type="text"
|
||||
label={_t("Phone")}
|
||||
label={_t("auth|msisdn_field_label")}
|
||||
value={this.props.phoneNumber}
|
||||
prefixComponent={phoneCountry}
|
||||
onChange={this.onPhoneNumberChanged}
|
||||
|
@ -378,7 +378,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
|||
kind="link"
|
||||
onClick={this.onForgotPasswordClick}
|
||||
>
|
||||
{_t("Forgot password?")}
|
||||
{_t("auth|reset_password_button")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
@ -396,7 +396,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
|||
if (!SdkConfig.get().disable_3pid_login) {
|
||||
loginType = (
|
||||
<div className="mx_Login_type_container">
|
||||
<label className="mx_Login_type_label">{_t("Sign in with")}</label>
|
||||
<label className="mx_Login_type_label">{_t("auth|identifier_label")}</label>
|
||||
<Field
|
||||
element="select"
|
||||
value={this.state.loginType}
|
||||
|
@ -410,7 +410,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
|||
{_t("Email address")}
|
||||
</option>
|
||||
<option key={LoginField.Password} value={LoginField.Password}>
|
||||
{_t("Phone")}
|
||||
{_t("auth|msisdn_field_label")}
|
||||
</option>
|
||||
</Field>
|
||||
</div>
|
||||
|
|
|
@ -267,7 +267,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
|||
};
|
||||
|
||||
private validateEmailRules = withValidation({
|
||||
description: () => _t("Use an email address to recover your account"),
|
||||
description: () => _t("auth|reset_password_email_field_description"),
|
||||
hideDescriptionIfValid: true,
|
||||
rules: [
|
||||
{
|
||||
|
@ -275,12 +275,12 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
|||
test(this: RegistrationForm, { value, allowEmpty }) {
|
||||
return allowEmpty || !this.authStepIsRequired("m.login.email.identity") || !!value;
|
||||
},
|
||||
invalid: () => _t("Enter email address (required on this homeserver)"),
|
||||
invalid: () => _t("auth|reset_password_email_field_required_invalid"),
|
||||
},
|
||||
{
|
||||
key: "email",
|
||||
test: ({ value }) => !value || Email.looksValid(value),
|
||||
invalid: () => _t("Doesn't look like a valid email address"),
|
||||
invalid: () => _t("auth|email_field_label_invalid"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -324,7 +324,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
|||
};
|
||||
|
||||
private validatePhoneNumberRules = withValidation({
|
||||
description: () => _t("Other users can invite you to rooms using your contact details"),
|
||||
description: () => _t("auth|msisdn_field_description"),
|
||||
hideDescriptionIfValid: true,
|
||||
rules: [
|
||||
{
|
||||
|
@ -332,12 +332,12 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
|||
test(this: RegistrationForm, { value, allowEmpty }) {
|
||||
return allowEmpty || !this.authStepIsRequired("m.login.msisdn") || !!value;
|
||||
},
|
||||
invalid: () => _t("Enter phone number (required on this homeserver)"),
|
||||
invalid: () => _t("auth|registration_msisdn_field_required_invalid"),
|
||||
},
|
||||
{
|
||||
key: "email",
|
||||
test: ({ value }) => !value || phoneNumberLooksValid(value),
|
||||
invalid: () => _t("That phone number doesn't look quite right, please check and try again"),
|
||||
invalid: () => _t("auth|msisdn_field_number_invalid"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -358,7 +358,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
|||
description: (_, results) => {
|
||||
// omit the description if the only failing result is the `available` one as it makes no sense for it.
|
||||
if (results.every(({ key, valid }) => key === "available" || valid)) return null;
|
||||
return _t("Use lowercase letters, numbers, dashes and underscores only");
|
||||
return _t("auth|registration_username_validation");
|
||||
},
|
||||
hideDescriptionIfValid: true,
|
||||
async deriveData(this: RegistrationForm, { value }) {
|
||||
|
@ -380,7 +380,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
|||
{
|
||||
key: "required",
|
||||
test: ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||
invalid: () => _t("Enter username"),
|
||||
invalid: () => _t("auth|username_field_required_invalid"),
|
||||
},
|
||||
{
|
||||
key: "safeLocalpart",
|
||||
|
@ -401,8 +401,8 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
|||
},
|
||||
invalid: (usernameAvailable) =>
|
||||
usernameAvailable === UsernameAvailableStatus.Error
|
||||
? _t("Unable to check if username has been taken. Try again later.")
|
||||
: _t("Someone already has that username. Try another or if it is you, sign in below."),
|
||||
? _t("auth|registration_username_unable_check")
|
||||
: _t("auth|registration_username_in_use"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -451,7 +451,9 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
|||
if (!this.showEmail()) {
|
||||
return null;
|
||||
}
|
||||
const emailLabel = this.authStepIsRequired("m.login.email.identity") ? _td("Email") : _td("Email (optional)");
|
||||
const emailLabel = this.authStepIsRequired("m.login.email.identity")
|
||||
? _td("auth|email_field_label")
|
||||
: _td("Email (optional)");
|
||||
return (
|
||||
<EmailField
|
||||
fieldRef={(field) => (this[RegistrationField.Email] = field)}
|
||||
|
@ -496,7 +498,9 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
|||
if (!this.showPhoneNumber()) {
|
||||
return null;
|
||||
}
|
||||
const phoneLabel = this.authStepIsRequired("m.login.msisdn") ? _t("Phone") : _t("Phone (optional)");
|
||||
const phoneLabel = this.authStepIsRequired("m.login.msisdn")
|
||||
? _t("auth|phone_label")
|
||||
: _t("auth|phone_optional_label");
|
||||
const phoneCountry = (
|
||||
<CountryDropdown
|
||||
value={this.state.phoneCountry}
|
||||
|
@ -549,15 +553,13 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
|||
if (this.showPhoneNumber()) {
|
||||
emailHelperText = (
|
||||
<div>
|
||||
{_t("Add an email to be able to reset your password.")}{" "}
|
||||
{_t("Use email or phone to optionally be discoverable by existing contacts.")}
|
||||
{_t("auth|email_help_text")} {_t("auth|email_phone_discovery_text")}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
emailHelperText = (
|
||||
<div>
|
||||
{_t("Add an email to be able to reset your password.")}{" "}
|
||||
{_t("Use email to optionally be discoverable by existing contacts.")}
|
||||
{_t("auth|email_help_text")} {_t("auth|email_discovery_text")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -132,7 +132,7 @@ const SpaceContextMenu: React.FC<IProps> = ({ space, hideHeader, onFinished, ...
|
|||
devtoolsOption = (
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_SpacePanel_iconSettings"
|
||||
label={_t("See room timeline (devtools)")}
|
||||
label={_t("space|context_menu|devtools_open_timeline")}
|
||||
onClick={onViewTimelineClick}
|
||||
/>
|
||||
);
|
||||
|
@ -243,13 +243,13 @@ const SpaceContextMenu: React.FC<IProps> = ({ space, hideHeader, onFinished, ...
|
|||
<IconizedContextMenuOptionList first>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_SpacePanel_iconHome"
|
||||
label={_t("Space home")}
|
||||
label={_t("space|context_menu|home")}
|
||||
onClick={onHomeClick}
|
||||
/>
|
||||
{inviteOption}
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_SpacePanel_iconExplore"
|
||||
label={canAddRooms ? _t("Manage & explore rooms") : _t("Explore rooms")}
|
||||
label={canAddRooms ? _t("space|context_menu|manage_and_explore") : _t("space|context_menu|explore")}
|
||||
onClick={onExploreRoomsClick}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import type CreateKeyBackupDialog from "../../../async-components/views/dialogs/security/CreateKeyBackupDialog";
|
||||
|
@ -35,10 +34,28 @@ interface IProps {
|
|||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
enum BackupStatus {
|
||||
/** we're trying to figure out if there is an active backup */
|
||||
LOADING,
|
||||
|
||||
/** crypto is disabled in this client (so no need to back up) */
|
||||
NO_CRYPTO,
|
||||
|
||||
/** Key backup is active and working */
|
||||
BACKUP_ACTIVE,
|
||||
|
||||
/** there is a backup on the server but we are not backing up to it */
|
||||
SERVER_BACKUP_BUT_DISABLED,
|
||||
|
||||
/** backup is not set up locally and there is no backup on the server */
|
||||
NO_BACKUP,
|
||||
|
||||
/** there was an error fetching the state */
|
||||
ERROR,
|
||||
}
|
||||
|
||||
interface IState {
|
||||
shouldLoadBackupStatus: boolean;
|
||||
loading: boolean;
|
||||
backupInfo: IKeyBackupInfo | null;
|
||||
backupStatus: BackupStatus;
|
||||
}
|
||||
|
||||
export default class LogoutDialog extends React.Component<IProps, IState> {
|
||||
|
@ -49,33 +66,40 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
|
|||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
const shouldLoadBackupStatus = cli.isCryptoEnabled() && !cli.getKeyBackupEnabled();
|
||||
|
||||
this.state = {
|
||||
shouldLoadBackupStatus: shouldLoadBackupStatus,
|
||||
loading: shouldLoadBackupStatus,
|
||||
backupInfo: null,
|
||||
backupStatus: BackupStatus.LOADING,
|
||||
};
|
||||
|
||||
if (shouldLoadBackupStatus) {
|
||||
this.loadBackupStatus();
|
||||
}
|
||||
// we can't call setState() immediately, so wait a beat
|
||||
window.setTimeout(() => this.startLoadBackupStatus(), 0);
|
||||
}
|
||||
|
||||
/** kick off the asynchronous calls to populate `state.backupStatus` in the background */
|
||||
private startLoadBackupStatus(): void {
|
||||
this.loadBackupStatus().catch((e) => {
|
||||
logger.log("Unable to fetch key backup status", e);
|
||||
this.setState({
|
||||
backupStatus: BackupStatus.ERROR,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async loadBackupStatus(): Promise<void> {
|
||||
try {
|
||||
const backupInfo = await MatrixClientPeg.safeGet().getKeyBackupVersion();
|
||||
this.setState({
|
||||
loading: false,
|
||||
backupInfo,
|
||||
});
|
||||
} catch (e) {
|
||||
logger.log("Unable to fetch key backup status", e);
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
const client = MatrixClientPeg.safeGet();
|
||||
const crypto = client.getCrypto();
|
||||
if (!crypto) {
|
||||
this.setState({ backupStatus: BackupStatus.NO_CRYPTO });
|
||||
return;
|
||||
}
|
||||
|
||||
if ((await crypto.getActiveSessionBackupVersion()) !== null) {
|
||||
this.setState({ backupStatus: BackupStatus.BACKUP_ACTIVE });
|
||||
return;
|
||||
}
|
||||
|
||||
// backup is not active. see if there is a backup version on the server we ought to back up to.
|
||||
const backupInfo = await client.getKeyBackupVersion();
|
||||
this.setState({ backupStatus: backupInfo ? BackupStatus.SERVER_BACKUP_BUT_DISABLED : BackupStatus.NO_BACKUP });
|
||||
}
|
||||
|
||||
private onExportE2eKeysClicked = (): void => {
|
||||
|
@ -98,7 +122,7 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private onSetRecoveryMethodClick = (): void => {
|
||||
if (this.state.backupInfo) {
|
||||
if (this.state.backupStatus === BackupStatus.SERVER_BACKUP_BUT_DISABLED) {
|
||||
// A key backup exists for this account, but the creating device is not
|
||||
// verified, so restore the backup which will give us the keys from it and
|
||||
// allow us to trust it (ie. upload keys to it)
|
||||
|
@ -132,82 +156,106 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
|
|||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
if (this.state.shouldLoadBackupStatus) {
|
||||
const description = (
|
||||
<div>
|
||||
<p>
|
||||
{_t(
|
||||
"Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.",
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{_t(
|
||||
"When you sign out, these keys will be deleted from this device, which means you won't be able to read encrypted messages unless you have the keys for them on your other devices, or backed them up to the server.",
|
||||
)}
|
||||
</p>
|
||||
<p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
|
||||
</div>
|
||||
);
|
||||
/**
|
||||
* Show a dialog prompting the user to set up key backup.
|
||||
*
|
||||
* Either there is no backup at all ({@link BackupStatus.NO_BACKUP}), there is a backup on the server but
|
||||
* we are not connected to it ({@link BackupStatus.SERVER_BACKUP_BUT_DISABLED}), or we were unable to pull the
|
||||
* backup data ({@link BackupStatus.ERROR}). In all three cases, we should prompt the user to set up key backup.
|
||||
*/
|
||||
private renderSetupBackupDialog(): React.ReactNode {
|
||||
const description = (
|
||||
<div>
|
||||
<p>
|
||||
{_t(
|
||||
"Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.",
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{_t(
|
||||
"When you sign out, these keys will be deleted from this device, which means you won't be able to read encrypted messages unless you have the keys for them on your other devices, or backed them up to the server.",
|
||||
)}
|
||||
</p>
|
||||
<p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
let dialogContent;
|
||||
if (this.state.loading) {
|
||||
dialogContent = <Spinner />;
|
||||
} else {
|
||||
let setupButtonCaption;
|
||||
if (this.state.backupInfo) {
|
||||
setupButtonCaption = _t("Connect this session to Key Backup");
|
||||
} else {
|
||||
// if there's an error fetching the backup info, we'll just assume there's
|
||||
// no backup for the purpose of the button caption
|
||||
setupButtonCaption = _t("Start using Key Backup");
|
||||
}
|
||||
|
||||
dialogContent = (
|
||||
<div>
|
||||
<div className="mx_Dialog_content" id="mx_Dialog_content">
|
||||
{description}
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={setupButtonCaption}
|
||||
hasCancel={false}
|
||||
onPrimaryButtonClick={this.onSetRecoveryMethodClick}
|
||||
focus={true}
|
||||
>
|
||||
<button onClick={this.onLogoutConfirm}>{_t("I don't want my encrypted messages")}</button>
|
||||
</DialogButtons>
|
||||
<details>
|
||||
<summary>{_t("Advanced")}</summary>
|
||||
<p>
|
||||
<button onClick={this.onExportE2eKeysClicked}>{_t("Manually export keys")}</button>
|
||||
</p>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// Not quite a standard question dialog as the primary button cancels
|
||||
// the action and does something else instead, whilst non-default button
|
||||
// confirms the action.
|
||||
return (
|
||||
<BaseDialog
|
||||
title={_t("You'll lose access to your encrypted messages")}
|
||||
contentId="mx_Dialog_content"
|
||||
hasCancel={true}
|
||||
onFinished={this.onFinished}
|
||||
>
|
||||
{dialogContent}
|
||||
</BaseDialog>
|
||||
);
|
||||
let setupButtonCaption;
|
||||
if (this.state.backupStatus === BackupStatus.SERVER_BACKUP_BUT_DISABLED) {
|
||||
setupButtonCaption = _t("Connect this session to Key Backup");
|
||||
} else {
|
||||
return (
|
||||
<QuestionDialog
|
||||
hasCancelButton={true}
|
||||
title={_t("action|sign_out")}
|
||||
description={_t("Are you sure you want to sign out?")}
|
||||
button={_t("action|sign_out")}
|
||||
onFinished={this.onFinished}
|
||||
/>
|
||||
);
|
||||
// if there's an error fetching the backup info, we'll just assume there's
|
||||
// no backup for the purpose of the button caption
|
||||
setupButtonCaption = _t("Start using Key Backup");
|
||||
}
|
||||
|
||||
const dialogContent = (
|
||||
<div>
|
||||
<div className="mx_Dialog_content" id="mx_Dialog_content">
|
||||
{description}
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={setupButtonCaption}
|
||||
hasCancel={false}
|
||||
onPrimaryButtonClick={this.onSetRecoveryMethodClick}
|
||||
focus={true}
|
||||
>
|
||||
<button onClick={this.onLogoutConfirm}>{_t("I don't want my encrypted messages")}</button>
|
||||
</DialogButtons>
|
||||
<details>
|
||||
<summary>{_t("Advanced")}</summary>
|
||||
<p>
|
||||
<button onClick={this.onExportE2eKeysClicked}>{_t("Manually export keys")}</button>
|
||||
</p>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
// Not quite a standard question dialog as the primary button cancels
|
||||
// the action and does something else instead, whilst non-default button
|
||||
// confirms the action.
|
||||
return (
|
||||
<BaseDialog
|
||||
title={_t("You'll lose access to your encrypted messages")}
|
||||
contentId="mx_Dialog_content"
|
||||
hasCancel={true}
|
||||
onFinished={this.onFinished}
|
||||
>
|
||||
{dialogContent}
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
switch (this.state.backupStatus) {
|
||||
case BackupStatus.LOADING:
|
||||
// while we're deciding if we have backups, show a spinner
|
||||
return (
|
||||
<BaseDialog
|
||||
title={_t("action|sign_out")}
|
||||
contentId="mx_Dialog_content"
|
||||
hasCancel={true}
|
||||
onFinished={this.onFinished}
|
||||
>
|
||||
<Spinner />;
|
||||
</BaseDialog>
|
||||
);
|
||||
|
||||
case BackupStatus.NO_CRYPTO:
|
||||
case BackupStatus.BACKUP_ACTIVE:
|
||||
return (
|
||||
<QuestionDialog
|
||||
hasCancelButton={true}
|
||||
title={_t("action|sign_out")}
|
||||
description={_t("Are you sure you want to sign out?")}
|
||||
button={_t("action|sign_out")}
|
||||
onFinished={this.onFinished}
|
||||
/>
|
||||
);
|
||||
|
||||
case BackupStatus.NO_BACKUP:
|
||||
case BackupStatus.SERVER_BACKUP_BUT_DISABLED:
|
||||
case BackupStatus.ERROR:
|
||||
return this.renderSetupBackupDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -88,9 +88,9 @@ export function ManualDeviceKeyVerificationDialog({
|
|||
|
||||
return (
|
||||
<QuestionDialog
|
||||
title={_t("Verify session")}
|
||||
title={_t("settings|sessions|verify_session")}
|
||||
description={body}
|
||||
button={_t("Verify session")}
|
||||
button={_t("settings|sessions|verify_session")}
|
||||
onFinished={onLegacyFinished}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -136,6 +136,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
|||
clientId: ELEMENT_CLIENT_ID,
|
||||
clientTheme: SettingsStore.getValue("theme"),
|
||||
clientLanguage: getUserLanguage(),
|
||||
baseUrl: MatrixClientPeg.safeGet().baseUrl,
|
||||
});
|
||||
|
||||
const parsed = new URL(templated);
|
||||
|
|
|
@ -163,7 +163,7 @@ class RoomSettingsDialog extends React.Component<IProps, IState> {
|
|||
tabs.push(
|
||||
new Tab(
|
||||
RoomSettingsTab.Security,
|
||||
_td("Security & Privacy"),
|
||||
_td("room_settings|security|title"),
|
||||
"mx_RoomSettingsDialog_securityIcon",
|
||||
<SecurityRoomSettingsTab room={this.state.room} closeSettingsFn={() => this.props.onFinished(true)} />,
|
||||
"RoomSettingsSecurityPrivacy",
|
||||
|
@ -172,7 +172,7 @@ class RoomSettingsDialog extends React.Component<IProps, IState> {
|
|||
tabs.push(
|
||||
new Tab(
|
||||
RoomSettingsTab.Roles,
|
||||
_td("Roles & Permissions"),
|
||||
_td("room_settings|permissions|title"),
|
||||
"mx_RoomSettingsDialog_rolesIcon",
|
||||
<RolesRoomSettingsTab room={this.state.room} />,
|
||||
"RoomSettingsRolesPermissions",
|
||||
|
|
|
@ -80,8 +80,8 @@ export const SlidingSyncOptionsDialog: React.FC<{ onFinished(enabled: boolean):
|
|||
nativeSupport = _t("Checking…");
|
||||
} else {
|
||||
nativeSupport = hasNativeSupport
|
||||
? _t("Your server has native support")
|
||||
: _t("Your server lacks native support");
|
||||
? _t("labs|sliding_sync_server_support")
|
||||
: _t("labs|sliding_sync_server_no_support");
|
||||
}
|
||||
|
||||
const validProxy = withValidation<undefined, { error?: unknown }>({
|
||||
|
@ -97,7 +97,7 @@ export const SlidingSyncOptionsDialog: React.FC<{ onFinished(enabled: boolean):
|
|||
{
|
||||
key: "required",
|
||||
test: async ({ value }) => !!value || !!hasNativeSupport,
|
||||
invalid: () => _t("Your server lacks native support, you must specify a proxy"),
|
||||
invalid: () => _t("labs|sliding_sync_server_specify_proxy"),
|
||||
},
|
||||
{
|
||||
key: "working",
|
||||
|
@ -111,16 +111,20 @@ export const SlidingSyncOptionsDialog: React.FC<{ onFinished(enabled: boolean):
|
|||
|
||||
return (
|
||||
<TextInputDialog
|
||||
title={_t("Sliding Sync configuration")}
|
||||
title={_t("labs|sliding_sync_configuration")}
|
||||
description={
|
||||
<div>
|
||||
<div>
|
||||
<b>{_t("To disable you will need to log out and back in, use with caution!")}</b>
|
||||
<b>{_t("labs|sliding_sync_disable_warning")}</b>
|
||||
</div>
|
||||
{nativeSupport}
|
||||
</div>
|
||||
}
|
||||
placeholder={hasNativeSupport ? _t("Proxy URL (optional)") : _t("Proxy URL")}
|
||||
placeholder={
|
||||
hasNativeSupport
|
||||
? _t("labs|sliding_sync_proxy_url_optional_label")
|
||||
: _t("labs|sliding_sync_proxy_url_label")
|
||||
}
|
||||
value={currentProxy}
|
||||
button={_t("action|enable")}
|
||||
validator={validProxy}
|
||||
|
|
|
@ -67,7 +67,7 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
|
|||
),
|
||||
new Tab(
|
||||
SpaceSettingsTab.Roles,
|
||||
_td("Roles & Permissions"),
|
||||
_td("room_settings|permissions|title"),
|
||||
"mx_RoomSettingsDialog_rolesIcon",
|
||||
<RolesRoomSettingsTab room={space} />,
|
||||
),
|
||||
|
@ -84,7 +84,7 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
|
|||
|
||||
return (
|
||||
<BaseDialog
|
||||
title={_t("Settings - %(spaceName)s", { spaceName: space.name || _t("common|unnamed_space") })}
|
||||
title={_t("space_settings|title", { spaceName: space.name || _t("common|unnamed_space") })}
|
||||
className="mx_SpaceSettingsDialog"
|
||||
contentId="mx_SpaceSettingsDialog"
|
||||
onFinished={onFinished}
|
||||
|
|
|
@ -117,7 +117,7 @@ export default class TermsDialog extends React.PureComponent<ITermsDialogProps,
|
|||
</div>
|
||||
);
|
||||
case SERVICE_TYPES.IM:
|
||||
return <div>{_t("Use bots, bridges, widgets and sticker packs")}</div>;
|
||||
return <div>{_t("terms|integration_manager")}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -192,19 +192,19 @@ export default class TermsDialog extends React.PureComponent<ITermsDialogProps,
|
|||
<BaseDialog
|
||||
fixedWidth={false}
|
||||
onFinished={this.onCancelClick}
|
||||
title={_t("Terms of Service")}
|
||||
title={_t("terms|tos")}
|
||||
contentId="mx_Dialog_content"
|
||||
hasCancel={false}
|
||||
>
|
||||
<div id="mx_Dialog_content">
|
||||
<p>{_t("To continue you need to accept the terms of this service.")}</p>
|
||||
<p>{_t("terms|intro")}</p>
|
||||
|
||||
<table className="mx_TermsDialog_termsTable">
|
||||
<tbody>
|
||||
<tr className="mx_TermsDialog_termsTableHeader">
|
||||
<th>{_t("Service")}</th>
|
||||
<th>{_t("Summary")}</th>
|
||||
<th>{_t("Document")}</th>
|
||||
<th>{_t("terms|column_service")}</th>
|
||||
<th>{_t("terms|column_summary")}</th>
|
||||
<th>{_t("terms|column_document")}</th>
|
||||
<th>{_t("action|accept")}</th>
|
||||
</tr>
|
||||
{rows}
|
||||
|
|
|
@ -144,7 +144,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
|
|||
tabs.push(
|
||||
new Tab(
|
||||
UserTab.Security,
|
||||
_td("Security & Privacy"),
|
||||
_td("room_settings|security|title"),
|
||||
"mx_UserSettingsDialog_securityIcon",
|
||||
<SecurityUserSettingsTab closeSettingsFn={this.props.onFinished} />,
|
||||
"UserSettingsSecurityPrivacy",
|
||||
|
@ -179,7 +179,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
|
|||
tabs.push(
|
||||
new Tab(
|
||||
UserTab.Mjolnir,
|
||||
_td("Ignored users"),
|
||||
_td("labs_mjolnir|title"),
|
||||
"mx_UserSettingsDialog_mjolnirIcon",
|
||||
<MjolnirUserSettingsTab />,
|
||||
"UserSettingMjolnir",
|
||||
|
|
|
@ -479,6 +479,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
fallbackUserId={mxEvent.getSender()}
|
||||
size="32px"
|
||||
viewUserOnClick={true}
|
||||
className="mx_Dialog_nonDialogButton"
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -103,7 +103,7 @@ export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room
|
|||
const classes = classNames("mx_Pill", {
|
||||
mx_AtRoomPill: type === PillType.AtRoomMention,
|
||||
mx_RoomPill: type === PillType.RoomMention,
|
||||
mx_SpacePill: type === "space",
|
||||
mx_SpacePill: type === "space" || targetRoom?.isSpaceRoom(),
|
||||
mx_UserPill: type === PillType.UserMention,
|
||||
mx_UserPill_me: resourceId === MatrixClientPeg.safeGet().getUserId(),
|
||||
mx_EventPill: type === PillType.EventInOtherRoom || type === PillType.EventInSameRoom,
|
||||
|
|
|
@ -63,8 +63,8 @@ const MAX_OPTION_LENGTH = 340;
|
|||
|
||||
function creatingInitialState(): IState {
|
||||
return {
|
||||
title: _t("Create poll"),
|
||||
actionLabel: _t("Create Poll"),
|
||||
title: _t("poll|create_poll_title"),
|
||||
actionLabel: _t("poll|create_poll_action"),
|
||||
canSubmit: false, // need to add a question and at least one option first
|
||||
question: "",
|
||||
options: arraySeed("", DEFAULT_NUM_OPTIONS),
|
||||
|
@ -79,7 +79,7 @@ function editingInitialState(editingMxEvent: MatrixEvent): IState {
|
|||
if (!poll?.isEquivalentTo(M_POLL_START)) return creatingInitialState();
|
||||
|
||||
return {
|
||||
title: _t("Edit poll"),
|
||||
title: _t("poll|edit_poll_title"),
|
||||
actionLabel: _t("action|done"),
|
||||
canSubmit: true,
|
||||
question: poll.question.text,
|
||||
|
@ -175,8 +175,8 @@ export default class PollCreateDialog extends ScrollableBaseModal<IProps, IState
|
|||
.catch((e) => {
|
||||
console.error("Failed to post poll:", e);
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: _t("Failed to post poll"),
|
||||
description: _t("Sorry, the poll you tried to create was not posted."),
|
||||
title: _t("poll|failed_send_poll_title"),
|
||||
description: _t("poll|failed_send_poll_description"),
|
||||
button: _t("action|try_again"),
|
||||
cancelButton: _t("action|cancel"),
|
||||
onFinished: (tryAgain: boolean) => {
|
||||
|
@ -197,37 +197,37 @@ export default class PollCreateDialog extends ScrollableBaseModal<IProps, IState
|
|||
protected renderContent(): React.ReactNode {
|
||||
return (
|
||||
<div className="mx_PollCreateDialog">
|
||||
<h2>{_t("Poll type")}</h2>
|
||||
<h2>{_t("poll|type_heading")}</h2>
|
||||
<Field element="select" value={this.state.kind.name} onChange={this.onPollTypeChange}>
|
||||
<option key={M_POLL_KIND_DISCLOSED.name} value={M_POLL_KIND_DISCLOSED.name}>
|
||||
{_t("Open poll")}
|
||||
{_t("poll|type_open")}
|
||||
</option>
|
||||
<option key={M_POLL_KIND_UNDISCLOSED.name} value={M_POLL_KIND_UNDISCLOSED.name}>
|
||||
{_t("Closed poll")}
|
||||
{_t("poll|type_closed")}
|
||||
</option>
|
||||
</Field>
|
||||
<p>{pollTypeNotes(this.state.kind)}</p>
|
||||
<h2>{_t("What is your poll question or topic?")}</h2>
|
||||
<h2>{_t("poll|topic_heading")}</h2>
|
||||
<Field
|
||||
id="poll-topic-input"
|
||||
value={this.state.question}
|
||||
maxLength={MAX_QUESTION_LENGTH}
|
||||
label={_t("Question or topic")}
|
||||
placeholder={_t("Write something…")}
|
||||
label={_t("poll|topic_label")}
|
||||
placeholder={_t("poll|topic_placeholder")}
|
||||
onChange={this.onQuestionChange}
|
||||
usePlaceholderAsHint={true}
|
||||
disabled={this.state.busy}
|
||||
autoFocus={this.state.autoFocusTarget === FocusTarget.Topic}
|
||||
/>
|
||||
<h2>{_t("Create options")}</h2>
|
||||
<h2>{_t("poll|options_heading")}</h2>
|
||||
{this.state.options.map((op, i) => (
|
||||
<div key={`option_${i}`} className="mx_PollCreateDialog_option">
|
||||
<Field
|
||||
id={`pollcreate_option_${i}`}
|
||||
value={op}
|
||||
maxLength={MAX_OPTION_LENGTH}
|
||||
label={_t("Option %(number)s", { number: i + 1 })}
|
||||
placeholder={_t("Write an option")}
|
||||
label={_t("poll|options_label", { number: i + 1 })}
|
||||
placeholder={_t("poll|options_placeholder")}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => this.onOptionChange(i, e)}
|
||||
usePlaceholderAsHint={true}
|
||||
disabled={this.state.busy}
|
||||
|
@ -250,7 +250,7 @@ export default class PollCreateDialog extends ScrollableBaseModal<IProps, IState
|
|||
className="mx_PollCreateDialog_addOption"
|
||||
inputRef={this.addOptionRef}
|
||||
>
|
||||
{_t("Add option")}
|
||||
{_t("poll|options_add_button")}
|
||||
</AccessibleButton>
|
||||
{this.state.busy && (
|
||||
<div className="mx_PollCreateDialog_busy">
|
||||
|
@ -270,8 +270,8 @@ export default class PollCreateDialog extends ScrollableBaseModal<IProps, IState
|
|||
|
||||
function pollTypeNotes(kind: KnownPollKind): string {
|
||||
if (M_POLL_KIND_DISCLOSED.matches(kind.name)) {
|
||||
return _t("Voters see results as soon as they have voted");
|
||||
return _t("poll|disclosed_notes");
|
||||
} else {
|
||||
return _t("Results are only revealed when you end the poll");
|
||||
return _t("poll|notes");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -99,7 +99,7 @@ export default function RoomTopic({ room, ...props }: IProps): JSX.Element {
|
|||
dis.dispatch({ action: "open_room_settings" });
|
||||
}}
|
||||
>
|
||||
{_t("Edit topic")}
|
||||
{_t("room|edit_topic")}
|
||||
</AccessibleButton>
|
||||
)}
|
||||
</div>
|
||||
|
@ -119,7 +119,7 @@ export default function RoomTopic({ room, ...props }: IProps): JSX.Element {
|
|||
onClick={onClick}
|
||||
dir="auto"
|
||||
tooltipTargetClassName={className}
|
||||
label={_t("Click to read topic")}
|
||||
label={_t("room|read_topic")}
|
||||
alignment={Alignment.Bottom}
|
||||
ignoreHover={ignoreHover}
|
||||
>
|
||||
|
|
|
@ -106,7 +106,7 @@ const EncryptionInfo: React.FC<IProps> = ({
|
|||
return (
|
||||
<React.Fragment>
|
||||
<div data-testid="encryption-info-description" className="mx_UserInfo_container">
|
||||
<h3>{_t("Encryption")}</h3>
|
||||
<h3>{_t("settings|security|encryption_section")}</h3>
|
||||
{description}
|
||||
</div>
|
||||
<div className="mx_UserInfo_container">
|
||||
|
|
|
@ -78,7 +78,7 @@ export default class RoomPublishSetting extends React.PureComponent<IProps, ISta
|
|||
value={this.state.isRoomPublished}
|
||||
onChange={this.onRoomPublishChange}
|
||||
disabled={!enabled}
|
||||
label={_t("Publish this room to the public in %(domain)s's room directory?", {
|
||||
label={_t("room_settings|general|publish_toggle", {
|
||||
domain: client.getDomain(),
|
||||
})}
|
||||
/>
|
||||
|
|
|
@ -53,7 +53,7 @@ export default class UrlPreviewSettings extends React.Component<IProps> {
|
|||
const accountEnabled = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled");
|
||||
if (accountEnabled) {
|
||||
previewsForAccount = _t(
|
||||
"You have <a>enabled</a> URL previews by default.",
|
||||
"room_settings|general|user_url_previews_default_on",
|
||||
{},
|
||||
{
|
||||
a: (sub) => (
|
||||
|
@ -65,7 +65,7 @@ export default class UrlPreviewSettings extends React.Component<IProps> {
|
|||
);
|
||||
} else {
|
||||
previewsForAccount = _t(
|
||||
"You have <a>disabled</a> URL previews by default.",
|
||||
"room_settings|general|user_url_previews_default_off",
|
||||
{},
|
||||
{
|
||||
a: (sub) => (
|
||||
|
@ -87,16 +87,14 @@ export default class UrlPreviewSettings extends React.Component<IProps> {
|
|||
/>
|
||||
);
|
||||
} else {
|
||||
let str = _td("URL previews are enabled by default for participants in this room.");
|
||||
let str = _td("room_settings|general|default_url_previews_on");
|
||||
if (!SettingsStore.getValueAt(SettingLevel.ROOM, "urlPreviewsEnabled", roomId, /*explicit=*/ true)) {
|
||||
str = _td("URL previews are disabled by default for participants in this room.");
|
||||
str = _td("room_settings|general|default_url_previews_off");
|
||||
}
|
||||
previewsForRoom = <div>{_t(str)}</div>;
|
||||
}
|
||||
} else {
|
||||
previewsForAccount = _t(
|
||||
"In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.",
|
||||
);
|
||||
previewsForAccount = _t("room_settings|general|url_preview_encryption_warning");
|
||||
}
|
||||
|
||||
const previewsForRoomAccount = // in an e2ee room we use a special key to enforce per-room opt-in
|
||||
|
@ -110,17 +108,13 @@ export default class UrlPreviewSettings extends React.Component<IProps> {
|
|||
|
||||
const description = (
|
||||
<>
|
||||
<p>
|
||||
{_t(
|
||||
"When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.",
|
||||
)}
|
||||
</p>
|
||||
<p>{_t("room_settings|general|url_preview_explainer")}</p>
|
||||
<p>{previewsForAccount}</p>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<SettingsFieldset legend={_t("URL Previews")} description={description}>
|
||||
<SettingsFieldset legend={_t("room_settings|general|url_previews_section")} description={description}>
|
||||
{previewsForRoom}
|
||||
{previewsForRoomAccount}
|
||||
</SettingsFieldset>
|
||||
|
|
|
@ -813,7 +813,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
initialTabId: UserTab.Labs,
|
||||
});
|
||||
const betaPill = isVideoRoom ? (
|
||||
<BetaPill onClick={viewLabs} tooltipTitle={_t("Video rooms are a beta feature")} />
|
||||
<BetaPill onClick={viewLabs} tooltipTitle={_t("labs|video_rooms_beta")} />
|
||||
) : null;
|
||||
|
||||
return (
|
||||
|
|
|
@ -46,14 +46,14 @@ function hasExpectedEncryptionSettings(matrixClient: MatrixClient, room: Room):
|
|||
|
||||
const determineIntroMessage = (room: Room, encryptedSingle3rdPartyInvite: boolean): TranslationKey => {
|
||||
if (room instanceof LocalRoom) {
|
||||
return _td("Send your first message to invite <displayName/> to chat");
|
||||
return _td("room|intro|send_message_start_dm");
|
||||
}
|
||||
|
||||
if (encryptedSingle3rdPartyInvite) {
|
||||
return _td("Once everyone has joined, you’ll be able to chat");
|
||||
return _td("room|intro|encrypted_3pid_dm_pending_join");
|
||||
}
|
||||
|
||||
return _td("This is the beginning of your direct message history with <displayName/>.");
|
||||
return _td("room|intro|start_of_dm_history");
|
||||
};
|
||||
|
||||
const NewRoomIntro: React.FC = () => {
|
||||
|
@ -78,7 +78,7 @@ const NewRoomIntro: React.FC = () => {
|
|||
!encryptedSingle3rdPartyInvite &&
|
||||
room.getJoinedMemberCount() + room.getInvitedMemberCount() === 2
|
||||
) {
|
||||
caption = _t("Only the two of you are in this conversation, unless either of you invites anyone to join.");
|
||||
caption = _t("room|intro|dm_caption");
|
||||
}
|
||||
|
||||
const member = room?.getMember(dmPartner);
|
||||
|
@ -133,7 +133,7 @@ const NewRoomIntro: React.FC = () => {
|
|||
let topicText;
|
||||
if (canAddTopic && topic) {
|
||||
topicText = _t(
|
||||
"Topic: %(topic)s (<a>edit</a>)",
|
||||
"room|intro|topic_edit",
|
||||
{ topic },
|
||||
{
|
||||
a: (sub) => (
|
||||
|
@ -144,10 +144,10 @@ const NewRoomIntro: React.FC = () => {
|
|||
},
|
||||
);
|
||||
} else if (topic) {
|
||||
topicText = _t("Topic: %(topic)s ", { topic });
|
||||
topicText = _t("room|intro|topic", { topic });
|
||||
} else if (canAddTopic) {
|
||||
topicText = _t(
|
||||
"<a>Add a topic</a> to help people know what it is about.",
|
||||
"room|intro|no_topic",
|
||||
{},
|
||||
{
|
||||
a: (sub) => (
|
||||
|
@ -164,9 +164,9 @@ const NewRoomIntro: React.FC = () => {
|
|||
|
||||
let createdText: string;
|
||||
if (creator === cli.getUserId()) {
|
||||
createdText = _t("You created this room.");
|
||||
createdText = _t("room|intro|you_created");
|
||||
} else {
|
||||
createdText = _t("%(displayName)s created this room.", {
|
||||
createdText = _t("room|intro|user_created", {
|
||||
displayName: creatorName,
|
||||
});
|
||||
}
|
||||
|
@ -200,7 +200,7 @@ const NewRoomIntro: React.FC = () => {
|
|||
defaultDispatcher.dispatch({ action: "view_invite", roomId });
|
||||
}}
|
||||
>
|
||||
{_t("Invite to just this room")}
|
||||
{_t("room|intro|room_invite")}
|
||||
</AccessibleButton>
|
||||
)}
|
||||
</div>
|
||||
|
@ -228,7 +228,7 @@ const NewRoomIntro: React.FC = () => {
|
|||
avatar = (
|
||||
<MiniAvatarUploader
|
||||
hasAvatar={false}
|
||||
noAvatarLabel={_t("Add a photo, so people can easily spot your room.")}
|
||||
noAvatarLabel={_t("room|intro|no_avatar_label")}
|
||||
setAvatarUrl={(url) => cli.sendStateEvent(roomId, EventType.RoomAvatar, { url }, "")}
|
||||
>
|
||||
{avatar}
|
||||
|
@ -245,7 +245,7 @@ const NewRoomIntro: React.FC = () => {
|
|||
<p>
|
||||
{createdText}{" "}
|
||||
{_t(
|
||||
"This is the start of <roomName/>.",
|
||||
"room|intro|start_of_room",
|
||||
{},
|
||||
{
|
||||
roomName: () => <b>{room.name}</b>,
|
||||
|
@ -266,9 +266,7 @@ const NewRoomIntro: React.FC = () => {
|
|||
});
|
||||
}
|
||||
|
||||
const subText = _t(
|
||||
"Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites.",
|
||||
);
|
||||
const subText = _t("room|intro|private_unencrypted_warning");
|
||||
|
||||
let subButton: JSX.Element | undefined;
|
||||
if (
|
||||
|
@ -277,7 +275,7 @@ const NewRoomIntro: React.FC = () => {
|
|||
) {
|
||||
subButton = (
|
||||
<AccessibleButton kind="link_inline" onClick={openRoomSettings}>
|
||||
{_t("Enable encryption in settings.")}
|
||||
{_t("room|intro|enable_encryption_prompt")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
@ -294,7 +292,7 @@ const NewRoomIntro: React.FC = () => {
|
|||
{!hasExpectedEncryptionSettings(cli, room) && (
|
||||
<EventTileBubble
|
||||
className="mx_cryptoEvent mx_cryptoEvent_icon_warning"
|
||||
title={_t("End-to-end encryption isn't enabled")}
|
||||
title={_t("room|intro|unencrypted_warning")}
|
||||
subtitle={subtitle}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -47,6 +47,7 @@ import { useRoomState } from "../../../hooks/useRoomState";
|
|||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import { formatCount } from "../../../utils/FormattingUtils";
|
||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||
import { Linkify, topicToHtml } from "../../../HtmlUtils";
|
||||
|
||||
/**
|
||||
* A helper to transform a notification color to the what the Compound Icon Button
|
||||
|
@ -100,6 +101,11 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
|
|||
|
||||
const notificationsEnabled = useFeatureEnabled("feature_notifications");
|
||||
|
||||
const roomTopicBody = useMemo(
|
||||
() => topicToHtml(roomTopic?.text, roomTopic?.html),
|
||||
[roomTopic?.html, roomTopic?.text],
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
as="header"
|
||||
|
@ -159,7 +165,7 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
|
|||
</BodyText>
|
||||
{roomTopic && (
|
||||
<BodyText as="div" size="sm" className="mx_RoomHeader_topic">
|
||||
{roomTopic.text}
|
||||
<Linkify>{roomTopicBody}</Linkify>
|
||||
</BodyText>
|
||||
)}
|
||||
</Box>
|
||||
|
|
|
@ -162,7 +162,7 @@ const RoomPreviewCard: FC<IProps> = ({ room, onJoinButtonClicked, onRejectButton
|
|||
<>
|
||||
<RoomAvatar room={room} size="50px" viewAvatarOnClick />
|
||||
<div className="mx_RoomPreviewCard_video" />
|
||||
<BetaPill onClick={viewLabs} tooltipTitle={_t("Video rooms are a beta feature")} />
|
||||
<BetaPill onClick={viewLabs} tooltipTitle={_t("labs|video_rooms_beta")} />
|
||||
</>
|
||||
);
|
||||
} else if (room.isSpaceRoom()) {
|
||||
|
|
|
@ -78,7 +78,7 @@ export default class RoomUpgradeWarningBar extends React.PureComponent<IProps, I
|
|||
</p>
|
||||
<p>
|
||||
{_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.",
|
||||
"room_settings|advanced|room_upgrade_warning",
|
||||
{},
|
||||
{
|
||||
b: (sub) => <b>{sub}</b>,
|
||||
|
@ -89,7 +89,7 @@ export default class RoomUpgradeWarningBar extends React.PureComponent<IProps, I
|
|||
</div>
|
||||
<p className="mx_RoomUpgradeWarningBar_upgradelink">
|
||||
<AccessibleButton onClick={this.onUpgradeClick}>
|
||||
{_t("Upgrade this room to the recommended room version")}
|
||||
{_t("room_settings|advanced|room_upgrade_button")}
|
||||
</AccessibleButton>
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
@ -89,7 +89,7 @@ export default class BridgeTile extends React.PureComponent<IProps> {
|
|||
creator = (
|
||||
<li>
|
||||
{_t(
|
||||
"This bridge was provisioned by <user />.",
|
||||
"labs|bridge_state_creator",
|
||||
{},
|
||||
{
|
||||
user: () => (
|
||||
|
@ -109,7 +109,7 @@ export default class BridgeTile extends React.PureComponent<IProps> {
|
|||
const bot = (
|
||||
<li>
|
||||
{_t(
|
||||
"This bridge is managed by <user />.",
|
||||
"labs|bridge_state_manager",
|
||||
{},
|
||||
{
|
||||
user: () => (
|
||||
|
@ -154,7 +154,7 @@ export default class BridgeTile extends React.PureComponent<IProps> {
|
|||
);
|
||||
}
|
||||
networkItem = _t(
|
||||
"Workspace: <networkLink/>",
|
||||
"labs|bridge_state_workspace",
|
||||
{},
|
||||
{
|
||||
networkLink: () => networkLink,
|
||||
|
@ -181,7 +181,7 @@ export default class BridgeTile extends React.PureComponent<IProps> {
|
|||
{networkItem}
|
||||
<span className="mx_RoomSettingsDialog_channel">
|
||||
{_t(
|
||||
"Channel: <channelLink/>",
|
||||
"labs|bridge_state_channel",
|
||||
{},
|
||||
{
|
||||
channelLink: () => channelLink,
|
||||
|
|
|
@ -127,7 +127,7 @@ export default class ChangePassword extends React.Component<IProps, IState> {
|
|||
this.props.onError(err);
|
||||
} else {
|
||||
this.props.onError(
|
||||
new UserFriendlyError("Error while changing password: %(error)s", {
|
||||
new UserFriendlyError("auth|change_password_error", {
|
||||
error: String(err),
|
||||
cause: undefined,
|
||||
}),
|
||||
|
@ -155,16 +155,16 @@ export default class ChangePassword extends React.Component<IProps, IState> {
|
|||
*/
|
||||
private checkPassword(oldPass: string, newPass: string, confirmPass: string): void {
|
||||
if (newPass !== confirmPass) {
|
||||
throw new UserFriendlyError("New passwords don't match");
|
||||
throw new UserFriendlyError("auth|change_password_mismatch");
|
||||
} else if (!newPass || newPass.length === 0) {
|
||||
throw new UserFriendlyError("Passwords can't be empty");
|
||||
throw new UserFriendlyError("auth|change_password_empty");
|
||||
}
|
||||
}
|
||||
|
||||
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("auth|set_email_prompt"),
|
||||
});
|
||||
return modal.finished.then(([confirmed]) => !!confirmed);
|
||||
}
|
||||
|
@ -194,7 +194,7 @@ export default class ChangePassword extends React.Component<IProps, IState> {
|
|||
{
|
||||
key: "required",
|
||||
test: ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||
invalid: () => _t("Passwords can't be empty"),
|
||||
invalid: () => _t("auth|change_password_empty"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -226,14 +226,14 @@ export default class ChangePassword extends React.Component<IProps, IState> {
|
|||
{
|
||||
key: "required",
|
||||
test: ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||
invalid: () => _t("Confirm password"),
|
||||
invalid: () => _t("auth|change_password_confirm_label"),
|
||||
},
|
||||
{
|
||||
key: "match",
|
||||
test({ value }) {
|
||||
return !value || value === this.state.newPassword;
|
||||
},
|
||||
invalid: () => _t("Passwords don't match"),
|
||||
invalid: () => _t("auth|change_password_confirm_invalid"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -261,7 +261,7 @@ export default class ChangePassword extends React.Component<IProps, IState> {
|
|||
this.props.onError(err);
|
||||
} else {
|
||||
this.props.onError(
|
||||
new UserFriendlyError("Error while changing password: %(error)s", {
|
||||
new UserFriendlyError("auth|change_password_error", {
|
||||
error: String(err),
|
||||
cause: undefined,
|
||||
}),
|
||||
|
@ -344,7 +344,7 @@ export default class ChangePassword extends React.Component<IProps, IState> {
|
|||
<Field
|
||||
ref={(field) => (this[FIELD_OLD_PASSWORD] = field)}
|
||||
type="password"
|
||||
label={_t("Current password")}
|
||||
label={_t("auth|change_password_current_label")}
|
||||
value={this.state.oldPassword}
|
||||
onChange={this.onChangeOldPassword}
|
||||
onValidate={this.onOldPasswordValidate}
|
||||
|
@ -354,7 +354,7 @@ export default class ChangePassword extends React.Component<IProps, IState> {
|
|||
<PassphraseField
|
||||
fieldRef={(field) => (this[FIELD_NEW_PASSWORD] = field)}
|
||||
type="password"
|
||||
label={_td("New Password")}
|
||||
label={_td("auth|change_password_new_label")}
|
||||
minScore={PASSWORD_MIN_SCORE}
|
||||
value={this.state.newPassword}
|
||||
autoFocus={this.props.autoFocusNewPasswordInput}
|
||||
|
@ -367,7 +367,7 @@ export default class ChangePassword extends React.Component<IProps, IState> {
|
|||
<Field
|
||||
ref={(field) => (this[FIELD_NEW_PASSWORD_CONFIRM] = field)}
|
||||
type="password"
|
||||
label={_t("Confirm password")}
|
||||
label={_t("auth|change_password_confirm_label")}
|
||||
value={this.state.newPasswordConfirm}
|
||||
onChange={this.onChangeNewPasswordConfirm}
|
||||
onValidate={this.onNewPasswordConfirmValidate}
|
||||
|
@ -379,7 +379,7 @@ export default class ChangePassword extends React.Component<IProps, IState> {
|
|||
kind={this.props.buttonKind}
|
||||
onClick={this.onClickChange}
|
||||
>
|
||||
{this.props.buttonLabel || _t("Change Password")}
|
||||
{this.props.buttonLabel || _t("auth|change_password_action")}
|
||||
</AccessibleButton>
|
||||
</form>
|
||||
);
|
||||
|
|
|
@ -263,32 +263,52 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
|
|||
<summary>{_t("Advanced")}</summary>
|
||||
<table className="mx_CrossSigningPanel_statusList">
|
||||
<tr>
|
||||
<th scope="row">{_t("Cross-signing public keys:")}</th>
|
||||
<td>{crossSigningPublicKeysOnDevice ? _t("in memory") : _t("not found")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{_t("Cross-signing private keys:")}</th>
|
||||
<th scope="row">{_t("settings|security|cross_signing_public_keys")}</th>
|
||||
<td>
|
||||
{crossSigningPrivateKeysInStorage
|
||||
? _t("in secret storage")
|
||||
: _t("not found in storage")}
|
||||
{crossSigningPublicKeysOnDevice
|
||||
? _t("settings|security|cross_signing_in_memory")
|
||||
: _t("settings|security|cross_signing_not_found")}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{_t("Master private key:")}</th>
|
||||
<td>{masterPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td>
|
||||
<th scope="row">{_t("settings|security|cross_signing_private_keys")}</th>
|
||||
<td>
|
||||
{crossSigningPrivateKeysInStorage
|
||||
? _t("settings|security|cross_signing_in_4s")
|
||||
: _t("settings|security|cross_signing_not_in_4s")}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{_t("Self signing private key:")}</th>
|
||||
<td>{selfSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td>
|
||||
<th scope="row">{_t("settings|security|cross_signing_master_private_Key")}</th>
|
||||
<td>
|
||||
{masterPrivateKeyCached
|
||||
? _t("settings|security|cross_signing_cached")
|
||||
: _t("settings|security|cross_signing_not_cached")}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{_t("User signing private key:")}</th>
|
||||
<td>{userSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td>
|
||||
<th scope="row">{_t("settings|security|cross_signing_self_signing_private_key")}</th>
|
||||
<td>
|
||||
{selfSigningPrivateKeyCached
|
||||
? _t("settings|security|cross_signing_cached")
|
||||
: _t("settings|security|cross_signing_not_cached")}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{_t("Homeserver feature support:")}</th>
|
||||
<td>{homeserverSupportsCrossSigning ? _t("exists") : _t("not found")}</td>
|
||||
<th scope="row">{_t("settings|security|cross_signing_user_signing_private_key")}</th>
|
||||
<td>
|
||||
{userSigningPrivateKeyCached
|
||||
? _t("settings|security|cross_signing_cached")
|
||||
: _t("settings|security|cross_signing_not_cached")}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{_t("settings|security|cross_signing_homeserver_support")}</th>
|
||||
<td>
|
||||
{homeserverSupportsCrossSigning
|
||||
? _t("settings|security|cross_signing_homeserver_support_exists")
|
||||
: _t("settings|security|cross_signing_not_found")}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</details>
|
||||
|
|
|
@ -52,10 +52,10 @@ export default class CryptographyPanel extends React.Component<IProps, IState> {
|
|||
importExportButtons = (
|
||||
<div className="mx_CryptographyPanel_importExportButtons">
|
||||
<AccessibleButton kind="primary" onClick={this.onExportE2eKeysClicked}>
|
||||
{_t("Export E2E room keys")}
|
||||
{_t("settings|security|export_megolm_keys")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="primary" onClick={this.onImportE2eKeysClicked}>
|
||||
{_t("Import E2E room keys")}
|
||||
{_t("settings|security|import_megolm_keys")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
|
@ -73,17 +73,17 @@ export default class CryptographyPanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
return (
|
||||
<SettingsSubsection heading={_t("Cryptography")}>
|
||||
<SettingsSubsection heading={_t("settings|security|cryptography_section")}>
|
||||
<SettingsSubsectionText>
|
||||
<table className="mx_CryptographyPanel_sessionInfo">
|
||||
<tr>
|
||||
<th scope="row">{_t("Session ID:")}</th>
|
||||
<th scope="row">{_t("settings|security|session_id")}</th>
|
||||
<td>
|
||||
<code>{deviceId}</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{_t("Session key:")}</th>
|
||||
<th scope="row">{_t("settings|security|session_key")}</th>
|
||||
<td>
|
||||
<code>
|
||||
<b>{identityKey}</b>
|
||||
|
|
|
@ -26,7 +26,7 @@ const SETTING_MANUALLY_VERIFY_ALL_SESSIONS = "e2ee.manuallyVerifyAllSessions";
|
|||
|
||||
const E2eAdvancedPanel: React.FC = () => {
|
||||
return (
|
||||
<SettingsSubsection heading={_t("Encryption")}>
|
||||
<SettingsSubsection heading={_t("settings|security|encryption_section")}>
|
||||
<SettingsFlag name={SETTING_MANUALLY_VERIFY_ALL_SESSIONS} level={SettingLevel.DEVICE} />
|
||||
<SettingsSubsectionText>
|
||||
{_t(
|
||||
|
|
|
@ -67,7 +67,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
|
|||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.checkKeyBackupStatus();
|
||||
this.loadBackupStatus();
|
||||
|
||||
MatrixClientPeg.safeGet().on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus);
|
||||
MatrixClientPeg.safeGet().on(CryptoEvent.KeyBackupSessionsRemaining, this.onKeyBackupSessionsRemaining);
|
||||
|
@ -97,28 +97,6 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
|
|||
this.loadBackupStatus();
|
||||
};
|
||||
|
||||
private async checkKeyBackupStatus(): Promise<void> {
|
||||
this.getUpdatedDiagnostics();
|
||||
try {
|
||||
const keyBackupResult = await MatrixClientPeg.safeGet().checkKeyBackup();
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: false,
|
||||
backupInfo: keyBackupResult?.backupInfo ?? null,
|
||||
backupSigStatus: keyBackupResult?.trustInfo ?? null,
|
||||
});
|
||||
} catch (e) {
|
||||
logger.log("Unable to fetch check backup status", e);
|
||||
if (this.unmounted) return;
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: true,
|
||||
backupInfo: null,
|
||||
backupSigStatus: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async loadBackupStatus(): Promise<void> {
|
||||
this.setState({ loading: true });
|
||||
this.getUpdatedDiagnostics();
|
||||
|
@ -388,18 +366,28 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
|
|||
<table className="mx_SecureBackupPanel_statusList">
|
||||
<tr>
|
||||
<th scope="row">{_t("Backup key stored:")}</th>
|
||||
<td>{backupKeyStored === true ? _t("in secret storage") : _t("not stored")}</td>
|
||||
<td>
|
||||
{backupKeyStored === true
|
||||
? _t("settings|security|cross_signing_in_4s")
|
||||
: _t("not stored")}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{_t("Backup key cached:")}</th>
|
||||
<td>
|
||||
{backupKeyCached ? _t("cached locally") : _t("not found locally")}
|
||||
{backupKeyCached
|
||||
? _t("settings|security|cross_signing_cached")
|
||||
: _t("settings|security|cross_signing_not_cached")}
|
||||
{backupKeyWellFormedText}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{_t("Secret storage public key:")}</th>
|
||||
<td>{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}</td>
|
||||
<td>
|
||||
{secretStorageKeyInAccount
|
||||
? _t("in account data")
|
||||
: _t("settings|security|cross_signing_not_found")}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{_t("Secret storage:")}</th>
|
||||
|
|
|
@ -33,16 +33,16 @@ function installUpdate(): void {
|
|||
function getStatusText(status: UpdateCheckStatus, errorDetail?: string): ReactNode {
|
||||
switch (status) {
|
||||
case UpdateCheckStatus.Error:
|
||||
return _t("Error encountered (%(errorDetail)s).", { errorDetail });
|
||||
return _t("update|error_encountered", { errorDetail });
|
||||
case UpdateCheckStatus.Checking:
|
||||
return _t("Checking for an update…");
|
||||
return _t("update|checking");
|
||||
case UpdateCheckStatus.NotAvailable:
|
||||
return _t("No update available.");
|
||||
return _t("update|no_update");
|
||||
case UpdateCheckStatus.Downloading:
|
||||
return _t("Downloading update…");
|
||||
return _t("update|downloading");
|
||||
case UpdateCheckStatus.Ready:
|
||||
return _t(
|
||||
"New version available. <a>Update now.</a>",
|
||||
"update|new_version_available",
|
||||
{},
|
||||
{
|
||||
a: (sub) => (
|
||||
|
@ -86,7 +86,7 @@ const UpdateCheckButton: React.FC = () => {
|
|||
return (
|
||||
<React.Fragment>
|
||||
<AccessibleButton onClick={onCheckForUpdateClick} kind="primary" disabled={busy}>
|
||||
{_t("Check for update")}
|
||||
{_t("update|check_action")}
|
||||
</AccessibleButton>
|
||||
{suffix}
|
||||
</React.Fragment>
|
||||
|
|
|
@ -68,7 +68,7 @@ const CurrentDeviceSectionHeading: React.FC<CurrentDeviceSectionHeadingProps> =
|
|||
? [
|
||||
<IconizedContextMenuOption
|
||||
key="sign-out-all-others"
|
||||
label={_t("Sign out of all other sessions (%(otherSessionsCount)s)", { otherSessionsCount })}
|
||||
label={_t("settings|sessions|sign_out_all_other_sessions", { otherSessionsCount })}
|
||||
onClick={signOutAllOtherSessions}
|
||||
isDestructive
|
||||
/>,
|
||||
|
@ -76,7 +76,7 @@ const CurrentDeviceSectionHeading: React.FC<CurrentDeviceSectionHeadingProps> =
|
|||
: []),
|
||||
];
|
||||
return (
|
||||
<SettingsSubsectionHeading heading={_t("Current session")}>
|
||||
<SettingsSubsectionHeading heading={_t("settings|sessions|current_session")}>
|
||||
<KebabContextMenu
|
||||
disabled={disabled}
|
||||
title={_t("common|options")}
|
||||
|
|
|
@ -60,7 +60,7 @@ const DeviceNameEditor: React.FC<Props & { stopEditing: () => void }> = ({ devic
|
|||
return (
|
||||
<form aria-disabled={isLoading} className="mx_DeviceDetailHeading_renameForm" onSubmit={onSubmit} method="post">
|
||||
<p id={headingId} className="mx_DeviceDetailHeading_renameFormHeading">
|
||||
{_t("Rename session")}
|
||||
{_t("settings|sessions|rename_form_heading")}
|
||||
</p>
|
||||
<div>
|
||||
<Field
|
||||
|
@ -77,21 +77,13 @@ const DeviceNameEditor: React.FC<Props & { stopEditing: () => void }> = ({ devic
|
|||
maxLength={100}
|
||||
/>
|
||||
<Caption id={descriptionId}>
|
||||
{_t("Please be aware that session names are also visible to people you communicate with.")}
|
||||
{_t("settings|sessions|rename_form_caption")}
|
||||
<LearnMore
|
||||
title={_t("Renaming sessions")}
|
||||
title={_t("settings|sessions|rename_form_learn_more")}
|
||||
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>
|
||||
<p>{_t("settings|sessions|rename_form_learn_more_description_1")}</p>
|
||||
<p>{_t("settings|sessions|rename_form_learn_more_description_2")}</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -64,9 +64,9 @@ const DeviceDetails: React.FC<Props> = ({
|
|||
{
|
||||
id: "session",
|
||||
values: [
|
||||
{ label: _t("Session ID"), value: device.device_id },
|
||||
{ label: _t("settings|sessions|session_id"), value: device.device_id },
|
||||
{
|
||||
label: _t("Last activity"),
|
||||
label: _t("settings|sessions|last_activity"),
|
||||
value: device.last_seen_ts && formatDate(new Date(device.last_seen_ts)),
|
||||
},
|
||||
],
|
||||
|
@ -77,7 +77,7 @@ const DeviceDetails: React.FC<Props> = ({
|
|||
values: [
|
||||
{ label: _t("common|name"), value: device.appName },
|
||||
{ label: _t("common|version"), value: device.appVersion },
|
||||
{ label: _t("URL"), value: device.url },
|
||||
{ label: _t("settings|sessions|url"), value: device.url },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -85,9 +85,9 @@ const DeviceDetails: React.FC<Props> = ({
|
|||
heading: _t("common|device"),
|
||||
values: [
|
||||
{ label: _t("common|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("settings|sessions|os"), value: device.deviceOperatingSystem },
|
||||
{ label: _t("settings|sessions|browser"), value: device.client },
|
||||
{ label: _t("settings|sessions|ip"), value: device.last_seen_ip },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
@ -122,7 +122,7 @@ const DeviceDetails: React.FC<Props> = ({
|
|||
<DeviceVerificationStatusCard device={device} onVerifyDevice={onVerifyDevice} isCurrentDevice />
|
||||
</section>
|
||||
<section className="mx_DeviceDetails_section">
|
||||
<p className="mx_DeviceDetails_sectionHeading">{_t("Session details")}</p>
|
||||
<p className="mx_DeviceDetails_sectionHeading">{_t("settings|sessions|details_heading")}</p>
|
||||
{metadata.map(({ heading, values, id }, index) => (
|
||||
<table
|
||||
className="mx_DeviceDetails_metadataTable"
|
||||
|
@ -158,13 +158,13 @@ const DeviceDetails: React.FC<Props> = ({
|
|||
checked={isPushNotificationsEnabled(pusher, localNotificationSettings)}
|
||||
disabled={isCheckboxDisabled(pusher, localNotificationSettings)}
|
||||
onChange={(checked) => setPushNotifications?.(device.device_id, checked)}
|
||||
title={_t("Toggle push notifications on this session.")}
|
||||
title={_t("settings|sessions|push_toggle")}
|
||||
data-testid="device-detail-push-notification-checkbox"
|
||||
/>
|
||||
<p className="mx_DeviceDetails_sectionHeading">
|
||||
{_t("Push notifications")}
|
||||
{_t("settings|sessions|push_heading")}
|
||||
<small className="mx_DeviceDetails_sectionSubheading">
|
||||
{_t("Receive push notifications on this session.")}
|
||||
{_t("settings|sessions|push_subheading")}
|
||||
</small>
|
||||
</p>
|
||||
</section>
|
||||
|
@ -177,7 +177,7 @@ const DeviceDetails: React.FC<Props> = ({
|
|||
data-testid="device-detail-sign-out-cta"
|
||||
>
|
||||
<span className="mx_DeviceDetails_signOutButtonContent">
|
||||
{_t("Sign out of this session")}
|
||||
{_t("settings|sessions|sign_out")}
|
||||
{isSigningOut && <Spinner w={16} h={16} />}
|
||||
</span>
|
||||
</AccessibleButton>
|
||||
|
|
|
@ -27,7 +27,7 @@ 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");
|
||||
const label = isExpanded ? _t("settings|sessions|hide_details") : _t("settings|sessions|show_details");
|
||||
return (
|
||||
<AccessibleTooltipButton
|
||||
{...rest}
|
||||
|
|
|
@ -50,7 +50,7 @@ const getInactiveMetadata = (device: ExtendedDevice): { id: string; value: React
|
|||
value: (
|
||||
<>
|
||||
<InactiveIcon className="mx_DeviceTile_inactiveIcon" />
|
||||
{_t("Inactive for %(inactiveAgeDays)s+ days", { inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS }) +
|
||||
{_t("settings|sessions|inactive_days", { inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS }) +
|
||||
` (${formatLastActivity(device.last_seen_ts)})`}
|
||||
</>
|
||||
),
|
||||
|
@ -62,7 +62,8 @@ const DeviceMetaDatum: React.FC<{ value: string | React.ReactNode; id: string }>
|
|||
|
||||
export const DeviceMetaData: React.FC<Props> = ({ device }) => {
|
||||
const inactive = getInactiveMetadata(device);
|
||||
const lastActivity = device.last_seen_ts && `${_t("Last activity")} ${formatLastActivity(device.last_seen_ts)}`;
|
||||
const lastActivity =
|
||||
device.last_seen_ts && `${_t("settings|sessions|last_activity")} ${formatLastActivity(device.last_seen_ts)}`;
|
||||
const verificationStatus = device.isVerified ? _t("common|verified") : _t("common|unverified");
|
||||
// if device is inactive, don't display last activity or verificationStatus
|
||||
const metadata = inactive
|
||||
|
|
|
@ -32,73 +32,41 @@ const securityCardContent: Record<
|
|||
}
|
||||
> = {
|
||||
[DeviceSecurityVariation.Verified]: {
|
||||
title: _t("Verified sessions"),
|
||||
title: _t("settings|sessions|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>
|
||||
<p>{_t("settings|sessions|verified_sessions_explainer_1")}</p>
|
||||
<p>{_t("settings|sessions|verified_sessions_explainer_2")}</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
[DeviceSecurityVariation.Unverified]: {
|
||||
title: _t("Unverified sessions"),
|
||||
title: _t("settings|sessions|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>
|
||||
<p>{_t("settings|sessions|unverified_sessions_explainer_1")}</p>
|
||||
<p>{_t("settings|sessions|unverified_sessions_explainer_2")}</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
// unverifiable uses single-session case
|
||||
// because it is only ever displayed on a single session detail
|
||||
[DeviceSecurityVariation.Unverifiable]: {
|
||||
title: _t("Unverified session"),
|
||||
title: _t("settings|sessions|unverified_session"),
|
||||
description: (
|
||||
<>
|
||||
<p>{_t(`This session doesn't support encryption and thus 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>
|
||||
<p>{_t("settings|sessions|unverified_session_explainer_1")}</p>
|
||||
<p>{_t("settings|sessions|unverified_session_explainer_2")}</p>
|
||||
<p>{_t("settings|sessions|unverified_session_explainer_3")}</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
[DeviceSecurityVariation.Inactive]: {
|
||||
title: _t("Inactive sessions"),
|
||||
title: _t("settings|sessions|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>
|
||||
<p>{_t("settings|sessions|inactive_sessions_explainer_1")}</p>
|
||||
<p>{_t("settings|sessions|inactive_sessions_explainer_2")}</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
|
|
@ -23,7 +23,7 @@ import { Icon as WebIcon } from "../../../../../res/img/element-icons/settings/w
|
|||
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 { _t, _td, TranslationKey } from "../../../../languageHandler";
|
||||
import { ExtendedDevice } from "./types";
|
||||
import { DeviceType } from "../../../../utils/device/parseUserAgent";
|
||||
|
||||
|
@ -39,16 +39,16 @@ const deviceTypeIcon: Record<DeviceType, React.FC<React.SVGProps<SVGSVGElement>>
|
|||
[DeviceType.Web]: WebIcon,
|
||||
[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"),
|
||||
const deviceTypeLabel: Record<DeviceType, TranslationKey> = {
|
||||
[DeviceType.Desktop]: _td("settings|sessions|desktop_session"),
|
||||
[DeviceType.Mobile]: _td("settings|sessions|mobile_session"),
|
||||
[DeviceType.Web]: _td("settings|sessions|web_session"),
|
||||
[DeviceType.Unknown]: _td("settings|sessions|unknown_session"),
|
||||
};
|
||||
|
||||
export const DeviceTypeIcon: React.FC<Props> = ({ isVerified, isSelected, deviceType }) => {
|
||||
const Icon = deviceTypeIcon[deviceType!] || deviceTypeIcon[DeviceType.Unknown];
|
||||
const label = deviceTypeLabel[deviceType!] || deviceTypeLabel[DeviceType.Unknown];
|
||||
const label = _t(deviceTypeLabel[deviceType!] || deviceTypeLabel[DeviceType.Unknown]);
|
||||
return (
|
||||
<div
|
||||
className={classNames("mx_DeviceTypeIcon", {
|
||||
|
|
|
@ -38,11 +38,11 @@ const getCardProps = (
|
|||
} => {
|
||||
if (device.isVerified) {
|
||||
const descriptionText = isCurrentDevice
|
||||
? _t("Your current session is ready for secure messaging.")
|
||||
: _t("This session is ready for secure messaging.");
|
||||
? _t("settings|sessions|device_verified_description_current")
|
||||
: _t("settings|sessions|device_verified_description");
|
||||
return {
|
||||
variation: DeviceSecurityVariation.Verified,
|
||||
heading: _t("Verified session"),
|
||||
heading: _t("settings|sessions|verified_session"),
|
||||
description: (
|
||||
<>
|
||||
{descriptionText}
|
||||
|
@ -54,10 +54,10 @@ const getCardProps = (
|
|||
if (device.isVerified === null) {
|
||||
return {
|
||||
variation: DeviceSecurityVariation.Unverified,
|
||||
heading: _t("Unverified session"),
|
||||
heading: _t("settings|sessions|unverified_session"),
|
||||
description: (
|
||||
<>
|
||||
{_t(`This session doesn't support encryption and thus can't be verified.`)}
|
||||
{_t("settings|sessions|unverified_session_explainer_1")}
|
||||
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Unverifiable} />
|
||||
</>
|
||||
),
|
||||
|
@ -65,11 +65,11 @@ const getCardProps = (
|
|||
}
|
||||
|
||||
const descriptionText = isCurrentDevice
|
||||
? _t("Verify your current session for enhanced secure messaging.")
|
||||
: _t("Verify or sign out from this session for best security and reliability.");
|
||||
? _t("settings|sessions|device_unverified_description_current")
|
||||
: _t("settings|sessions|device_unverified_description");
|
||||
return {
|
||||
variation: DeviceSecurityVariation.Unverified,
|
||||
heading: _t("Unverified session"),
|
||||
heading: _t("settings|sessions|unverified_session"),
|
||||
description: (
|
||||
<>
|
||||
{descriptionText}
|
||||
|
@ -95,7 +95,7 @@ export const DeviceVerificationStatusCard: React.FC<DeviceVerificationStatusCard
|
|||
onClick={onVerifyDevice}
|
||||
data-testid={`verification-status-button-${device.device_id}`}
|
||||
>
|
||||
{_t("Verify session")}
|
||||
{_t("settings|sessions|verify_session")}
|
||||
</AccessibleButton>
|
||||
)}
|
||||
</DeviceSecurityCard>
|
||||
|
|
|
@ -73,36 +73,6 @@ const getFilteredSortedDevices = (devices: DevicesDictionary, filter?: FilterVar
|
|||
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 isSecurityVariation = (filter?: DeviceFilterKey): filter is FilterVariation =>
|
||||
!!filter &&
|
||||
(
|
||||
|
@ -115,6 +85,33 @@ const isSecurityVariation = (filter?: DeviceFilterKey): filter is FilterVariatio
|
|||
|
||||
const FilterSecurityCard: React.FC<{ filter?: DeviceFilterKey }> = ({ filter }) => {
|
||||
if (isSecurityVariation(filter)) {
|
||||
const securityCardContent: Record<
|
||||
DeviceSecurityVariation,
|
||||
{
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
> = {
|
||||
[DeviceSecurityVariation.Verified]: {
|
||||
title: _t("settings|sessions|verified_sessions"),
|
||||
description: _t("settings|sessions|verified_sessions_list_description"),
|
||||
},
|
||||
[DeviceSecurityVariation.Unverified]: {
|
||||
title: _t("settings|sessions|unverified_sessions"),
|
||||
description: _t("settings|sessions|unverified_sessions_list_description"),
|
||||
},
|
||||
[DeviceSecurityVariation.Unverifiable]: {
|
||||
title: _t("settings|sessions|unverified_session"),
|
||||
description: _t("settings|sessions|unverified_session_explainer_1"),
|
||||
},
|
||||
[DeviceSecurityVariation.Inactive]: {
|
||||
title: _t("settings|sessions|inactive_sessions"),
|
||||
description: _t("settings|sessions|inactive_sessions_list_description", {
|
||||
inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const { title, description } = securityCardContent[filter];
|
||||
return (
|
||||
<div className="mx_FilteredDeviceList_securityCard">
|
||||
|
@ -138,13 +135,13 @@ 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("settings|sessions|no_verified_sessions");
|
||||
case DeviceSecurityVariation.Unverified:
|
||||
return _t("No unverified sessions found.");
|
||||
return _t("settings|sessions|no_unverified_sessions");
|
||||
case DeviceSecurityVariation.Inactive:
|
||||
return _t("No inactive sessions found.");
|
||||
return _t("settings|sessions|no_inactive_sessions");
|
||||
default:
|
||||
return _t("No sessions found.");
|
||||
return _t("settings|sessions|no_sessions");
|
||||
}
|
||||
};
|
||||
interface NoResultsProps {
|
||||
|
@ -281,21 +278,21 @@ export const FilteredDeviceList = forwardRef(
|
|||
};
|
||||
|
||||
const options: FilterDropdownOption<DeviceFilterKey>[] = [
|
||||
{ id: ALL_FILTER_ID, label: _t("All") },
|
||||
{ id: ALL_FILTER_ID, label: _t("settings|sessions|filter_all") },
|
||||
{
|
||||
id: DeviceSecurityVariation.Verified,
|
||||
label: _t("common|verified"),
|
||||
description: _t("Ready for secure messaging"),
|
||||
description: _t("settings|sessions|filter_verified_description"),
|
||||
},
|
||||
{
|
||||
id: DeviceSecurityVariation.Unverified,
|
||||
label: _t("common|unverified"),
|
||||
description: _t("Not ready for secure messaging"),
|
||||
description: _t("settings|sessions|filter_unverified_description"),
|
||||
},
|
||||
{
|
||||
id: DeviceSecurityVariation.Inactive,
|
||||
label: _t("Inactive"),
|
||||
description: _t("Inactive for %(inactiveAgeDays)s days or longer", {
|
||||
label: _t("settings|sessions|filter_inactive"),
|
||||
description: _t("settings|sessions|filter_inactive_description", {
|
||||
inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS,
|
||||
}),
|
||||
},
|
||||
|
@ -349,7 +346,7 @@ export const FilteredDeviceList = forwardRef(
|
|||
) : (
|
||||
<FilterDropdown<DeviceFilterKey>
|
||||
id="device-list-filter"
|
||||
label={_t("Filter devices")}
|
||||
label={_t("settings|sessions|filter_label")}
|
||||
value={filter || ALL_FILTER_ID}
|
||||
onOptionChange={onFilterOptionChange}
|
||||
options={options}
|
||||
|
|
|
@ -37,7 +37,7 @@ const FilteredDeviceListHeader: React.FC<Props> = ({
|
|||
children,
|
||||
...rest
|
||||
}) => {
|
||||
const checkboxLabel = isAllSelected ? _t("Deselect all") : _t("Select all");
|
||||
const checkboxLabel = isAllSelected ? _t("common|deselect_all") : _t("common|select_all");
|
||||
return (
|
||||
<div className="mx_FilteredDeviceListHeader" {...rest}>
|
||||
{!isSelectDisabled && (
|
||||
|
@ -54,7 +54,7 @@ const FilteredDeviceListHeader: React.FC<Props> = ({
|
|||
)}
|
||||
<span className="mx_FilteredDeviceListHeader_label">
|
||||
{selectedDeviceCount > 0
|
||||
? _t("%(count)s sessions selected", { count: selectedDeviceCount })
|
||||
? _t("settings|sessions|n_sessions_selected", { count: selectedDeviceCount })
|
||||
: _t("Sessions")}
|
||||
</span>
|
||||
{children}
|
||||
|
|
|
@ -52,15 +52,13 @@ export default class LoginWithQRSection extends React.Component<IProps> {
|
|||
}
|
||||
|
||||
return (
|
||||
<SettingsSubsection heading={_t("Sign in with QR code")}>
|
||||
<SettingsSubsection heading={_t("settings|sessions|sign_in_with_qr")}>
|
||||
<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.",
|
||||
)}
|
||||
{_t("settings|sessions|sign_in_with_qr_description")}
|
||||
</p>
|
||||
<AccessibleButton onClick={this.props.onShowQr} kind="primary">
|
||||
{_t("Show QR code")}
|
||||
{_t("settings|sessions|sign_in_with_qr_button")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</SettingsSubsection>
|
||||
|
|
|
@ -41,14 +41,14 @@ export const OtherSessionsSectionHeading: React.FC<Props> = ({
|
|||
signOutAllOtherSessions ? (
|
||||
<IconizedContextMenuOption
|
||||
key="sign-out-all-others"
|
||||
label={_t("Sign out of %(count)s sessions", { count: otherSessionsCount })}
|
||||
label={_t("settings|sessions|sign_out_n_sessions", { count: otherSessionsCount })}
|
||||
onClick={signOutAllOtherSessions}
|
||||
isDestructive
|
||||
/>
|
||||
) : null,
|
||||
]);
|
||||
return (
|
||||
<SettingsSubsectionHeading heading={_t("Other sessions")}>
|
||||
<SettingsSubsectionHeading heading={_t("settings|sessions|other_sessions_heading")}>
|
||||
{!!menuOptions.length && (
|
||||
<KebabContextMenu
|
||||
disabled={disabled}
|
||||
|
|
|
@ -52,19 +52,17 @@ const SecurityRecommendations: React.FC<Props> = ({ devices, currentDeviceId, go
|
|||
|
||||
return (
|
||||
<SettingsSubsection
|
||||
heading={_t("Security recommendations")}
|
||||
description={_t("Improve your account security by following these recommendations.")}
|
||||
heading={_t("settings|sessions|security_recommendations")}
|
||||
description={_t("settings|sessions|security_recommendations_description")}
|
||||
data-testid="security-recommendations-section"
|
||||
>
|
||||
{!!unverifiedDevicesCount && (
|
||||
<DeviceSecurityCard
|
||||
variation={DeviceSecurityVariation.Unverified}
|
||||
heading={_t("Unverified sessions")}
|
||||
heading={_t("settings|sessions|unverified_sessions")}
|
||||
description={
|
||||
<>
|
||||
{_t(
|
||||
"Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.",
|
||||
)}
|
||||
{_t("settings|sessions|unverified_sessions_list_description")}
|
||||
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Unverified} />
|
||||
</>
|
||||
}
|
||||
|
@ -83,13 +81,10 @@ const SecurityRecommendations: React.FC<Props> = ({ devices, currentDeviceId, go
|
|||
{!!unverifiedDevicesCount && <div className="mx_SecurityRecommendations_spacing" />}
|
||||
<DeviceSecurityCard
|
||||
variation={DeviceSecurityVariation.Inactive}
|
||||
heading={_t("Inactive sessions")}
|
||||
heading={_t("settings|sessions|inactive_sessions")}
|
||||
description={
|
||||
<>
|
||||
{_t(
|
||||
"Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore.",
|
||||
{ inactiveAgeDays },
|
||||
)}
|
||||
{_t("settings|sessions|inactive_sessions_list_description", { inactiveAgeDays })}
|
||||
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Inactive} />
|
||||
</>
|
||||
}
|
||||
|
|
|
@ -53,20 +53,20 @@ export const deleteDevicesWithInteractiveAuth = async (
|
|||
const dialogAesthetics = {
|
||||
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||
title: _t("Use Single Sign On to continue"),
|
||||
body: _t("Confirm logging out these devices by using Single Sign On to prove your identity.", {
|
||||
body: _t("settings|sessions|confirm_sign_out_sso", {
|
||||
count: numDevices,
|
||||
}),
|
||||
continueText: _t("auth|sso"),
|
||||
continueKind: "primary",
|
||||
},
|
||||
[SSOAuthEntry.PHASE_POSTAUTH]: {
|
||||
title: _t("Confirm signing out these devices", {
|
||||
title: _t("settings|sessions|confirm_sign_out", {
|
||||
count: numDevices,
|
||||
}),
|
||||
body: _t("Click the button below to confirm signing out these devices.", {
|
||||
body: _t("settings|sessions|confirm_sign_out_body", {
|
||||
count: numDevices,
|
||||
}),
|
||||
continueText: _t("Sign out devices", { count: numDevices }),
|
||||
continueText: _t("settings|sessions|confirm_sign_out_continue", { count: numDevices }),
|
||||
continueKind: "danger",
|
||||
},
|
||||
};
|
||||
|
|
|
@ -111,7 +111,7 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
|
|||
|
||||
let unfederatableSection: JSX.Element | undefined;
|
||||
if (room.currentState.getStateEvents(EventType.RoomCreate, "")?.getContent()["m.federate"] === false) {
|
||||
unfederatableSection = <div>{_t("This room is not accessible by remote Matrix servers")}</div>;
|
||||
unfederatableSection = <div>{_t("room_settings|advanced|unfederated")}</div>;
|
||||
}
|
||||
|
||||
let roomUpgradeButton;
|
||||
|
@ -120,7 +120,7 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
|
|||
<div>
|
||||
<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.",
|
||||
"room_settings|advanced|room_upgrade_warning",
|
||||
{},
|
||||
{
|
||||
b: (sub) => <b>{sub}</b>,
|
||||
|
@ -130,8 +130,8 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
|
|||
</p>
|
||||
<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("room_settings|advanced|space_upgrade_button")
|
||||
: _t("room_settings|advanced|room_upgrade_button")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
|
@ -141,9 +141,9 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
|
|||
if (this.state.oldRoomId) {
|
||||
let copy: string;
|
||||
if (isSpace) {
|
||||
copy = _t("View older version of %(spaceName)s.", { spaceName: room.name ?? this.state.oldRoomId });
|
||||
copy = _t("room_settings|advanced|space_predecessor", { spaceName: room.name ?? this.state.oldRoomId });
|
||||
} else {
|
||||
copy = _t("View older messages in %(roomName)s.", { roomName: room.name ?? this.state.oldRoomId });
|
||||
copy = _t("room_settings|advanced|room_predecessor", { roomName: room.name ?? this.state.oldRoomId });
|
||||
}
|
||||
|
||||
oldRoomLink = (
|
||||
|
@ -158,16 +158,16 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
|
|||
<SettingsSection heading={_t("Advanced")}>
|
||||
<SettingsSubsection heading={room.isSpaceRoom() ? _t("Space information") : _t("Room information")}>
|
||||
<div>
|
||||
<span>{_t("Internal room ID")}</span>
|
||||
<span>{_t("room_settings|advanced|room_id")}</span>
|
||||
<CopyableText getTextToCopy={() => this.props.room.roomId}>
|
||||
{this.props.room.roomId}
|
||||
</CopyableText>
|
||||
</div>
|
||||
{unfederatableSection}
|
||||
</SettingsSubsection>
|
||||
<SettingsSubsection heading={_t("Room version")}>
|
||||
<SettingsSubsection heading={_t("room_settings|advanced|room_version_section")}>
|
||||
<div>
|
||||
<span>{_t("Room version:")}</span>
|
||||
<span>{_t("room_settings|advanced|room_version")}</span>
|
||||
{room.getVersion()}
|
||||
</div>
|
||||
{oldRoomLink}
|
||||
|
|
|
@ -341,7 +341,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("room_settings|permissions|no_privileged_users")}</div>;
|
||||
let mutedUsersSection;
|
||||
if (Object.keys(userLevels).length) {
|
||||
const privilegedUsers: JSX.Element[] = [];
|
||||
|
@ -391,11 +391,17 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
|
||||
if (privilegedUsers.length) {
|
||||
privilegedUsersSection = (
|
||||
<SettingsFieldset legend={_t("Privileged Users")}>{privilegedUsers}</SettingsFieldset>
|
||||
<SettingsFieldset legend={_t("room_settings|permissions|privileged_users_section")}>
|
||||
{privilegedUsers}
|
||||
</SettingsFieldset>
|
||||
);
|
||||
}
|
||||
if (mutedUsers.length) {
|
||||
mutedUsersSection = <SettingsFieldset legend={_t("Muted Users")}>{mutedUsers}</SettingsFieldset>;
|
||||
mutedUsersSection = (
|
||||
<SettingsFieldset legend={_t("room_settings|permissions|muted_users_section")}>
|
||||
{mutedUsers}
|
||||
</SettingsFieldset>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -404,7 +410,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
if (banned?.length) {
|
||||
const canBanUsers = currentUserLevel >= banLevel;
|
||||
bannedUsersSection = (
|
||||
<SettingsFieldset legend={_t("Banned users")}>
|
||||
<SettingsFieldset legend={_t("room_settings|permissions|banned_users_section")}>
|
||||
<ul className="mx_RolesRoomSettingsTab_bannedList">
|
||||
{banned.map((member) => {
|
||||
const banEvent = member.events.member?.getContent();
|
||||
|
@ -468,7 +474,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
const brand = SdkConfig.get("element_call").brand ?? DEFAULTS.element_call.brand;
|
||||
label = _t(translationKeyForEvent, { brand });
|
||||
} else {
|
||||
label = _t("Send %(eventType)s events", { eventType });
|
||||
label = _t("room_settings|permissions|send_event_type", { eventType });
|
||||
}
|
||||
return (
|
||||
<div key={eventType}>
|
||||
|
@ -487,17 +493,17 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
|
||||
return (
|
||||
<SettingsTab>
|
||||
<SettingsSection heading={_t("Roles & Permissions")}>
|
||||
<SettingsSection heading={_t("room_settings|permissions|title")}>
|
||||
{privilegedUsersSection}
|
||||
{canChangeLevels && <AddPrivilegedUsers room={room} defaultUserLevel={defaultUserLevel} />}
|
||||
{mutedUsersSection}
|
||||
{bannedUsersSection}
|
||||
<SettingsFieldset
|
||||
legend={_t("Permissions")}
|
||||
legend={_t("room_settings|permissions|permissions_section")}
|
||||
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("room_settings|permissions|permissions_section_description_space")
|
||||
: _t("room_settings|permissions|permissions_section_description_room")
|
||||
}
|
||||
>
|
||||
{powerSelectors}
|
||||
|
|
|
@ -116,13 +116,13 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
private onEncryptionChange = async (): Promise<void> => {
|
||||
if (this.props.room.getJoinRule() === JoinRule.Public) {
|
||||
const dialog = Modal.createDialog(QuestionDialog, {
|
||||
title: _t("Are you sure you want to add encryption to this public room?"),
|
||||
title: _t("room_settings|security|enable_encryption_public_room_confirm_title"),
|
||||
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.",
|
||||
"room_settings|security|enable_encryption_public_room_confirm_description_1",
|
||||
undefined,
|
||||
{ b: (sub) => <b>{sub}</b> },
|
||||
)}{" "}
|
||||
|
@ -130,7 +130,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
<p>
|
||||
{" "}
|
||||
{_t(
|
||||
"To avoid these issues, create a <a>new encrypted room</a> for the conversation you plan to have.",
|
||||
"room_settings|security|enable_encryption_public_room_confirm_description_2",
|
||||
undefined,
|
||||
{
|
||||
a: (sub) => (
|
||||
|
@ -158,9 +158,9 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
}
|
||||
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: _t("Enable encryption?"),
|
||||
title: _t("room_settings|security|enable_encryption_confirm_title"),
|
||||
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_settings|security|enable_encryption_confirm_description",
|
||||
{},
|
||||
{
|
||||
a: (sub) => <ExternalLink href={SdkConfig.get("help_encryption_url")}>{sub}</ExternalLink>,
|
||||
|
@ -259,11 +259,11 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
aliasWarning = (
|
||||
<div className="mx_SecurityRoomSettingsTab_warning">
|
||||
<WarningIcon width={15} height={15} />
|
||||
<span>{_t("To link to this room, please add an address.")}</span>
|
||||
<span>{_t("room_settings|security|public_without_alias_warning")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const description = _t("Decide who can join %(roomName)s.", {
|
||||
const description = _t("room_settings|security|join_rule_description", {
|
||||
roomName: room.name,
|
||||
});
|
||||
|
||||
|
@ -309,37 +309,31 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
private onBeforeJoinRuleChange = async (joinRule: JoinRule): Promise<boolean> => {
|
||||
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?"),
|
||||
title: _t("room_settings|security|encrypted_room_public_confirm_title"),
|
||||
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.",
|
||||
undefined,
|
||||
{ b: (sub) => <b>{sub}</b> },
|
||||
)}{" "}
|
||||
{_t("room_settings|security|encrypted_room_public_confirm_description_1", undefined, {
|
||||
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.",
|
||||
undefined,
|
||||
{
|
||||
a: (sub) => (
|
||||
<AccessibleButton
|
||||
kind="link_inline"
|
||||
onClick={(): void => {
|
||||
dialog.close();
|
||||
this.createNewRoom(true, false);
|
||||
}}
|
||||
>
|
||||
{" "}
|
||||
{sub}{" "}
|
||||
</AccessibleButton>
|
||||
),
|
||||
},
|
||||
)}{" "}
|
||||
{_t("room_settings|security|encrypted_room_public_confirm_description_2", undefined, {
|
||||
a: (sub) => (
|
||||
<AccessibleButton
|
||||
kind="link_inline"
|
||||
onClick={(): void => {
|
||||
dialog.close();
|
||||
this.createNewRoom(true, false);
|
||||
}}
|
||||
>
|
||||
{" "}
|
||||
{sub}{" "}
|
||||
</AccessibleButton>
|
||||
),
|
||||
})}{" "}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
|
@ -366,15 +360,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("room_settings|security|history_visibility_shared"),
|
||||
},
|
||||
{
|
||||
value: HistoryVisibility.Invited,
|
||||
label: _t("Members only (since they were invited)"),
|
||||
label: _t("room_settings|security|history_visibility_invited"),
|
||||
},
|
||||
{
|
||||
value: HistoryVisibility.Joined,
|
||||
label: _t("Members only (since they joined)"),
|
||||
label: _t("room_settings|security|history_visibility_joined"),
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -382,16 +376,14 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
if (!this.state.encrypted || history === HistoryVisibility.WorldReadable) {
|
||||
options.unshift({
|
||||
value: HistoryVisibility.WorldReadable,
|
||||
label: _t("Anyone"),
|
||||
label: _t("room_settings|security|history_visibility_world_readable"),
|
||||
});
|
||||
}
|
||||
|
||||
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("room_settings|security|history_visibility_warning");
|
||||
|
||||
return (
|
||||
<SettingsFieldset legend={_t("Who can read history?")} description={description}>
|
||||
<SettingsFieldset legend={_t("room_settings|security|history_visibility_legend")} description={description}>
|
||||
<StyledRadioGroup
|
||||
name="historyVis"
|
||||
value={history}
|
||||
|
@ -421,11 +413,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
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>
|
||||
<p>{_t("room_settings|security|guest_access_warning")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -454,13 +442,13 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
|
||||
return (
|
||||
<SettingsTab>
|
||||
<SettingsSection heading={_t("Security & Privacy")}>
|
||||
<SettingsSection heading={_t("room_settings|security|title")}>
|
||||
<SettingsFieldset
|
||||
legend={_t("Encryption")}
|
||||
legend={_t("settings|security|encryption_section")}
|
||||
description={
|
||||
isEncryptionForceDisabled && !isEncrypted
|
||||
? undefined
|
||||
: _t("Once enabled, encryption cannot be disabled.")
|
||||
: _t("room_settings|security|encryption_permanent")
|
||||
}
|
||||
>
|
||||
<LabelledToggleSwitch
|
||||
|
@ -470,7 +458,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
disabled={!canEnableEncryption}
|
||||
/>
|
||||
{isEncryptionForceDisabled && !isEncrypted && (
|
||||
<Caption>{_t("Your server requires encryption to be disabled.")}</Caption>
|
||||
<Caption>{_t("room_settings|security|encryption_forced")}</Caption>
|
||||
)}
|
||||
{encryptionSettings}
|
||||
</SettingsFieldset>
|
||||
|
|
|
@ -402,14 +402,18 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
|
|||
href={this.state.externalAccountManagementUrl}
|
||||
data-testid="external-account-management-link"
|
||||
>
|
||||
{_t("Manage account")}
|
||||
{_t("settings|general|oidc_manage_button")}
|
||||
</AccessibleButton>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<SettingsSubsection heading={_t("Account")} stretchContent data-testid="accountSection">
|
||||
<SettingsSubsection
|
||||
heading={_t("settings|general|account_section")}
|
||||
stretchContent
|
||||
data-testid="accountSection"
|
||||
>
|
||||
{externalAccountManagement}
|
||||
{passwordChangeSection}
|
||||
</SettingsSubsection>
|
||||
|
@ -421,7 +425,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
|
|||
private renderLanguageSection(): JSX.Element {
|
||||
// TODO: Convert to new-styled Field
|
||||
return (
|
||||
<SettingsSubsection heading={_t("Language and region")} stretchContent>
|
||||
<SettingsSubsection heading={_t("settings|general|language_section")} stretchContent>
|
||||
<LanguageDropdown
|
||||
className="mx_GeneralUserSettingsTab_section_languageInput"
|
||||
onOptionChange={this.onLanguageChange}
|
||||
|
@ -433,7 +437,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
|
|||
|
||||
private renderSpellCheckSection(): JSX.Element {
|
||||
const heading = (
|
||||
<SettingsSubsectionHeading heading={_t("Spell check")}>
|
||||
<SettingsSubsectionHeading heading={_t("settings|general|spell_check_section")}>
|
||||
<ToggleSwitch checked={!!this.state.spellCheckEnabled} onChange={this.onSpellCheckEnabledChange} />
|
||||
</SettingsSubsectionHeading>
|
||||
);
|
||||
|
|
|
@ -110,21 +110,18 @@ export default class LabsUserSettingsTab extends React.Component<{}> {
|
|||
|
||||
return (
|
||||
<SettingsTab>
|
||||
<SettingsSection heading={_t("Upcoming features")}>
|
||||
<SettingsSection heading={_t("labs|beta_section")}>
|
||||
<SettingsSubsectionText>
|
||||
{_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") },
|
||||
)}
|
||||
{_t("labs|beta_description", { brand: SdkConfig.get("brand") })}
|
||||
</SettingsSubsectionText>
|
||||
{betaSection}
|
||||
</SettingsSection>
|
||||
|
||||
{labsSections && (
|
||||
<SettingsSection heading={_t("Early previews")}>
|
||||
<SettingsSection heading={_t("labs|experimental_section")}>
|
||||
<SettingsSubsectionText>
|
||||
{_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>.",
|
||||
"labs|experimental_description",
|
||||
{},
|
||||
{
|
||||
a: (sub) => {
|
||||
|
|
|
@ -69,14 +69,14 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
|
|||
this.setState({ busy: true });
|
||||
try {
|
||||
const list = await Mjolnir.sharedInstance().getOrCreatePersonalList();
|
||||
await list.banEntity(kind, this.state.newPersonalRule, _t("Ignored/Blocked"));
|
||||
await list.banEntity(kind, this.state.newPersonalRule, _t("labs_mjolnir|ban_reason"));
|
||||
this.setState({ newPersonalRule: "" }); // this will also cause the new rule to be rendered
|
||||
} catch (e) {
|
||||
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("labs_mjolnir|error_adding_ignore"),
|
||||
description: _t("labs_mjolnir|something_went_wrong"),
|
||||
});
|
||||
} finally {
|
||||
this.setState({ busy: false });
|
||||
|
@ -96,8 +96,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("labs_mjolnir|error_adding_list_title"),
|
||||
description: _t("labs_mjolnir|error_adding_list_description"),
|
||||
});
|
||||
} finally {
|
||||
this.setState({ busy: false });
|
||||
|
@ -113,8 +113,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("labs_mjolnir|error_removing_ignore"),
|
||||
description: _t("labs_mjolnir|something_went_wrong"),
|
||||
});
|
||||
} finally {
|
||||
this.setState({ busy: false });
|
||||
|
@ -130,8 +130,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("labs_mjolnir|error_removing_list_title"),
|
||||
description: _t("labs_mjolnir|error_removing_list_description"),
|
||||
});
|
||||
} finally {
|
||||
this.setState({ busy: false });
|
||||
|
@ -157,12 +157,12 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
|
|||
};
|
||||
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: _t("Ban list rules - %(roomName)s", { roomName: name }),
|
||||
title: _t("labs_mjolnir|rules_title", { roomName: name }),
|
||||
description: (
|
||||
<div>
|
||||
<h3>{_t("Server rules")}</h3>
|
||||
<h3>{_t("labs_mjolnir|rules_server")}</h3>
|
||||
{renderRules(list.serverRules)}
|
||||
<h3>{_t("User rules")}</h3>
|
||||
<h3>{_t("labs_mjolnir|rules_user")}</h3>
|
||||
{renderRules(list.userRules)}
|
||||
</div>
|
||||
),
|
||||
|
@ -174,7 +174,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
|
|||
private renderPersonalBanListRules(): JSX.Element {
|
||||
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("labs_mjolnir|personal_empty")}</i>;
|
||||
|
||||
const tiles: JSX.Element[] = [];
|
||||
for (const rule of rules) {
|
||||
|
@ -195,7 +195,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
|
|||
|
||||
return (
|
||||
<div>
|
||||
<p>{_t("You are currently ignoring:")}</p>
|
||||
<p>{_t("labs_mjolnir|personal_section")}</p>
|
||||
<ul>{tiles}</ul>
|
||||
</div>
|
||||
);
|
||||
|
@ -206,7 +206,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
|
|||
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("labs_mjolnir|no_lists")}</i>;
|
||||
|
||||
const tiles: JSX.Element[] = [];
|
||||
for (const list of lists) {
|
||||
|
@ -233,7 +233,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
|
|||
onClick={() => this.viewListRules(list)}
|
||||
disabled={this.state.busy}
|
||||
>
|
||||
{_t("View rules")}
|
||||
{_t("labs_mjolnir|view_rules")}
|
||||
</AccessibleButton>
|
||||
|
||||
{name}
|
||||
|
@ -243,7 +243,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
|
|||
|
||||
return (
|
||||
<div>
|
||||
<p>{_t("You are currently subscribed to:")}</p>
|
||||
<p>{_t("labs_mjolnir|lists")}</p>
|
||||
<ul>{tiles}</ul>
|
||||
</div>
|
||||
);
|
||||
|
@ -254,37 +254,24 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
|
|||
|
||||
return (
|
||||
<SettingsTab>
|
||||
<SettingsSection heading={_t("Ignored users")}>
|
||||
<SettingsSection heading={_t("labs_mjolnir|title")}>
|
||||
<SettingsSubsectionText>
|
||||
<span className="warning">{_t("⚠ These settings are meant for advanced users.")}</span>
|
||||
<p>
|
||||
{_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> },
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{_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.",
|
||||
)}
|
||||
</p>
|
||||
<span className="warning">{_t("labs_mjolnir|advanced_warning")}</span>
|
||||
<p>{_t("labs_mjolnir|explainer_1", { brand }, { code: (s) => <code>{s}</code> })}</p>
|
||||
<p>{_t("labs_mjolnir|explainer_2")}</p>
|
||||
</SettingsSubsectionText>
|
||||
<SettingsSubsection
|
||||
heading={_t("Personal ban list")}
|
||||
description={_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 '%(myBanList)s' - stay in this room to keep the ban list in effect.",
|
||||
{
|
||||
myBanList: _t("labs_mjolnir|room_name"),
|
||||
},
|
||||
)}
|
||||
heading={_t("labs_mjolnir|personal_heading")}
|
||||
description={_t("labs_mjolnir|personal_description", {
|
||||
myBanList: _t("labs_mjolnir|room_name"),
|
||||
})}
|
||||
>
|
||||
{this.renderPersonalBanListRules()}
|
||||
<form onSubmit={this.onAddPersonalRule} autoComplete="off">
|
||||
<Field
|
||||
type="text"
|
||||
label={_t("Server or user ID to ignore")}
|
||||
placeholder={_t("eg: @bot:* or example.org")}
|
||||
label={_t("labs_mjolnir|personal_new_label")}
|
||||
placeholder={_t("labs_mjolnir|personal_new_placeholder")}
|
||||
value={this.state.newPersonalRule}
|
||||
onChange={this.onPersonalRuleChanged}
|
||||
/>
|
||||
|
@ -299,16 +286,12 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
|
|||
</form>
|
||||
</SettingsSubsection>
|
||||
<SettingsSubsection
|
||||
heading={_t("Subscribed lists")}
|
||||
heading={_t("labs_mjolnir|lists_heading")}
|
||||
description={
|
||||
<>
|
||||
<span className="warning">
|
||||
{_t("Subscribing to a ban list will cause you to join it!")}
|
||||
</span>
|
||||
<span className="warning">{_t("labs_mjolnir|lists_description_1")}</span>
|
||||
|
||||
<span>
|
||||
{_t("If this isn't what you want, please use a different tool to ignore users.")}
|
||||
</span>
|
||||
<span>{_t("labs_mjolnir|lists_description_2")}</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
|
@ -316,7 +299,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
|
|||
<form onSubmit={this.onSubscribeList} autoComplete="off">
|
||||
<Field
|
||||
type="text"
|
||||
label={_t("Room ID or address of ban list")}
|
||||
label={_t("labs_mjolnir|lists_new_label")}
|
||||
value={this.state.newList}
|
||||
onChange={this.onNewListChanged}
|
||||
/>
|
||||
|
|
|
@ -358,7 +358,7 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
|
|||
return (
|
||||
<SettingsTab>
|
||||
{warning}
|
||||
<SettingsSection heading={_t("Encryption")}>
|
||||
<SettingsSection heading={_t("settings|security|encryption_section")}>
|
||||
{secureBackup}
|
||||
{eventIndex}
|
||||
{crossSigning}
|
||||
|
|
|
@ -104,6 +104,7 @@ export const HomeButtonContextMenu: React.FC<ComponentProps<typeof SpaceContextM
|
|||
label={_t("Show all rooms")}
|
||||
active={allRoomsInHome}
|
||||
onClick={() => {
|
||||
onFinished();
|
||||
SettingsStore.setValue("Spaces.allRoomsInHome", null, SettingLevel.ACCOUNT, !allRoomsInHome);
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -29,14 +29,10 @@ export default class VerificationComplete extends React.Component<IProps> {
|
|||
<div>
|
||||
<h2>{_t("encryption|verification|complete_title")}</h2>
|
||||
<p>{_t("encryption|verification|complete_description")}</p>
|
||||
<p>
|
||||
{_t(
|
||||
"Secure messages with this user are end-to-end encrypted and not able to be read by third parties.",
|
||||
)}
|
||||
</p>
|
||||
<p>{_t("encryption|verification|explainer")}</p>
|
||||
<DialogButtons
|
||||
onPrimaryButtonClick={this.props.onDone}
|
||||
primaryButton={_t("Got It")}
|
||||
primaryButton={_t("encryption|verification|complete_action")}
|
||||
hasCancel={false}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -133,18 +133,18 @@ export default class VerificationShowSas extends React.Component<IProps, IState>
|
|||
</div>
|
||||
);
|
||||
sasCaption = this.props.isSelf
|
||||
? _t("Confirm the emoji below are displayed on both devices, in the same order:")
|
||||
: _t("Verify this user by confirming the following emoji appear on their screen.");
|
||||
? _t("encryption|verification|sas_emoji_caption_self")
|
||||
: _t("encryption|verification|sas_emoji_caption_user");
|
||||
} else if (this.props.sas.decimal) {
|
||||
const numberBlocks = this.props.sas.decimal.map((num, i) => <span key={i}>{num}</span>);
|
||||
sasDisplay = <div className="mx_VerificationShowSas_decimalSas">{numberBlocks}</div>;
|
||||
sasCaption = this.props.isSelf
|
||||
? _t("Verify this device by confirming the following number appears on its screen.")
|
||||
: _t("Verify this user by confirming the following number appears on their screen.");
|
||||
? _t("encryption|verification|sas_caption_self")
|
||||
: _t("encryption|verification|sas_caption_user");
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
{_t("Unable to find a supported verification method.")}
|
||||
{_t("encryption|verification|unsupported_method")}
|
||||
<AccessibleButton kind="primary" onClick={this.props.onCancel}>
|
||||
{_t("action|cancel")}
|
||||
</AccessibleButton>
|
||||
|
@ -159,21 +159,21 @@ export default class VerificationShowSas extends React.Component<IProps, IState>
|
|||
// logged out during verification
|
||||
const otherDevice = this.props.otherDeviceDetails;
|
||||
if (otherDevice) {
|
||||
text = _t("Waiting for you to verify on your other device, %(deviceName)s (%(deviceId)s)…", {
|
||||
text = _t("encryption|verification|waiting_other_device_details", {
|
||||
deviceName: otherDevice.displayName,
|
||||
deviceId: otherDevice.deviceId,
|
||||
});
|
||||
} else {
|
||||
text = _t("Waiting for you to verify on your other device…");
|
||||
text = _t("encryption|verification|waiting_other_device");
|
||||
}
|
||||
confirm = <p>{text}</p>;
|
||||
} else if (this.state.pending || this.state.cancelling) {
|
||||
let text;
|
||||
if (this.state.pending) {
|
||||
const { displayName } = this.props;
|
||||
text = _t("Waiting for %(displayName)s to verify…", { displayName });
|
||||
text = _t("encryption|verification|waiting_other_user", { displayName });
|
||||
} else {
|
||||
text = _t("Cancelling…");
|
||||
text = _t("encryption|verification|cancelling");
|
||||
}
|
||||
confirm = <PendingActionSpinner text={text} />;
|
||||
} else {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue