diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 5b936e822c6..fe997803ce7 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -42,6 +42,10 @@ import {UIFeature} from "../../../settings/UIFeature"; import CountlyAnalytics from "../../../CountlyAnalytics"; import {Room} from "matrix-js-sdk/src/models/room"; import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; +import {EventTimeline} from "matrix-js-sdk/src/models/event-timeline"; +import {MatrixEvent} from "matrix-js-sdk/src/models/event"; +import {getAddressType} from "../../../UserAddress"; +import {sleep} from "../../../utils/promise"; // we have a number of types defined from the Matrix spec which can't reasonably be altered here. /* eslint-disable camelcase */ @@ -53,6 +57,70 @@ export const KIND_CALL_TRANSFER = "call_transfer"; const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked +/** + * Iterate backwards through the message history, returning messages that are + * visible to all members of the room (`m.room.history_visibility` is + * `world_readable` or `shared`), until the most recent message that is not. + * + * This function is intended to be used with MatrixClient.shareKeysForMessages. + */ +function iterateShareableHistoryForRoom(client, room) { + let timeline = room.getLiveTimeline(); + const visibilityEvent = timeline.getState(EventTimeline.FORWARDS) + .getStateEvents("m.room.history_visibility", ""); + let visibility = visibilityEvent && visibilityEvent.getContent() && + visibilityEvent.getContent().history_visibility; + let events = timeline.getEvents(); + let index = events.length; + let paginationToken; + const next = async () => { + if (index === 0) { + // get prev chunk + if (!paginationToken) { + const prevTimeline = timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS); + if (prevTimeline) { + timeline = prevTimeline; + events = timeline.getEvents(); + } else { + paginationToken = timeline.getPaginationToken(EventTimeline.BACKWARDS); + timeline = undefined; + } + } + if (!timeline && !paginationToken) { + return; + } else if (paginationToken) { + await sleep(1000); + const res = await client._createMessagesRequest( + room.roomId, paginationToken, 30, "b", + ); + if (res.end === paginationToken || res.chunk.length === 0) { + // no new messages + paginationToken = undefined; + return; + } else { + paginationToken = res.end; + events = res.chunk.reverse().map(e => new MatrixEvent(e)); + } + } + index = events.length; + } + index--; + const event = events[index]; + if (event.isState()) { + if (event.getType() === "m.room.history_visibility") { + visibility = event.getPrevContent() && + event.getPrevContent().history_visibility; + } + return next(); + } + if (visibility !== "world_readable" && visibility !== "shared") { + return; + } + return event; + }; + return next; +} + // This is the interface that is expected by various components in this file. It is a bit // awkward because it also matches the RoomMember class from the js-sdk with some extra support // for 3PIDs/email addresses. @@ -676,14 +744,15 @@ export default class InviteDialog extends React.PureComponent { + _inviteUsers = async () => { const startTime = CountlyAnalytics.getTimestamp(); this.setState({busy: true}); this._convertFilter(); const targets = this._convertFilter(); const targetIds = targets.map(t => t.userId); - const room = MatrixClientPeg.get().getRoom(this.props.roomId); + const cli = MatrixClientPeg.get(); + const room = cli.getRoom(this.props.roomId); if (!room) { console.error("Failed to find the room to invite users to"); this.setState({ @@ -693,12 +762,35 @@ export default class InviteDialog extends React.PureComponent { + try { + const res = await inviteMultipleToRoom(this.props.roomId, targetIds) CountlyAnalytics.instance.trackSendInvite(startTime, this.props.roomId, targetIds.length); - if (!this._shouldAbortAfterInviteError(result)) { // handles setting error message too + if (!this._shouldAbortAfterInviteError(res)) { // handles setting error message too this.props.onFinished(); } - }).catch(err => { + + if (cli.isRoomEncrypted(this.props.roomId) && + SettingsStore.getValue("feature_room_history_key_sharing")) { + const visibilityEvent = room.currentState.getStateEvents( + "m.room.history_visibility", "", + ); + const visibility = visibilityEvent && visibilityEvent.getContent() && + visibilityEvent.getContent().history_visibility; + if (visibility == "world_readable" || visibility == "shared") { + const invitedUsers = []; + for (const [addr, state] of Object.entries(res.states)) { + if (state === "invited" && getAddressType(addr) === "mx-user-id") { + invitedUsers.push(addr); + } + } + console.log("Sharing history with", invitedUsers); + cli.shareKeysForMessages( + this.props.roomId, invitedUsers, + iterateShareableHistoryForRoom(cli, room), + ); + } + } + } catch (err) { console.error(err); this.setState({ busy: false, @@ -706,7 +798,7 @@ export default class InviteDialog extends React.PureComponent { @@ -1187,10 +1279,12 @@ export default class InviteDialog extends React.PureComponent; const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer); - const userId = MatrixClientPeg.get().getUserId(); + const cli = MatrixClientPeg.get(); + const userId = cli.getUserId(); if (this.props.kind === KIND_DM) { title = _t("Direct Messages"); @@ -1281,6 +1375,22 @@ export default class InviteDialog extends React.PureComponent + {_t("Note: Decryption keys for old messages will be shared with invited users.")} + ; + } + } } else if (this.props.kind === KIND_CALL_TRANSFER) { title = _t("Transfer"); buttonText = _t("Transfer"); @@ -1314,6 +1424,7 @@ export default class InviteDialog extends React.PureComponent + {keySharingWarning} {this._renderIdentityServerWarning()}
{this.state.errorText}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 38460a5f6e1..dc808cb8bd7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -791,6 +791,7 @@ "Show message previews for reactions in DMs": "Show message previews for reactions in DMs", "Show message previews for reactions in all rooms": "Show message previews for reactions in all rooms", "Offline encrypted messaging using dehydrated devices": "Offline encrypted messaging using dehydrated devices", + "Share decryption keys for room history when inviting users": "Share decryption keys for room history when inviting users", "Enable advanced debugging for the room list": "Enable advanced debugging for the room list", "Show info about bridges in room settings": "Show info about bridges in room settings", "Font size": "Font size", @@ -2153,6 +2154,7 @@ "Go": "Go", "Invite someone using their name, email address, username (like ) or share this room.": "Invite someone using their name, email address, username (like ) or share this room.", "Invite someone using their name, username (like ) or share this room.": "Invite someone using their name, username (like ) or share this room.", + "Note: Decryption keys for old messages will be shared with invited users.": "Note: Decryption keys for old messages will be shared with invited users.", "Transfer": "Transfer", "a new master key signature": "a new master key signature", "a new cross-signing key signature": "a new cross-signing key signature", diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 43210021e56..77b0f187c72 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -214,6 +214,12 @@ export const SETTINGS: {[setting: string]: ISetting} = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_room_history_key_sharing": { + isFeature: true, + displayName: _td("Share decryption keys for room history when inviting users"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "advancedRoomListLogging": { // TODO: Remove flag before launch: https://github.com/vector-im/element-web/issues/14231 displayName: _td("Enable advanced debugging for the room list"),