Room Invite Flows#
A room invite in Matrix follows the path: inviter sends m.room.member invite event → invitee sees the invite in their room list → invitee accepts (joins) or declines. Element Web adds several layers on top of this protocol-level flow:
- Identity confirmation — warns before inviting users whose cryptographic identity is unknown
- Error normalization — translates invite-specific join failures (e.g. "unreachable room") into user-friendly messages
- Encrypted history sharing (MSC4268) — optionally sends a bundle of historical room keys to the invited user so they can decrypt past messages
The implementation spans two repositories: element-web (UI, dialogs, RoomViewStore) and matrix-js-sdk (client API, crypto backend, rust-crypto).
Invite UI: Unknown Identity Confirmation#
Before sending any invite, InviteDialog.tsx checks whether each target's cryptographic identity is known. The check runs in onGoButtonPressed(): for each target it calls crypto.getUserVerificationStatus(userId).known . Users that return false (or that are not Matrix user IDs) are collected into unknownIdentityUsers.
If any unknown-identity users are present, the dialog transitions to UnknownIdentityUsersWarningDialog instead of proceeding to send invites. The warning dialog has two modes :
- Invite mode — "Invite" (continue anyway) or "Remove" (remove the unknown users from the target list)
- DM mode — "Continue" or "Cancel"
If the unknown-identity list is empty, startDmOrSendInvites() is called directly . This feature was introduced in .
Join Error Handling and Unreachable Rooms#
When a user accepts an invite and the join attempt fails, RoomViewStore.showJoinRoomError() normalizes the error and shows a modal. The key error cases are:
| Condition | Message shown |
|---|---|
ConnectionError | Generic connection error |
M_INCOMPATIBLE_ROOM_VERSION | Room version mismatch |
| HTTP 404 + invite present, same HS | "The person who invited you has already left." |
| HTTP 404 + invite present, federated | "The person who invited you has already left, or their server is offline." |
| HTTP 404, no invite, no via-servers | Explanation about needing via-servers |
The 404 "unreachable room" path is the most invite-specific case. getInvitingUserId() identifies the inviter by checking the room's current invite membership event, then the server suffix of the inviting user ID determines which of the two messages is shown .
Type safety : JoinRoomErrorPayload.err was widened from MatrixError to generic Error, and roomId was made string | null. Non-Error values thrown during join are now wrapped in UserFriendlyError before dispatch. The null guard roomId && this.getInvitingUserId(roomId) prevents crashes when roomId is unavailable . joinRoomError() calls showJoinRoomError() only when !canAskToJoin , so "knock" flows are not affected.
Encrypted History Sharing (MSC4268)#
MSC4268 defines how a room member can share historical encryption keys with a newly invited user via an m.room_key_bundle to-device message. Three interlinked matrix-js-sdk PRs implement this in the rust-crypto backend.
History Visibility Gate#
Key bundles are only sent when the room's current history visibility is shared or world_readable . If visibility is invited or joined, shareRoomHistoryWithUser() skips the bundle and logs a debug message. This prevents accidentally exposing encrypted history to users who should not see pre-invite messages. The check uses room.getHistoryVisibility() in the invite flow in src/client.ts .
Late Bundle Arrival#
There is a race condition where the invitee may join the room before the m.room_key_bundle to-device message arrives from the inviter's server. The fix in :
joinRoom({ acceptSharedHistory: true })callsmaybeAcceptKeyBundle()on the crypto backend, which returnsPromise<boolean>true→ bundle is already present and imported immediatelyfalse→ bundle not yet arrived;markRoomAsPendingKeyBundle(roomId, inviterId)is called so the backend will auto-import the bundle when it arrives via to-device delivery
The markRoomAsPendingKeyBundle() method is a new addition to the CryptoBackend interface . Handling across session reloads was deferred to a separate PR (partially fixes element-web #30740).
Key Backup Before Bundle Construction#
When building the key bundle to send to an invitee, the local client may be missing keys for older messages (e.g., if the client recently logged in). To avoid incomplete bundles, shareRoomHistoryWithUser() in src/rust-crypto/rust-crypto.ts now:
- Checks
olmMachine.hasDownloadedAllRoomKeys()for the room - If not downloaded, calls
backupManager.downloadLatestRoomKeyBackup(roomId)— a new method insrc/rust-crypto/backup.tsthat hits/room_keys/keys/{roomId}and imports the result - Marks the room as fully downloaded to avoid redundant backup fetches on repeat invites
This ensures the bundle contains a complete key set for all shared-history messages (closes matrix-rust-sdk #5111).
Key Source Files and Entry Points#
| File | Purpose |
|---|---|
apps/web/src/components/views/dialogs/InviteDialog.tsx | Main invite UI; onGoButtonPressed() triggers identity checks |
apps/web/src/components/views/dialogs/invite/UnknownIdentityUsersWarningDialog.tsx | Warning dialog for unknown-identity users (DM & invite modes) |
apps/web/src/stores/RoomViewStore.tsx | showJoinRoomError(), getInvitingUserId() — join error normalization |
apps/web/src/dispatcher/payloads/JoinRoomErrorPayload.ts | JoinRoomErrorPayload type (err: Error, roomId: string | null) |
src/client.ts (matrix-js-sdk) | invite(), joinRoom(), history visibility gate for MSC4268 |
src/rust-crypto/rust-crypto.ts | shareRoomHistoryWithUser() — builds and sends key bundle |
src/rust-crypto/backup.ts | downloadLatestRoomKeyBackup(roomId) — fetches room keys from backup |
src/common-crypto/CryptoBackend.ts | CryptoBackend interface: maybeAcceptKeyBundle(), markRoomAsPendingKeyBundle() |
spec/integ/crypto/history-sharing.spec.ts | Integration tests for MSC4268 history sharing flows |
Related PRs: element-web #32621 (join error handling), #33171 (unknown identity dialog), matrix-js-sdk #5216 (history visibility gate), #5080 (late bundle arrival), #5171 (backup before bundle)