mirror of
https://github.com/element-hq/element-web
synced 2024-11-22 01:05:42 +03:00
Merge branch 'develop' into travis/media-customization
This commit is contained in:
commit
a9a4bd50ca
19 changed files with 357 additions and 194 deletions
|
@ -489,54 +489,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
||||||
margin-top: 69px;
|
margin-top: 69px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Beta {
|
|
||||||
color: red;
|
|
||||||
margin-right: 10px;
|
|
||||||
position: relative;
|
|
||||||
top: -3px;
|
|
||||||
background-color: white;
|
|
||||||
padding: 0 4px;
|
|
||||||
border-radius: 3px;
|
|
||||||
border: 1px solid darkred;
|
|
||||||
cursor: help;
|
|
||||||
transition-duration: 200ms;
|
|
||||||
font-size: smaller;
|
|
||||||
filter: opacity(0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_Beta:hover {
|
|
||||||
color: white;
|
|
||||||
border: 1px solid gray;
|
|
||||||
background-color: darkred;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_TintableSvgButton {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
align-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_TintableSvgButton object {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_TintableSvgButton span {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
opacity: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
// username colors
|
// username colors
|
||||||
// used by SenderProfile & RoomPreviewBar
|
// used by SenderProfile & RoomPreviewBar
|
||||||
.mx_Username_color1 {
|
.mx_Username_color1 {
|
||||||
|
|
|
@ -20,6 +20,8 @@ $SpaceRoomViewInnerWidth: 428px;
|
||||||
.mx_MainSplit > div:first-child {
|
.mx_MainSplit > div:first-child {
|
||||||
padding: 80px 60px;
|
padding: 80px 60px;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -69,9 +71,116 @@ $SpaceRoomViewInnerWidth: 428px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SpaceRoomView_landing {
|
.mx_SpaceRoomView_preview {
|
||||||
overflow-y: auto;
|
padding: 32px 24px !important; // override default padding from above
|
||||||
|
margin: auto;
|
||||||
|
max-width: 480px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
box-shadow: 2px 15px 30px $dialog-shadow-color;
|
||||||
|
border: 1px solid $input-border-color;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.mx_SpaceRoomView_preview_inviter {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: $font-15px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
margin-left: 8px;
|
||||||
|
|
||||||
|
.mx_SpaceRoomView_preview_inviter_name {
|
||||||
|
line-height: $font-18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpaceRoomView_preview_inviter_mxid {
|
||||||
|
line-height: $font-24px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .mx_BaseAvatar_image,
|
||||||
|
> .mx_BaseAvatar > .mx_BaseAvatar_image {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1.mx_SpaceRoomView_preview_name {
|
||||||
|
margin: 20px 0 !important; // override default margin from above
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpaceRoomView_preview_info {
|
||||||
|
color: $tertiary-fg-color;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
margin: 20px 0;
|
||||||
|
|
||||||
|
.mx_SpaceRoomView_preview_info_public,
|
||||||
|
.mx_SpaceRoomView_preview_info_private {
|
||||||
|
padding-left: 20px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
top: 0;
|
||||||
|
left: -2px;
|
||||||
|
mask-position: center;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
background-color: $tertiary-fg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpaceRoomView_preview_info_public::before {
|
||||||
|
mask-size: 12px;
|
||||||
|
mask-image: url("$(res)/img/globe.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpaceRoomView_preview_info_private::before {
|
||||||
|
mask-size: 14px;
|
||||||
|
mask-image: url("$(res)/img/element-icons/lock.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton_kind_link {
|
||||||
|
color: inherit;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 16px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "·"; // visual separator
|
||||||
|
position: absolute;
|
||||||
|
left: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpaceRoomView_preview_topic {
|
||||||
|
font-size: $font-14px;
|
||||||
|
line-height: $font-22px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
margin: 20px 0;
|
||||||
|
max-height: 160px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpaceRoomView_preview_joinButtons {
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
.mx_AccessibleButton {
|
||||||
|
width: 200px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 14px 0;
|
||||||
|
|
||||||
|
& + .mx_AccessibleButton {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpaceRoomView_landing {
|
||||||
> .mx_BaseAvatar_image,
|
> .mx_BaseAvatar_image,
|
||||||
> .mx_BaseAvatar > .mx_BaseAvatar_image {
|
> .mx_BaseAvatar > .mx_BaseAvatar_image {
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
@ -128,14 +237,6 @@ $SpaceRoomViewInnerWidth: 428px;
|
||||||
font-size: $font-15px;
|
font-size: $font-15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SpaceRoomView_landing_joinButtons {
|
|
||||||
margin-top: 24px;
|
|
||||||
|
|
||||||
.mx_FormButton {
|
|
||||||
padding: 8px 22px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_SpaceRoomView_landing_adminButtons {
|
.mx_SpaceRoomView_landing_adminButtons {
|
||||||
margin-top: 32px;
|
margin-top: 32px;
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,9 @@ limitations under the License.
|
||||||
padding: 7px 18px;
|
padding: 7px 18px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,4 +33,10 @@ limitations under the License.
|
||||||
color: $notice-primary-color;
|
color: $notice-primary-color;
|
||||||
background-color: $notice-primary-bg-color;
|
background-color: $notice-primary-bg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.mx_AccessibleButton_kind_secondary {
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
border: 1px solid $secondary-fg-color;
|
||||||
|
background-color: unset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,13 +14,11 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_UserInfo {
|
.mx_EncryptionInfo_spinner {
|
||||||
.mx_EncryptionInfo_spinner {
|
|
||||||
.mx_Spinner {
|
.mx_Spinner {
|
||||||
margin-top: 25px;
|
margin-top: 25px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
const { promises: fsp } = fs;
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const glob = require('glob');
|
const glob = require('glob');
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
|
@ -25,6 +26,8 @@ async function reskindex() {
|
||||||
const header = args.h || args.header;
|
const header = args.h || args.header;
|
||||||
|
|
||||||
const strm = fs.createWriteStream(componentIndexTmp);
|
const strm = fs.createWriteStream(componentIndexTmp);
|
||||||
|
// Wait for the open event to ensure the file descriptor is set
|
||||||
|
await new Promise(resolve => strm.once("open", resolve));
|
||||||
|
|
||||||
if (header) {
|
if (header) {
|
||||||
strm.write(fs.readFileSync(header));
|
strm.write(fs.readFileSync(header));
|
||||||
|
@ -53,14 +56,9 @@ async function reskindex() {
|
||||||
|
|
||||||
strm.write("export {components};\n");
|
strm.write("export {components};\n");
|
||||||
// Ensure the file has been fully written to disk before proceeding
|
// Ensure the file has been fully written to disk before proceeding
|
||||||
|
await util.promisify(fs.fsync)(strm.fd);
|
||||||
await util.promisify(strm.end);
|
await util.promisify(strm.end);
|
||||||
fs.rename(componentIndexTmp, componentIndex, function(err) {
|
await fsp.rename(componentIndexTmp, componentIndex);
|
||||||
if (err) {
|
|
||||||
console.error("Error moving new index into place: " + err);
|
|
||||||
} else {
|
|
||||||
console.log('Reskindex: completed');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expects both arrays of file names to be sorted
|
// Expects both arrays of file names to be sorted
|
||||||
|
@ -77,9 +75,17 @@ function filesHaveChanged(files, prevFiles) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wrapper since await at the top level is not well supported yet
|
||||||
|
function run() {
|
||||||
|
(async function() {
|
||||||
|
await reskindex();
|
||||||
|
console.log("Reskindex completed");
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
// -w indicates watch mode where any FS events will trigger reskindex
|
// -w indicates watch mode where any FS events will trigger reskindex
|
||||||
if (!args.w) {
|
if (!args.w) {
|
||||||
reskindex();
|
run();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,5 +93,5 @@ let watchDebouncer = null;
|
||||||
chokidar.watch(path.join(componentsDir, componentJsGlob)).on('all', (event, path) => {
|
chokidar.watch(path.join(componentsDir, componentJsGlob)).on('all', (event, path) => {
|
||||||
if (path === componentIndex) return;
|
if (path === componentIndex) return;
|
||||||
if (watchDebouncer) clearTimeout(watchDebouncer);
|
if (watchDebouncer) clearTimeout(watchDebouncer);
|
||||||
watchDebouncer = setTimeout(reskindex, 1000);
|
watchDebouncer = setTimeout(run, 1000);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1913,7 +1913,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.room?.isSpaceRoom()) {
|
if (SettingsStore.getValue("feature_spaces") && this.state.room?.isSpaceRoom()) {
|
||||||
return <SpaceRoomView
|
return <SpaceRoomView
|
||||||
space={this.state.room}
|
space={this.state.room}
|
||||||
justCreatedOpts={this.props.justCreatedOpts}
|
justCreatedOpts={this.props.justCreatedOpts}
|
||||||
|
|
|
@ -94,26 +94,95 @@ const useMyRoomMembership = (room: Room) => {
|
||||||
return membership;
|
return membership;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => {
|
const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
const myMembership = useMyRoomMembership(space);
|
const myMembership = useMyRoomMembership(space);
|
||||||
const joinRule = space.getJoinRule();
|
|
||||||
const userId = cli.getUserId();
|
|
||||||
|
|
||||||
|
let inviterSection;
|
||||||
let joinButtons;
|
let joinButtons;
|
||||||
if (myMembership === "invite") {
|
if (myMembership === "invite") {
|
||||||
joinButtons = <div className="mx_SpaceRoomView_landing_joinButtons">
|
const inviteSender = space.getMember(cli.getUserId())?.events.member?.getSender();
|
||||||
<FormButton label={_t("Accept Invite")} onClick={onJoinButtonClicked} />
|
const inviter = inviteSender && space.getMember(inviteSender);
|
||||||
<AccessibleButton kind="link" onClick={onRejectButtonClicked}>
|
|
||||||
{_t("Decline")}
|
if (inviteSender) {
|
||||||
</AccessibleButton>
|
inviterSection = <div className="mx_SpaceRoomView_preview_inviter">
|
||||||
</div>;
|
<MemberAvatar member={inviter} width={32} height={32} />
|
||||||
} else if (myMembership !== "join" && joinRule === "public") {
|
<div>
|
||||||
joinButtons = <div className="mx_SpaceRoomView_landing_joinButtons">
|
<div className="mx_SpaceRoomView_preview_inviter_name">
|
||||||
<FormButton label={_t("Join")} onClick={onJoinButtonClicked} />
|
{ _t("<inviter/> invites you", {}, {
|
||||||
|
inviter: () => <b>{ inviter.name || inviteSender }</b>,
|
||||||
|
}) }
|
||||||
|
</div>
|
||||||
|
{ inviter ? <div className="mx_SpaceRoomView_preview_inviter_mxid">
|
||||||
|
{ inviteSender }
|
||||||
|
</div> : null }
|
||||||
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
joinButtons = <>
|
||||||
|
<FormButton label={_t("Reject")} kind="secondary" onClick={onRejectButtonClicked} />
|
||||||
|
<FormButton label={_t("Accept")} onClick={onJoinButtonClicked} />
|
||||||
|
</>;
|
||||||
|
} else {
|
||||||
|
joinButtons = <FormButton label={_t("Join")} onClick={onJoinButtonClicked} />
|
||||||
|
}
|
||||||
|
|
||||||
|
let visibilitySection;
|
||||||
|
if (space.getJoinRule() === "public") {
|
||||||
|
visibilitySection = <span className="mx_SpaceRoomView_preview_info_public">
|
||||||
|
{ _t("Public space") }
|
||||||
|
</span>;
|
||||||
|
} else {
|
||||||
|
visibilitySection = <span className="mx_SpaceRoomView_preview_info_private">
|
||||||
|
{ _t("Private space") }
|
||||||
|
</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="mx_SpaceRoomView_preview">
|
||||||
|
{ inviterSection }
|
||||||
|
<RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
|
||||||
|
<h1 className="mx_SpaceRoomView_preview_name">
|
||||||
|
<RoomName room={space} />
|
||||||
|
</h1>
|
||||||
|
<div className="mx_SpaceRoomView_preview_info">
|
||||||
|
{ visibilitySection }
|
||||||
|
<RoomMemberCount room={space}>
|
||||||
|
{(count) => count > 0 ? (
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_SpaceRoomView_preview_memberCount"
|
||||||
|
kind="link"
|
||||||
|
onClick={() => {
|
||||||
|
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||||
|
action: Action.SetRightPanelPhase,
|
||||||
|
phase: RightPanelPhases.RoomMemberList,
|
||||||
|
refireParams: { space },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ _t("%(count)s members", { count }) }
|
||||||
|
</AccessibleButton>
|
||||||
|
) : null}
|
||||||
|
</RoomMemberCount>
|
||||||
|
</div>
|
||||||
|
<RoomTopic room={space}>
|
||||||
|
{(topic, ref) =>
|
||||||
|
<div className="mx_SpaceRoomView_preview_topic" ref={ref}>
|
||||||
|
{ topic }
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</RoomTopic>
|
||||||
|
<div className="mx_SpaceRoomView_preview_joinButtons">
|
||||||
|
{ joinButtons }
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SpaceLanding = ({ space }) => {
|
||||||
|
const cli = useContext(MatrixClientContext);
|
||||||
|
const myMembership = useMyRoomMembership(space);
|
||||||
|
const userId = cli.getUserId();
|
||||||
|
|
||||||
let inviteButton;
|
let inviteButton;
|
||||||
if (myMembership === "join" && space.canInvite(userId)) {
|
if (myMembership === "join" && space.canInvite(userId)) {
|
||||||
inviteButton = (
|
inviteButton = (
|
||||||
|
@ -227,26 +296,7 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
||||||
) : null}
|
) : null}
|
||||||
</RoomMemberCount>
|
</RoomMemberCount>
|
||||||
</div> };
|
</div> };
|
||||||
if (myMembership === "invite") {
|
if (shouldShowSpaceSettings(cli, space)) {
|
||||||
const inviteSender = space.getMember(userId)?.events.member?.getSender();
|
|
||||||
const inviter = inviteSender && space.getMember(inviteSender);
|
|
||||||
|
|
||||||
if (inviteSender) {
|
|
||||||
return _t("<inviter/> invited you to <name/>", {}, {
|
|
||||||
name: tags.name,
|
|
||||||
inviter: () => inviter
|
|
||||||
? <span className="mx_SpaceRoomView_landing_inviter">
|
|
||||||
<MemberAvatar member={inviter} width={26} height={26} viewUserOnClick={true} />
|
|
||||||
{ inviter.name }
|
|
||||||
</span>
|
|
||||||
: <span className="mx_SpaceRoomView_landing_inviter">
|
|
||||||
{ inviteSender }
|
|
||||||
</span>,
|
|
||||||
}) as JSX.Element;
|
|
||||||
} else {
|
|
||||||
return _t("You have been invited to <name/>", {}, tags) as JSX.Element;
|
|
||||||
}
|
|
||||||
} else if (shouldShowSpaceSettings(cli, space)) {
|
|
||||||
if (space.getJoinRule() === "public") {
|
if (space.getJoinRule() === "public") {
|
||||||
return _t("Your public space <name/>", {}, tags) as JSX.Element;
|
return _t("Your public space <name/>", {}, tags) as JSX.Element;
|
||||||
} else {
|
} else {
|
||||||
|
@ -260,7 +310,6 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
||||||
<div className="mx_SpaceRoomView_landing_topic">
|
<div className="mx_SpaceRoomView_landing_topic">
|
||||||
<RoomTopic room={space} />
|
<RoomTopic room={space} />
|
||||||
</div>
|
</div>
|
||||||
{ joinButtons }
|
|
||||||
<div className="mx_SpaceRoomView_landing_adminButtons">
|
<div className="mx_SpaceRoomView_landing_adminButtons">
|
||||||
{ inviteButton }
|
{ inviteButton }
|
||||||
{ addRoomButtons }
|
{ addRoomButtons }
|
||||||
|
@ -548,12 +597,15 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
|
||||||
private renderBody() {
|
private renderBody() {
|
||||||
switch (this.state.phase) {
|
switch (this.state.phase) {
|
||||||
case Phase.Landing:
|
case Phase.Landing:
|
||||||
return <SpaceLanding
|
if (this.props.space.getMyMembership() === "join") {
|
||||||
|
return <SpaceLanding space={this.props.space} />;
|
||||||
|
} else {
|
||||||
|
return <SpacePreview
|
||||||
space={this.props.space}
|
space={this.props.space}
|
||||||
onJoinButtonClicked={this.props.onJoinButtonClicked}
|
onJoinButtonClicked={this.props.onJoinButtonClicked}
|
||||||
onRejectButtonClicked={this.props.onRejectButtonClicked}
|
onRejectButtonClicked={this.props.onRejectButtonClicked}
|
||||||
/>;
|
/>;
|
||||||
|
}
|
||||||
case Phase.PublicCreateRooms:
|
case Phase.PublicCreateRooms:
|
||||||
return <SpaceSetupFirstRooms
|
return <SpaceSetupFirstRooms
|
||||||
space={this.props.space}
|
space={this.props.space}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { _t } from '../../../languageHandler';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import {
|
import {
|
||||||
SetupEncryptionStore,
|
SetupEncryptionStore,
|
||||||
|
PHASE_LOADING,
|
||||||
PHASE_INTRO,
|
PHASE_INTRO,
|
||||||
PHASE_BUSY,
|
PHASE_BUSY,
|
||||||
PHASE_DONE,
|
PHASE_DONE,
|
||||||
|
@ -60,7 +61,9 @@ export default class CompleteSecurity extends React.Component {
|
||||||
let icon;
|
let icon;
|
||||||
let title;
|
let title;
|
||||||
|
|
||||||
if (phase === PHASE_INTRO) {
|
if (phase === PHASE_LOADING) {
|
||||||
|
return null;
|
||||||
|
} else if (phase === PHASE_INTRO) {
|
||||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||||
title = _t("Verify this login");
|
title = _t("Verify this login");
|
||||||
} else if (phase === PHASE_DONE) {
|
} else if (phase === PHASE_DONE) {
|
||||||
|
|
|
@ -17,11 +17,13 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
|
import Modal from '../../../Modal';
|
||||||
|
import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import {
|
import {
|
||||||
SetupEncryptionStore,
|
SetupEncryptionStore,
|
||||||
|
PHASE_LOADING,
|
||||||
PHASE_INTRO,
|
PHASE_INTRO,
|
||||||
PHASE_BUSY,
|
PHASE_BUSY,
|
||||||
PHASE_DONE,
|
PHASE_DONE,
|
||||||
|
@ -83,6 +85,22 @@ export default class SetupEncryptionBody extends React.Component {
|
||||||
store.usePassPhrase();
|
store.usePassPhrase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onVerifyClick = () => {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
const userId = cli.getUserId();
|
||||||
|
const requestPromise = cli.requestVerification(userId);
|
||||||
|
|
||||||
|
this.props.onFinished(true);
|
||||||
|
Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, {
|
||||||
|
verificationRequestPromise: requestPromise,
|
||||||
|
member: cli.getUser(userId),
|
||||||
|
onFinished: async () => {
|
||||||
|
const request = await requestPromise;
|
||||||
|
request.cancel();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onSkipClick = () => {
|
onSkipClick = () => {
|
||||||
const store = SetupEncryptionStore.sharedInstance();
|
const store = SetupEncryptionStore.sharedInstance();
|
||||||
store.skip();
|
store.skip();
|
||||||
|
@ -134,32 +152,22 @@ export default class SetupEncryptionBody extends React.Component {
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const brand = SdkConfig.get().brand;
|
let verifyButton;
|
||||||
|
if (store.hasDevicesToVerifyAgainst) {
|
||||||
|
verifyButton = <AccessibleButton kind="primary" onClick={this._onVerifyClick}>
|
||||||
|
{ _t("Verify with another session") }
|
||||||
|
</AccessibleButton>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>{_t(
|
<p>{_t(
|
||||||
"Confirm your identity by verifying this login from one of your other sessions, " +
|
"Verify this login to access your encrypted messages and " +
|
||||||
"granting it access to encrypted messages.",
|
"prove to others that this login is really you.",
|
||||||
)}</p>
|
)}</p>
|
||||||
<p>{_t(
|
|
||||||
"This requires the latest %(brand)s on your other devices:",
|
|
||||||
{ brand },
|
|
||||||
)}</p>
|
|
||||||
|
|
||||||
<div className="mx_CompleteSecurity_clients">
|
|
||||||
<div className="mx_CompleteSecurity_clients_desktop">
|
|
||||||
<div>{_t("%(brand)s Web", { brand })}</div>
|
|
||||||
<div>{_t("%(brand)s Desktop", { brand })}</div>
|
|
||||||
</div>
|
|
||||||
<div className="mx_CompleteSecurity_clients_mobile">
|
|
||||||
<div>{_t("%(brand)s iOS", { brand })}</div>
|
|
||||||
<div>{_t("%(brand)s Android", { brand })}</div>
|
|
||||||
</div>
|
|
||||||
<p>{_t("or another cross-signing capable Matrix client")}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mx_CompleteSecurity_actionRow">
|
<div className="mx_CompleteSecurity_actionRow">
|
||||||
|
{verifyButton}
|
||||||
{useRecoveryKeyButton}
|
{useRecoveryKeyButton}
|
||||||
<AccessibleButton kind="danger" onClick={this.onSkipClick}>
|
<AccessibleButton kind="danger" onClick={this.onSkipClick}>
|
||||||
{_t("Skip")}
|
{_t("Skip")}
|
||||||
|
@ -217,7 +225,7 @@ export default class SetupEncryptionBody extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (phase === PHASE_BUSY) {
|
} else if (phase === PHASE_BUSY || phase === PHASE_LOADING) {
|
||||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
const Spinner = sdk.getComponent('views.elements.Spinner');
|
||||||
return <Spinner />;
|
return <Spinner />;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -66,6 +66,10 @@ export default class NewSessionReviewDialog extends React.PureComponent {
|
||||||
Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, {
|
Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, {
|
||||||
verificationRequestPromise: requestPromise,
|
verificationRequestPromise: requestPromise,
|
||||||
member: cli.getUser(userId),
|
member: cli.getUser(userId),
|
||||||
|
onFinished: async () => {
|
||||||
|
const request = await requestPromise;
|
||||||
|
request.cancel();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,11 +27,11 @@ export default class VerificationRequestDialog extends React.Component {
|
||||||
verificationRequest: PropTypes.object,
|
verificationRequest: PropTypes.object,
|
||||||
verificationRequestPromise: PropTypes.object,
|
verificationRequestPromise: PropTypes.object,
|
||||||
onFinished: PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
|
member: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(...args) {
|
constructor(...args) {
|
||||||
super(...args);
|
super(...args);
|
||||||
this.onFinished = this.onFinished.bind(this);
|
|
||||||
this.state = {};
|
this.state = {};
|
||||||
if (this.props.verificationRequest) {
|
if (this.props.verificationRequest) {
|
||||||
this.state.verificationRequest = this.props.verificationRequest;
|
this.state.verificationRequest = this.props.verificationRequest;
|
||||||
|
@ -52,7 +52,7 @@ export default class VerificationRequestDialog extends React.Component {
|
||||||
const title = request && request.isSelfVerification ?
|
const title = request && request.isSelfVerification ?
|
||||||
_t("Verify other session") : _t("Verification Request");
|
_t("Verify other session") : _t("Verification Request");
|
||||||
|
|
||||||
return <BaseDialog className="mx_InfoDialog" onFinished={this.onFinished}
|
return <BaseDialog className="mx_InfoDialog" onFinished={this.props.onFinished}
|
||||||
contentId="mx_Dialog_content"
|
contentId="mx_Dialog_content"
|
||||||
title={title}
|
title={title}
|
||||||
hasCancel={true}
|
hasCancel={true}
|
||||||
|
@ -66,13 +66,4 @@ export default class VerificationRequestDialog extends React.Component {
|
||||||
/>
|
/>
|
||||||
</BaseDialog>;
|
</BaseDialog>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async onFinished() {
|
|
||||||
this.props.onFinished();
|
|
||||||
let request = this.props.verificationRequest;
|
|
||||||
if (!request && this.props.verificationRequestPromise) {
|
|
||||||
request = await this.props.verificationRequestPromise;
|
|
||||||
}
|
|
||||||
request.cancel();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,7 @@ import EncryptionPanel from "./EncryptionPanel";
|
||||||
import {useAsyncMemo} from '../../../hooks/useAsyncMemo';
|
import {useAsyncMemo} from '../../../hooks/useAsyncMemo';
|
||||||
import {legacyVerifyUser, verifyDevice, verifyUser} from '../../../verification';
|
import {legacyVerifyUser, verifyDevice, verifyUser} from '../../../verification';
|
||||||
import {Action} from "../../../dispatcher/actions";
|
import {Action} from "../../../dispatcher/actions";
|
||||||
|
import { USER_SECURITY_TAB } from "../dialogs/UserSettingsDialog";
|
||||||
import {useIsEncrypted} from "../../../hooks/useIsEncrypted";
|
import {useIsEncrypted} from "../../../hooks/useIsEncrypted";
|
||||||
import BaseCard from "./BaseCard";
|
import BaseCard from "./BaseCard";
|
||||||
import {E2EStatus} from "../../../utils/ShieldUtils";
|
import {E2EStatus} from "../../../utils/ShieldUtils";
|
||||||
|
@ -1368,6 +1369,20 @@ const BasicUserInfo: React.FC<{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let editDevices;
|
||||||
|
if (member.userId == cli.getUserId()) {
|
||||||
|
editDevices = (<p>
|
||||||
|
<AccessibleButton className="mx_UserInfo_field" onClick={() => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: Action.ViewUserSettings,
|
||||||
|
initialTabId: USER_SECURITY_TAB,
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
{ _t("Edit devices") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</p>)
|
||||||
|
}
|
||||||
|
|
||||||
const securitySection = (
|
const securitySection = (
|
||||||
<div className="mx_UserInfo_container">
|
<div className="mx_UserInfo_container">
|
||||||
<h3>{ _t("Security") }</h3>
|
<h3>{ _t("Security") }</h3>
|
||||||
|
@ -1377,6 +1392,7 @@ const BasicUserInfo: React.FC<{
|
||||||
loading={showDeviceListSpinner}
|
loading={showDeviceListSpinner}
|
||||||
devices={devices}
|
devices={devices}
|
||||||
userId={member.userId} /> }
|
userId={member.userId} /> }
|
||||||
|
{ editDevices }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -158,14 +158,14 @@ export default class ReadReceiptMarker extends React.PureComponent {
|
||||||
// then shift to the rightmost column,
|
// then shift to the rightmost column,
|
||||||
// and then it will drop down to its resting position
|
// and then it will drop down to its resting position
|
||||||
//
|
//
|
||||||
// XXX: We use a fractional left value to trick velocity-animate into actually animating.
|
// XXX: We use a small left value to trick velocity-animate into actually animating.
|
||||||
// This is a very annoying bug where if it thinks there's no change to `left` then it'll
|
// This is a very annoying bug where if it thinks there's no change to `left` then it'll
|
||||||
// skip applying it, thus making our read receipt at +14px instead of +0px like it
|
// skip applying it, thus making our read receipt at +14px instead of +0px like it
|
||||||
// should be. This does cause a tiny amount of drift for read receipts, however with a
|
// should be. This does cause a tiny amount of drift for read receipts, however with a
|
||||||
// value so small it's not perceived by a user.
|
// value so small it's not perceived by a user.
|
||||||
// Note: Any smaller values (or trying to interchange units) might cause read receipts to
|
// Note: Any smaller values (or trying to interchange units) might cause read receipts to
|
||||||
// fail to fall down or cause gaps.
|
// fail to fall down or cause gaps.
|
||||||
startStyles.push({ top: startTopOffset+'px', left: '0.001px' });
|
startStyles.push({ top: startTopOffset+'px', left: '1px' });
|
||||||
enterTransitionOpts.push({
|
enterTransitionOpts.push({
|
||||||
duration: bounce ? Math.min(Math.log(Math.abs(startTopOffset)) * 200, 3000) : 300,
|
duration: bounce ? Math.min(Math.log(Math.abs(startTopOffset)) * 200, 3000) : 300,
|
||||||
easing: bounce ? 'easeOutBounce' : 'easeOutCubic',
|
easing: bounce ? 'easeOutBounce' : 'easeOutCubic',
|
||||||
|
|
|
@ -39,6 +39,7 @@ interface IProps {
|
||||||
interface IState {
|
interface IState {
|
||||||
counter: number;
|
counter: number;
|
||||||
device?: DeviceInfo;
|
device?: DeviceInfo;
|
||||||
|
ip?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.toasts.VerificationRequestToast")
|
@replaceableComponent("views.toasts.VerificationRequestToast")
|
||||||
|
@ -68,9 +69,15 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
|
||||||
// a toast hanging around after logging in if you did a verification as part of login).
|
// a toast hanging around after logging in if you did a verification as part of login).
|
||||||
this._checkRequestIsPending();
|
this._checkRequestIsPending();
|
||||||
|
|
||||||
|
|
||||||
if (request.isSelfVerification) {
|
if (request.isSelfVerification) {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
this.setState({device: cli.getStoredDevice(cli.getUserId(), request.channel.deviceId)});
|
const device = await cli.getDevice(request.channel.deviceId);
|
||||||
|
const ip = device.last_seen_ip;
|
||||||
|
this.setState({
|
||||||
|
device: cli.getStoredDevice(cli.getUserId(), request.channel.deviceId),
|
||||||
|
ip,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,6 +127,9 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
|
||||||
const VerificationRequestDialog = sdk.getComponent("views.dialogs.VerificationRequestDialog");
|
const VerificationRequestDialog = sdk.getComponent("views.dialogs.VerificationRequestDialog");
|
||||||
Modal.createTrackedDialog('Incoming Verification', '', VerificationRequestDialog, {
|
Modal.createTrackedDialog('Incoming Verification', '', VerificationRequestDialog, {
|
||||||
verificationRequest: request,
|
verificationRequest: request,
|
||||||
|
onFinished: () => {
|
||||||
|
request.cancel();
|
||||||
|
},
|
||||||
}, null, /* priority = */ false, /* static = */ true);
|
}, null, /* priority = */ false, /* static = */ true);
|
||||||
}
|
}
|
||||||
await request.accept();
|
await request.accept();
|
||||||
|
@ -133,9 +143,10 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
|
||||||
let nameLabel;
|
let nameLabel;
|
||||||
if (request.isSelfVerification) {
|
if (request.isSelfVerification) {
|
||||||
if (this.state.device) {
|
if (this.state.device) {
|
||||||
nameLabel = _t("From %(deviceName)s (%(deviceId)s)", {
|
nameLabel = _t("From %(deviceName)s (%(deviceId)s) at %(ip)s", {
|
||||||
deviceName: this.state.device.getDisplayName(),
|
deviceName: this.state.device.getDisplayName(),
|
||||||
deviceId: this.state.device.deviceId,
|
deviceId: this.state.device.deviceId,
|
||||||
|
ip: this.state.ip,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -728,7 +728,7 @@
|
||||||
"Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.": "Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.",
|
"Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.": "Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.",
|
||||||
"Yes": "Yes",
|
"Yes": "Yes",
|
||||||
"No": "No",
|
"No": "No",
|
||||||
"Review where you’re logged in": "Review where you’re logged in",
|
"You have unverified logins": "You have unverified logins",
|
||||||
"Verify all your sessions to ensure your account & messages are safe": "Verify all your sessions to ensure your account & messages are safe",
|
"Verify all your sessions to ensure your account & messages are safe": "Verify all your sessions to ensure your account & messages are safe",
|
||||||
"Review": "Review",
|
"Review": "Review",
|
||||||
"Later": "Later",
|
"Later": "Later",
|
||||||
|
@ -753,7 +753,8 @@
|
||||||
"Safeguard against losing access to encrypted messages & data": "Safeguard against losing access to encrypted messages & data",
|
"Safeguard against losing access to encrypted messages & data": "Safeguard against losing access to encrypted messages & data",
|
||||||
"Other users may not trust it": "Other users may not trust it",
|
"Other users may not trust it": "Other users may not trust it",
|
||||||
"New login. Was this you?": "New login. Was this you?",
|
"New login. Was this you?": "New login. Was this you?",
|
||||||
"Verify the new login accessing your account: %(name)s": "Verify the new login accessing your account: %(name)s",
|
"A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s",
|
||||||
|
"Check your devices": "Check your devices",
|
||||||
"What's new?": "What's new?",
|
"What's new?": "What's new?",
|
||||||
"What's New": "What's New",
|
"What's New": "What's New",
|
||||||
"Update": "Update",
|
"Update": "Update",
|
||||||
|
@ -980,7 +981,7 @@
|
||||||
"Folder": "Folder",
|
"Folder": "Folder",
|
||||||
"Pin": "Pin",
|
"Pin": "Pin",
|
||||||
"Your server isn't responding to some <a>requests</a>.": "Your server isn't responding to some <a>requests</a>.",
|
"Your server isn't responding to some <a>requests</a>.": "Your server isn't responding to some <a>requests</a>.",
|
||||||
"From %(deviceName)s (%(deviceId)s)": "From %(deviceName)s (%(deviceId)s)",
|
"From %(deviceName)s (%(deviceId)s) at %(ip)s": "From %(deviceName)s (%(deviceId)s) at %(ip)s",
|
||||||
"Decline (%(counter)s)": "Decline (%(counter)s)",
|
"Decline (%(counter)s)": "Decline (%(counter)s)",
|
||||||
"Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
|
"Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
|
||||||
"Delete": "Delete",
|
"Delete": "Delete",
|
||||||
|
@ -1756,6 +1757,7 @@
|
||||||
"Failed to deactivate user": "Failed to deactivate user",
|
"Failed to deactivate user": "Failed to deactivate user",
|
||||||
"Role": "Role",
|
"Role": "Role",
|
||||||
"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.",
|
||||||
|
"Edit devices": "Edit devices",
|
||||||
"Security": "Security",
|
"Security": "Security",
|
||||||
"The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.": "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.",
|
"The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.": "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.",
|
||||||
"Verify by scanning": "Verify by scanning",
|
"Verify by scanning": "Verify by scanning",
|
||||||
|
@ -2609,14 +2611,14 @@
|
||||||
"Promoted to users": "Promoted to users",
|
"Promoted to users": "Promoted to users",
|
||||||
"Manage rooms": "Manage rooms",
|
"Manage rooms": "Manage rooms",
|
||||||
"Find a room...": "Find a room...",
|
"Find a room...": "Find a room...",
|
||||||
"Accept Invite": "Accept Invite",
|
"<inviter/> invites you": "<inviter/> invites you",
|
||||||
|
"Public space": "Public space",
|
||||||
|
"Private space": "Private space",
|
||||||
|
"%(count)s members|other": "%(count)s members",
|
||||||
|
"%(count)s members|one": "%(count)s member",
|
||||||
"Add existing rooms & spaces": "Add existing rooms & spaces",
|
"Add existing rooms & spaces": "Add existing rooms & spaces",
|
||||||
"Default Rooms": "Default Rooms",
|
"Default Rooms": "Default Rooms",
|
||||||
"Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.",
|
"Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.",
|
||||||
"%(count)s members|other": "%(count)s members",
|
|
||||||
"%(count)s members|one": "%(count)s member",
|
|
||||||
"<inviter/> invited you to <name/>": "<inviter/> invited you to <name/>",
|
|
||||||
"You have been invited to <name/>": "You have been invited to <name/>",
|
|
||||||
"Your public space <name/>": "Your public space <name/>",
|
"Your public space <name/>": "Your public space <name/>",
|
||||||
"Your private space <name/>": "Your private space <name/>",
|
"Your private space <name/>": "Your private space <name/>",
|
||||||
"Welcome to <name/>": "Welcome to <name/>",
|
"Welcome to <name/>": "Welcome to <name/>",
|
||||||
|
@ -2720,13 +2722,8 @@
|
||||||
"Decide where your account is hosted": "Decide where your account is hosted",
|
"Decide where your account is hosted": "Decide where your account is hosted",
|
||||||
"Use Security Key or Phrase": "Use Security Key or Phrase",
|
"Use Security Key or Phrase": "Use Security Key or Phrase",
|
||||||
"Use Security Key": "Use Security Key",
|
"Use Security Key": "Use Security Key",
|
||||||
"Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.",
|
"Verify with another session": "Verify with another session",
|
||||||
"This requires the latest %(brand)s on your other devices:": "This requires the latest %(brand)s on your other devices:",
|
"Verify this login to access your encrypted messages and prove to others that this login is really you.": "Verify this login to access your encrypted messages and prove to others that this login is really you.",
|
||||||
"%(brand)s Web": "%(brand)s Web",
|
|
||||||
"%(brand)s Desktop": "%(brand)s Desktop",
|
|
||||||
"%(brand)s iOS": "%(brand)s iOS",
|
|
||||||
"%(brand)s Android": "%(brand)s Android",
|
|
||||||
"or another cross-signing capable Matrix client": "or another cross-signing capable Matrix client",
|
|
||||||
"Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.",
|
"Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.",
|
||||||
"Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.",
|
"Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.",
|
||||||
"Without completing security on this session, it won’t have access to encrypted messages.": "Without completing security on this session, it won’t have access to encrypted messages.",
|
"Without completing security on this session, it won’t have access to encrypted messages.": "Without completing security on this session, it won’t have access to encrypted messages.",
|
||||||
|
|
|
@ -19,11 +19,12 @@ import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||||
import { accessSecretStorage, AccessCancelledError } from '../SecurityManager';
|
import { accessSecretStorage, AccessCancelledError } from '../SecurityManager';
|
||||||
import { PHASE_DONE as VERIF_PHASE_DONE } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
import { PHASE_DONE as VERIF_PHASE_DONE } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||||
|
|
||||||
export const PHASE_INTRO = 0;
|
export const PHASE_LOADING = 0;
|
||||||
export const PHASE_BUSY = 1;
|
export const PHASE_INTRO = 1;
|
||||||
export const PHASE_DONE = 2; //final done stage, but still showing UX
|
export const PHASE_BUSY = 2;
|
||||||
export const PHASE_CONFIRM_SKIP = 3;
|
export const PHASE_DONE = 3; //final done stage, but still showing UX
|
||||||
export const PHASE_FINISHED = 4; //UX can be closed
|
export const PHASE_CONFIRM_SKIP = 4;
|
||||||
|
export const PHASE_FINISHED = 5; //UX can be closed
|
||||||
|
|
||||||
export class SetupEncryptionStore extends EventEmitter {
|
export class SetupEncryptionStore extends EventEmitter {
|
||||||
static sharedInstance() {
|
static sharedInstance() {
|
||||||
|
@ -36,7 +37,7 @@ export class SetupEncryptionStore extends EventEmitter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._started = true;
|
this._started = true;
|
||||||
this.phase = PHASE_BUSY;
|
this.phase = PHASE_LOADING;
|
||||||
this.verificationRequest = null;
|
this.verificationRequest = null;
|
||||||
this.backupInfo = null;
|
this.backupInfo = null;
|
||||||
|
|
||||||
|
@ -75,7 +76,8 @@ export class SetupEncryptionStore extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchKeyInfo() {
|
async fetchKeyInfo() {
|
||||||
const keys = await MatrixClientPeg.get().isSecretStored('m.cross_signing.master', false);
|
const cli = MatrixClientPeg.get();
|
||||||
|
const keys = await cli.isSecretStored('m.cross_signing.master', false);
|
||||||
if (keys === null || Object.keys(keys).length === 0) {
|
if (keys === null || Object.keys(keys).length === 0) {
|
||||||
this.keyId = null;
|
this.keyId = null;
|
||||||
this.keyInfo = null;
|
this.keyInfo = null;
|
||||||
|
@ -85,7 +87,20 @@ export class SetupEncryptionStore extends EventEmitter {
|
||||||
this.keyInfo = keys[this.keyId];
|
this.keyInfo = keys[this.keyId];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// do we have any other devices which are E2EE which we can verify against?
|
||||||
|
const dehydratedDevice = await cli.getDehydratedDevice();
|
||||||
|
this.hasDevicesToVerifyAgainst = cli.getStoredDevicesForUser(cli.getUserId()).some(
|
||||||
|
device =>
|
||||||
|
device.getIdentityKey() &&
|
||||||
|
(!dehydratedDevice || (device.deviceId != dehydratedDevice.device_id)),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!this.hasDevicesToVerifyAgainst && !this.keyInfo) {
|
||||||
|
// skip before we can even render anything.
|
||||||
|
this.phase = PHASE_FINISHED;
|
||||||
|
} else {
|
||||||
this.phase = PHASE_INTRO;
|
this.phase = PHASE_INTRO;
|
||||||
|
}
|
||||||
this.emit("update");
|
this.emit("update");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,7 @@ export const showToast = (deviceIds: Set<string>) => {
|
||||||
|
|
||||||
ToastStore.sharedInstance().addOrReplaceToast({
|
ToastStore.sharedInstance().addOrReplaceToast({
|
||||||
key: TOAST_KEY,
|
key: TOAST_KEY,
|
||||||
title: _t("Review where you’re logged in"),
|
title: _t("You have unverified logins"),
|
||||||
icon: "verification_warning",
|
icon: "verification_warning",
|
||||||
props: {
|
props: {
|
||||||
description: _t("Verify all your sessions to ensure your account & messages are safe"),
|
description: _t("Verify all your sessions to ensure your account & messages are safe"),
|
||||||
|
|
|
@ -15,38 +15,34 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { _t } from '../languageHandler';
|
import { _t } from '../languageHandler';
|
||||||
|
import dis from "../dispatcher/dispatcher";
|
||||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||||
import Modal from '../Modal';
|
|
||||||
import DeviceListener from '../DeviceListener';
|
import DeviceListener from '../DeviceListener';
|
||||||
import NewSessionReviewDialog from '../components/views/dialogs/NewSessionReviewDialog';
|
|
||||||
import ToastStore from "../stores/ToastStore";
|
import ToastStore from "../stores/ToastStore";
|
||||||
import GenericToast from "../components/views/toasts/GenericToast";
|
import GenericToast from "../components/views/toasts/GenericToast";
|
||||||
|
import { Action } from "../dispatcher/actions";
|
||||||
|
import { USER_SECURITY_TAB } from "../components/views/dialogs/UserSettingsDialog";
|
||||||
|
|
||||||
function toastKey(deviceId: string) {
|
function toastKey(deviceId: string) {
|
||||||
return "unverified_session_" + deviceId;
|
return "unverified_session_" + deviceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const showToast = (deviceId: string) => {
|
export const showToast = async (deviceId: string) => {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
const onAccept = () => {
|
const onAccept = () => {
|
||||||
Modal.createTrackedDialog('New Session Review', 'Starting dialog', NewSessionReviewDialog, {
|
|
||||||
userId: cli.getUserId(),
|
|
||||||
device: cli.getStoredDevice(cli.getUserId(), deviceId),
|
|
||||||
onFinished: (r) => {
|
|
||||||
if (!r) {
|
|
||||||
/* This'll come back false if the user clicks "this wasn't me" and saw a warning dialog */
|
|
||||||
DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]);
|
DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]);
|
||||||
}
|
dis.dispatch({
|
||||||
},
|
action: Action.ViewUserSettings,
|
||||||
}, null, /* priority = */ false, /* static = */ true);
|
initialTabId: USER_SECURITY_TAB,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onReject = () => {
|
const onReject = () => {
|
||||||
DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]);
|
DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const device = cli.getStoredDevice(cli.getUserId(), deviceId);
|
const device = await cli.getDevice(deviceId);
|
||||||
|
|
||||||
ToastStore.sharedInstance().addOrReplaceToast({
|
ToastStore.sharedInstance().addOrReplaceToast({
|
||||||
key: toastKey(deviceId),
|
key: toastKey(deviceId),
|
||||||
|
@ -54,8 +50,13 @@ export const showToast = (deviceId: string) => {
|
||||||
icon: "verification_warning",
|
icon: "verification_warning",
|
||||||
props: {
|
props: {
|
||||||
description: _t(
|
description: _t(
|
||||||
"Verify the new login accessing your account: %(name)s", { name: device.getDisplayName()}),
|
"A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s", {
|
||||||
acceptLabel: _t("Verify"),
|
name: device.display_name,
|
||||||
|
deviceID: deviceId,
|
||||||
|
ip: device.last_seen_ip,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
acceptLabel: _t("Check your devices"),
|
||||||
onAccept,
|
onAccept,
|
||||||
rejectLabel: _t("Later"),
|
rejectLabel: _t("Later"),
|
||||||
onReject,
|
onReject,
|
||||||
|
|
Loading…
Reference in a new issue