diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 30845f47bad..0d1806b8eff 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1534,6 +1534,9 @@ "render_reaction_images_description": "Sometimes referred to as \"custom emojis\".", "report_to_moderators": "Report to moderators", "report_to_moderators_description": "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.", + "share_history_on_invite": "Share encrypted history with new members", + "share_history_on_invite_description": "When inviting a user to an encrypted room that has history visibility set to \"shared\", share encrypted history with that user, and accept encrypted history when you are invited to such a room.", + "share_history_on_invite_warning": "This feature is EXPERIMENTAL and not all security precautions are implemented. Do not enable on production accounts.", "sliding_sync": "Sliding Sync mode", "sliding_sync_description": "Under active development, cannot be disabled.", "sliding_sync_disabled_notice": "Log out and back in to disable", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 62a24214b94..3adb29a23eb 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -205,6 +205,7 @@ export interface Settings { "feature_mjolnir": IFeature; "feature_custom_themes": IFeature; "feature_exclude_insecure_devices": IFeature; + "feature_share_history_on_invite": IFeature; "feature_html_topic": IFeature; "feature_bridge_state": IFeature; "feature_jump_to_date": IFeature; @@ -503,6 +504,29 @@ export const SETTINGS: Settings = { supportedLevelsAreOrdered: true, default: false, }, + "feature_share_history_on_invite": { + isFeature: true, + labsGroup: LabGroup.Encryption, + displayName: _td("labs|share_history_on_invite"), + description: () => ( + <> + {_t("labs|share_history_on_invite_description")} +
+ {_t( + "settings|warning", + {}, + { + w: (sub) => {sub}, + description: _t("labs|share_history_on_invite_warning"), + }, + )} +
+ + ), + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG_PRIORITISED, + supportedLevelsAreOrdered: true, + default: false, + }, "useOnlyCurrentProfiles": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td("settings|disable_historical_profile"), diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index 633d55f3f63..4e1b89d8e8c 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import React, { type ReactNode } from "react"; import * as utils from "matrix-js-sdk/src/utils"; -import { MatrixError, JoinRule, type Room, type MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { MatrixError, JoinRule, type Room, type MatrixEvent, type IJoinRoomOpts } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; import { type ViewRoom as ViewRoomEvent } from "@matrix-org/analytics-events/types/typescript/ViewRoom"; @@ -512,15 +512,19 @@ export class RoomViewStore extends EventEmitter { // take a copy of roomAlias & roomId as they may change by the time the join is complete const { roomAlias, roomId = payload.roomId } = this.state; const address = roomAlias || roomId!; - const viaServers = this.state.viaServers || []; + + const joinOpts: IJoinRoomOpts = { + viaServers: this.state.viaServers || [], + ...(payload.opts ?? {}), + }; + if (SettingsStore.getValue("feature_share_history_on_invite")) { + joinOpts.acceptSharedHistory = true; + } + try { const cli = MatrixClientPeg.safeGet(); await retry( - () => - cli.joinRoom(address, { - viaServers, - ...(payload.opts || {}), - }), + () => cli.joinRoom(address, joinOpts), NUM_JOIN_RETRY, (err) => { // if we received a Gateway timeout or Cloudflare timeout then retry diff --git a/src/utils/MultiInviter.ts b/src/utils/MultiInviter.ts index f8310de8bdf..9ad16c05493 100644 --- a/src/utils/MultiInviter.ts +++ b/src/utils/MultiInviter.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { MatrixError, type MatrixClient, EventType, type EmptyObject } from "matrix-js-sdk/src/matrix"; +import { MatrixError, type MatrixClient, EventType, type EmptyObject, type InviteOpts } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; @@ -183,7 +183,11 @@ export default class MultiInviter { } } - return this.matrixClient.invite(roomId, addr, this.reason); + const opts: InviteOpts = {}; + if (this.reason !== undefined) opts.reason = this.reason; + if (SettingsStore.getValue("feature_share_history_on_invite")) opts.shareEncryptedHistory = true; + + return this.matrixClient.invite(roomId, addr, opts); } else { throw new Error("Unsupported address"); } diff --git a/test/unit-tests/stores/RoomViewStore-test.ts b/test/unit-tests/stores/RoomViewStore-test.ts index 71bc57160de..53840b0c324 100644 --- a/test/unit-tests/stores/RoomViewStore-test.ts +++ b/test/unit-tests/stores/RoomViewStore-test.ts @@ -440,6 +440,17 @@ describe("RoomViewStore", function () { }); expect(mocked(dis.dispatch).mock.calls[2][0]).toEqual({ action: "prompt_ask_to_join" }); }); + + it("sets 'acceptSharedHistory' if that option is enabled", async () => { + jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName, roomId, value) => { + return settingName === "feature_share_history_on_invite"; // this is enabled, everything else is disabled. + }); + + dis.dispatch({ action: Action.ViewRoom, room_id: roomId }); + dis.dispatch({ action: Action.JoinRoom }); + await untilDispatch(Action.JoinRoomReady, dis); + expect(mockClient.joinRoom).toHaveBeenCalledWith(roomId, { acceptSharedHistory: true, viaServers: [] }); + }); }); describe("Action.JoinRoomError", () => { diff --git a/test/unit-tests/utils/MultiInviter-test.ts b/test/unit-tests/utils/MultiInviter-test.ts index 998334c9af4..8fcc6731433 100644 --- a/test/unit-tests/utils/MultiInviter-test.ts +++ b/test/unit-tests/utils/MultiInviter-test.ts @@ -96,9 +96,9 @@ describe("MultiInviter", () => { const result = await inviter.invite([MXID1, MXID2, MXID3]); expect(client.invite).toHaveBeenCalledTimes(3); - expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, undefined); - expect(client.invite).toHaveBeenNthCalledWith(2, ROOMID, MXID2, undefined); - expect(client.invite).toHaveBeenNthCalledWith(3, ROOMID, MXID3, undefined); + expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, {}); + expect(client.invite).toHaveBeenNthCalledWith(2, ROOMID, MXID2, {}); + expect(client.invite).toHaveBeenNthCalledWith(3, ROOMID, MXID3, {}); expectAllInvitedResult(result); }); @@ -114,9 +114,9 @@ describe("MultiInviter", () => { const result = await inviter.invite([MXID1, MXID2, MXID3]); expect(client.invite).toHaveBeenCalledTimes(3); - expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, undefined); - expect(client.invite).toHaveBeenNthCalledWith(2, ROOMID, MXID2, undefined); - expect(client.invite).toHaveBeenNthCalledWith(3, ROOMID, MXID3, undefined); + expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, {}); + expect(client.invite).toHaveBeenNthCalledWith(2, ROOMID, MXID2, {}); + expect(client.invite).toHaveBeenNthCalledWith(3, ROOMID, MXID3, {}); expectAllInvitedResult(result); }); @@ -129,7 +129,7 @@ describe("MultiInviter", () => { const result = await inviter.invite([MXID1, MXID2, MXID3]); expect(client.invite).toHaveBeenCalledTimes(1); - expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, undefined); + expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, {}); // The resolved state is 'invited' for all users. // With the above client expectations, the test ensures that only the first user is invited. @@ -231,5 +231,15 @@ describe("MultiInviter", () => { `"This space is unfederated. You cannot invite people from external servers."`, ); }); + + it("should set shareEncryptedHistory if that setting is enabled", async () => { + mocked(SettingsStore.getValue).mockImplementation((settingName, roomId, value) => { + return settingName === "feature_share_history_on_invite"; // this is enabled, everything else is disabled. + }); + await inviter.invite([MXID1]); + + expect(client.invite).toHaveBeenCalledTimes(1); + expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, { shareEncryptedHistory: true }); + }); }); });