Update history visibility UX (#31635)

* Update history visibility UX

* don't change voip strings
This commit is contained in:
David Langley 2026-01-05 12:24:42 +00:00 committed by GitHub
parent 5d1cb24a6c
commit 05e7203f1b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 146 additions and 72 deletions

View File

@ -74,9 +74,7 @@ test.describe("Roles & Permissions room settings tab", () => {
await settingsGroupAccess.getByText("Private (invite only)").click();
// Element should have automatically set the room to "sharing" history visibility
await expect(
settingsGroupHistory.getByText("Members only (since the point in time of selecting this option)"),
).toBeChecked();
await expect(settingsGroupHistory.getByText("Members (full history)")).toBeChecked();
},
);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 90 KiB

View File

@ -419,36 +419,60 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
const state = this.props.room.currentState;
const canChangeHistory = state?.mayClientSendStateEvent(EventType.RoomHistoryVisibility, client);
const options = [
{
value: HistoryVisibility.Shared,
label: _t("room_settings|security|history_visibility_shared"),
},
{
// Map 'joined' to 'invited' for display purposes
const displayHistory = history === HistoryVisibility.Joined ? HistoryVisibility.Invited : history;
const isPublicRoom = this.props.room.getJoinRule() === JoinRule.Public;
const isEncrypted = this.state.encrypted;
const options: Array<{ value: HistoryVisibility; label: string }> = [];
// Show "invited" when room's join rule is NOT public OR E2EE is turned on, or if currently selected
if (
!isPublicRoom ||
isEncrypted ||
history === HistoryVisibility.Invited ||
history === HistoryVisibility.Joined
) {
options.push({
value: HistoryVisibility.Invited,
label: _t("room_settings|security|history_visibility_invited"),
},
{
value: HistoryVisibility.Joined,
label: _t("room_settings|security|history_visibility_joined"),
},
];
});
}
// World readable doesn't make sense for encrypted rooms
if (!this.state.encrypted || history === HistoryVisibility.WorldReadable) {
options.unshift({
// Always show "shared" option
options.push({
value: HistoryVisibility.Shared,
label: _t("room_settings|security|history_visibility_shared"),
});
// Show "world_readable" when (is public AND not encrypted) OR currently selected
if ((isPublicRoom && !isEncrypted) || history === HistoryVisibility.WorldReadable) {
options.push({
value: HistoryVisibility.WorldReadable,
label: _t("room_settings|security|history_visibility_world_readable"),
});
}
const description = _t("room_settings|security|history_visibility_warning");
const description = (
<>
{_t(
"room_settings|security|history_visibility_warning",
{},
{
a: (sub) => (
<ExternalLink href="https://element.io/en/help#e2ee-history-sharing">{sub}</ExternalLink>
),
},
)}
</>
);
return (
<SettingsFieldset legend={_t("room_settings|security|history_visibility_legend")} description={description}>
<StyledRadioGroup
name="historyVis"
value={history}
value={displayHistory}
onChange={this.onHistoryRadioToggle}
disabled={!canChangeHistory}
definitions={options}

View File

@ -2431,12 +2431,11 @@
"error_join_rule_change_title": "Failed to update the join rules",
"error_join_rule_change_unknown": "Unknown failure",
"guest_access_warning": "People with supported clients will be able to join the room without having a registered account.",
"history_visibility_invited": "Members only (since they were invited)",
"history_visibility_joined": "Members only (since they joined)",
"history_visibility_invited": "Members since invited",
"history_visibility_legend": "Who can read history?",
"history_visibility_shared": "Members only (since the point in time of selecting this option)",
"history_visibility_warning": "The visibility of existing history will not be changed.",
"history_visibility_world_readable": "Anyone",
"history_visibility_shared": "Members (full history)",
"history_visibility_warning": "Changes won't affect past messages, only new ones. <a>Learn more</a>",
"history_visibility_world_readable": "Anyone (history is public)\n",
"join_rule_description": "Decide who can join %(roomName)s.",
"join_rule_invite": "Private (invite only)",
"join_rule_invite_description": "Only invited people can join.",

View File

@ -377,6 +377,76 @@ describe("<SecurityRoomSettingsTab />", () => {
expect(screen.getByDisplayValue(HistoryVisibility.Shared)).toBeChecked();
expect(logger.error).toHaveBeenCalledWith("oups");
});
it("maps 'joined' history visibility to 'invited' for display", () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room, undefined, undefined, HistoryVisibility.Joined);
getComponent(room);
// Should display as 'invited' even though underlying value is 'joined'
expect(screen.getByDisplayValue(HistoryVisibility.Invited)).toBeChecked();
// Should not have a 'joined' option visible
expect(screen.queryByDisplayValue(HistoryVisibility.Joined)).not.toBeInTheDocument();
});
it("shows 'invited' option for non-public rooms", () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room, JoinRule.Invite);
getComponent(room);
expect(screen.getByDisplayValue(HistoryVisibility.Invited)).toBeInTheDocument();
});
it("shows 'invited' option for encrypted rooms even if public", async () => {
const room = new Room(roomId, client, userId);
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
setRoomStateEvents(room, JoinRule.Public);
getComponent(room);
await waitFor(() => expect(screen.getByDisplayValue(HistoryVisibility.Invited)).toBeInTheDocument());
});
it("does not show 'invited' option for public unencrypted rooms unless selected", async () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room, JoinRule.Public, undefined, HistoryVisibility.Shared);
getComponent(room);
await waitFor(() => expect(screen.queryByDisplayValue(HistoryVisibility.Invited)).not.toBeInTheDocument());
});
it("shows 'world_readable' option for public unencrypted rooms", async () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room, JoinRule.Public);
getComponent(room);
await waitFor(() => expect(screen.getByDisplayValue(HistoryVisibility.WorldReadable)).toBeInTheDocument());
});
it("does not show 'world_readable' option for private encrypted rooms unless selected", async () => {
const room = new Room(roomId, client, userId);
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
setRoomStateEvents(room, JoinRule.Invite);
getComponent(room);
await waitFor(() =>
expect(screen.queryByDisplayValue(HistoryVisibility.WorldReadable)).not.toBeInTheDocument(),
);
});
it("always shows 'shared' option", () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room);
getComponent(room);
expect(screen.getByDisplayValue(HistoryVisibility.Shared)).toBeInTheDocument();
});
});
describe("encryption", () => {

View File

@ -15,7 +15,32 @@ exports[`<SecurityRoomSettingsTab /> history visibility uses shared as default h
<div
class="mx_SettingsSubsection_text"
>
The visibility of existing history will not be changed.
<span>
Changes won't affect past messages, only new ones.
<a
class="mx_ExternalLink"
href="https://element.io/en/help#e2ee-history-sharing"
rel="noreferrer noopener"
target="_blank"
>
Learn more
<svg
class="mx_ExternalLink_icon"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 3h6a1 1 0 1 1 0 2H5v14h14v-6a1 1 0 1 1 2 0v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2"
/>
<path
d="M15 3h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V6.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L17.586 5H15a1 1 0 1 1 0-2"
/>
</svg>
</a>
</span>
</div>
</div>
<div
@ -25,10 +50,10 @@ exports[`<SecurityRoomSettingsTab /> history visibility uses shared as default h
class="mx_StyledRadioButton mx_StyledRadioButton_enabled"
>
<input
id="historyVis-world_readable"
id="historyVis-invited"
name="historyVis"
type="radio"
value="world_readable"
value="invited"
/>
<div>
<div />
@ -36,7 +61,7 @@ exports[`<SecurityRoomSettingsTab /> history visibility uses shared as default h
<div
class="mx_StyledRadioButton_content"
>
Anyone
Members since invited
</div>
<div
class="mx_StyledRadioButton_spacer"
@ -58,49 +83,7 @@ exports[`<SecurityRoomSettingsTab /> history visibility uses shared as default h
<div
class="mx_StyledRadioButton_content"
>
Members only (since the point in time of selecting this option)
</div>
<div
class="mx_StyledRadioButton_spacer"
/>
</label>
<label
class="mx_StyledRadioButton mx_StyledRadioButton_enabled"
>
<input
id="historyVis-invited"
name="historyVis"
type="radio"
value="invited"
/>
<div>
<div />
</div>
<div
class="mx_StyledRadioButton_content"
>
Members only (since they were invited)
</div>
<div
class="mx_StyledRadioButton_spacer"
/>
</label>
<label
class="mx_StyledRadioButton mx_StyledRadioButton_enabled"
>
<input
id="historyVis-joined"
name="historyVis"
type="radio"
value="joined"
/>
<div>
<div />
</div>
<div
class="mx_StyledRadioButton_content"
>
Members only (since they joined)
Members (full history)
</div>
<div
class="mx_StyledRadioButton_spacer"