Phase 1, split out UserInfo into a generic Pane, use for EncInfo
parent
0078c2f099
commit
210616c737
|
@ -49,12 +49,17 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_UserInfo_container {
|
.mx_UserInfo_container {
|
||||||
padding: 0 16px 16px 16px;
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_UserInfo_separator {
|
||||||
border-bottom: 1px solid lightgray;
|
border-bottom: 1px solid lightgray;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_UserInfo_memberDetailsContainer {
|
.mx_UserInfo_memberDetailsContainer {
|
||||||
|
padding-top: 0;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomTile_nameContainer {
|
.mx_RoomTile_nameContainer {
|
||||||
|
@ -204,10 +209,9 @@ limitations under the License.
|
||||||
padding-bottom: 16px;
|
padding-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_UserInfo_scrollContainer .mx_UserInfo_container {
|
.mx_UserInfo_scrollContainer:not(.mx_UserInfo_separator) {
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
border-bottom: none;
|
|
||||||
|
|
||||||
> :not(h3) {
|
> :not(h3) {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
|
@ -264,3 +268,10 @@ limitations under the License.
|
||||||
margin: 16px 0;
|
margin: 16px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_UserInfo.mx_UserInfo_smallAvatar {
|
||||||
|
.mx_UserInfo_avatar > div {
|
||||||
|
max-width: 72px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -238,7 +238,18 @@ export default class RightPanel extends React.Component {
|
||||||
} else if (this.state.phase === RIGHT_PANEL_PHASES.FilePanel) {
|
} else if (this.state.phase === RIGHT_PANEL_PHASES.FilePanel) {
|
||||||
panel = <FilePanel roomId={this.props.roomId} resizeNotifier={this.props.resizeNotifier} />;
|
panel = <FilePanel roomId={this.props.roomId} resizeNotifier={this.props.resizeNotifier} />;
|
||||||
} else if (this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel) {
|
} else if (this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel) {
|
||||||
panel = <EncryptionPanel member={this.state.member} verificationRequest={this.state.verificationRequest} />;
|
const onClose = () => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: "view_user",
|
||||||
|
member: this.state.member,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
panel = (
|
||||||
|
<EncryptionPanel
|
||||||
|
member={this.state.member}
|
||||||
|
verificationRequest={this.state.verificationRequest}
|
||||||
|
onClose={onClose} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const classes = classNames("mx_RightPanel", "mx_fadable", {
|
const classes = classNames("mx_RightPanel", "mx_fadable", {
|
||||||
|
|
|
@ -21,11 +21,17 @@ import {_t} from "../../../languageHandler";
|
||||||
export default class EncryptionInfo extends React.PureComponent {
|
export default class EncryptionInfo extends React.PureComponent {
|
||||||
render() {
|
render() {
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
return (<div className="mx_UserInfo"><div className="mx_UserInfo_container">
|
return (
|
||||||
<h3>{_t("Verify User")}</h3>
|
<div className="mx_UserInfo_container">
|
||||||
<p>{_t("For extra security, verify this user by checking a one-time code on both of your devices.")}</p>
|
<h3>{_t("Verify User")}</h3>
|
||||||
<p>{_t("For maximum security, do this in person.")}</p>
|
<div>
|
||||||
<AccessibleButton kind="primary" onClick={this.props.onStartVerification}>{_t("Start Verification")}</AccessibleButton>
|
<p>{_t("For extra security, verify this user by checking a one-time code on both of your devices.")}</p>
|
||||||
</div></div>);
|
<p>{_t("For maximum security, do this in person.")}</p>
|
||||||
|
<AccessibleButton kind="primary" className="mx_UserInfo_verify" onClick={this.props.onStartVerification}>
|
||||||
|
{_t("Start Verification")}
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -19,6 +19,8 @@ import EncryptionInfo from "./EncryptionInfo";
|
||||||
import VerificationPanel from "./VerificationPanel";
|
import VerificationPanel from "./VerificationPanel";
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||||
import {ensureDMExists} from "../../../createRoom";
|
import {ensureDMExists} from "../../../createRoom";
|
||||||
|
import {UserInfoPane} from "./UserInfo";
|
||||||
|
import {_t} from "../../../languageHandler";
|
||||||
|
|
||||||
export default class EncryptionPanel extends React.PureComponent {
|
export default class EncryptionPanel extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -27,15 +29,30 @@ export default class EncryptionPanel extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
let content;
|
||||||
const request = this.props.verificationRequest || this.state.verificationRequest;
|
const request = this.props.verificationRequest || this.state.verificationRequest;
|
||||||
const {member} = this.props;
|
const {member} = this.props;
|
||||||
if (request) {
|
if (request) {
|
||||||
return <VerificationPanel request={request} key={request.channel.transactionId} />;
|
content = <VerificationPanel request={request} key={request.channel.transactionId} />;
|
||||||
} else if (member) {
|
} else if (member) {
|
||||||
return <EncryptionInfo onStartVerification={this._onStartVerification} member={member} />;
|
content = <EncryptionInfo onStartVerification={this._onStartVerification} member={member} />;
|
||||||
} else {
|
} else {
|
||||||
return <p>Not a member nor request, not sure what to render</p>;
|
content = <p>Not a member nor request, not sure what to render</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UserInfoPane className="mx_UserInfo_smallAvatar" member={member} onClose={this.props.onClose} e2eStatus="">
|
||||||
|
<div className="mx_UserInfo_container">
|
||||||
|
<h3>{_t("Encryption")}</h3>
|
||||||
|
<div>
|
||||||
|
<p>{_t("Messages in this room are end-to-end encrypted.")}</p>
|
||||||
|
<p>{_t("Your messages are secured and only you and the recipient have the unique keys to unlock them.")}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ content }
|
||||||
|
</UserInfoPane>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onStartVerification = async () => {
|
_onStartVerification = async () => {
|
||||||
|
|
|
@ -59,7 +59,7 @@ const _disambiguateDevices = (devices) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const _getE2EStatus = (cli, userId, devices) => {
|
export const getE2EStatus = (cli, userId, devices) => {
|
||||||
if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||||
const hasUnverifiedDevice = devices.some((device) => device.isUnverified());
|
const hasUnverifiedDevice = devices.some((device) => device.isUnverified());
|
||||||
return hasUnverifiedDevice ? "warning" : "verified";
|
return hasUnverifiedDevice ? "warning" : "verified";
|
||||||
|
@ -1047,6 +1047,117 @@ const PowerLevelEditor = ({user, room, roomPermissions, onFinished}) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const UserInfoPane = ({children, className, onClose, e2eStatus, member}) => {
|
||||||
|
const cli = useContext(MatrixClientContext);
|
||||||
|
|
||||||
|
let closeButton;
|
||||||
|
if (onClose) {
|
||||||
|
closeButton = <AccessibleButton className="mx_UserInfo_cancel" onClick={onClose} title={_t('Close')}>
|
||||||
|
<div />
|
||||||
|
</AccessibleButton>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let presenceState;
|
||||||
|
let presenceLastActiveAgo;
|
||||||
|
let presenceCurrentlyActive;
|
||||||
|
let statusMessage;
|
||||||
|
|
||||||
|
if (member instanceof RoomMember && member.user) {
|
||||||
|
presenceState = member.user.presence;
|
||||||
|
presenceLastActiveAgo = member.user.lastActiveAgo;
|
||||||
|
presenceCurrentlyActive = member.user.currentlyActive;
|
||||||
|
|
||||||
|
if (SettingsStore.isFeatureEnabled("feature_custom_status")) {
|
||||||
|
statusMessage = member.user._unstable_statusMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"];
|
||||||
|
let showPresence = true;
|
||||||
|
if (enablePresenceByHsUrl && enablePresenceByHsUrl[cli.baseUrl] !== undefined) {
|
||||||
|
showPresence = enablePresenceByHsUrl[cli.baseUrl];
|
||||||
|
}
|
||||||
|
|
||||||
|
let presenceLabel = null;
|
||||||
|
if (showPresence) {
|
||||||
|
const PresenceLabel = sdk.getComponent('rooms.PresenceLabel');
|
||||||
|
presenceLabel = <PresenceLabel activeAgo={presenceLastActiveAgo}
|
||||||
|
currentlyActive={presenceCurrentlyActive}
|
||||||
|
presenceState={presenceState} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
let statusLabel = null;
|
||||||
|
if (statusMessage) {
|
||||||
|
statusLabel = <span className="mx_UserInfo_statusMessage">{ statusMessage }</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMemberAvatarClick = useCallback(() => {
|
||||||
|
const avatarUrl = member.getMxcAvatarUrl ? member.getMxcAvatarUrl() : member.avatarUrl;
|
||||||
|
if (!avatarUrl) return;
|
||||||
|
|
||||||
|
const httpUrl = cli.mxcUrlToHttp(avatarUrl);
|
||||||
|
const ImageView = sdk.getComponent("elements.ImageView");
|
||||||
|
const params = {
|
||||||
|
src: httpUrl,
|
||||||
|
name: member.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
||||||
|
}, [cli, member]);
|
||||||
|
|
||||||
|
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||||
|
const avatarElement = (
|
||||||
|
<div className="mx_UserInfo_avatar">
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<MemberAvatar
|
||||||
|
member={member}
|
||||||
|
width={2 * 0.3 * window.innerHeight} // 2x@30vh
|
||||||
|
height={2 * 0.3 * window.innerHeight} // 2x@30vh
|
||||||
|
resizeMethod="scale"
|
||||||
|
fallbackUserId={member.userId}
|
||||||
|
onClick={onMemberAvatarClick}
|
||||||
|
urls={member.avatarUrl ? [member.avatarUrl] : undefined} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
let e2eIcon;
|
||||||
|
if (e2eStatus) {
|
||||||
|
e2eIcon = <E2EIcon size={18} status={e2eStatus} isUser={true} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayName = member.name || member.displayname;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames("mx_UserInfo", className)} role="tabpanel">
|
||||||
|
<AutoHideScrollbar className="mx_UserInfo_scrollContainer">
|
||||||
|
{ closeButton }
|
||||||
|
{ avatarElement }
|
||||||
|
|
||||||
|
<div className="mx_UserInfo_container mx_UserInfo_separator">
|
||||||
|
<div className="mx_UserInfo_profile">
|
||||||
|
<div>
|
||||||
|
<h2 aria-label={displayName}>
|
||||||
|
{ e2eIcon }
|
||||||
|
{ displayName }
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div>{ member.userId }</div>
|
||||||
|
<div className="mx_UserInfo_profileStatus">
|
||||||
|
{presenceLabel}
|
||||||
|
{statusLabel}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ children }
|
||||||
|
</AutoHideScrollbar>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const UserInfo = ({user, groupId, roomId, onClose}) => {
|
const UserInfo = ({user, groupId, roomId, onClose}) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
|
|
||||||
|
@ -1117,20 +1228,6 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
|
||||||
}
|
}
|
||||||
}, [cli, user.userId]);
|
}, [cli, user.userId]);
|
||||||
|
|
||||||
const onMemberAvatarClick = useCallback(() => {
|
|
||||||
const avatarUrl = member.getMxcAvatarUrl ? member.getMxcAvatarUrl() : member.avatarUrl;
|
|
||||||
if (!avatarUrl) return;
|
|
||||||
|
|
||||||
const httpUrl = cli.mxcUrlToHttp(avatarUrl);
|
|
||||||
const ImageView = sdk.getComponent("elements.ImageView");
|
|
||||||
const params = {
|
|
||||||
src: httpUrl,
|
|
||||||
name: member.name,
|
|
||||||
};
|
|
||||||
|
|
||||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
|
||||||
}, [cli, member]);
|
|
||||||
|
|
||||||
let synapseDeactivateButton;
|
let synapseDeactivateButton;
|
||||||
let spinner;
|
let spinner;
|
||||||
|
|
||||||
|
@ -1180,68 +1277,6 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
|
||||||
spinner = <Loader imgClassName="mx_ContextualMenu_spinner" />;
|
spinner = <Loader imgClassName="mx_ContextualMenu_spinner" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayName = member.name || member.displayname;
|
|
||||||
|
|
||||||
let presenceState;
|
|
||||||
let presenceLastActiveAgo;
|
|
||||||
let presenceCurrentlyActive;
|
|
||||||
let statusMessage;
|
|
||||||
|
|
||||||
if (member instanceof RoomMember && member.user) {
|
|
||||||
presenceState = member.user.presence;
|
|
||||||
presenceLastActiveAgo = member.user.lastActiveAgo;
|
|
||||||
presenceCurrentlyActive = member.user.currentlyActive;
|
|
||||||
|
|
||||||
if (SettingsStore.isFeatureEnabled("feature_custom_status")) {
|
|
||||||
statusMessage = member.user._unstable_statusMessage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"];
|
|
||||||
let showPresence = true;
|
|
||||||
if (enablePresenceByHsUrl && enablePresenceByHsUrl[cli.baseUrl] !== undefined) {
|
|
||||||
showPresence = enablePresenceByHsUrl[cli.baseUrl];
|
|
||||||
}
|
|
||||||
|
|
||||||
let presenceLabel = null;
|
|
||||||
if (showPresence) {
|
|
||||||
const PresenceLabel = sdk.getComponent('rooms.PresenceLabel');
|
|
||||||
presenceLabel = <PresenceLabel activeAgo={presenceLastActiveAgo}
|
|
||||||
currentlyActive={presenceCurrentlyActive}
|
|
||||||
presenceState={presenceState} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
let statusLabel = null;
|
|
||||||
if (statusMessage) {
|
|
||||||
statusLabel = <span className="mx_UserInfo_statusMessage">{ statusMessage }</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// const avatarUrl = user.getMxcAvatarUrl ? user.getMxcAvatarUrl() : user.avatarUrl;
|
|
||||||
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
|
||||||
const avatarElement = (
|
|
||||||
<div className="mx_UserInfo_avatar">
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<MemberAvatar
|
|
||||||
member={member}
|
|
||||||
width={2 * 0.3 * window.innerHeight} // 2x@30vh
|
|
||||||
height={2 * 0.3 * window.innerHeight} // 2x@30vh
|
|
||||||
resizeMethod="scale"
|
|
||||||
fallbackUserId={member.userId}
|
|
||||||
onClick={onMemberAvatarClick}
|
|
||||||
urls={member.avatarUrl ? [member.avatarUrl] : undefined} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
let closeButton;
|
|
||||||
if (onClose) {
|
|
||||||
closeButton = <AccessibleButton className="mx_UserInfo_cancel" onClick={onClose} title={_t('Close')}>
|
|
||||||
<div />
|
|
||||||
</AccessibleButton>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const memberDetails = (
|
const memberDetails = (
|
||||||
<PowerLevelSection
|
<PowerLevelSection
|
||||||
powerLevels={powerLevels}
|
powerLevels={powerLevels}
|
||||||
|
@ -1347,53 +1382,30 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
let e2eIcon;
|
let e2eStatus;
|
||||||
if (isRoomEncrypted && devices) {
|
if (isRoomEncrypted && devices) {
|
||||||
const e2eStatus = _getE2EStatus(cli, user.userId, devices);
|
e2eStatus = getE2EStatus(cli, user.userId, devices);
|
||||||
e2eIcon = <E2EIcon size={18} status={e2eStatus} isUser={true} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <UserInfoPane onClose={onClose} e2eStatus={e2eStatus} member={member}>
|
||||||
<div className="mx_UserInfo" role="tabpanel">
|
{ memberDetails &&
|
||||||
<AutoHideScrollbar className="mx_UserInfo_scrollContainer">
|
<div className="mx_UserInfo_container mx_UserInfo_separator mx_UserInfo_memberDetailsContainer">
|
||||||
{ closeButton }
|
<div className="mx_UserInfo_memberDetails">
|
||||||
{ avatarElement }
|
{ memberDetails }
|
||||||
|
</div>
|
||||||
|
</div> }
|
||||||
|
|
||||||
<div className="mx_UserInfo_container">
|
{ securitySection }
|
||||||
<div className="mx_UserInfo_profile">
|
<UserOptionsSection
|
||||||
<div>
|
devices={devices}
|
||||||
<h2 aria-label={displayName}>
|
canInvite={roomPermissions.canInvite}
|
||||||
{ e2eIcon }
|
isIgnored={isIgnored}
|
||||||
{ displayName }
|
member={member} />
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div>{ user.userId }</div>
|
|
||||||
<div className="mx_UserInfo_profileStatus">
|
|
||||||
{presenceLabel}
|
|
||||||
{statusLabel}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{ memberDetails && <div className="mx_UserInfo_container mx_UserInfo_memberDetailsContainer">
|
{ adminToolsContainer }
|
||||||
<div className="mx_UserInfo_memberDetails">
|
|
||||||
{ memberDetails }
|
|
||||||
</div>
|
|
||||||
</div> }
|
|
||||||
|
|
||||||
{ securitySection }
|
{ spinner }
|
||||||
<UserOptionsSection
|
</UserInfoPane>;
|
||||||
devices={devices}
|
|
||||||
canInvite={roomPermissions.canInvite}
|
|
||||||
isIgnored={isIgnored}
|
|
||||||
member={member} />
|
|
||||||
|
|
||||||
{ adminToolsContainer }
|
|
||||||
|
|
||||||
{ spinner }
|
|
||||||
</AutoHideScrollbar>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
UserInfo.propTypes = {
|
UserInfo.propTypes = {
|
||||||
|
|
|
@ -1128,6 +1128,8 @@
|
||||||
"For extra security, verify this user by checking a one-time code on both of your devices.": "For extra security, verify this user by checking a one-time code on both of your devices.",
|
"For extra security, verify this user by checking a one-time code on both of your devices.": "For extra security, verify this user by checking a one-time code on both of your devices.",
|
||||||
"For maximum security, do this in person.": "For maximum security, do this in person.",
|
"For maximum security, do this in person.": "For maximum security, do this in person.",
|
||||||
"Start Verification": "Start Verification",
|
"Start Verification": "Start Verification",
|
||||||
|
"Messages in this room are end-to-end encrypted.": "Messages in this room are end-to-end encrypted.",
|
||||||
|
"Your messages are secured and only you and the recipient have the unique keys to unlock them.": "Your messages are secured and only you and the recipient have the unique keys to unlock them.",
|
||||||
"Members": "Members",
|
"Members": "Members",
|
||||||
"Files": "Files",
|
"Files": "Files",
|
||||||
"Trusted": "Trusted",
|
"Trusted": "Trusted",
|
||||||
|
@ -1144,7 +1146,6 @@
|
||||||
"<strong>%(role)s</strong> in %(roomName)s": "<strong>%(role)s</strong> in %(roomName)s",
|
"<strong>%(role)s</strong> in %(roomName)s": "<strong>%(role)s</strong> in %(roomName)s",
|
||||||
"This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.",
|
"This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.",
|
||||||
"Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.",
|
"Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.",
|
||||||
"Messages in this room are end-to-end encrypted.": "Messages in this room are end-to-end encrypted.",
|
|
||||||
"Security": "Security",
|
"Security": "Security",
|
||||||
"Sunday": "Sunday",
|
"Sunday": "Sunday",
|
||||||
"Monday": "Monday",
|
"Monday": "Monday",
|
||||||
|
|
Loading…
Reference in New Issue