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 });
+ });
});
});