Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/cr/72
# Conflicts: # src/components/views/rooms/RoomHeader.tsx # test/components/views/rooms/__snapshots__/RoomHeader-test.tsx.snap
This commit is contained in:
commit
c839123b83
123 changed files with 6527 additions and 6069 deletions
|
@ -129,6 +129,7 @@ import { WaitingForThirdPartyRoomView } from "./WaitingForThirdPartyRoomView";
|
|||
import { isNotUndefined } from "../../Typeguards";
|
||||
import { CancelAskToJoinPayload } from "../../dispatcher/payloads/CancelAskToJoinPayload";
|
||||
import { SubmitAskToJoinPayload } from "../../dispatcher/payloads/SubmitAskToJoinPayload";
|
||||
import RightPanelStore from "../../stores/right-panel/RightPanelStore";
|
||||
|
||||
const DEBUG = false;
|
||||
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
|
||||
|
@ -1248,6 +1249,33 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
this.messagePanel?.jumpToLiveTimeline();
|
||||
}
|
||||
break;
|
||||
case Action.ViewUser:
|
||||
if (payload.member) {
|
||||
if (payload.push) {
|
||||
RightPanelStore.instance.pushCard({
|
||||
phase: RightPanelPhases.RoomMemberInfo,
|
||||
state: { member: payload.member },
|
||||
});
|
||||
} else {
|
||||
RightPanelStore.instance.setCards([
|
||||
{ phase: RightPanelPhases.RoomSummary },
|
||||
{ phase: RightPanelPhases.RoomMemberList },
|
||||
{ phase: RightPanelPhases.RoomMemberInfo, state: { member: payload.member } },
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.RoomMemberList);
|
||||
}
|
||||
break;
|
||||
case "view_3pid_invite":
|
||||
if (payload.event) {
|
||||
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.Room3pidMemberInfo, {
|
||||
memberInfoEvent: payload.event,
|
||||
});
|
||||
} else {
|
||||
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.RoomMemberList);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -36,7 +36,6 @@ import { inviteMultipleToRoom, showRoomInviteDialog } from "../../RoomInvite";
|
|||
import { UIComponent } from "../../settings/UIFeature";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import RightPanelStore from "../../stores/right-panel/RightPanelStore";
|
||||
import { IRightPanelCard } from "../../stores/right-panel/RightPanelStoreIPanelState";
|
||||
import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases";
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import {
|
||||
|
@ -669,33 +668,6 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
|
|||
this.setState({ phase: Phase.Landing });
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.action !== Action.ViewUser && payload.action !== "view_3pid_invite") return;
|
||||
|
||||
if (payload.action === Action.ViewUser && payload.member) {
|
||||
const spaceMemberInfoCard: IRightPanelCard = {
|
||||
phase: RightPanelPhases.SpaceMemberInfo,
|
||||
state: { spaceId: this.props.space.roomId, member: payload.member },
|
||||
};
|
||||
if (payload.push) {
|
||||
RightPanelStore.instance.pushCard(spaceMemberInfoCard);
|
||||
} else {
|
||||
RightPanelStore.instance.setCards([
|
||||
{ phase: RightPanelPhases.SpaceMemberList, state: { spaceId: this.props.space.roomId } },
|
||||
spaceMemberInfoCard,
|
||||
]);
|
||||
}
|
||||
} else if (payload.action === "view_3pid_invite" && payload.event) {
|
||||
RightPanelStore.instance.setCard({
|
||||
phase: RightPanelPhases.Space3pidMemberInfo,
|
||||
state: { spaceId: this.props.space.roomId, memberInfoEvent: payload.event },
|
||||
});
|
||||
} else {
|
||||
RightPanelStore.instance.setCard({
|
||||
phase: RightPanelPhases.SpaceMemberList,
|
||||
state: { spaceId: this.props.space.roomId },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private goToFirstRoom = async (): Promise<void> => {
|
||||
|
|
|
@ -295,7 +295,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
topSection = (
|
||||
<div className="mx_UserMenu_contextMenu_header mx_UserMenu_contextMenu_guestPrompts">
|
||||
{_t(
|
||||
"Got an account? <a>Sign in</a>",
|
||||
"auth|sign_in_prompt",
|
||||
{},
|
||||
{
|
||||
a: (sub) => (
|
||||
|
@ -307,7 +307,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
)}
|
||||
{SettingsStore.getValue(UIFeature.Registration)
|
||||
? _t(
|
||||
"New here? <a>Create an account</a>",
|
||||
"auth|create_account_prompt",
|
||||
{},
|
||||
{
|
||||
a: (sub) => (
|
||||
|
@ -338,7 +338,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
feedbackButton = (
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconMessage"
|
||||
label={_t("Feedback")}
|
||||
label={_t("common|feedback")}
|
||||
onClick={this.onProvideFeedback}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -224,7 +224,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
|||
let errorText: ReactNode;
|
||||
// Some error strings only apply for logging in
|
||||
if (error.httpStatus === 400 && username && username.indexOf("@") > 0) {
|
||||
errorText = _t("This homeserver does not support login using email address.");
|
||||
errorText = _t("auth|unsupported_auth_email");
|
||||
} else {
|
||||
errorText = messageForLoginError(error, this.props.serverConfig);
|
||||
}
|
||||
|
@ -273,7 +273,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
|||
} catch (e) {
|
||||
logger.error("Problem parsing URL or unhandled error doing .well-known discovery:", e);
|
||||
|
||||
let message = _t("Failed to perform homeserver discovery");
|
||||
let message = _t("auth|failed_homeserver_discovery");
|
||||
if (e instanceof UserFriendlyError && e.translatedMessage) {
|
||||
message = e.translatedMessage;
|
||||
}
|
||||
|
@ -398,9 +398,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
|||
|
||||
if (supportedFlows.length === 0) {
|
||||
this.setState({
|
||||
errorText: _t(
|
||||
"This homeserver doesn't offer any login flows that are supported by this client.",
|
||||
),
|
||||
errorText: _t("auth|unsupported_auth"),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -532,12 +530,10 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
|||
<div className="mx_AuthBody_paddedFooter">
|
||||
<div className="mx_AuthBody_paddedFooter_title">
|
||||
<InlineSpinner w={20} h={20} />
|
||||
{this.props.isSyncing ? _t("Syncing…") : _t("Signing In…")}
|
||||
{this.props.isSyncing ? _t("auth|syncing") : _t("auth|signing_in")}
|
||||
</div>
|
||||
{this.props.isSyncing && (
|
||||
<div className="mx_AuthBody_paddedFooter_subtitle">
|
||||
{_t("If you've joined lots of rooms, this might take a while")}
|
||||
</div>
|
||||
<div className="mx_AuthBody_paddedFooter_subtitle">{_t("auth|sync_footer_subtitle")}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -545,7 +541,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
|||
footer = (
|
||||
<span className="mx_AuthBody_changeFlow">
|
||||
{_t(
|
||||
"New? <a>Create account</a>",
|
||||
"auth|create_account_prompt",
|
||||
{},
|
||||
{
|
||||
a: (sub) => (
|
||||
|
|
|
@ -263,7 +263,7 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
} else {
|
||||
this.setState({
|
||||
serverErrorIsFatal: true, // fatal because user cannot continue on this server
|
||||
errorText: _t("Registration has been disabled on this homeserver."),
|
||||
errorText: _t("auth|registration_disabled"),
|
||||
// add empty flows array to get rid of spinner
|
||||
flows: [],
|
||||
});
|
||||
|
@ -271,7 +271,7 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
} else {
|
||||
logger.log("Unable to query for supported registration methods.", e);
|
||||
this.setState({
|
||||
errorText: _t("Unable to query for supported registration methods."),
|
||||
errorText: _t("auth|failed_query_registration_methods"),
|
||||
// add empty flows array to get rid of spinner
|
||||
flows: [],
|
||||
});
|
||||
|
@ -326,12 +326,12 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
const flows = (response as IAuthData).flows ?? [];
|
||||
const msisdnAvailable = flows.some((flow) => flow.stages.includes(AuthType.Msisdn));
|
||||
if (!msisdnAvailable) {
|
||||
errorText = _t("This server does not support authentication with a phone number.");
|
||||
errorText = _t("auth|unsupported_auth_msisdn");
|
||||
}
|
||||
} else if (response instanceof MatrixError && response.errcode === "M_USER_IN_USE") {
|
||||
errorText = _t("Someone already has that username, please try another.");
|
||||
errorText = _t("auth|username_in_use");
|
||||
} else if (response instanceof MatrixError && response.errcode === "M_THREEPID_IN_USE") {
|
||||
errorText = _t("That e-mail address or phone number is already in use.");
|
||||
errorText = _t("auth|3pid_in_use");
|
||||
}
|
||||
|
||||
this.setState({
|
||||
|
|
|
@ -161,7 +161,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
|
|||
e.errcode === "M_FORBIDDEN" &&
|
||||
(e.httpStatus === 401 || e.httpStatus === 403)
|
||||
) {
|
||||
errorText = _t("Incorrect password");
|
||||
errorText = _t("auth|incorrect_password");
|
||||
}
|
||||
|
||||
this.setState({
|
||||
|
@ -173,7 +173,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
|
|||
|
||||
Lifecycle.hydrateSession(credentials).catch((e) => {
|
||||
logger.error(e);
|
||||
this.setState({ busy: false, errorText: _t("Failed to re-authenticate") });
|
||||
this.setState({ busy: false, errorText: _t("auth|failed_soft_logout_auth") });
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -239,7 +239,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
|
|||
{_t("action|sign_in")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton onClick={this.onForgotPassword} kind="link">
|
||||
{_t("Forgotten your password?")}
|
||||
{_t("auth|forgot_password_prompt")}
|
||||
</AccessibleButton>
|
||||
</form>
|
||||
);
|
||||
|
@ -270,11 +270,11 @@ export default class SoftLogout extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
if (this.state.loginView === LoginView.Password) {
|
||||
return this.renderPasswordForm(_t("Enter your password to sign in and regain access to your account."));
|
||||
return this.renderPasswordForm(_t("auth|soft_logout_intro_password"));
|
||||
}
|
||||
|
||||
if (this.state.loginView === LoginView.SSO || this.state.loginView === LoginView.CAS) {
|
||||
return this.renderSsoForm(_t("Sign in and regain access to your account."));
|
||||
return this.renderSsoForm(_t("auth|soft_logout_intro_sso"));
|
||||
}
|
||||
|
||||
if (this.state.loginView === LoginView.PasswordWithSocialSignOn) {
|
||||
|
@ -284,7 +284,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
|
|||
// Note: "mx_AuthBody_centered" text taken from registration page.
|
||||
return (
|
||||
<>
|
||||
<p>{_t("Sign in and regain access to your account.")}</p>
|
||||
<p>{_t("auth|soft_logout_intro_sso")}</p>
|
||||
{this.renderSsoForm(null)}
|
||||
<h2 className="mx_AuthBody_centered">
|
||||
{_t("auth|sso_or_username_password", {
|
||||
|
@ -298,11 +298,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
// Default: assume unsupported/error
|
||||
return (
|
||||
<p>
|
||||
{_t("You cannot sign in to your account. Please contact your homeserver admin for more information.")}
|
||||
</p>
|
||||
);
|
||||
return <p>{_t("auth|soft_logout_intro_unsupported_auth")}</p>;
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
|
@ -310,7 +306,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
|
|||
<AuthPage>
|
||||
<AuthHeader />
|
||||
<AuthBody>
|
||||
<h1>{_t("You're signed out")}</h1>
|
||||
<h1>{_t("auth|soft_logout_heading")}</h1>
|
||||
|
||||
<h2>{_t("action|sign_in")}</h2>
|
||||
<div>{this.renderSignInSection()}</div>
|
||||
|
|
|
@ -55,20 +55,18 @@ export const CheckEmail: React.FC<CheckEmailProps> = ({
|
|||
<EMailPromptIcon className="mx_AuthBody_emailPromptIcon--shifted" />
|
||||
<h1>{_t("Check your email to continue")}</h1>
|
||||
<div className="mx_AuthBody_text">
|
||||
<p>
|
||||
{_t("Follow the instructions sent to <b>%(email)s</b>", { email: email }, { b: (t) => <b>{t}</b> })}
|
||||
</p>
|
||||
<p>{_t("auth|check_email_explainer", { email: email }, { b: (t) => <b>{t}</b> })}</p>
|
||||
<div className="mx_AuthBody_did-not-receive">
|
||||
<span className="mx_VerifyEMailDialog_text-light">{_t("Wrong email address?")}</span>
|
||||
<span className="mx_VerifyEMailDialog_text-light">{_t("auth|check_email_wrong_email_prompt")}</span>
|
||||
<AccessibleButton className="mx_AuthBody_resend-button" kind="link" onClick={onReEnterEmailClick}>
|
||||
{_t("Re-enter email address")}
|
||||
{_t("auth|check_email_wrong_email_button")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
{errorText && <ErrorMessage message={errorText} />}
|
||||
<input onClick={onSubmitForm} type="button" className="mx_Login_submit" value={_t("action|next")} />
|
||||
<div className="mx_AuthBody_did-not-receive">
|
||||
<span className="mx_VerifyEMailDialog_text-light">{_t("Did not receive it?")}</span>
|
||||
<span className="mx_VerifyEMailDialog_text-light">{_t("auth|check_email_resend_prompt")}</span>
|
||||
<AccessibleButton
|
||||
className="mx_AuthBody_resend-button"
|
||||
kind="link"
|
||||
|
@ -79,7 +77,7 @@ export const CheckEmail: React.FC<CheckEmailProps> = ({
|
|||
{_t("action|resend")}
|
||||
<Tooltip
|
||||
id={tooltipId}
|
||||
label={_t("Verification link email resent!")}
|
||||
label={_t("auth|check_email_resend_tooltip")}
|
||||
alignment={Alignment.Top}
|
||||
visible={tooltipVisible}
|
||||
/>
|
||||
|
|
|
@ -63,13 +63,9 @@ export const EnterEmail: React.FC<EnterEmailProps> = ({
|
|||
return (
|
||||
<>
|
||||
<EmailIcon className="mx_AuthBody_icon" />
|
||||
<h1>{_t("Enter your email to reset password")}</h1>
|
||||
<h1>{_t("auth|enter_email_heading")}</h1>
|
||||
<p className="mx_AuthBody_text">
|
||||
{_t(
|
||||
"<b>%(homeserver)s</b> will send you a verification link to let you reset your password.",
|
||||
{ homeserver },
|
||||
{ b: (t) => <b>{t}</b> },
|
||||
)}
|
||||
{_t("auth|enter_email_explainer", { homeserver }, { b: (t) => <b>{t}</b> })}
|
||||
</p>
|
||||
<form onSubmit={onSubmit}>
|
||||
<fieldset disabled={loading}>
|
||||
|
@ -77,8 +73,8 @@ export const EnterEmail: React.FC<EnterEmailProps> = ({
|
|||
<EmailField
|
||||
name="reset_email" // define a name so browser's password autofill gets less confused
|
||||
label="Email address"
|
||||
labelRequired={_td("The email address linked to your account must be entered.")}
|
||||
labelInvalid={_td("The email address doesn't appear to be valid.")}
|
||||
labelRequired={_td("auth|forgot_password_email_required")}
|
||||
labelInvalid={_td("auth|forgot_password_email_invalid")}
|
||||
value={email}
|
||||
autoFocus={true}
|
||||
onChange={(event: React.FormEvent<HTMLInputElement>) => onInputChanged("email", event)}
|
||||
|
@ -99,7 +95,7 @@ export const EnterEmail: React.FC<EnterEmailProps> = ({
|
|||
onLoginClick();
|
||||
}}
|
||||
>
|
||||
{_t("Sign in instead")}
|
||||
{_t("auth|sign_in_instead")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
|
|
@ -51,10 +51,10 @@ export const VerifyEmailModal: React.FC<Props> = ({
|
|||
return (
|
||||
<>
|
||||
<EmailPromptIcon className="mx_AuthBody_emailPromptIcon" />
|
||||
<h1>{_t("Verify your email to continue")}</h1>
|
||||
<h1>{_t("auth|verify_email_heading")}</h1>
|
||||
<p>
|
||||
{_t(
|
||||
"We need to know it’s you before resetting your password. Click the link in the email we just sent to <b>%(email)s</b>",
|
||||
"auth|verify_email_explainer",
|
||||
{
|
||||
email,
|
||||
},
|
||||
|
@ -65,7 +65,7 @@ export const VerifyEmailModal: React.FC<Props> = ({
|
|||
</p>
|
||||
|
||||
<div className="mx_AuthBody_did-not-receive">
|
||||
<span className="mx_VerifyEMailDialog_text-light">{_t("Did not receive it?")}</span>
|
||||
<span className="mx_VerifyEMailDialog_text-light">{_t("auth|check_email_resend_prompt")}</span>
|
||||
<AccessibleButton
|
||||
className="mx_AuthBody_resend-button"
|
||||
kind="link"
|
||||
|
@ -76,7 +76,7 @@ export const VerifyEmailModal: React.FC<Props> = ({
|
|||
{_t("action|resend")}
|
||||
<Tooltip
|
||||
id={tooltipId}
|
||||
label={_t("Verification link email resent!")}
|
||||
label={_t("auth|check_email_resend_tooltip")}
|
||||
alignment={Alignment.Top}
|
||||
visible={tooltipVisible}
|
||||
/>
|
||||
|
@ -85,9 +85,9 @@ export const VerifyEmailModal: React.FC<Props> = ({
|
|||
</div>
|
||||
|
||||
<div className="mx_AuthBody_did-not-receive">
|
||||
<span className="mx_VerifyEMailDialog_text-light">{_t("Wrong email address?")}</span>
|
||||
<span className="mx_VerifyEMailDialog_text-light">{_t("auth|check_email_wrong_email_prompt")}</span>
|
||||
<AccessibleButton className="mx_AuthBody_resend-button" kind="link" onClick={onReEnterEmailClick}>
|
||||
{_t("Re-enter email address")}
|
||||
{_t("auth|check_email_wrong_email_button")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ export default class AuthFooter extends React.Component {
|
|||
return (
|
||||
<footer className="mx_AuthFooter" role="contentinfo">
|
||||
<a href="https://matrix.org" target="_blank" rel="noreferrer noopener">
|
||||
{_t("powered by Matrix")}
|
||||
{_t("auth|footer_powered_by_matrix")}
|
||||
</a>
|
||||
</footer>
|
||||
);
|
||||
|
|
|
@ -19,16 +19,12 @@ import classNames from "classnames";
|
|||
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import AuthPage from "./AuthPage";
|
||||
import { _td } from "../../../languageHandler";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import LanguageSelector from "./LanguageSelector";
|
||||
import EmbeddedPage from "../../structures/EmbeddedPage";
|
||||
import { MATRIX_LOGO_HTML } from "../../structures/static-page-vars";
|
||||
|
||||
// translatable strings for Welcome pages
|
||||
_td("Sign in with SSO");
|
||||
|
||||
interface IProps {}
|
||||
|
||||
export default class Welcome extends React.PureComponent<IProps> {
|
||||
|
|
|
@ -86,7 +86,7 @@ const BetaCard: React.FC<IProps> = ({ title: titleOverride, featureId }) => {
|
|||
}}
|
||||
kind="primary"
|
||||
>
|
||||
{_t("Feedback")}
|
||||
{_t("common|feedback")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -257,7 +257,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||
{
|
||||
key: "required",
|
||||
test: async ({ value }) => !!value,
|
||||
invalid: () => _t("Please enter a name for the room"),
|
||||
invalid: () => _t("create_room|name_validation_required"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -285,54 +285,48 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||
publicPrivateLabel = (
|
||||
<p>
|
||||
{_t(
|
||||
"Everyone in <SpaceName/> will be able to find and join this room.",
|
||||
"create_room|join_rule_restricted_label",
|
||||
{},
|
||||
{
|
||||
SpaceName: () => <b>{this.props.parentSpace?.name ?? _t("common|unnamed_space")}</b>,
|
||||
},
|
||||
)}
|
||||
|
||||
{_t("You can change this at any time from room settings.")}
|
||||
{_t("create_room|join_rule_change_notice")}
|
||||
</p>
|
||||
);
|
||||
} else if (this.state.joinRule === JoinRule.Public && this.props.parentSpace) {
|
||||
publicPrivateLabel = (
|
||||
<p>
|
||||
{_t(
|
||||
"Anyone will be able to find and join this room, not just members of <SpaceName/>.",
|
||||
"create_room|join_rule_public_parent_space_label",
|
||||
{},
|
||||
{
|
||||
SpaceName: () => <b>{this.props.parentSpace?.name ?? _t("common|unnamed_space")}</b>,
|
||||
},
|
||||
)}
|
||||
|
||||
{_t("You can change this at any time from room settings.")}
|
||||
{_t("create_room|join_rule_change_notice")}
|
||||
</p>
|
||||
);
|
||||
} else if (this.state.joinRule === JoinRule.Public) {
|
||||
publicPrivateLabel = (
|
||||
<p>
|
||||
{_t("Anyone will be able to find and join this room.")}
|
||||
{_t("create_room|join_rule_public_label")}
|
||||
|
||||
{_t("You can change this at any time from room settings.")}
|
||||
{_t("create_room|join_rule_change_notice")}
|
||||
</p>
|
||||
);
|
||||
} else if (this.state.joinRule === JoinRule.Invite) {
|
||||
publicPrivateLabel = (
|
||||
<p>
|
||||
{_t("Only people invited will be able to find and join this room.")}
|
||||
{_t("create_room|join_rule_invite_label")}
|
||||
|
||||
{_t("You can change this at any time from room settings.")}
|
||||
{_t("create_room|join_rule_change_notice")}
|
||||
</p>
|
||||
);
|
||||
} else if (this.state.joinRule === JoinRule.Knock) {
|
||||
publicPrivateLabel = (
|
||||
<p>
|
||||
{_t(
|
||||
"Anyone can request to join, but admins or moderators need to grant access. You can change this later.",
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
publicPrivateLabel = <p>{_t("create_room|join_rule_knock_label")}</p>;
|
||||
}
|
||||
|
||||
let visibilitySection: JSX.Element | undefined;
|
||||
|
@ -353,10 +347,10 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||
if (privateShouldBeEncrypted(MatrixClientPeg.safeGet())) {
|
||||
if (this.state.canChangeEncryption) {
|
||||
microcopy = isVideoRoom
|
||||
? _t("You can't disable this later. The room will be encrypted but the embedded call will not.")
|
||||
: _t("You can't disable this later. Bridges & most bots won't work yet.");
|
||||
? _t("create_room|encrypted_video_room_warning")
|
||||
: _t("create_room|encrypted_warning");
|
||||
} else {
|
||||
microcopy = _t("Your server requires encryption to be enabled in private rooms.");
|
||||
microcopy = _t("create_room|encryption_forced");
|
||||
}
|
||||
} else {
|
||||
microcopy = _t(
|
||||
|
@ -366,7 +360,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||
e2eeSection = (
|
||||
<React.Fragment>
|
||||
<LabelledToggleSwitch
|
||||
label={_t("Enable end-to-end encryption")}
|
||||
label={_t("create_room|encryption_label")}
|
||||
onChange={this.onEncryptedChange}
|
||||
value={this.state.isEncrypted}
|
||||
className="mx_CreateRoomDialog_e2eSwitch" // for end-to-end tests
|
||||
|
@ -377,15 +371,11 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
let federateLabel = _t(
|
||||
"You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.",
|
||||
);
|
||||
let federateLabel = _t("create_room|unfederated_label_default_off");
|
||||
if (SdkConfig.get().default_federate === false) {
|
||||
// We only change the label if the default setting is different to avoid jarring text changes to the
|
||||
// user. They will have read the implications of turning this off/on, so no need to rephrase for them.
|
||||
federateLabel = _t(
|
||||
"You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.",
|
||||
);
|
||||
federateLabel = _t("create_room|unfederated_label_default_on");
|
||||
}
|
||||
|
||||
let title: string;
|
||||
|
@ -418,18 +408,20 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||
className="mx_CreateRoomDialog_name"
|
||||
/>
|
||||
<Field
|
||||
label={_t("Topic (optional)")}
|
||||
label={_t("create_room|topic_label")}
|
||||
onChange={this.onTopicChange}
|
||||
value={this.state.topic}
|
||||
className="mx_CreateRoomDialog_topic"
|
||||
/>
|
||||
|
||||
<JoinRuleDropdown
|
||||
label={_t("Room visibility")}
|
||||
labelInvite={_t("Private room (invite only)")}
|
||||
label={_t("create_room|room_visibility_label")}
|
||||
labelInvite={_t("create_room|join_rule_invite")}
|
||||
labelKnock={this.askToJoinEnabled ? _t("Ask to join") : undefined}
|
||||
labelPublic={_t("Public room")}
|
||||
labelRestricted={this.supportsRestricted ? _t("Visible to space members") : undefined}
|
||||
labelRestricted={
|
||||
this.supportsRestricted ? _t("create_room|join_rule_restricted") : undefined
|
||||
}
|
||||
value={this.state.joinRule}
|
||||
onChange={this.onJoinRuleChange}
|
||||
/>
|
||||
|
@ -443,7 +435,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||
{this.state.detailsOpen ? _t("Hide advanced") : _t("Show advanced")}
|
||||
</summary>
|
||||
<LabelledToggleSwitch
|
||||
label={_t("Block anyone not part of %(serverName)s from ever joining this room.", {
|
||||
label={_t("create_room|unfederated", {
|
||||
serverName: MatrixClientPeg.getHomeserverName(),
|
||||
})}
|
||||
onChange={this.onNoFederateChange}
|
||||
|
|
|
@ -164,7 +164,7 @@ const CreateSubspaceDialog: React.FC<IProps> = ({ space, onAddExistingSpaceClick
|
|||
label={_t("Space visibility")}
|
||||
labelInvite={_t("Private space (invite only)")}
|
||||
labelPublic={_t("Public space")}
|
||||
labelRestricted={_t("Visible to space members")}
|
||||
labelRestricted={_t("create_room|join_rule_restricted")}
|
||||
width={478}
|
||||
value={joinRule}
|
||||
onChange={setJoinRule}
|
||||
|
|
|
@ -56,7 +56,7 @@ const FeedbackDialog: React.FC<IProps> = (props: IProps) => {
|
|||
submitFeedback(label, comment, canContact);
|
||||
|
||||
Modal.createDialog(InfoDialog, {
|
||||
title: _t("Feedback sent"),
|
||||
title: _t("feedback|sent"),
|
||||
description: _t("Thank you!"),
|
||||
});
|
||||
}
|
||||
|
@ -67,13 +67,13 @@ const FeedbackDialog: React.FC<IProps> = (props: IProps) => {
|
|||
if (hasFeedback) {
|
||||
feedbackSection = (
|
||||
<div className="mx_FeedbackDialog_section mx_FeedbackDialog_rateApp">
|
||||
<h3>{_t("Comment")}</h3>
|
||||
<h3>{_t("feedback|comment_label")}</h3>
|
||||
|
||||
<p>{_t("Your platform and username will be noted to help us use your feedback as much as we can.")}</p>
|
||||
<p>{_t("feedback|platform_username")}</p>
|
||||
|
||||
<Field
|
||||
id="feedbackComment"
|
||||
label={_t("Feedback")}
|
||||
label={_t("common|feedback")}
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
value={comment}
|
||||
|
@ -85,7 +85,7 @@ const FeedbackDialog: React.FC<IProps> = (props: IProps) => {
|
|||
/>
|
||||
|
||||
<StyledCheckbox checked={canContact} onChange={toggleCanContact}>
|
||||
{_t("You may contact me if you want to follow up or to let me test out upcoming ideas")}
|
||||
{_t("feedback|may_contact_label")}
|
||||
</StyledCheckbox>
|
||||
</div>
|
||||
);
|
||||
|
@ -96,7 +96,7 @@ const FeedbackDialog: React.FC<IProps> = (props: IProps) => {
|
|||
bugReports = (
|
||||
<p className="mx_FeedbackDialog_section_microcopy">
|
||||
{_t(
|
||||
"PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.",
|
||||
"feedback|pro_type",
|
||||
{},
|
||||
{
|
||||
debugLogsLink: (sub) => (
|
||||
|
@ -117,14 +117,14 @@ const FeedbackDialog: React.FC<IProps> = (props: IProps) => {
|
|||
<QuestionDialog
|
||||
className="mx_FeedbackDialog"
|
||||
hasCancelButton={hasFeedback}
|
||||
title={_t("Feedback")}
|
||||
title={_t("common|feedback")}
|
||||
description={
|
||||
<React.Fragment>
|
||||
<div className="mx_FeedbackDialog_section mx_FeedbackDialog_reportBug">
|
||||
<h3>{_t("common|report_a_bug")}</h3>
|
||||
<p>
|
||||
{_t(
|
||||
"Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.",
|
||||
"feedback|existing_issue_link",
|
||||
{},
|
||||
{
|
||||
existingIssuesLink: (sub) => {
|
||||
|
@ -153,7 +153,7 @@ const FeedbackDialog: React.FC<IProps> = (props: IProps) => {
|
|||
{feedbackSection}
|
||||
</React.Fragment>
|
||||
}
|
||||
button={hasFeedback ? _t("Send feedback") : _t("action|go_back")}
|
||||
button={hasFeedback ? _t("feedback|send_feedback_action") : _t("action|go_back")}
|
||||
buttonDisabled={hasFeedback && !comment}
|
||||
onFinished={onFinished}
|
||||
/>
|
||||
|
|
|
@ -69,14 +69,14 @@ const GenericFeatureFeedbackDialog: React.FC<IProps> = ({
|
|||
<div className="mx_GenericFeatureFeedbackDialog_subheading">
|
||||
{subheading}
|
||||
|
||||
{_t("Your platform and username will be noted to help us use your feedback as much as we can.")}
|
||||
{_t("feedback|platform_username")}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<Field
|
||||
id="feedbackComment"
|
||||
label={_t("Feedback")}
|
||||
label={_t("common|feedback")}
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
value={comment}
|
||||
|
@ -95,7 +95,7 @@ const GenericFeatureFeedbackDialog: React.FC<IProps> = ({
|
|||
</StyledCheckbox>
|
||||
</React.Fragment>
|
||||
}
|
||||
button={_t("Send feedback")}
|
||||
button={_t("feedback|send_feedback_action")}
|
||||
buttonDisabled={!comment}
|
||||
onFinished={sendFeedback}
|
||||
/>
|
||||
|
|
|
@ -112,7 +112,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
|||
|
||||
const stateForError = AutoDiscoveryUtils.authComponentStateForError(e);
|
||||
if (stateForError.serverErrorIsFatal) {
|
||||
let error = _t("Unable to validate homeserver");
|
||||
let error = _t("auth|server_picker_failed_validate_homeserver");
|
||||
if (e instanceof UserFriendlyError && e.translatedMessage) {
|
||||
error = e.translatedMessage;
|
||||
}
|
||||
|
@ -129,7 +129,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
|||
return {};
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
return { error: _t("Invalid URL") };
|
||||
return { error: _t("auth|server_picker_invalid_url") };
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -137,7 +137,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
|||
{
|
||||
key: "required",
|
||||
test: ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||
invalid: () => _t("Specify a homeserver"),
|
||||
invalid: () => _t("auth|server_picker_required"),
|
||||
},
|
||||
{
|
||||
key: "valid",
|
||||
|
@ -176,7 +176,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
|||
public render(): React.ReactNode {
|
||||
let text: string | undefined;
|
||||
if (this.defaultServer.hsName === "matrix.org") {
|
||||
text = _t("Matrix.org is the biggest public homeserver in the world, so it's a good place for many.");
|
||||
text = _t("auth|server_picker_matrix.org");
|
||||
}
|
||||
|
||||
let defaultServerName: React.ReactNode = this.defaultServer.hsName;
|
||||
|
@ -190,7 +190,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
|||
|
||||
return (
|
||||
<BaseDialog
|
||||
title={this.props.title || _t("Sign into your homeserver")}
|
||||
title={this.props.title || _t("auth|server_picker_title")}
|
||||
className="mx_ServerPickerDialog"
|
||||
contentId="mx_ServerPickerDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
|
@ -199,7 +199,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
|||
>
|
||||
<form className="mx_Dialog_content" id="mx_ServerPickerDialog" onSubmit={this.onSubmit}>
|
||||
<p>
|
||||
{_t("We call the places where you can host your account 'homeservers'.")} {text}
|
||||
{_t("auth|server_picker_intro")} {text}
|
||||
</p>
|
||||
|
||||
<StyledRadioButton
|
||||
|
@ -219,12 +219,12 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
|||
checked={!this.state.defaultChosen}
|
||||
onChange={this.onOtherChosen}
|
||||
childrenInLabel={false}
|
||||
aria-label={_t("Other homeserver")}
|
||||
aria-label={_t("auth|server_picker_custom")}
|
||||
>
|
||||
<Field
|
||||
type="text"
|
||||
className="mx_ServerPickerDialog_otherHomeserver"
|
||||
label={_t("Other homeserver")}
|
||||
label={_t("auth|server_picker_custom")}
|
||||
onChange={this.onHomeserverChange}
|
||||
onFocus={this.onOtherChosen}
|
||||
ref={this.fieldRef}
|
||||
|
@ -236,7 +236,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
|||
id="mx_homeserverInput"
|
||||
/>
|
||||
</StyledRadioButton>
|
||||
<p>{_t("Use your preferred Matrix homeserver if you have one, or host your own.")}</p>
|
||||
<p>{_t("auth|server_picker_explainer")}</p>
|
||||
|
||||
<AccessibleButton className="mx_ServerPickerDialog_continue" kind="primary" onClick={this.onSubmit}>
|
||||
{_t("action|continue")}
|
||||
|
@ -248,7 +248,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
|||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{_t("About homeservers")}
|
||||
{_t("auth|server_picker_learn_more")}
|
||||
</ExternalLink>
|
||||
</form>
|
||||
</BaseDialog>
|
||||
|
|
|
@ -110,7 +110,7 @@ const EncryptionPanel: React.FC<IProps> = (props: IProps) => {
|
|||
if (!roomId) {
|
||||
throw new Error("Unable to create Room for verification");
|
||||
}
|
||||
verificationRequest_ = await cli.requestVerificationDM(member.userId, roomId);
|
||||
verificationRequest_ = await cli.getCrypto()!.requestVerificationDM(member.userId, roomId);
|
||||
} catch (e) {
|
||||
console.error("Error starting verification", e);
|
||||
setRequesting(false);
|
||||
|
|
|
@ -23,10 +23,9 @@ import React from "react";
|
|||
import dis from "../../../dispatcher/dispatcher";
|
||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||
import { IRightPanelCardState } from "../../../stores/right-panel/RightPanelStoreIPanelState";
|
||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
export enum HeaderKind {
|
||||
Room = "room",
|
||||
|
@ -37,6 +36,7 @@ interface IState {
|
|||
phase: RightPanelPhases | null;
|
||||
threadNotificationColor: NotificationColor;
|
||||
globalNotificationColor: NotificationColor;
|
||||
notificationsEnabled?: boolean;
|
||||
}
|
||||
|
||||
interface IProps {}
|
||||
|
@ -44,6 +44,7 @@ interface IProps {}
|
|||
export default abstract class HeaderButtons<P = {}> extends React.Component<IProps & P, IState> {
|
||||
private unmounted = false;
|
||||
private dispatcherRef?: string = undefined;
|
||||
private readonly watcherRef: string;
|
||||
|
||||
public constructor(props: IProps & P, kind: HeaderKind) {
|
||||
super(props);
|
||||
|
@ -54,30 +55,22 @@ export default abstract class HeaderButtons<P = {}> extends React.Component<IPro
|
|||
phase: rps.currentCard.phase,
|
||||
threadNotificationColor: NotificationColor.None,
|
||||
globalNotificationColor: NotificationColor.None,
|
||||
notificationsEnabled: SettingsStore.getValue("feature_notifications"),
|
||||
};
|
||||
this.watcherRef = SettingsStore.watchSetting("feature_notifications", null, (...[, , , value]) =>
|
||||
this.setState({ notificationsEnabled: value }),
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
|
||||
this.dispatcherRef = dis.register(this.onAction.bind(this)); // used by subclasses
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
this.unmounted = true;
|
||||
RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate);
|
||||
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
protected abstract onAction(payload: ActionPayload): void;
|
||||
|
||||
public setPhase(phase: RightPanelPhases, cardState?: Partial<IRightPanelCardState>): void {
|
||||
const rps = RightPanelStore.instance;
|
||||
if (rps.currentCard.phase == phase && !cardState && rps.isOpen) {
|
||||
rps.togglePanel(null);
|
||||
} else {
|
||||
RightPanelStore.instance.setCard({ phase, state: cardState });
|
||||
if (!rps.isOpen) rps.togglePanel(null);
|
||||
}
|
||||
if (this.watcherRef) SettingsStore.unwatchSetting(this.watcherRef);
|
||||
}
|
||||
|
||||
public isPhase(phases: string | string[]): boolean {
|
||||
|
|
|
@ -26,7 +26,6 @@ import { _t } from "../../../languageHandler";
|
|||
import HeaderButton from "./HeaderButton";
|
||||
import HeaderButtons, { HeaderKind } from "./HeaderButtons";
|
||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||
import { useReadPinnedEvents, usePinnedEvents } from "./PinnedMessagesCard";
|
||||
|
@ -203,59 +202,34 @@ export default class LegacyRoomHeaderButtons extends HeaderButtons<IProps> {
|
|||
});
|
||||
};
|
||||
|
||||
protected onAction(payload: ActionPayload): void {
|
||||
if (payload.action === Action.ViewUser) {
|
||||
if (payload.member) {
|
||||
if (payload.push) {
|
||||
RightPanelStore.instance.pushCard({
|
||||
phase: RightPanelPhases.RoomMemberInfo,
|
||||
state: { member: payload.member },
|
||||
});
|
||||
} else {
|
||||
RightPanelStore.instance.setCards([
|
||||
{ phase: RightPanelPhases.RoomSummary },
|
||||
{ phase: RightPanelPhases.RoomMemberList },
|
||||
{ phase: RightPanelPhases.RoomMemberInfo, state: { member: payload.member } },
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
this.setPhase(RightPanelPhases.RoomMemberList);
|
||||
}
|
||||
} else if (payload.action === "view_3pid_invite") {
|
||||
if (payload.event) {
|
||||
this.setPhase(RightPanelPhases.Room3pidMemberInfo, { memberInfoEvent: payload.event });
|
||||
} else {
|
||||
this.setPhase(RightPanelPhases.RoomMemberList);
|
||||
}
|
||||
}
|
||||
}
|
||||
protected onAction(payload: ActionPayload): void {}
|
||||
|
||||
private onRoomSummaryClicked = (): void => {
|
||||
// use roomPanelPhase rather than this.state.phase as it remembers the latest one if we close
|
||||
const currentPhase = RightPanelStore.instance.currentCard.phase;
|
||||
if (currentPhase && ROOM_INFO_PHASES.includes(currentPhase)) {
|
||||
if (this.state.phase === currentPhase) {
|
||||
this.setPhase(currentPhase);
|
||||
RightPanelStore.instance.showOrHidePanel(currentPhase);
|
||||
} else {
|
||||
this.setPhase(currentPhase, RightPanelStore.instance.currentCard.state);
|
||||
RightPanelStore.instance.showOrHidePanel(currentPhase, RightPanelStore.instance.currentCard.state);
|
||||
}
|
||||
} else {
|
||||
// This toggles for us, if needed
|
||||
this.setPhase(RightPanelPhases.RoomSummary);
|
||||
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.RoomSummary);
|
||||
}
|
||||
};
|
||||
|
||||
private onNotificationsClicked = (): void => {
|
||||
// This toggles for us, if needed
|
||||
this.setPhase(RightPanelPhases.NotificationPanel);
|
||||
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.NotificationPanel);
|
||||
};
|
||||
|
||||
private onPinnedMessagesClicked = (): void => {
|
||||
// This toggles for us, if needed
|
||||
this.setPhase(RightPanelPhases.PinnedMessages);
|
||||
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.PinnedMessages);
|
||||
};
|
||||
private onTimelineCardClicked = (): void => {
|
||||
this.setPhase(RightPanelPhases.Timeline);
|
||||
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.Timeline);
|
||||
};
|
||||
|
||||
private onThreadsPanelClicked = (ev: ButtonEvent): void => {
|
||||
|
@ -308,21 +282,23 @@ export default class LegacyRoomHeaderButtons extends HeaderButtons<IProps> {
|
|||
<UnreadIndicator color={this.state.threadNotificationColor} />
|
||||
</HeaderButton>,
|
||||
);
|
||||
rightPanelPhaseButtons.set(
|
||||
RightPanelPhases.NotificationPanel,
|
||||
<HeaderButton
|
||||
key="notifsButton"
|
||||
name="notifsButton"
|
||||
title={_t("Notifications")}
|
||||
isHighlighted={this.isPhase(RightPanelPhases.NotificationPanel)}
|
||||
onClick={this.onNotificationsClicked}
|
||||
isUnread={this.globalNotificationState.color === NotificationColor.Red}
|
||||
>
|
||||
{this.globalNotificationState.color === NotificationColor.Red ? (
|
||||
<UnreadIndicator color={this.globalNotificationState.color} />
|
||||
) : null}
|
||||
</HeaderButton>,
|
||||
);
|
||||
if (this.state.notificationsEnabled) {
|
||||
rightPanelPhaseButtons.set(
|
||||
RightPanelPhases.NotificationPanel,
|
||||
<HeaderButton
|
||||
key="notifsButton"
|
||||
name="notifsButton"
|
||||
title={_t("Notifications")}
|
||||
isHighlighted={this.isPhase(RightPanelPhases.NotificationPanel)}
|
||||
onClick={this.onNotificationsClicked}
|
||||
isUnread={this.globalNotificationState.color === NotificationColor.Red}
|
||||
>
|
||||
{this.globalNotificationState.color === NotificationColor.Red ? (
|
||||
<UnreadIndicator color={this.globalNotificationState.color} />
|
||||
) : null}
|
||||
</HeaderButton>,
|
||||
);
|
||||
}
|
||||
rightPanelPhaseButtons.set(
|
||||
RightPanelPhases.RoomSummary,
|
||||
<HeaderButton
|
||||
|
|
|
@ -105,9 +105,15 @@ export const disambiguateDevices = (devices: IDevice[]): void => {
|
|||
}
|
||||
};
|
||||
|
||||
export const getE2EStatus = async (cli: MatrixClient, userId: string, devices: IDevice[]): Promise<E2EStatus> => {
|
||||
export const getE2EStatus = async (
|
||||
cli: MatrixClient,
|
||||
userId: string,
|
||||
devices: IDevice[],
|
||||
): Promise<E2EStatus | undefined> => {
|
||||
const crypto = cli.getCrypto();
|
||||
if (!crypto) return undefined;
|
||||
const isMe = userId === cli.getUserId();
|
||||
const userTrust = cli.checkUserTrust(userId);
|
||||
const userTrust = await crypto.getUserVerificationStatus(userId);
|
||||
if (!userTrust.isCrossSigningVerified()) {
|
||||
return userTrust.wasCrossSigningVerified() ? E2EStatus.Warning : E2EStatus.Normal;
|
||||
}
|
||||
|
@ -119,7 +125,7 @@ export const getE2EStatus = async (cli: MatrixClient, userId: string, devices: I
|
|||
// cross-signing so that other users can then safely trust you.
|
||||
// For other people's devices, the more general verified check that
|
||||
// includes locally verified devices can be used.
|
||||
const deviceTrust = await cli.getCrypto()?.getDeviceVerificationStatus(userId, deviceId);
|
||||
const deviceTrust = await crypto.getDeviceVerificationStatus(userId, deviceId);
|
||||
return isMe ? !deviceTrust?.crossSigningVerified : !deviceTrust?.isVerified();
|
||||
});
|
||||
return anyDeviceUnverified ? E2EStatus.Warning : E2EStatus.Verified;
|
||||
|
@ -152,11 +158,7 @@ function useHasCrossSigningKeys(
|
|||
}
|
||||
setUpdating(true);
|
||||
try {
|
||||
// We call it to populate the user keys and devices
|
||||
await cli.getCrypto()?.getUserDeviceInfo([member.userId], true);
|
||||
const xsi = cli.getStoredCrossSigningForUser(member.userId);
|
||||
const key = xsi && xsi.getId();
|
||||
return !!key;
|
||||
return await cli.getCrypto()?.userHasCrossSigningKeys(member.userId, true);
|
||||
} finally {
|
||||
setUpdating(false);
|
||||
}
|
||||
|
|
|
@ -282,9 +282,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
};
|
||||
|
||||
private showPlaceholder(): void {
|
||||
// escape single quotes
|
||||
const placeholder = this.props.placeholder?.replace(/'/g, "\\'");
|
||||
this.editorRef.current?.style.setProperty("--placeholder", `'${placeholder}'`);
|
||||
this.editorRef.current?.style.setProperty("--placeholder", `'${CSS.escape(this.props.placeholder ?? "")}'`);
|
||||
this.editorRef.current?.classList.add("mx_BasicMessageComposer_inputEmpty");
|
||||
}
|
||||
|
||||
|
|
|
@ -18,17 +18,17 @@ limitations under the License.
|
|||
import React, { createRef, forwardRef, MouseEvent, ReactNode, useRef } from "react";
|
||||
import classNames from "classnames";
|
||||
import {
|
||||
EventType,
|
||||
MsgType,
|
||||
RelationType,
|
||||
EventStatus,
|
||||
EventType,
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
RoomMember,
|
||||
MsgType,
|
||||
NotificationCountType,
|
||||
Relations,
|
||||
RelationType,
|
||||
Room,
|
||||
RoomEvent,
|
||||
Relations,
|
||||
RoomMember,
|
||||
Thread,
|
||||
ThreadEvent,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
|
@ -36,6 +36,7 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||
import { CallErrorCode } from "matrix-js-sdk/src/webrtc/call";
|
||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
||||
import { UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning";
|
||||
import { EventShieldColour, EventShieldReason } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import ReplyChain from "../elements/ReplyChain";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
@ -44,7 +45,6 @@ import { Layout } from "../../../settings/enums/Layout";
|
|||
import { formatTime } from "../../../DateUtils";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { DecryptionFailureBody } from "../messages/DecryptionFailureBody";
|
||||
import { E2EState } from "./E2EIcon";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import MessageContextMenu from "../context_menus/MessageContextMenu";
|
||||
import { aboveRightOf } from "../../structures/ContextMenu";
|
||||
|
@ -236,8 +236,19 @@ export interface EventTileProps {
|
|||
interface IState {
|
||||
// Whether the action bar is focused.
|
||||
actionBarFocused: boolean;
|
||||
// Whether the event's sender has been verified.
|
||||
verified: string | null;
|
||||
|
||||
/**
|
||||
* E2EE shield we should show for decryption problems.
|
||||
*
|
||||
* Note this will be `EventShieldColour.NONE` for all unencrypted events, **including those in encrypted rooms**.
|
||||
*/
|
||||
shieldColour: EventShieldColour;
|
||||
|
||||
/**
|
||||
* Reason code for the E2EE shield. `null` if `shieldColour` is `EventShieldColour.NONE`
|
||||
*/
|
||||
shieldReason: EventShieldReason | null;
|
||||
|
||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||
reactions?: Relations | null | undefined;
|
||||
|
||||
|
@ -299,9 +310,10 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||
this.state = {
|
||||
// Whether the action bar is focused.
|
||||
actionBarFocused: false,
|
||||
// Whether the event's sender has been verified. `null` if no attempt has yet been made to verify
|
||||
// (including if the event is not encrypted).
|
||||
verified: null,
|
||||
|
||||
shieldColour: EventShieldColour.NONE,
|
||||
shieldReason: null,
|
||||
|
||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||
reactions: this.getReactions(),
|
||||
|
||||
|
@ -437,8 +449,9 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Readonly<EventTileProps>, prevState: Readonly<IState>): void {
|
||||
// If the verification state changed, the height might have changed
|
||||
if (prevState.verified !== this.state.verified && this.props.onHeightChanged) {
|
||||
// If the shield state changed, the height might have changed.
|
||||
// XXX: does the shield *actually* cause a change in height? Not sure.
|
||||
if (prevState.shieldColour !== this.state.shieldColour && this.props.onHeightChanged) {
|
||||
this.props.onHeightChanged();
|
||||
}
|
||||
// If we're not listening for receipts and expect to be, register a listener.
|
||||
|
@ -576,65 +589,33 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||
this.verifyEvent();
|
||||
};
|
||||
|
||||
private async verifyEvent(): Promise<void> {
|
||||
private verifyEvent(): void {
|
||||
this.doVerifyEvent().catch((e) => {
|
||||
const event = this.props.mxEvent;
|
||||
logger.error("Error getting encryption info on event", e, event);
|
||||
});
|
||||
}
|
||||
|
||||
private async doVerifyEvent(): Promise<void> {
|
||||
// if the event was edited, show the verification info for the edit, not
|
||||
// the original
|
||||
const mxEvent = this.props.mxEvent.replacingEvent() ?? this.props.mxEvent;
|
||||
|
||||
if (!mxEvent.isEncrypted() || mxEvent.isRedacted()) {
|
||||
this.setState({ verified: null });
|
||||
this.setState({ shieldColour: EventShieldColour.NONE, shieldReason: null });
|
||||
return;
|
||||
}
|
||||
|
||||
const encryptionInfo = MatrixClientPeg.safeGet().getEventEncryptionInfo(mxEvent);
|
||||
const senderId = mxEvent.getSender();
|
||||
if (!senderId) {
|
||||
// something definitely wrong is going on here
|
||||
this.setState({ verified: E2EState.Warning });
|
||||
return;
|
||||
}
|
||||
|
||||
const userTrust = MatrixClientPeg.safeGet().checkUserTrust(senderId);
|
||||
|
||||
if (encryptionInfo.mismatchedSender) {
|
||||
// something definitely wrong is going on here
|
||||
this.setState({ verified: E2EState.Warning });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userTrust.isCrossSigningVerified()) {
|
||||
// If the message is unauthenticated, then display a grey
|
||||
// shield, otherwise if the user isn't cross-signed then
|
||||
// nothing's needed
|
||||
this.setState({ verified: encryptionInfo.authenticated ? E2EState.Normal : E2EState.Unauthenticated });
|
||||
return;
|
||||
}
|
||||
|
||||
const eventSenderTrust =
|
||||
senderId &&
|
||||
encryptionInfo.sender &&
|
||||
(await MatrixClientPeg.safeGet()
|
||||
.getCrypto()
|
||||
?.getDeviceVerificationStatus(senderId, encryptionInfo.sender.deviceId));
|
||||
|
||||
const encryptionInfo =
|
||||
(await MatrixClientPeg.safeGet().getCrypto()?.getEncryptionInfoForEvent(mxEvent)) ?? null;
|
||||
if (this.unmounted) return;
|
||||
|
||||
if (!eventSenderTrust) {
|
||||
this.setState({ verified: E2EState.Unknown });
|
||||
if (encryptionInfo === null) {
|
||||
// likely a decryption error
|
||||
this.setState({ shieldColour: EventShieldColour.NONE, shieldReason: null });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!eventSenderTrust.isVerified()) {
|
||||
this.setState({ verified: E2EState.Warning });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!encryptionInfo.authenticated) {
|
||||
this.setState({ verified: E2EState.Unauthenticated });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ verified: E2EState.Verified });
|
||||
this.setState({ shieldColour: encryptionInfo.shieldColour, shieldReason: encryptionInfo.shieldReason });
|
||||
}
|
||||
|
||||
private propsEqual(objA: EventTileProps, objB: EventTileProps): boolean {
|
||||
|
@ -751,18 +732,42 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||
return <E2ePadlockDecryptionFailure />;
|
||||
}
|
||||
|
||||
// event is encrypted and not redacted, display padlock corresponding to whether or not it is verified
|
||||
if (ev.isEncrypted() && !ev.isRedacted()) {
|
||||
if (this.state.verified === E2EState.Normal) {
|
||||
return null; // no icon if we've not even cross-signed the user
|
||||
} else if (this.state.verified === E2EState.Verified) {
|
||||
return null; // no icon for verified
|
||||
} else if (this.state.verified === E2EState.Unauthenticated) {
|
||||
return <E2ePadlockUnauthenticated />;
|
||||
} else if (this.state.verified === E2EState.Unknown) {
|
||||
return <E2ePadlockUnknown />;
|
||||
if (this.state.shieldColour !== EventShieldColour.NONE) {
|
||||
let shieldReasonMessage: string;
|
||||
switch (this.state.shieldReason) {
|
||||
case null:
|
||||
case EventShieldReason.UNKNOWN:
|
||||
shieldReasonMessage = _t("Unknown error");
|
||||
break;
|
||||
|
||||
case EventShieldReason.UNVERIFIED_IDENTITY:
|
||||
shieldReasonMessage = _t("Encrypted by an unverified user.");
|
||||
break;
|
||||
|
||||
case EventShieldReason.UNSIGNED_DEVICE:
|
||||
shieldReasonMessage = _t("Encrypted by a device not verified by its owner.");
|
||||
break;
|
||||
|
||||
case EventShieldReason.UNKNOWN_DEVICE:
|
||||
shieldReasonMessage = _t("Encrypted by an unknown or deleted device.");
|
||||
break;
|
||||
|
||||
case EventShieldReason.AUTHENTICITY_NOT_GUARANTEED:
|
||||
shieldReasonMessage = _t(
|
||||
"The authenticity of this encrypted message can't be guaranteed on this device.",
|
||||
);
|
||||
break;
|
||||
|
||||
case EventShieldReason.MISMATCHED_SENDER_KEY:
|
||||
shieldReasonMessage = _t("Encrypted by an unverified session");
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.state.shieldColour === EventShieldColour.GREY) {
|
||||
return <E2ePadlock icon={E2ePadlockIcon.Normal} title={shieldReasonMessage} />;
|
||||
} else {
|
||||
return <E2ePadlockUnverified />;
|
||||
// red, by elimination
|
||||
return <E2ePadlock icon={E2ePadlockIcon.Warning} title={shieldReasonMessage} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -781,8 +786,10 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||
if (ev.isRedacted()) {
|
||||
return null; // we expect this to be unencrypted
|
||||
}
|
||||
// if the event is not encrypted, but it's an e2e room, show the open padlock
|
||||
return <E2ePadlockUnencrypted />;
|
||||
if (!ev.isEncrypted()) {
|
||||
// if the event is not encrypted, but it's an e2e room, show a warning
|
||||
return <E2ePadlockUnencrypted />;
|
||||
}
|
||||
}
|
||||
|
||||
// no padlock needed
|
||||
|
@ -1460,28 +1467,10 @@ const SafeEventTile = forwardRef<UnwrappedEventTile, EventTileProps>((props, ref
|
|||
});
|
||||
export default SafeEventTile;
|
||||
|
||||
function E2ePadlockUnverified(props: Omit<IE2ePadlockProps, "title" | "icon">): JSX.Element {
|
||||
return <E2ePadlock title={_t("Encrypted by an unverified session")} icon={E2ePadlockIcon.Warning} {...props} />;
|
||||
}
|
||||
|
||||
function E2ePadlockUnencrypted(props: Omit<IE2ePadlockProps, "title" | "icon">): JSX.Element {
|
||||
return <E2ePadlock title={_t("Unencrypted")} icon={E2ePadlockIcon.Warning} {...props} />;
|
||||
}
|
||||
|
||||
function E2ePadlockUnknown(props: Omit<IE2ePadlockProps, "title" | "icon">): JSX.Element {
|
||||
return <E2ePadlock title={_t("Encrypted by a deleted session")} icon={E2ePadlockIcon.Normal} {...props} />;
|
||||
}
|
||||
|
||||
function E2ePadlockUnauthenticated(props: Omit<IE2ePadlockProps, "title" | "icon">): JSX.Element {
|
||||
return (
|
||||
<E2ePadlock
|
||||
title={_t("The authenticity of this encrypted message can't be guaranteed on this device.")}
|
||||
icon={E2ePadlockIcon.Normal}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function E2ePadlockDecryptionFailure(props: Omit<IE2ePadlockProps, "title" | "icon">): JSX.Element {
|
||||
return (
|
||||
<E2ePadlock
|
||||
|
@ -1493,8 +1482,13 @@ function E2ePadlockDecryptionFailure(props: Omit<IE2ePadlockProps, "title" | "ic
|
|||
}
|
||||
|
||||
enum E2ePadlockIcon {
|
||||
/** grey shield */
|
||||
Normal = "normal",
|
||||
|
||||
/** red shield with (!) */
|
||||
Warning = "warning",
|
||||
|
||||
/** key in grey circle */
|
||||
DecryptionFailure = "decryption_failure",
|
||||
}
|
||||
|
||||
|
|
|
@ -30,15 +30,19 @@ const HistoryTile: React.FC = () => {
|
|||
|
||||
let subtitle: string | undefined;
|
||||
if (historyState == "invited") {
|
||||
subtitle = _t("You don't have permission to view messages from before you were invited.");
|
||||
subtitle = _t("timeline|no_permission_messages_before_invite");
|
||||
} else if (historyState == "joined") {
|
||||
subtitle = _t("You don't have permission to view messages from before you joined.");
|
||||
subtitle = _t("timeline|no_permission_messages_before_join");
|
||||
} else if (encryptionState) {
|
||||
subtitle = _t("Encrypted messages before this point are unavailable.");
|
||||
subtitle = _t("timeline|encrypted_historical_messages_unavailable");
|
||||
}
|
||||
|
||||
return (
|
||||
<EventTileBubble className="mx_HistoryTile" title={_t("You can't see earlier messages")} subtitle={subtitle} />
|
||||
<EventTileBubble
|
||||
className="mx_HistoryTile"
|
||||
title={_t("timeline|historical_messages_unavailable")}
|
||||
subtitle={subtitle}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -27,7 +27,6 @@ import { EventType, JoinRule, type Room } from "matrix-js-sdk/src/matrix";
|
|||
|
||||
import { useRoomName } from "../../../hooks/useRoomName";
|
||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||
import { useTopic } from "../../../hooks/room/useTopic";
|
||||
import { useAccountData } from "../../../hooks/useAccountData";
|
||||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
||||
|
@ -47,6 +46,7 @@ import FacePile from "../elements/FacePile";
|
|||
import { useRoomState } from "../../../hooks/useRoomState";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import { formatCount } from "../../../utils/FormattingUtils";
|
||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||
|
||||
/**
|
||||
* A helper to transform a notification color to the what the Compound Icon Button
|
||||
|
@ -62,16 +62,6 @@ function notificationColorToIndicator(color: NotificationColor): React.Component
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper to show or hide the right panel
|
||||
*/
|
||||
function showOrHidePanel(phase: RightPanelPhases): void {
|
||||
const rightPanel = RightPanelStore.instance;
|
||||
rightPanel.isOpen && rightPanel.currentCard.phase === phase
|
||||
? rightPanel.togglePanel(null)
|
||||
: rightPanel.setCard({ phase });
|
||||
}
|
||||
|
||||
export default function RoomHeader({ room }: { room: Room }): JSX.Element {
|
||||
const client = useMatrixClientContext();
|
||||
|
||||
|
@ -108,6 +98,8 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
|
|||
}, [room, directRoomsList]);
|
||||
const e2eStatus = useEncryptionStatus(client, room);
|
||||
|
||||
const notificationsEnabled = useFeatureEnabled("feature_notifications");
|
||||
|
||||
return (
|
||||
<Flex
|
||||
as="header"
|
||||
|
@ -115,7 +107,7 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
|
|||
gap="var(--cpd-space-3x)"
|
||||
className="mx_RoomHeader light-panel"
|
||||
onClick={() => {
|
||||
showOrHidePanel(RightPanelPhases.RoomSummary);
|
||||
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.RoomSummary);
|
||||
}}
|
||||
>
|
||||
<RoomAvatar room={room} size="40px" />
|
||||
|
@ -197,25 +189,27 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
|
|||
indicator={notificationColorToIndicator(threadNotifications)}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
showOrHidePanel(RightPanelPhases.ThreadPanel);
|
||||
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.ThreadPanel);
|
||||
}}
|
||||
aria-label={_t("common|threads")}
|
||||
>
|
||||
<ThreadsIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip label={_t("Notifications")}>
|
||||
<IconButton
|
||||
indicator={notificationColorToIndicator(globalNotificationState.color)}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
showOrHidePanel(RightPanelPhases.NotificationPanel);
|
||||
}}
|
||||
aria-label={_t("Notifications")}
|
||||
>
|
||||
<NotificationsIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{notificationsEnabled && (
|
||||
<Tooltip label={_t("Notifications")}>
|
||||
<IconButton
|
||||
indicator={notificationColorToIndicator(globalNotificationState.color)}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.NotificationPanel);
|
||||
}}
|
||||
aria-label={_t("Notifications")}
|
||||
>
|
||||
<NotificationsIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Flex>
|
||||
{!isDirectMessage && (
|
||||
<BodyText
|
||||
|
@ -224,7 +218,7 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
|
|||
weight="medium"
|
||||
aria-label={_t("%(count)s members", { count: memberCount })}
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
showOrHidePanel(RightPanelPhases.RoomMemberList);
|
||||
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.RoomMemberList);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -90,10 +90,9 @@ export function attachMentions(
|
|||
replyToEvent: MatrixEvent | undefined,
|
||||
editedContent: IContent | null = null,
|
||||
): void {
|
||||
// If this feature is disabled, do nothing.
|
||||
if (!SettingsStore.getValue("feature_intentional_mentions")) {
|
||||
return;
|
||||
}
|
||||
// We always attach the mentions even if the home server doesn't yet support
|
||||
// intentional mentions. This is safe because m.mentions is an additive change
|
||||
// that should simply be ignored by incapable home servers.
|
||||
|
||||
// The mentions property *always* gets included to disable legacy push rules.
|
||||
const mentions: IMentions = (content["m.mentions"] = {});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue