Skip to content
Draft
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
47ca834
feat: Implement UI for history visibility acknowledgement.
kaylendog Nov 3, 2025
efe05c7
tests: Add test suite for `RoomStatusBarHistoryVisible`.
kaylendog Nov 3, 2025
568adbe
docs: Document `RoomStatusBarHistoryVisible` and props interface.
kaylendog Nov 3, 2025
426c957
feat: Use newer `@vector-im/compound` components.
kaylendog Nov 3, 2025
a21f850
test: Update snapshots for `RoomStatusBarHistoryVisible` tests.
kaylendog Nov 3, 2025
409d1eb
chore: Update playwright screenshots.
kaylendog Nov 4, 2025
a899629
feat: Move `RoomStatusBarHistoryVisible` to `shared-components`.
kaylendog Nov 4, 2025
6a51c18
fix: Address review comments on `RoomStatusBarHistoryVisible`.
kaylendog Nov 7, 2025
a5fdc5b
fix: Address review comments on `RoomStatusBar` and tests.
kaylendog Nov 7, 2025
7bc5d67
chore: Move `RoomStatusBarHistoryVisible` to `room/RoomStatusBarHisto…
kaylendog Nov 7, 2025
636ecc9
chore: Fix linting issues.
kaylendog Nov 7, 2025
4ae0b01
feat: Gate behind history visibility labs flag.
kaylendog Nov 13, 2025
6d7bff7
feat: Add link to history sharing docs.
kaylendog Nov 13, 2025
a729c86
fix: Resolve build issue with shared-components.
kaylendog Nov 13, 2025
30c41d1
tests: Enable history sharing lab for unit tests.
kaylendog Nov 13, 2025
7444e3b
Merge remote-tracking branch 'upstream/develop' into kaylendog/histor…
kaylendog Nov 13, 2025
6e49704
tests: Set labs flag in SettingsStore mock.
kaylendog Nov 13, 2025
8620722
fix: Remove non-existent arg - documentation should be updated!
kaylendog Nov 13, 2025
ccec28f
chore: Remove old CSS rule filter.
kaylendog Nov 13, 2025
b8a54b9
fix: Use package name for import over relative path.
kaylendog Nov 13, 2025
bed81c3
fix: Mark styles as important due to improper CSS load order.
kaylendog Nov 13, 2025
bdc1067
docs: Add doc comments to `!important` directives.
kaylendog Nov 13, 2025
746acc9
docs: Correct license header.
kaylendog Nov 13, 2025
f0377f8
tests: Update `RoomStatusBarHistoryVisible` snapshot.
kaylendog Nov 13, 2025
90a0cab
Merge remote-tracking branch 'upstream/develop' into kaylendog/histor…
kaylendog Nov 17, 2025
4e77fab
tests: Update shared history invite screenshot.
kaylendog Nov 17, 2025
4c84126
tests: Revert spurious screenshot changes.
kaylendog Nov 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/shared-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export * from "./pill-input/Pill";
export * from "./pill-input/PillInput";
export * from "./rich-list/RichItem";
export * from "./rich-list/RichList";
export * from "./room/RoomStatusBarHistoryVisible";
export * from "./utils/Box";
export * from "./utils/Flex";

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/

.historyVisibility {
border-radius: 0;
border-left: none;
border-right: none;
border-bottom: none;
}

.historyVisibilityButton {
white-space: nowrap;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/

import { type Meta, type StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";

import { RoomStatusBarHistoryVisible } from "./RoomStatusBarHistoryVisible";

const meta = {
title: "Structures/RoomStatusBarHistoryVisible",
component: RoomStatusBarHistoryVisible,
tags: ["autodocs"],
args: {
onClose: fn(),
},
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/design/96hBf15is3HCxTt3X7nnLW/ER-144--Encrypted-Room-History?node-id=1-62053&t=zFsl3I946nBW8qq0-4",
},
},
} satisfies Meta<typeof RoomStatusBarHistoryVisible>;

export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/

import React, { type MouseEventHandler, type ReactElement } from "react";
import { Alert, Button } from "@vector-im/compound-web";

import styles from "./RoomStatusBarHistoryVisible.module.css";
import { _t } from "../../utils/i18n";

interface RoomStatusBarHistoryVisibleProps {
/**
* Called when the user presses the "dismiss" button.
*/
onClose: MouseEventHandler<HTMLButtonElement>;
}

/**
* A component to alert that history is shared to new members of the room.
*
* @example
* ```tsx
* <RoomStatusBarHistoryVisible onClose={onCloseHandler} />
* ```
*/
export function RoomStatusBarHistoryVisible(props: RoomStatusBarHistoryVisibleProps): ReactElement {
return (
<Alert
className={styles.historyVisibility}
type="info"
title={_t("room|status_bar|history_visible")}
actions={
<>
<Button
as="a"
href="https://element.io/en/help#e2ee-history-sharing"
target="_blank"
className={styles.historyVisibilityButton}
kind="tertiary"
size="sm"
>
{_t("action|learn_more")}
</Button>
<Button className={styles.historyVisibilityButton} kind="primary" size="sm" onClick={props.onClose}>
{_t("action|dismiss")}
</Button>
</>
}
>
{_t("room|status_bar|history_visible_description")}
</Alert>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/

export { RoomStatusBarHistoryVisible } from "./RoomStatusBarHistoryVisible";
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion res/css/structures/_RoomStatusBar.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/

.mx_RoomStatusBar:not(.mx_RoomStatusBar_unsentMessages) {
.mx_RoomStatusBar:not(.mx_RoomStatusBar_unsentMessages):not(.mx_RoomStatusBar_historyVisibility) {
margin-left: 65px;
min-height: 50px;
}
Expand Down
119 changes: 118 additions & 1 deletion src/components/structures/RoomStatusBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ import {
RoomEvent,
type SyncState,
type SyncStateData,
HistoryVisibility,
RoomStateEvent,
type RoomState,
} from "matrix-js-sdk/src/matrix";
import { WarningIcon } from "@vector-im/compound-design-tokens/assets/web/icons";

import { RoomStatusBarHistoryVisible } from "../../../packages/shared-components/src/room/RoomStatusBarHistoryVisible";
import { _t, _td } from "../../languageHandler";
import Resend from "../../Resend";
import dis from "../../dispatcher/dispatcher";
Expand All @@ -30,6 +34,8 @@ import InlineSpinner from "../views/elements/InlineSpinner";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import { RoomStatusBarUnsentMessages } from "./RoomStatusBarUnsentMessages";
import ExternalLink from "../views/elements/ExternalLink";
import SettingsStore from "../../settings/SettingsStore";
import { SettingLevel } from "../../settings/SettingLevel";

const STATUS_BAR_HIDDEN = 0;
const STATUS_BAR_EXPANDED = 1;
Expand Down Expand Up @@ -84,6 +90,9 @@ interface IState {
syncStateData: SyncStateData | null;
unsentMessages: MatrixEvent[];
isResending: boolean;
roomHistoryVisibility: HistoryVisibility;
roomHasEncryptionStateEvent: boolean;
acknowledgedHistoryVisibility: boolean;
}

export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
Expand All @@ -99,6 +108,9 @@ export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
syncStateData: this.context.getSyncStateData(),
unsentMessages: getUnsentMessages(this.props.room),
isResending: false,
roomHistoryVisibility: this.props.room.getHistoryVisibility(),
roomHasEncryptionStateEvent: this.props.room.hasEncryptionStateEvent(),
acknowledgedHistoryVisibility: this.getUpdatedAcknowledgedHistoryVisibility(),
};
}

Expand All @@ -108,6 +120,7 @@ export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
const client = this.context;
client.on(ClientEvent.Sync, this.onSyncStateChange);
client.on(RoomEvent.LocalEchoUpdated, this.onRoomLocalEchoUpdated);
client.on(RoomStateEvent.Update, this.onRoomStateEventUpdate);

this.checkSize();
}
Expand All @@ -123,6 +136,7 @@ export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
if (client) {
client.removeListener(ClientEvent.Sync, this.onSyncStateChange);
client.removeListener(RoomEvent.LocalEchoUpdated, this.onRoomLocalEchoUpdated);
client.removeListener(RoomStateEvent.Update, this.onRoomStateEventUpdate);
}
}

Expand Down Expand Up @@ -159,6 +173,22 @@ export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
});
};

private onRoomStateEventUpdate = (state: RoomState): void => {
// prevent useless re-renders if the flag is disabled.
if (!SettingsStore.getValue("feature_share_history_on_invite")) {
return;
}

if (state.roomId !== this.props.room.roomId) {
return;
}
this.setState({
acknowledgedHistoryVisibility: this.getUpdatedAcknowledgedHistoryVisibility(),
roomHistoryVisibility: this.props.room.getHistoryVisibility(),
roomHasEncryptionStateEvent: this.props.room.hasEncryptionStateEvent(),
});
};

// Check whether current size is greater than 0, if yes call props.onVisible
private checkSize(): void {
if (this.getSize()) {
Expand All @@ -174,7 +204,11 @@ export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
private getSize(): number {
if (this.shouldShowConnectionError()) {
return STATUS_BAR_EXPANDED;
} else if (this.state.unsentMessages.length > 0 || this.state.isResending) {
} else if (
this.state.unsentMessages.length > 0 ||
this.state.isResending ||
this.shouldShowHistoryVisibilityContent()
) {
return STATUS_BAR_EXPANDED_LARGE;
}
return STATUS_BAR_HIDDEN;
Expand Down Expand Up @@ -265,6 +299,85 @@ export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
);
}

/**
* Gets the latest acknowledgement flag from the setting store.
*
* If the room history visibility was changed to `HistoryVisibility.Joined` since this method
* was last called, the flag is cleared in both the component state and settings.
*
* Additionally, while this feature is in development, this method will always return true unless
* the `feature_share_history_on_invite` lab is enabled.
*/
private getUpdatedAcknowledgedHistoryVisibility(): boolean {
if (!SettingsStore.getValue("feature_share_history_on_invite")) {
return true;
}

let acknowledgedHistoryVisibility = SettingsStore.getValue(
"acknowledgedHistoryVisibility",
this.props.room.roomId,
);
if (this.props.room.getHistoryVisibility() === HistoryVisibility.Joined) {
acknowledgedHistoryVisibility = false;
// Clear the dismissed flag to ensure if a room is changed public -> private -> public,
// we show the banner again when it is set back to public.
void SettingsStore.setValue(
"acknowledgedHistoryVisibility",
this.props.room.roomId,
SettingLevel.ROOM_ACCOUNT,
false,
);
}
return acknowledgedHistoryVisibility;
}

/**
* Returns true if the history visibility acknowledgement banner should be shown.
*
* Shown when:
* - The room is encrypted
* - The room's history visibility is not set to `HistoryVisibility.Joined`
* - The user has not already acknowledged the alert
*
* Additionally, while this feature is in development, this will always return `false`
* unless the `feature_share_history_on_invite` lab is enabled.
*/
private shouldShowHistoryVisibilityContent(): boolean {
if (!SettingsStore.getValue("feature_share_history_on_invite")) {
return false;
}

return (
this.state.roomHasEncryptionStateEvent &&
this.state.roomHistoryVisibility !== HistoryVisibility.Joined &&
!this.state.acknowledgedHistoryVisibility
);
}

/**
* Get the content for the history visibility acknowledgement banner.
*
* When the user closes the banner, the acknowledgement is saved in SettingsStore
* for this room, and the banner will not be shown again unless the history visibility changes.
*/
private getHistoryVisibilityContent(): JSX.Element {
return (
<RoomStatusBarHistoryVisible
onClose={async () => {
void SettingsStore.setValue(
"acknowledgedHistoryVisibility",
this.props.room.roomId,
SettingLevel.ROOM_ACCOUNT,
true,
);
this.setState({
acknowledgedHistoryVisibility: true,
});
}}
/>
);
}

public render(): React.ReactNode {
if (this.shouldShowConnectionError()) {
return (
Expand All @@ -290,6 +403,10 @@ export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
return this.getUnsentMessageContent();
}

if (this.shouldShowHistoryVisibilityContent()) {
return this.getHistoryVisibilityContent();
}

return null;
}
}
2 changes: 2 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -2110,6 +2110,8 @@
"status_bar": {
"delete_all": "Delete all",
"exceeded_resource_limit": "Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.",
"history_visible": "Messages you send will be shared with new members invited to this room.",
"history_visible_description": "room|status_bar|history_visible_description",
"homeserver_blocked": "Your message wasn't sent because this homeserver has been blocked by its administrator. Please <a>contact your service administrator</a> to continue using the service.",
"monthly_user_limit_reached": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.",
"requires_consent_agreement": "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.",
Expand Down
5 changes: 5 additions & 0 deletions src/settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ export interface Settings {
"mediaPreviewConfig": IBaseSetting<MediaPreviewConfig>;
"inviteRules": IBaseSetting<ComputedInviteConfig>;
"Developer.elementCallUrl": IBaseSetting<string>;
"acknowledgedHistoryVisibility": IBaseSetting<boolean>;
}

export type SettingKey = keyof Settings;
Expand Down Expand Up @@ -1473,4 +1474,8 @@ export const SETTINGS: Settings = {
displayName: _td("devtools|settings|elementCallUrl"),
default: "",
},
"acknowledgedHistoryVisibility": {
supportedLevels: [SettingLevel.ROOM_ACCOUNT],
default: false,
},
};
2 changes: 1 addition & 1 deletion test/test-utils/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ Please see LICENSE files in the repository root for full details.
import EventEmitter from "events";
import { mocked, type MockedObject } from "jest-mock";
import {
type EventTimeline,
MatrixEvent,
type Room,
type User,
type IContent,
type IEvent,
type RoomMember,
type MatrixClient,
type EventTimeline,
type RoomState,
EventType,
type IEventRelation,
Expand Down
Loading
Loading