Promote "Share encrypted history" from labs (#33281)

* Promote "Share encrypted history" from labs

* fix lint

* Update labs.md

* update snapshots

* Update unit tests

* update playwright screenshot
This commit is contained in:
Richard van der Hoff 2026-04-23 17:04:09 +01:00 committed by GitHub
parent c02b970d35
commit c4638d1773
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 72 additions and 107 deletions

View File

@ -13,7 +13,6 @@ import { createRoom, sendMessageInCurrentRoom } from "./utils";
test.use({
displayName: "Alice",
labsFlags: ["feature_share_history_on_invite"],
});
/** Tests for MSC4268: encrypted history sharing */

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -446,7 +446,6 @@ export default function RoomHeader({
const roomName = useRoomName(room);
const joinRule = useRoomState(room, (state) => state.getJoinRule());
const historyVisibility = useRoomState(room, (state) => state.getHistoryVisibility());
const historySharingEnabled = useFeatureEnabled("feature_share_history_on_invite");
const dmMember = useDmMember(room);
const isDirectMessage = !!dmMember;
const isRoomEncrypted = useIsEncrypted(client, room);
@ -532,7 +531,7 @@ export default function RoomHeader({
</Tooltip>
)}
{isRoomEncrypted && historySharingEnabled && historyVisibilityIcon(historyVisibility)}
{isRoomEncrypted && historyVisibilityIcon(historyVisibility)}
</Text>
</Box>
</button>

View File

@ -1577,9 +1577,6 @@
"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.",
"room_list_sections": "Room list sections",
"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. Currently, not compatible with Element Call.",
"sliding_sync_disabled_notice": "Sign in again to disable",

View File

@ -212,7 +212,6 @@ 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;
@ -522,29 +521,6 @@ 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")}
<div className="mx_SettingsFlag_microcopy">
{_t(
"settings|warning",
{},
{
w: (sub) => <span className="mx_SettingsTab_microcopy_warning">{sub}</span>,
description: _t("labs|share_history_on_invite_warning"),
},
)}
</div>
</>
),
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG_PRIORITISED,
supportedLevelsAreOrdered: true,
default: false,
},
// Defaulted to true Feb 26, intention is to remove entirely, all being well,
// as this fixes bugs where display name / avatar are missing and also makes
// Element Web consistent with Element X.

View File

@ -544,11 +544,9 @@ export class RoomViewStore extends EventEmitter {
const joinOpts: IJoinRoomOpts = {
viaServers,
acceptSharedHistory: true,
...(payload.opts ?? {}),
};
if (SettingsStore.getValue("feature_share_history_on_invite")) {
joinOpts.acceptSharedHistory = true;
}
try {
const cli = MatrixClientPeg.safeGet();
await retry<Room, MatrixError>(

View File

@ -228,10 +228,10 @@ export default class MultiInviter {
}
}
const opts: InviteOpts = {};
const opts: InviteOpts = {
shareEncryptedHistory: true,
};
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");

View File

@ -212,7 +212,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
</div>
</div>
<div
aria-labelledby="_r_1c3_"
aria-labelledby="_r_1cl_"
class="_banner_n7ud0_8"
data-type="critical"
role="status"
@ -238,7 +238,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
>
<p
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _title_1xryk_24"
id="_r_1c3_"
id="_r_1cl_"
>
Could not start a chat with this user
</p>
@ -417,7 +417,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
>
<svg
aria-label="Messages in this room are not end-to-end encrypted"
aria-labelledby="_r_197_"
aria-labelledby="_r_19j_"
class="mx_E2EIcon mx_MessageComposer_e2eIcon"
color="var(--cpd-color-icon-info-primary)"
fill="currentColor"
@ -693,6 +693,24 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
>
@user:example.com
</span>
<svg
aria-label="New members see history"
aria-labelledby="_r_1cf_"
class="mx_RoomHeader_icon"
color="var(--cpd-color-icon-info-primary)"
fill="currentColor"
height="16px"
viewBox="0 0 24 24"
width="16px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M18.93 8A8 8 0 1 1 4 12a1 1 0 1 0-2 0c0 5.523 4.477 10 10 10s10-4.477 10-10a10 10 0 0 0-.832-4A10 10 0 0 0 12 2a9.99 9.99 0 0 0-8 3.999V4a1 1 0 0 0-2 0v4a1 1 0 0 0 1 1h4a1 1 0 0 0 0-2H5.755A7.99 7.99 0 0 1 12 4a8 8 0 0 1 6.93 4"
/>
<path
d="M13 8a1 1 0 1 0-2 0v4a1 1 0 0 0 .293.707l2.83 2.83a1 1 0 0 0 1.414-1.414L13 11.586z"
/>
</svg>
</div>
</div>
</button>
@ -2838,6 +2856,24 @@ exports[`RoomView should not display the timeline when the room encryption is lo
>
!roomviewshouldnotdisplaythetimelinewhentheroomencryptionisloading:example.org
</span>
<svg
aria-label="New members see history"
aria-labelledby="_r_gc_"
class="mx_RoomHeader_icon"
color="var(--cpd-color-icon-info-primary)"
fill="currentColor"
height="16px"
viewBox="0 0 24 24"
width="16px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M18.93 8A8 8 0 1 1 4 12a1 1 0 1 0-2 0c0 5.523 4.477 10 10 10s10-4.477 10-10a10 10 0 0 0-.832-4A10 10 0 0 0 12 2a9.99 9.99 0 0 0-8 3.999V4a1 1 0 0 0-2 0v4a1 1 0 0 0 1 1h4a1 1 0 0 0 0-2H5.755A7.99 7.99 0 0 1 12 4a8 8 0 0 1 6.93 4"
/>
<path
d="M13 8a1 1 0 1 0-2 0v4a1 1 0 0 0 .293.707l2.83 2.83a1 1 0 0 0 1.414-1.414L13 11.586z"
/>
</svg>
</div>
</div>
</button>
@ -3023,7 +3059,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
tabindex="0"
>
<div
aria-labelledby="_r_gc_"
aria-labelledby="_r_gh_"
class="mx_E2EIcon mx_MessageComposer_e2eIcon"
data-testid="e2e-icon"
style="width: 12px; height: 12px;"
@ -3358,7 +3394,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
</button>
<button
aria-label="Chat"
aria-labelledby="_r_kg_"
aria-labelledby="_r_ks_"
class="_icon-button_1215g_8"
data-kind="primary"
role="button"
@ -3385,7 +3421,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
</button>
<button
aria-label="Threads"
aria-labelledby="_r_kl_"
aria-labelledby="_r_l1_"
class="_icon-button_1215g_8"
data-kind="primary"
role="button"
@ -3412,7 +3448,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
</button>
<button
aria-label="Room info"
aria-labelledby="_r_kq_"
aria-labelledby="_r_l6_"
class="_icon-button_1215g_8"
data-kind="primary"
role="button"
@ -3442,7 +3478,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
>
<div
aria-label="0 members"
aria-labelledby="_r_kv_"
aria-labelledby="_r_lb_"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
@ -3527,7 +3563,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
</p>
</div>
<button
aria-labelledby="_r_l8_"
aria-labelledby="_r_lk_"
class="_icon-button_1215g_8"
data-kind="secondary"
data-testid="base-card-close-button"
@ -3587,7 +3623,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
>
<svg
aria-label="Messages in this room are not end-to-end encrypted"
aria-labelledby="_r_ld_"
aria-labelledby="_r_lp_"
class="mx_E2EIcon mx_MessageComposer_e2eIcon"
color="var(--cpd-color-icon-info-primary)"
fill="currentColor"

View File

@ -60,7 +60,6 @@ import WidgetStore, { type IApp } from "../../../../../../src/stores/WidgetStore
import { UIFeature } from "../../../../../../src/settings/UIFeature";
import { SettingLevel } from "../../../../../../src/settings/SettingLevel";
import { ElementCallMemberEventType } from "../../../../../../src/call-types";
import { defaultWatchManager } from "../../../../../../src/settings/Settings.tsx";
jest.mock("../../../../../../src/utils/ShieldUtils");
jest.mock("../../../../../../src/hooks/right-panel/useCurrentPhase", () => ({
@ -723,25 +722,9 @@ describe("RoomHeader", () => {
],
{ addToState: true },
);
let featureEnabled = true;
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(flag) => flag === "feature_share_history_on_invite" && featureEnabled,
);
render(<RoomHeader room={room} />, getWrapper());
await waitFor(() => getByLabelText(document.body, "New members see history"));
// Disable the labs flag and check the icon disappears
featureEnabled = false;
act(() =>
defaultWatchManager.notifyUpdate(
"feature_share_history_on_invite",
null,
SettingLevel.DEVICE,
featureEnabled,
),
);
expect(queryByLabelText(document.body, "New members see history")).not.toBeInTheDocument();
});
it("shows a user icon if the room is encrypted and has world readable history", async () => {
@ -758,10 +741,6 @@ describe("RoomHeader", () => {
],
{ addToState: true },
);
const featureEnabled = true;
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(flag) => flag === "feature_share_history_on_invite" && featureEnabled,
);
render(<RoomHeader room={room} />, getWrapper());
await waitFor(() => getByLabelText(document.body, "Anyone can see history"));

View File

@ -56,7 +56,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
style="--cpd-icon-button-size: 100%;"
>
<svg
aria-labelledby="_r_13s_"
aria-labelledby="_r_14k_"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@ -83,7 +83,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
style="--cpd-icon-button-size: 100%;"
>
<svg
aria-labelledby="_r_141_"
aria-labelledby="_r_14p_"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@ -98,7 +98,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
</button>
<button
aria-label="Threads"
aria-labelledby="_r_146_"
aria-labelledby="_r_14u_"
class="_icon-button_1215g_8"
data-kind="primary"
role="button"
@ -125,7 +125,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
</button>
<button
aria-label="Room info"
aria-labelledby="_r_14b_"
aria-labelledby="_r_153_"
class="_icon-button_1215g_8"
data-kind="primary"
role="button"

View File

@ -24,7 +24,7 @@ describe("/invite", () => {
await command.run(client, roomId, null, args).promise;
expect(client.invite).toHaveBeenCalledWith(roomId, "@u:s.co", {});
expect(client.invite).toHaveBeenCalledWith(roomId, "@u:s.co", { shareEncryptedHistory: true });
});
it("should provide the invite reason if we supply it", async () => {
@ -35,6 +35,9 @@ describe("/invite", () => {
await command.run(client, roomId, null, args).promise;
expect(client.invite).toHaveBeenCalledWith(roomId, "@u:s.co", { reason: "They are a very nice person" });
expect(client.invite).toHaveBeenCalledWith(roomId, "@u:s.co", {
reason: "They are a very nice person",
shareEncryptedHistory: true,
});
});
});

View File

@ -213,7 +213,7 @@ describe("RoomViewStore", function () {
dis.dispatch({ action: Action.ViewRoom, room_id: roomId });
dis.dispatch({ action: Action.JoinRoom });
await untilDispatch(Action.JoinRoomReady, dis);
expect(mockClient.joinRoom).toHaveBeenCalledWith(roomId, { viaServers: [] });
expect(mockClient.joinRoom).toHaveBeenCalledWith(roomId, { acceptSharedHistory: true, viaServers: [] });
expect(roomViewStore.isJoining()).toBe(true);
});
@ -229,14 +229,14 @@ describe("RoomViewStore", function () {
}),
);
await untilDispatch(Action.JoinRoomReady, dis);
expect(mockClient.joinRoom).toHaveBeenCalledWith(alias, { viaServers: ["server1"] });
expect(mockClient.joinRoom).toHaveBeenCalledWith(alias, { acceptSharedHistory: true, viaServers: ["server1"] });
expect(roomViewStore.isJoining()).toBe(true);
});
it("can auto-join a room", async () => {
dis.dispatch({ action: Action.ViewRoom, room_id: roomId, auto_join: true });
await untilDispatch(Action.JoinRoomReady, dis);
expect(mockClient.joinRoom).toHaveBeenCalledWith(roomId, { viaServers: [] });
expect(mockClient.joinRoom).toHaveBeenCalledWith(roomId, { acceptSharedHistory: true, viaServers: [] });
expect(roomViewStore.isJoining()).toBe(true);
});
@ -282,7 +282,7 @@ describe("RoomViewStore", function () {
await untilDispatch(Action.JoinRoomReady, dis);
expect(roomViewStore.isJoining()).toBeTruthy();
expect(mockClient.joinRoom).toHaveBeenCalledWith(alias, { viaServers: [] });
expect(mockClient.joinRoom).toHaveBeenCalledWith(alias, { acceptSharedHistory: true, viaServers: [] });
});
it("emits ViewRoomError if the alias lookup fails", async () => {
@ -548,11 +548,7 @@ 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.
});
it("sets 'acceptSharedHistory'", async () => {
dis.dispatch({ action: Action.ViewRoom, room_id: roomId });
dis.dispatch({ action: Action.JoinRoom });
await untilDispatch(Action.JoinRoomReady, dis);

View File

@ -120,9 +120,9 @@ describe("MultiInviter", () => {
const result = await inviter.invite([MXID1, MXID2, MXID3]);
expect(client.invite).toHaveBeenCalledTimes(3);
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, {});
expect(client.invite).toHaveBeenNthCalledWith(2, ROOMID, MXID2, {});
expect(client.invite).toHaveBeenNthCalledWith(3, ROOMID, MXID3, {});
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, { shareEncryptedHistory: true });
expect(client.invite).toHaveBeenNthCalledWith(2, ROOMID, MXID2, { shareEncryptedHistory: true });
expect(client.invite).toHaveBeenNthCalledWith(3, ROOMID, MXID3, { shareEncryptedHistory: true });
expectAllInvitedResult(result);
});
@ -138,9 +138,9 @@ describe("MultiInviter", () => {
const result = await inviter.invite([MXID1, MXID2, MXID3]);
expect(client.invite).toHaveBeenCalledTimes(3);
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, {});
expect(client.invite).toHaveBeenNthCalledWith(2, ROOMID, MXID2, {});
expect(client.invite).toHaveBeenNthCalledWith(3, ROOMID, MXID3, {});
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, { shareEncryptedHistory: true });
expect(client.invite).toHaveBeenNthCalledWith(2, ROOMID, MXID2, { shareEncryptedHistory: true });
expect(client.invite).toHaveBeenNthCalledWith(3, ROOMID, MXID3, { shareEncryptedHistory: true });
expectAllInvitedResult(result);
});
@ -153,7 +153,7 @@ describe("MultiInviter", () => {
const result = await inviter.invite([MXID1, MXID2, MXID3]);
expect(client.invite).toHaveBeenCalledTimes(1);
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, {});
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, { shareEncryptedHistory: true });
// The resolved state is 'invited' for all users.
// With the above client expectations, the test ensures that only the first user is invited.
@ -255,15 +255,5 @@ 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 });
});
});
});

View File

@ -119,14 +119,6 @@ Do not send or receive messages to/from devices that are not properly verified.
receive your messages at all on those devices, and if they send messages, you will not be able to read them, but you
will be aware that a message exists.
## Share encrypted history with new members (`feature_share_history_on_invite`) [In Development]
When inviting users to an encrypted room with shared history (i.e. a room with the "Who can read history?" setting set
to "Members only (since the point in time of selecting this option)"), send the keys for previous messages to the
invitee so they can read them.
Both the inviter and the invitee must set this labs flag, before the invitation is sent.
## Encrypted state events (MSC4362) (`feature_msc4362_encrypted_state_events`)
Encrypt most of the state events in the room, including the room name and topic.