From 95430cecbcac040e94ea3fd01fbeba987cdcd9fd Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 25 Jan 2024 16:53:41 +0000 Subject: [PATCH] Add notification dots to thread summary icons (#12146) * Add notification dots to thread summary icons Adopts new IndicatorIcon from compound to have threads icons with indicator dot (that aren't also buttons). Adds green & red dots on the threads icon in the thread summary to indicate notifications. Changes the notification level dots colours in the threads panel to be green to match. * Update test for new CSS class * Update snapshots with new class name * Another snapshot update for new class name * Replace more uses of old class name in tests * More snapshot updates for new class name * Unsure how this ever worked in chronological mode * More snapshot updates * Fix dot colours * Upgrade to compound-web 3 * Fix computed notification levels * Add test for notificationLevelToIndicator --- package.json | 2 +- res/css/views/rooms/_NotificationBadge.pcss | 23 ++++++++++++---- .../StatelessNotificationBadge.tsx | 3 ++- src/components/views/rooms/RoomHeader.tsx | 16 +---------- src/components/views/rooms/ThreadSummary.tsx | 9 +++++++ src/hooks/room/useRoomThreadNotifications.ts | 4 +-- src/utils/notifications.ts | 20 ++++++++++++++ .../structures/TimelinePanel-test.tsx | 2 +- .../__snapshots__/RoomStatusBar-test.tsx.snap | 4 +-- .../__snapshots__/RoomView-test.tsx.snap | 2 +- .../components/views/rooms/EventTile-test.tsx | 4 +-- .../StatelessNotificationBadge-test.tsx | 2 +- .../UnreadNotificationBadge-test.tsx | 10 +++---- .../VideoRoomChatButton-test.tsx.snap | 19 ++++++++++--- .../__snapshots__/RoomHeader-test.tsx.snap | 27 ++++++++++++++----- .../Notifications2-test.tsx.snap | 4 +-- .../SpaceTreeLevel-test.tsx.snap | 2 +- test/utils/notifications-test.ts | 20 ++++++++++++++ yarn.lock | 11 ++++---- 19 files changed, 130 insertions(+), 54 deletions(-) diff --git a/package.json b/package.json index 1e2bf0a3a0..9fed42d7a1 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "@sentry/browser": "^7.0.0", "@testing-library/react-hooks": "^8.0.1", "@vector-im/compound-design-tokens": "^0.1.0", - "@vector-im/compound-web": "2.0.0", + "@vector-im/compound-web": "3.0.0", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", "@zxcvbn-ts/language-en": "^3.0.2", diff --git a/res/css/views/rooms/_NotificationBadge.pcss b/res/css/views/rooms/_NotificationBadge.pcss index 68b44b124b..41b1e0f530 100644 --- a/res/css/views/rooms/_NotificationBadge.pcss +++ b/res/css/views/rooms/_NotificationBadge.pcss @@ -33,21 +33,34 @@ limitations under the License. align-items: center; justify-content: center; - &.mx_NotificationBadge_highlighted { - /* TODO: Use a more specific variable */ - background-color: $alert; - } - /* These are the 3 background types */ &.mx_NotificationBadge_dot { width: 8px; height: 8px; border-radius: 8px; + background-color: var(--cpd-color-text-primary); .mx_NotificationBadge_count { display: none; } + + /* Redundant sounding name, but a notification badge that indicates there is a regular, + * non-highlight notification + * The green colour only applies for notification dot: badges indicating the same notification + * level are the standard grey. + */ + &.mx_NotificationBadge_level_notification { + background-color: var(--cpd-color-icon-success-primary); + } + } + + /* Badges for highlight notifications. Style for notification level + * badges is in _EventTile.scss because it applies only to notification + * dots, not badges. + */ + &.mx_NotificationBadge_level_highlight { + background-color: var(--cpd-color-icon-critical-primary); } &.mx_NotificationBadge_knocked { diff --git a/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx b/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx index 03529defc1..69f756b3b7 100644 --- a/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx @@ -59,7 +59,8 @@ export const StatelessNotificationBadge = forwardRef= NotificationLevel.Highlight, + mx_NotificationBadge_level_notification: level == NotificationLevel.Notification, + mx_NotificationBadge_level_highlight: level >= NotificationLevel.Highlight, mx_NotificationBadge_dot: (isEmptyBadge && !knocked) || type === "dot", mx_NotificationBadge_knocked: knocked, mx_NotificationBadge_2char: type === "badge" && symbol && symbol.length > 0 && symbol.length < 3, diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index ec95e2a15c..6bd6b0a12b 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -37,7 +37,6 @@ import { Flex } from "../../utils/Flex"; import { Box } from "../../utils/Box"; import { useRoomCall } from "../../../hooks/room/useRoomCall"; import { useRoomThreadNotifications } from "../../../hooks/room/useRoomThreadNotifications"; -import { NotificationLevel } from "../../../stores/notifications/NotificationLevel"; import { useGlobalNotificationState } from "../../../hooks/useGlobalNotificationState"; import SdkConfig from "../../../SdkConfig"; import { useFeatureEnabled } from "../../../hooks/useSettings"; @@ -52,20 +51,7 @@ import { Linkify, topicToHtml } from "../../../HtmlUtils"; import PosthogTrackers from "../../../PosthogTrackers"; import { VideoRoomChatButton } from "./RoomHeader/VideoRoomChatButton"; import { RoomKnocksBar } from "./RoomKnocksBar"; - -/** - * A helper to transform a notification color to the what the Compound Icon Button - * expects - */ -function notificationLevelToIndicator(color: NotificationLevel): React.ComponentProps["indicator"] { - if (color <= NotificationLevel.None) { - return undefined; - } else if (color <= NotificationLevel.Notification) { - return "default"; - } else { - return "highlight"; - } -} +import { notificationLevelToIndicator } from "../../../utils/notifications"; export default function RoomHeader({ room, diff --git a/src/components/views/rooms/ThreadSummary.tsx b/src/components/views/rooms/ThreadSummary.tsx index 7a55c89580..1e30dcba9d 100644 --- a/src/components/views/rooms/ThreadSummary.tsx +++ b/src/components/views/rooms/ThreadSummary.tsx @@ -16,6 +16,8 @@ limitations under the License. import React, { useContext, useState } from "react"; import { Thread, ThreadEvent, IContent, MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/matrix"; +import { IndicatorIcon } from "@vector-im/compound-web"; +import { Icon as ThreadIconSolid } from "@vector-im/compound-design-tokens/icons/threads-solid.svg"; import { _t } from "../../../languageHandler"; import { CardContext } from "../right_panel/context"; @@ -30,6 +32,8 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { Action } from "../../../dispatcher/actions"; import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload"; import defaultDispatcher from "../../../dispatcher/dispatcher"; +import { useUnreadNotifications } from "../../../hooks/useUnreadNotifications"; +import { notificationLevelToIndicator } from "../../../utils/notifications"; interface IProps { mxEvent: MatrixEvent; @@ -40,6 +44,8 @@ const ThreadSummary: React.FC = ({ mxEvent, thread, ...props }) => { const roomContext = useContext(RoomContext); const cardContext = useContext(CardContext); const count = useTypedEventEmitterState(thread, ThreadEvent.Update, () => thread.length); + const { level } = useUnreadNotifications(thread.room, thread.id); + if (!count) return null; // We don't want to show a thread summary if the thread doesn't have replies yet let countSection: string | number = count; @@ -61,6 +67,9 @@ const ThreadSummary: React.FC = ({ mxEvent, thread, ...props }) => { }} aria-label={_t("threads|open_thread")} > + + + {countSection}
diff --git a/src/hooks/room/useRoomThreadNotifications.ts b/src/hooks/room/useRoomThreadNotifications.ts index 93c3b8eed0..374301d137 100644 --- a/src/hooks/room/useRoomThreadNotifications.ts +++ b/src/hooks/room/useRoomThreadNotifications.ts @@ -33,10 +33,10 @@ export const useRoomThreadNotifications = (room: Room): NotificationLevel => { switch (room?.threadsAggregateNotificationType) { case NotificationCountType.Highlight: setNotificationLevel(NotificationLevel.Highlight); - break; + return; case NotificationCountType.Total: setNotificationLevel(NotificationLevel.Notification); - break; + return; } // We don't have any notified messages, but we might have unread messages. Let's // find out. diff --git a/src/utils/notifications.ts b/src/utils/notifications.ts index e0cde92617..7f0e98f142 100644 --- a/src/utils/notifications.ts +++ b/src/utils/notifications.ts @@ -22,8 +22,10 @@ import { LocalNotificationSettings, ReceiptType, } from "matrix-js-sdk/src/matrix"; +import { IndicatorIcon } from "@vector-im/compound-web"; import SettingsStore from "../settings/SettingsStore"; +import { NotificationLevel } from "../stores/notifications/NotificationLevel"; export const deviceNotificationSettingsKeys = [ "notificationsEnabled", @@ -113,3 +115,21 @@ export function clearAllNotifications(client: MatrixClient): Promise["indicator"] { + if (level <= NotificationLevel.None) { + return undefined; + } else if (level <= NotificationLevel.Activity) { + return "default"; + } else if (level <= NotificationLevel.Notification) { + return "success"; + } else { + return "critical"; + } +} diff --git a/test/components/structures/TimelinePanel-test.tsx b/test/components/structures/TimelinePanel-test.tsx index c68e37c83f..a5312e43c5 100644 --- a/test/components/structures/TimelinePanel-test.tsx +++ b/test/components/structures/TimelinePanel-test.tsx @@ -817,7 +817,7 @@ describe("TimelinePanel", () => { client = MatrixClientPeg.safeGet(); Thread.hasServerSideSupport = FeatureSupport.Stable; - room = new Room("roomId", client, "userId"); + room = new Room("roomId", client, "userId", { pendingEventOrdering: PendingEventOrdering.Detached }); allThreads = new EventTimelineSet( room, { diff --git a/test/components/structures/__snapshots__/RoomStatusBar-test.tsx.snap b/test/components/structures/__snapshots__/RoomStatusBar-test.tsx.snap index ed969114ec..19b0cd3b49 100644 --- a/test/components/structures/__snapshots__/RoomStatusBar-test.tsx.snap +++ b/test/components/structures/__snapshots__/RoomStatusBar-test.tsx.snap @@ -12,7 +12,7 @@ exports[`RoomStatusBar unsent messages should render warning w class="mx_RoomStatusBar_unsentBadge" >
unsent messages should render warning w class="mx_RoomStatusBar_unsentBadge" >
{ }); expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(1); - expect(container.getElementsByClassName("mx_NotificationBadge_highlighted")).toHaveLength(0); + expect(container.getElementsByClassName("mx_NotificationBadge_level_highlight")).toHaveLength(0); act(() => { room.setThreadUnreadNotificationCount(mxEvent.getId()!, NotificationCountType.Highlight, 1); }); expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(1); - expect(container.getElementsByClassName("mx_NotificationBadge_highlighted")).toHaveLength(1); + expect(container.getElementsByClassName("mx_NotificationBadge_level_highlight")).toHaveLength(1); }); }); diff --git a/test/components/views/rooms/NotificationBadge/StatelessNotificationBadge-test.tsx b/test/components/views/rooms/NotificationBadge/StatelessNotificationBadge-test.tsx index a6f1cd66be..66ae273e24 100644 --- a/test/components/views/rooms/NotificationBadge/StatelessNotificationBadge-test.tsx +++ b/test/components/views/rooms/NotificationBadge/StatelessNotificationBadge-test.tsx @@ -25,7 +25,7 @@ describe("StatelessNotificationBadge", () => { const { container } = render( , ); - expect(container.querySelector(".mx_NotificationBadge_highlighted")).not.toBe(null); + expect(container.querySelector(".mx_NotificationBadge_level_highlight")).not.toBe(null); }); it("has knock style", () => { diff --git a/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx b/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx index 09fed6e73e..8f884eeb9c 100644 --- a/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx +++ b/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx @@ -92,26 +92,26 @@ describe("UnreadNotificationBadge", () => { const { container } = render(getComponent()); expect(container.querySelector(".mx_NotificationBadge_visible")).toBeTruthy(); - expect(container.querySelector(".mx_NotificationBadge_highlighted")).toBeFalsy(); + expect(container.querySelector(".mx_NotificationBadge_level_highlight")).toBeFalsy(); act(() => { room.setUnreadNotificationCount(NotificationCountType.Highlight, 1); }); - expect(container.querySelector(".mx_NotificationBadge_highlighted")).toBeTruthy(); + expect(container.querySelector(".mx_NotificationBadge_level_highlight")).toBeTruthy(); }); it("renders unread thread notification badge", () => { const { container } = render(getComponent(THREAD_ID)); expect(container.querySelector(".mx_NotificationBadge_visible")).toBeTruthy(); - expect(container.querySelector(".mx_NotificationBadge_highlighted")).toBeFalsy(); + expect(container.querySelector(".mx_NotificationBadge_level_highlight")).toBeFalsy(); act(() => { room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 1); }); - expect(container.querySelector(".mx_NotificationBadge_highlighted")).toBeTruthy(); + expect(container.querySelector(".mx_NotificationBadge_level_highlight")).toBeTruthy(); }); it("hides unread notification badge", () => { @@ -177,6 +177,6 @@ describe("UnreadNotificationBadge", () => { const { container } = render(getComponent(THREAD_ID)); expect(container.querySelector(".mx_NotificationBadge_dot")).toBeTruthy(); expect(container.querySelector(".mx_NotificationBadge_visible")).toBeTruthy(); - expect(container.querySelector(".mx_NotificationBadge_highlighted")).toBeFalsy(); + expect(container.querySelector(".mx_NotificationBadge_level_highlight")).toBeFalsy(); }); }); diff --git a/test/components/views/rooms/RoomHeader/__snapshots__/VideoRoomChatButton-test.tsx.snap b/test/components/views/rooms/RoomHeader/__snapshots__/VideoRoomChatButton-test.tsx.snap index 0c21eaa713..2d5e226d30 100644 --- a/test/components/views/rooms/RoomHeader/__snapshots__/VideoRoomChatButton-test.tsx.snap +++ b/test/components/views/rooms/RoomHeader/__snapshots__/VideoRoomChatButton-test.tsx.snap @@ -3,26 +3,37 @@ exports[` renders button when room is a video room 1`] = ` `; exports[` renders button with an unread marker when room is unread 1`] = ` `; diff --git a/test/components/views/rooms/__snapshots__/RoomHeader-test.tsx.snap b/test/components/views/rooms/__snapshots__/RoomHeader-test.tsx.snap index d24f614887..792d177727 100644 --- a/test/components/views/rooms/__snapshots__/RoomHeader-test.tsx.snap +++ b/test/components/views/rooms/__snapshots__/RoomHeader-test.tsx.snap @@ -46,34 +46,49 @@ exports[`RoomHeader does not show the face pile for DMs 1`] = `
diff --git a/test/components/views/settings/notifications/__snapshots__/Notifications2-test.tsx.snap b/test/components/views/settings/notifications/__snapshots__/Notifications2-test.tsx.snap index 5c75d69e0b..56d7e77aaa 100644 --- a/test/components/views/settings/notifications/__snapshots__/Notifications2-test.tsx.snap +++ b/test/components/views/settings/notifications/__snapshots__/Notifications2-test.tsx.snap @@ -480,7 +480,7 @@ exports[` correctly handles the loading/disabled state 1`] = ` Show a badge
matches the snapshot 1`] = ` Show a badge
diff --git a/test/utils/notifications-test.ts b/test/utils/notifications-test.ts index a556f3ffc3..62200c7ef9 100644 --- a/test/utils/notifications-test.ts +++ b/test/utils/notifications-test.ts @@ -24,11 +24,13 @@ import { deviceNotificationSettingsKeys, clearAllNotifications, clearRoomNotification, + notificationLevelToIndicator, } from "../../src/utils/notifications"; import SettingsStore from "../../src/settings/SettingsStore"; import { getMockClientWithEventEmitter } from "../test-utils/client"; import { mkMessage, stubClient } from "../test-utils/test-utils"; import { MatrixClientPeg } from "../../src/MatrixClientPeg"; +import { NotificationLevel } from "../../src/stores/notifications/NotificationLevel"; jest.mock("../../src/settings/SettingsStore"); @@ -215,4 +217,22 @@ describe("notifications", () => { expect(sendReadReceiptSpy).toHaveBeenCalledWith(message, ReceiptType.ReadPrivate, true); }); }); + + describe("notificationLevelToIndicator", () => { + it("returns undefined if notification level is None", () => { + expect(notificationLevelToIndicator(NotificationLevel.None)).toBeUndefined(); + }); + + it("returns default if notification level is Activity", () => { + expect(notificationLevelToIndicator(NotificationLevel.Activity)).toEqual("default"); + }); + + it("returns success if notification level is Notification", () => { + expect(notificationLevelToIndicator(NotificationLevel.Notification)).toEqual("success"); + }); + + it("returns critical if notification level is Highlight", () => { + expect(notificationLevelToIndicator(NotificationLevel.Highlight)).toEqual("critical"); + }); + }); }); diff --git a/yarn.lock b/yarn.lock index 72cc9b423a..6d1509fadc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2276,7 +2276,7 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-primitive" "1.0.3" -"@radix-ui/react-slot@1.0.2": +"@radix-ui/react-slot@1.0.2", "@radix-ui/react-slot@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.2.tgz#a9ff4423eade67f501ffb32ec22064bc9d3099ab" integrity sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg== @@ -3120,15 +3120,16 @@ dependencies: svg2vectordrawable "^2.9.1" -"@vector-im/compound-web@2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-2.0.0.tgz#9ffc621f32be11acabe74bb3ff59cc4d8bc845ac" - integrity sha512-vEhGayoBSq4WLf86VmFoX9h0KIxaAjlG+kmcJLWitsqnPEDOG0XPhScYqzEshFqdJFLWX6gBOnXYLeq065t57w== +"@vector-im/compound-web@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-3.0.0.tgz#8843c1c6a40891f89fdb3dbccf972e2fc2e1e387" + integrity sha512-6wkFoByaiXvwrqNmF0W9K5/krThpczPnYeJOBG3FM90RoC3MrqNB6fBPTvsd17pzJWN6fEV2B11JUeqAFW0z5A== dependencies: "@radix-ui/react-context-menu" "^2.1.5" "@radix-ui/react-dropdown-menu" "^2.0.6" "@radix-ui/react-form" "^0.0.3" "@radix-ui/react-separator" "^1.0.3" + "@radix-ui/react-slot" "^1.0.2" "@radix-ui/react-tooltip" "^1.0.6" classnames "^2.3.2" graphemer "^1.4.0"