Refactor NotificationBadge to shared MVVM (#33843)

* Refactor NotificationBadge to shared MVVM

* Added snapshot images

* Replace legacy NotificationBadge with adapter

* Fix Eslint

* Use regular NotificationBadge prop names

* Remove legacy NotificationBadge selectors

* Update TAC badge Playwright selectors

* Rename NotificationBadgeAdapter to NotificationBadge

Addresses review feedback: the component is the app-facing badge, not an
adapter, so name it NotificationBadge.

* Make NotificationBadgeView isClickable a required boolean

Addresses review feedback: there is no semantic difference between
`isClickable: false` and `isClickable: undefined` (both render the
non-interactive variant), so the field should be a plain boolean. The
view model always provides it.

* Drop redundant comparison guards in NotificationBadge view model setters

Addresses review feedback: Snapshot.merge already compares each field
with Object.is and only emits when something actually changed, so the
manual equality guards in each setter were redundant. Recompute the
snapshot unconditionally and let merge dedupe.

* Resolve notification badge labels inside NotificationBadgeView

Addresses review feedback: static UI labels belong in the view, not the
snapshot. The view now resolves the knock and unsent-message strings via
useI18n, and the snapshot exposes a showUnsentTooltip boolean instead of
a prebuilt tooltipLabel (and drops knockLabel entirely). Adds the
room|knock_sent and notifications|message_didnt_send keys to the
shared-components i18n catalogue so they resolve in tests and storybook.

* Remove unused translation because of shared components movement

* Fix typescript issue

* Fix lint issue

* Remove unused notification badge count setting

* Pass notification badge class names explicitly
This commit is contained in:
Zack
2026-08-10 08:32:23 +00:00
committed by GitHub
parent a73f8c389b
commit ce74b5f614
41 changed files with 668 additions and 398 deletions
@@ -12,7 +12,7 @@ import React from "react";
import { StatelessNotificationBadge } from "../../../../../../src/components/views/rooms/NotificationBadge/StatelessNotificationBadge";
import SettingsStore from "../../../../../../src/settings/SettingsStore";
import { NotificationLevel } from "../../../../../../src/stores/notifications/NotificationLevel";
import NotificationBadge from "../../../../../../src/components/views/rooms/NotificationBadge";
import { NotificationBadge } from "../../../../../../src/components/views/rooms/NotificationBadge/NotificationBadge";
import { NotificationState } from "../../../../../../src/stores/notifications/NotificationState";
class DummyNotificationState extends NotificationState {
@@ -26,25 +26,25 @@ describe("NotificationBadge", () => {
it("shows a dot if the level is activity", () => {
const notif = new DummyNotificationState(NotificationLevel.Activity);
const { container } = render(<NotificationBadge roomId="!foo:bar" notification={notif} />);
expect(container.querySelector(".mx_NotificationBadge_dot")).toBeInTheDocument();
expect(container.querySelector(".mx_NotificationBadge")).toBeInTheDocument();
const { container } = render(<NotificationBadge notification={notif} />);
expect(container.querySelector('[data-badge-type="dot"]')).toBeInTheDocument();
expect(container.querySelector('[data-testid="notification-badge"]')).toBeInTheDocument();
});
it("does not show a dot if the level is activity and hideIfDot is true", () => {
const notif = new DummyNotificationState(NotificationLevel.Activity);
const { container } = render(<NotificationBadge roomId="!foo:bar" notification={notif} hideIfDot={true} />);
expect(container.querySelector(".mx_NotificationBadge_dot")).not.toBeInTheDocument();
expect(container.querySelector(".mx_NotificationBadge")).not.toBeInTheDocument();
const { container } = render(<NotificationBadge notification={notif} hideIfDot={true} />);
expect(container.querySelector('[data-badge-type="dot"]')).not.toBeInTheDocument();
expect(container.querySelector('[data-testid="notification-badge"]')).not.toBeInTheDocument();
});
it("still shows an empty badge if hideIfDot us true", () => {
const notif = new DummyNotificationState(NotificationLevel.Notification);
const { container } = render(<NotificationBadge roomId="!foo:bar" notification={notif} hideIfDot={true} />);
expect(container.querySelector(".mx_NotificationBadge_dot")).not.toBeInTheDocument();
expect(container.querySelector(".mx_NotificationBadge")).toBeInTheDocument();
const { container } = render(<NotificationBadge notification={notif} hideIfDot={true} />);
expect(container.querySelector('[data-badge-type="dot"]')).not.toBeInTheDocument();
expect(container.querySelector('[data-testid="notification-badge"]')).toBeInTheDocument();
});
describe("StatelessNotificationBadge", () => {
@@ -17,14 +17,14 @@ describe("StatelessNotificationBadge", () => {
const { container } = render(
<StatelessNotificationBadge symbol="!" count={0} level={NotificationLevel.Unsent} />,
);
expect(container.querySelector(".mx_NotificationBadge_level_highlight")).not.toBe(null);
expect(container.querySelector('[data-notification-level="highlight"]')).toBeInTheDocument();
});
it("has knock style", () => {
const { container } = render(
<StatelessNotificationBadge symbol="!" count={0} level={NotificationLevel.Highlight} knocked={true} />,
);
expect(container.querySelector(".mx_NotificationBadge_dot")).not.toBeInTheDocument();
expect(container.querySelector('[data-badge-type="dot"]')).not.toBeInTheDocument();
expect(container.querySelector("svg")).toHaveAccessibleName("Request to join sent");
});
@@ -32,14 +32,14 @@ describe("StatelessNotificationBadge", () => {
const { container } = render(
<StatelessNotificationBadge symbol={null} count={3} level={NotificationLevel.Activity} />,
);
expect(container.querySelector(".mx_NotificationBadge_dot")).toBeInTheDocument();
expect(container.querySelector('[data-badge-type="dot"]')).toBeInTheDocument();
});
it("has badge style for notification", () => {
const { container } = render(
<StatelessNotificationBadge symbol={null} count={3} level={NotificationLevel.Notification} />,
);
expect(container.querySelector(".mx_NotificationBadge_dot")).not.toBeInTheDocument();
expect(container.querySelector('[data-badge-type="dot"]')).not.toBeInTheDocument();
});
it("has dot style for notification when forced", () => {
@@ -51,6 +51,6 @@ describe("StatelessNotificationBadge", () => {
forceDot={true}
/>,
);
expect(container.querySelector(".mx_NotificationBadge_dot")).toBeInTheDocument();
expect(container.querySelector('[data-badge-type="dot"]')).toBeInTheDocument();
});
});
@@ -640,10 +640,13 @@ exports[`<Notifications /> correctly handles the loading/disabled state 1`] = `
<span>
Show a badge
<div
class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_level_notification mx_NotificationBadge_2char cpd-theme-light"
class="mx_NotificationSettings2_notificationBadge _notificationBadge_kb9ny_8 _visible_kb9ny_15 _notification_kb9ny_8 _badge2Char_kb9ny_56 cpd-theme-light"
data-badge-type="badge_2char"
data-notification-level="notification"
data-testid="notification-badge"
>
<span
class="mx_NotificationBadge_count"
class="_count_kb9ny_39"
>
1
</span>
@@ -1655,10 +1658,13 @@ exports[`<Notifications /> matches the snapshot 1`] = `
<span>
Show a badge
<div
class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_level_notification mx_NotificationBadge_2char cpd-theme-light"
class="mx_NotificationSettings2_notificationBadge _notificationBadge_kb9ny_8 _visible_kb9ny_15 _notification_kb9ny_8 _badge2Char_kb9ny_56 cpd-theme-light"
data-badge-type="badge_2char"
data-notification-level="notification"
data-testid="notification-badge"
>
<span
class="mx_NotificationBadge_count"
class="_count_kb9ny_39"
>
1
</span>
@@ -127,7 +127,7 @@ describe("SpaceButton", () => {
/>,
);
expect(container.querySelector(".mx_NotificationBadge_count")).toHaveTextContent("8");
expect(getByTestId(container, "notification-badge")).toHaveTextContent("8");
expect(asFragment()).toMatchSnapshot();
});
});
@@ -124,7 +124,7 @@ describe("ThreadsActivityCentre", () => {
expect(tacRows.length).toEqual(1);
getByText(tacRows[0], "A notification");
expect(tacRows[0].getElementsByClassName("mx_NotificationBadge_level_notification").length).toEqual(1);
expect(tacRows[0].querySelector('[data-notification-level="notification"]')).toBeInTheDocument();
});
it("should render a room with a highlight notification in the TAC", async () => {
@@ -136,7 +136,7 @@ describe("ThreadsActivityCentre", () => {
expect(tacRows.length).toEqual(1);
getByText(tacRows[0], "This is a real highlight");
expect(tacRows[0].getElementsByClassName("mx_NotificationBadge_level_highlight").length).toEqual(1);
expect(tacRows[0].querySelector('[data-notification-level="highlight"]')).toBeInTheDocument();
});
it("renders notifications matching the snapshot", async () => {
@@ -25,17 +25,21 @@ exports[`SpaceButton metaspace should render notificationState if one is provide
<div
class="mx_SpacePanel_badgeContainer"
>
<div
class="mx_AccessibleButton mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_level_notification mx_NotificationBadge_2char cpd-theme-light"
role="button"
<button
aria-label="Jump to first unread room."
class="mx_SpacePanel_notificationBadge _notificationBadge_kb9ny_8 _visible_kb9ny_15 _notification_kb9ny_8 _badge2Char_kb9ny_56 cpd-theme-light"
data-badge-type="badge_2char"
data-notification-level="notification"
data-testid="notification-badge"
tabindex="-1"
type="button"
>
<span
class="mx_NotificationBadge_count"
class="_count_kb9ny_39"
>
8
</span>
</div>
</button>
</div>
</div>
<span
@@ -66,10 +66,13 @@ exports[`ThreadsActivityCentre renders notifications matching the snapshot 1`] =
/>
</svg>
<div
class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_level_highlight mx_NotificationBadge_dot"
class="mx_ThreadsActivityCentre_notificationBadge _notificationBadge_kb9ny_8 _visible_kb9ny_15 _highlight_kb9ny_47 _dot_kb9ny_32"
data-badge-type="dot"
data-notification-level="highlight"
data-testid="notification-badge"
>
<span
class="mx_NotificationBadge_count"
class="_count_kb9ny_39"
/>
</div>
</button>
@@ -114,10 +117,13 @@ exports[`ThreadsActivityCentre renders notifications matching the snapshot 1`] =
/>
</svg>
<div
class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_level_notification mx_NotificationBadge_dot"
class="mx_ThreadsActivityCentre_notificationBadge _notificationBadge_kb9ny_8 _visible_kb9ny_15 _notification_kb9ny_8 _dot_kb9ny_32"
data-badge-type="dot"
data-notification-level="notification"
data-testid="notification-badge"
>
<span
class="mx_NotificationBadge_count"
class="_count_kb9ny_39"
/>
</div>
</button>
@@ -225,10 +231,13 @@ exports[`ThreadsActivityCentre should order the room with the same notification
/>
</svg>
<div
class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_level_highlight mx_NotificationBadge_dot"
class="mx_ThreadsActivityCentre_notificationBadge _notificationBadge_kb9ny_8 _visible_kb9ny_15 _highlight_kb9ny_47 _dot_kb9ny_32"
data-badge-type="dot"
data-notification-level="highlight"
data-testid="notification-badge"
>
<span
class="mx_NotificationBadge_count"
class="_count_kb9ny_39"
/>
</div>
</button>
@@ -273,10 +282,13 @@ exports[`ThreadsActivityCentre should order the room with the same notification
/>
</svg>
<div
class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_level_highlight mx_NotificationBadge_dot"
class="mx_ThreadsActivityCentre_notificationBadge _notificationBadge_kb9ny_8 _visible_kb9ny_15 _highlight_kb9ny_47 _dot_kb9ny_32"
data-badge-type="dot"
data-notification-level="highlight"
data-testid="notification-badge"
>
<span
class="mx_NotificationBadge_count"
class="_count_kb9ny_39"
/>
</div>
</button>
@@ -321,10 +333,13 @@ exports[`ThreadsActivityCentre should order the room with the same notification
/>
</svg>
<div
class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_level_highlight mx_NotificationBadge_dot"
class="mx_ThreadsActivityCentre_notificationBadge _notificationBadge_kb9ny_8 _visible_kb9ny_15 _highlight_kb9ny_47 _dot_kb9ny_32"
data-badge-type="dot"
data-notification-level="highlight"
data-testid="notification-badge"
>
<span
class="mx_NotificationBadge_count"
class="_count_kb9ny_39"
/>
</div>
</button>