mirror of https://github.com/vector-im/riot-web
				
				
				
			Merge branch 'develop' into travis/media-customization
						commit
						a9a4bd50ca
					
				| 
						 | 
				
			
			@ -489,54 +489,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
 | 
			
		|||
    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
 | 
			
		||||
// used by SenderProfile & RoomPreviewBar
 | 
			
		||||
.mx_Username_color1 {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,6 +20,8 @@ $SpaceRoomViewInnerWidth: 428px;
 | 
			
		|||
    .mx_MainSplit > div:first-child {
 | 
			
		||||
        padding: 80px 60px;
 | 
			
		||||
        flex-grow: 1;
 | 
			
		||||
        max-height: 100%;
 | 
			
		||||
        overflow-y: auto;
 | 
			
		||||
 | 
			
		||||
        h1 {
 | 
			
		||||
            margin: 0;
 | 
			
		||||
| 
						 | 
				
			
			@ -69,9 +71,116 @@ $SpaceRoomViewInnerWidth: 428px;
 | 
			
		|||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .mx_SpaceRoomView_landing {
 | 
			
		||||
        overflow-y: auto;
 | 
			
		||||
    .mx_SpaceRoomView_preview {
 | 
			
		||||
        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 > .mx_BaseAvatar_image {
 | 
			
		||||
            border-radius: 12px;
 | 
			
		||||
| 
						 | 
				
			
			@ -128,14 +237,6 @@ $SpaceRoomViewInnerWidth: 428px;
 | 
			
		|||
            font-size: $font-15px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .mx_SpaceRoomView_landing_joinButtons {
 | 
			
		||||
            margin-top: 24px;
 | 
			
		||||
 | 
			
		||||
            .mx_FormButton {
 | 
			
		||||
                padding: 8px 22px;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .mx_SpaceRoomView_landing_adminButtons {
 | 
			
		||||
            margin-top: 32px;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,7 +26,9 @@ limitations under the License.
 | 
			
		|||
    padding: 7px 18px;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    border-radius: 8px;
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    display: inline-flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    font-size: $font-14px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,4 +33,10 @@ limitations under the License.
 | 
			
		|||
        color: $notice-primary-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.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
.mx_UserInfo {
 | 
			
		||||
    .mx_EncryptionInfo_spinner {
 | 
			
		||||
        .mx_Spinner {
 | 
			
		||||
            margin-top: 25px;
 | 
			
		||||
            margin-bottom: 15px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        text-align: center;
 | 
			
		||||
.mx_EncryptionInfo_spinner {
 | 
			
		||||
    .mx_Spinner {
 | 
			
		||||
        margin-top: 25px;
 | 
			
		||||
        margin-bottom: 15px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    text-align: center;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
#!/usr/bin/env node
 | 
			
		||||
const fs = require('fs');
 | 
			
		||||
const { promises: fsp } = fs;
 | 
			
		||||
const path = require('path');
 | 
			
		||||
const glob = require('glob');
 | 
			
		||||
const util = require('util');
 | 
			
		||||
| 
						 | 
				
			
			@ -25,6 +26,8 @@ async function reskindex() {
 | 
			
		|||
    const header = args.h || args.header;
 | 
			
		||||
 | 
			
		||||
    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) {
 | 
			
		||||
       strm.write(fs.readFileSync(header));
 | 
			
		||||
| 
						 | 
				
			
			@ -53,14 +56,9 @@ async function reskindex() {
 | 
			
		|||
 | 
			
		||||
    strm.write("export {components};\n");
 | 
			
		||||
    // Ensure the file has been fully written to disk before proceeding
 | 
			
		||||
    await util.promisify(fs.fsync)(strm.fd);
 | 
			
		||||
    await util.promisify(strm.end);
 | 
			
		||||
    fs.rename(componentIndexTmp, componentIndex, function(err) {
 | 
			
		||||
        if (err) {
 | 
			
		||||
            console.error("Error moving new index into place: " + err);
 | 
			
		||||
        } else {
 | 
			
		||||
            console.log('Reskindex: completed');
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    await fsp.rename(componentIndexTmp, componentIndex);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Expects both arrays of file names to be sorted
 | 
			
		||||
| 
						 | 
				
			
			@ -77,9 +75,17 @@ function filesHaveChanged(files, prevFiles) {
 | 
			
		|||
    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
 | 
			
		||||
if (!args.w) {
 | 
			
		||||
    reskindex();
 | 
			
		||||
    run();
 | 
			
		||||
    return;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -87,5 +93,5 @@ let watchDebouncer = null;
 | 
			
		|||
chokidar.watch(path.join(componentsDir, componentJsGlob)).on('all', (event, path) => {
 | 
			
		||||
    if (path === componentIndex) return;
 | 
			
		||||
    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
 | 
			
		||||
                space={this.state.room}
 | 
			
		||||
                justCreatedOpts={this.props.justCreatedOpts}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -94,26 +94,95 @@ const useMyRoomMembership = (room: Room) => {
 | 
			
		|||
    return membership;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => {
 | 
			
		||||
const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => {
 | 
			
		||||
    const cli = useContext(MatrixClientContext);
 | 
			
		||||
    const myMembership = useMyRoomMembership(space);
 | 
			
		||||
    const joinRule = space.getJoinRule();
 | 
			
		||||
    const userId = cli.getUserId();
 | 
			
		||||
 | 
			
		||||
    let inviterSection;
 | 
			
		||||
    let joinButtons;
 | 
			
		||||
    if (myMembership === "invite") {
 | 
			
		||||
        joinButtons = <div className="mx_SpaceRoomView_landing_joinButtons">
 | 
			
		||||
            <FormButton label={_t("Accept Invite")} onClick={onJoinButtonClicked} />
 | 
			
		||||
            <AccessibleButton kind="link" onClick={onRejectButtonClicked}>
 | 
			
		||||
                {_t("Decline")}
 | 
			
		||||
            </AccessibleButton>
 | 
			
		||||
        </div>;
 | 
			
		||||
    } else if (myMembership !== "join" && joinRule === "public") {
 | 
			
		||||
        joinButtons = <div className="mx_SpaceRoomView_landing_joinButtons">
 | 
			
		||||
            <FormButton label={_t("Join")} onClick={onJoinButtonClicked} />
 | 
			
		||||
        </div>;
 | 
			
		||||
        const inviteSender = space.getMember(cli.getUserId())?.events.member?.getSender();
 | 
			
		||||
        const inviter = inviteSender && space.getMember(inviteSender);
 | 
			
		||||
 | 
			
		||||
        if (inviteSender) {
 | 
			
		||||
            inviterSection = <div className="mx_SpaceRoomView_preview_inviter">
 | 
			
		||||
                <MemberAvatar member={inviter} width={32} height={32} />
 | 
			
		||||
                <div>
 | 
			
		||||
                    <div className="mx_SpaceRoomView_preview_inviter_name">
 | 
			
		||||
                        { _t("<inviter/> invites you", {}, {
 | 
			
		||||
                            inviter: () => <b>{ inviter.name || inviteSender }</b>,
 | 
			
		||||
                        }) }
 | 
			
		||||
                    </div>
 | 
			
		||||
                    { inviter ? <div className="mx_SpaceRoomView_preview_inviter_mxid">
 | 
			
		||||
                        { inviteSender }
 | 
			
		||||
                    </div> : null }
 | 
			
		||||
                </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;
 | 
			
		||||
    if (myMembership === "join" && space.canInvite(userId)) {
 | 
			
		||||
        inviteButton = (
 | 
			
		||||
| 
						 | 
				
			
			@ -227,26 +296,7 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
 | 
			
		|||
                            ) : null}
 | 
			
		||||
                        </RoomMemberCount>
 | 
			
		||||
                    </div> };
 | 
			
		||||
                    if (myMembership === "invite") {
 | 
			
		||||
                        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 (shouldShowSpaceSettings(cli, space)) {
 | 
			
		||||
                        if (space.getJoinRule() === "public") {
 | 
			
		||||
                            return _t("Your public space <name/>", {}, tags) as JSX.Element;
 | 
			
		||||
                        } else {
 | 
			
		||||
| 
						 | 
				
			
			@ -260,7 +310,6 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
 | 
			
		|||
        <div className="mx_SpaceRoomView_landing_topic">
 | 
			
		||||
            <RoomTopic room={space} />
 | 
			
		||||
        </div>
 | 
			
		||||
        { joinButtons }
 | 
			
		||||
        <div className="mx_SpaceRoomView_landing_adminButtons">
 | 
			
		||||
            { inviteButton }
 | 
			
		||||
            { addRoomButtons }
 | 
			
		||||
| 
						 | 
				
			
			@ -548,12 +597,15 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
 | 
			
		|||
    private renderBody() {
 | 
			
		||||
        switch (this.state.phase) {
 | 
			
		||||
            case Phase.Landing:
 | 
			
		||||
                return <SpaceLanding
 | 
			
		||||
                    space={this.props.space}
 | 
			
		||||
                    onJoinButtonClicked={this.props.onJoinButtonClicked}
 | 
			
		||||
                    onRejectButtonClicked={this.props.onRejectButtonClicked}
 | 
			
		||||
                />;
 | 
			
		||||
 | 
			
		||||
                if (this.props.space.getMyMembership() === "join") {
 | 
			
		||||
                    return <SpaceLanding space={this.props.space} />;
 | 
			
		||||
                } else {
 | 
			
		||||
                    return <SpacePreview
 | 
			
		||||
                        space={this.props.space}
 | 
			
		||||
                        onJoinButtonClicked={this.props.onJoinButtonClicked}
 | 
			
		||||
                        onRejectButtonClicked={this.props.onRejectButtonClicked}
 | 
			
		||||
                    />;
 | 
			
		||||
                }
 | 
			
		||||
            case Phase.PublicCreateRooms:
 | 
			
		||||
                return <SpaceSetupFirstRooms
 | 
			
		||||
                    space={this.props.space}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,6 +20,7 @@ import { _t } from '../../../languageHandler';
 | 
			
		|||
import * as sdk from '../../../index';
 | 
			
		||||
import {
 | 
			
		||||
    SetupEncryptionStore,
 | 
			
		||||
    PHASE_LOADING,
 | 
			
		||||
    PHASE_INTRO,
 | 
			
		||||
    PHASE_BUSY,
 | 
			
		||||
    PHASE_DONE,
 | 
			
		||||
| 
						 | 
				
			
			@ -60,7 +61,9 @@ export default class CompleteSecurity extends React.Component {
 | 
			
		|||
        let icon;
 | 
			
		||||
        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" />;
 | 
			
		||||
            title = _t("Verify this login");
 | 
			
		||||
        } else if (phase === PHASE_DONE) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,11 +17,13 @@ limitations under the License.
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { _t } from '../../../languageHandler';
 | 
			
		||||
import SdkConfig from '../../../SdkConfig';
 | 
			
		||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
 | 
			
		||||
import Modal from '../../../Modal';
 | 
			
		||||
import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog';
 | 
			
		||||
import * as sdk from '../../../index';
 | 
			
		||||
import {
 | 
			
		||||
    SetupEncryptionStore,
 | 
			
		||||
    PHASE_LOADING,
 | 
			
		||||
    PHASE_INTRO,
 | 
			
		||||
    PHASE_BUSY,
 | 
			
		||||
    PHASE_DONE,
 | 
			
		||||
| 
						 | 
				
			
			@ -83,6 +85,22 @@ export default class SetupEncryptionBody extends React.Component {
 | 
			
		|||
        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 = () => {
 | 
			
		||||
        const store = SetupEncryptionStore.sharedInstance();
 | 
			
		||||
        store.skip();
 | 
			
		||||
| 
						 | 
				
			
			@ -134,32 +152,22 @@ export default class SetupEncryptionBody extends React.Component {
 | 
			
		|||
                </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 (
 | 
			
		||||
                <div>
 | 
			
		||||
                    <p>{_t(
 | 
			
		||||
                        "Confirm your identity by verifying this login from one of your other sessions, " +
 | 
			
		||||
                        "granting it access to encrypted messages.",
 | 
			
		||||
                        "Verify this login to access your encrypted messages and " +
 | 
			
		||||
                        "prove to others that this login is really you.",
 | 
			
		||||
                    )}</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">
 | 
			
		||||
                        {verifyButton}
 | 
			
		||||
                        {useRecoveryKeyButton}
 | 
			
		||||
                        <AccessibleButton kind="danger" onClick={this.onSkipClick}>
 | 
			
		||||
                            {_t("Skip")}
 | 
			
		||||
| 
						 | 
				
			
			@ -217,7 +225,7 @@ export default class SetupEncryptionBody extends React.Component {
 | 
			
		|||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            );
 | 
			
		||||
        } else if (phase === PHASE_BUSY) {
 | 
			
		||||
        } else if (phase === PHASE_BUSY || phase === PHASE_LOADING) {
 | 
			
		||||
            const Spinner = sdk.getComponent('views.elements.Spinner');
 | 
			
		||||
            return <Spinner />;
 | 
			
		||||
        } else {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -66,6 +66,10 @@ export default class NewSessionReviewDialog extends React.PureComponent {
 | 
			
		|||
        Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, {
 | 
			
		||||
            verificationRequestPromise: requestPromise,
 | 
			
		||||
            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,
 | 
			
		||||
        verificationRequestPromise: PropTypes.object,
 | 
			
		||||
        onFinished: PropTypes.func.isRequired,
 | 
			
		||||
        member: PropTypes.string,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    constructor(...args) {
 | 
			
		||||
        super(...args);
 | 
			
		||||
        this.onFinished = this.onFinished.bind(this);
 | 
			
		||||
        this.state = {};
 | 
			
		||||
        if (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 ?
 | 
			
		||||
            _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"
 | 
			
		||||
                title={title}
 | 
			
		||||
                hasCancel={true}
 | 
			
		||||
| 
						 | 
				
			
			@ -66,13 +66,4 @@ export default class VerificationRequestDialog extends React.Component {
 | 
			
		|||
            />
 | 
			
		||||
        </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 {legacyVerifyUser, verifyDevice, verifyUser} from '../../../verification';
 | 
			
		||||
import {Action} from "../../../dispatcher/actions";
 | 
			
		||||
import { USER_SECURITY_TAB } from "../dialogs/UserSettingsDialog";
 | 
			
		||||
import {useIsEncrypted} from "../../../hooks/useIsEncrypted";
 | 
			
		||||
import BaseCard from "./BaseCard";
 | 
			
		||||
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 = (
 | 
			
		||||
        <div className="mx_UserInfo_container">
 | 
			
		||||
            <h3>{ _t("Security") }</h3>
 | 
			
		||||
| 
						 | 
				
			
			@ -1377,6 +1392,7 @@ const BasicUserInfo: React.FC<{
 | 
			
		|||
                loading={showDeviceListSpinner}
 | 
			
		||||
                devices={devices}
 | 
			
		||||
                userId={member.userId} /> }
 | 
			
		||||
            { editDevices }
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -158,14 +158,14 @@ export default class ReadReceiptMarker extends React.PureComponent {
 | 
			
		|||
        // then shift to the rightmost column,
 | 
			
		||||
        // 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
 | 
			
		||||
        // 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
 | 
			
		||||
        // value so small it's not perceived by a user.
 | 
			
		||||
        // Note: Any smaller values (or trying to interchange units) might cause read receipts to
 | 
			
		||||
        // fail to fall down or cause gaps.
 | 
			
		||||
        startStyles.push({ top: startTopOffset+'px', left: '0.001px' });
 | 
			
		||||
        startStyles.push({ top: startTopOffset+'px', left: '1px' });
 | 
			
		||||
        enterTransitionOpts.push({
 | 
			
		||||
            duration: bounce ? Math.min(Math.log(Math.abs(startTopOffset)) * 200, 3000) : 300,
 | 
			
		||||
            easing: bounce ? 'easeOutBounce' : 'easeOutCubic',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -39,6 +39,7 @@ interface IProps {
 | 
			
		|||
interface IState {
 | 
			
		||||
    counter: number;
 | 
			
		||||
    device?: DeviceInfo;
 | 
			
		||||
    ip?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@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).
 | 
			
		||||
        this._checkRequestIsPending();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        if (request.isSelfVerification) {
 | 
			
		||||
            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");
 | 
			
		||||
                Modal.createTrackedDialog('Incoming Verification', '', VerificationRequestDialog, {
 | 
			
		||||
                    verificationRequest: request,
 | 
			
		||||
                    onFinished: () => {
 | 
			
		||||
                        request.cancel();
 | 
			
		||||
                    },
 | 
			
		||||
                }, null, /* priority = */ false, /* static = */ true);
 | 
			
		||||
            }
 | 
			
		||||
            await request.accept();
 | 
			
		||||
| 
						 | 
				
			
			@ -133,9 +143,10 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
 | 
			
		|||
        let nameLabel;
 | 
			
		||||
        if (request.isSelfVerification) {
 | 
			
		||||
            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(),
 | 
			
		||||
                    deviceId: this.state.device.deviceId,
 | 
			
		||||
                    ip: this.state.ip,
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        } 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>.",
 | 
			
		||||
    "Yes": "Yes",
 | 
			
		||||
    "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",
 | 
			
		||||
    "Review": "Review",
 | 
			
		||||
    "Later": "Later",
 | 
			
		||||
| 
						 | 
				
			
			@ -753,7 +753,8 @@
 | 
			
		|||
    "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",
 | 
			
		||||
    "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",
 | 
			
		||||
    "Update": "Update",
 | 
			
		||||
| 
						 | 
				
			
			@ -980,7 +981,7 @@
 | 
			
		|||
    "Folder": "Folder",
 | 
			
		||||
    "Pin": "Pin",
 | 
			
		||||
    "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)",
 | 
			
		||||
    "Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
 | 
			
		||||
    "Delete": "Delete",
 | 
			
		||||
| 
						 | 
				
			
			@ -1756,6 +1757,7 @@
 | 
			
		|||
    "Failed to deactivate user": "Failed to deactivate user",
 | 
			
		||||
    "Role": "Role",
 | 
			
		||||
    "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.",
 | 
			
		||||
    "Edit devices": "Edit devices",
 | 
			
		||||
    "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.",
 | 
			
		||||
    "Verify by scanning": "Verify by scanning",
 | 
			
		||||
| 
						 | 
				
			
			@ -2609,14 +2611,14 @@
 | 
			
		|||
    "Promoted to users": "Promoted to users",
 | 
			
		||||
    "Manage rooms": "Manage rooms",
 | 
			
		||||
    "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",
 | 
			
		||||
    "Default Rooms": "Default Rooms",
 | 
			
		||||
    "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 private space <name/>": "Your private space <name/>",
 | 
			
		||||
    "Welcome to <name/>": "Welcome to <name/>",
 | 
			
		||||
| 
						 | 
				
			
			@ -2720,13 +2722,8 @@
 | 
			
		|||
    "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": "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.",
 | 
			
		||||
    "This requires the latest %(brand)s on your other devices:": "This requires the latest %(brand)s on your other devices:",
 | 
			
		||||
    "%(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",
 | 
			
		||||
    "Verify with another session": "Verify with another session",
 | 
			
		||||
    "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.",
 | 
			
		||||
    "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.",
 | 
			
		||||
    "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 { PHASE_DONE as VERIF_PHASE_DONE } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
 | 
			
		||||
 | 
			
		||||
export const PHASE_INTRO = 0;
 | 
			
		||||
export const PHASE_BUSY = 1;
 | 
			
		||||
export const PHASE_DONE = 2;    //final done stage, but still showing UX
 | 
			
		||||
export const PHASE_CONFIRM_SKIP = 3;
 | 
			
		||||
export const PHASE_FINISHED = 4; //UX can be closed
 | 
			
		||||
export const PHASE_LOADING = 0;
 | 
			
		||||
export const PHASE_INTRO = 1;
 | 
			
		||||
export const PHASE_BUSY = 2;
 | 
			
		||||
export const PHASE_DONE = 3;    //final done stage, but still showing UX
 | 
			
		||||
export const PHASE_CONFIRM_SKIP = 4;
 | 
			
		||||
export const PHASE_FINISHED = 5; //UX can be closed
 | 
			
		||||
 | 
			
		||||
export class SetupEncryptionStore extends EventEmitter {
 | 
			
		||||
    static sharedInstance() {
 | 
			
		||||
| 
						 | 
				
			
			@ -36,7 +37,7 @@ export class SetupEncryptionStore extends EventEmitter {
 | 
			
		|||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        this._started = true;
 | 
			
		||||
        this.phase = PHASE_BUSY;
 | 
			
		||||
        this.phase = PHASE_LOADING;
 | 
			
		||||
        this.verificationRequest = null;
 | 
			
		||||
        this.backupInfo = null;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -75,7 +76,8 @@ export class SetupEncryptionStore extends EventEmitter {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    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) {
 | 
			
		||||
            this.keyId = null;
 | 
			
		||||
            this.keyInfo = null;
 | 
			
		||||
| 
						 | 
				
			
			@ -85,7 +87,20 @@ export class SetupEncryptionStore extends EventEmitter {
 | 
			
		|||
            this.keyInfo = keys[this.keyId];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.phase = PHASE_INTRO;
 | 
			
		||||
        // 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.emit("update");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -39,7 +39,7 @@ export const showToast = (deviceIds: Set<string>) => {
 | 
			
		|||
 | 
			
		||||
    ToastStore.sharedInstance().addOrReplaceToast({
 | 
			
		||||
        key: TOAST_KEY,
 | 
			
		||||
        title: _t("Review where you’re logged in"),
 | 
			
		||||
        title: _t("You have unverified logins"),
 | 
			
		||||
        icon: "verification_warning",
 | 
			
		||||
        props: {
 | 
			
		||||
            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 dis from "../dispatcher/dispatcher";
 | 
			
		||||
import { MatrixClientPeg } from '../MatrixClientPeg';
 | 
			
		||||
import Modal from '../Modal';
 | 
			
		||||
import DeviceListener from '../DeviceListener';
 | 
			
		||||
import NewSessionReviewDialog from '../components/views/dialogs/NewSessionReviewDialog';
 | 
			
		||||
import ToastStore from "../stores/ToastStore";
 | 
			
		||||
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) {
 | 
			
		||||
    return "unverified_session_" + deviceId;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const showToast = (deviceId: string) => {
 | 
			
		||||
export const showToast = async (deviceId: string) => {
 | 
			
		||||
    const cli = MatrixClientPeg.get();
 | 
			
		||||
 | 
			
		||||
    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]);
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
        }, null, /* priority = */ false, /* static = */ true);
 | 
			
		||||
        DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]);
 | 
			
		||||
        dis.dispatch({
 | 
			
		||||
            action: Action.ViewUserSettings,
 | 
			
		||||
            initialTabId: USER_SECURITY_TAB,
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const onReject = () => {
 | 
			
		||||
        DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const device = cli.getStoredDevice(cli.getUserId(), deviceId);
 | 
			
		||||
    const device = await cli.getDevice(deviceId);
 | 
			
		||||
 | 
			
		||||
    ToastStore.sharedInstance().addOrReplaceToast({
 | 
			
		||||
        key: toastKey(deviceId),
 | 
			
		||||
| 
						 | 
				
			
			@ -54,8 +50,13 @@ export const showToast = (deviceId: string) => {
 | 
			
		|||
        icon: "verification_warning",
 | 
			
		||||
        props: {
 | 
			
		||||
            description: _t(
 | 
			
		||||
                "Verify the new login accessing your account: %(name)s", { name: device.getDisplayName()}),
 | 
			
		||||
            acceptLabel: _t("Verify"),
 | 
			
		||||
                "A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s", {
 | 
			
		||||
                    name: device.display_name,
 | 
			
		||||
                    deviceID: deviceId,
 | 
			
		||||
                    ip: device.last_seen_ip,
 | 
			
		||||
                },
 | 
			
		||||
            ),
 | 
			
		||||
            acceptLabel: _t("Check your devices"),
 | 
			
		||||
            onAccept,
 | 
			
		||||
            rejectLabel: _t("Later"),
 | 
			
		||||
            onReject,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue