From 6dd578e5a7cad0abe2fadcdfcd075b4fe6d3f621 Mon Sep 17 00:00:00 2001 From: Germain Date: Fri, 3 Feb 2023 10:07:24 +0000 Subject: [PATCH] Devtools for stuck notifications (#10042) --- .../views/dialogs/DevtoolsDialog.tsx | 4 +- .../dialogs/devtools/RoomNotifications.tsx | 180 ++++++++++++++++++ .../dialogs/devtools/VerificationExplorer.tsx | 4 +- src/i18n/strings/en_EN.json | 29 ++- src/stores/notifications/NotificationColor.ts | 19 ++ .../DevtoolsDialog-test.tsx.snap | 5 + .../devtools/RoomNotifications-test.tsx | 50 +++++ .../RoomNotifications-test.tsx.snap | 62 ++++++ .../notifications/NotificationColor-test.ts | 31 +++ 9 files changed, 378 insertions(+), 6 deletions(-) create mode 100644 src/components/views/dialogs/devtools/RoomNotifications.tsx create mode 100644 test/components/views/dialogs/devtools/RoomNotifications-test.tsx create mode 100644 test/components/views/dialogs/devtools/__snapshots__/RoomNotifications-test.tsx.snap create mode 100644 test/stores/notifications/NotificationColor-test.ts diff --git a/src/components/views/dialogs/DevtoolsDialog.tsx b/src/components/views/dialogs/DevtoolsDialog.tsx index f16dd94f6b..0df6ce4206 100644 --- a/src/components/views/dialogs/DevtoolsDialog.tsx +++ b/src/components/views/dialogs/DevtoolsDialog.tsx @@ -33,6 +33,7 @@ import { SettingLevel } from "../../../settings/SettingLevel"; import ServerInfo from "./devtools/ServerInfo"; import { Features } from "../../../settings/Settings"; import CopyableText from "../elements/CopyableText"; +import RoomNotifications from "./devtools/RoomNotifications"; enum Category { Room, @@ -44,13 +45,14 @@ const categoryLabels: Record = { [Category.Other]: _td("Other"), }; -export type Tool = React.FC; +export type Tool = React.FC | ((props: IDevtoolsProps) => JSX.Element); const Tools: Record = { [Category.Room]: [ [_td("Send custom timeline event"), TimelineEventEditor], [_td("Explore room state"), RoomStateExplorer], [_td("Explore room account data"), RoomAccountDataExplorer], [_td("View servers in room"), ServersInRoom], + [_td("Notifications debug"), RoomNotifications], [_td("Verification explorer"), VerificationExplorer], [_td("Active Widgets"), WidgetExplorer], ], diff --git a/src/components/views/dialogs/devtools/RoomNotifications.tsx b/src/components/views/dialogs/devtools/RoomNotifications.tsx new file mode 100644 index 0000000000..7ddfb5d8ba --- /dev/null +++ b/src/components/views/dialogs/devtools/RoomNotifications.tsx @@ -0,0 +1,180 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room"; +import { Thread } from "matrix-js-sdk/src/models/thread"; +import React, { useContext } from "react"; + +import MatrixClientContext from "../../../../contexts/MatrixClientContext"; +import { useNotificationState } from "../../../../hooks/useRoomNotificationState"; +import { _t } from "../../../../languageHandler"; +import { determineUnreadState } from "../../../../RoomNotifs"; +import { humanReadableNotificationColor } from "../../../../stores/notifications/NotificationColor"; +import { doesRoomOrThreadHaveUnreadMessages } from "../../../../Unread"; +import BaseTool, { DevtoolsContext, IDevtoolsProps } from "./BaseTool"; + +export default function RoomNotifications({ onBack }: IDevtoolsProps): JSX.Element { + const { room } = useContext(DevtoolsContext); + const cli = useContext(MatrixClientContext); + + const { color, count } = determineUnreadState(room); + const [notificationState] = useNotificationState(room); + + return ( + +
+

{_t("Room status")}

+
    +
  • + {_t("Room unread status: ")} + {humanReadableNotificationColor(color)} + {count > 0 && ( + <> + {_t(", count:")} {count} + + )} +
  • +
  • + {_t("Notification state is")} {notificationState} +
  • +
  • + {_t("Room is ")} + + {cli.isRoomEncrypted(room.roomId!) ? _t("encrypted ✅") : _t("not encrypted 🚨")} + +
  • +
+
+ +
+

{_t("Main timeline")}

+ +
    +
  • + {_t("Total: ")} {room.getRoomUnreadNotificationCount(NotificationCountType.Total)} +
  • +
  • + {_t("Highlight: ")} {room.getRoomUnreadNotificationCount(NotificationCountType.Highlight)} +
  • +
  • + {_t("Dot: ")} {doesRoomOrThreadHaveUnreadMessages(room) + ""} +
  • + {roomHasUnread(room) && ( + <> +
  • + {_t("User read up to: ")} + + {room.getReadReceiptForUserId(cli.getSafeUserId())?.eventId ?? + _t("No receipt found")} + +
  • +
  • + {_t("Last event:")} +
      +
    • + {_t("ID: ")} {room.timeline[room.timeline.length - 1].getId()} +
    • +
    • + {_t("Type: ")}{" "} + {room.timeline[room.timeline.length - 1].getType()} +
    • +
    • + {_t("Sender: ")}{" "} + {room.timeline[room.timeline.length - 1].getSender()} +
    • +
    +
  • + + )} +
+
+ +
+

{_t("Threads timeline")}

+
    + {room + .getThreads() + .filter((thread) => threadHasUnread(thread)) + .map((thread) => ( +
  • + {_t("Thread Id: ")} {thread.id} +
      +
    • + {_t("Total: ")} + + {room.getThreadUnreadNotificationCount( + thread.id, + NotificationCountType.Total, + )} + +
    • +
    • + {_t("Highlight: ")} + + {room.getThreadUnreadNotificationCount( + thread.id, + NotificationCountType.Highlight, + )} + +
    • +
    • + {_t("Dot: ")} {doesRoomOrThreadHaveUnreadMessages(thread) + ""} +
    • +
    • + {_t("User read up to: ")} + + {thread.getReadReceiptForUserId(cli.getSafeUserId())?.eventId ?? + _t("No receipt found")} + +
    • +
    • + {_t("Last event:")} +
        +
      • + {_t("ID: ")} {thread.lastReply()?.getId()} +
      • +
      • + {_t("Type: ")} {thread.lastReply()?.getType()} +
      • +
      • + {_t("Sender: ")} {thread.lastReply()?.getSender()} +
      • +
      +
    • +
    +
  • + ))} +
+
+
+ ); +} + +function threadHasUnread(thread: Thread): boolean { + const total = thread.room.getThreadUnreadNotificationCount(thread.id, NotificationCountType.Total); + const highlight = thread.room.getThreadUnreadNotificationCount(thread.id, NotificationCountType.Highlight); + const dot = doesRoomOrThreadHaveUnreadMessages(thread); + + return total > 0 || highlight > 0 || dot; +} + +function roomHasUnread(room: Room): boolean { + const total = room.getRoomUnreadNotificationCount(NotificationCountType.Total); + const highlight = room.getRoomUnreadNotificationCount(NotificationCountType.Highlight); + const dot = doesRoomOrThreadHaveUnreadMessages(room); + + return total > 0 || highlight > 0 || dot; +} diff --git a/src/components/views/dialogs/devtools/VerificationExplorer.tsx b/src/components/views/dialogs/devtools/VerificationExplorer.tsx index 7092d87f64..c535d32b32 100644 --- a/src/components/views/dialogs/devtools/VerificationExplorer.tsx +++ b/src/components/views/dialogs/devtools/VerificationExplorer.tsx @@ -25,7 +25,7 @@ import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import { useTypedEventEmitter, useTypedEventEmitterState } from "../../../../hooks/useEventEmitter"; import { _t, _td } from "../../../../languageHandler"; import MatrixClientContext from "../../../../contexts/MatrixClientContext"; -import BaseTool, { DevtoolsContext } from "./BaseTool"; +import BaseTool, { DevtoolsContext, IDevtoolsProps } from "./BaseTool"; import { Tool } from "../DevtoolsDialog"; const PHASE_MAP: Record = { @@ -81,7 +81,7 @@ const VerificationRequestExplorer: React.FC<{ ); }; -const VerificationExplorer: Tool = ({ onBack }) => { +const VerificationExplorer: Tool = ({ onBack }: IDevtoolsProps) => { const cli = useContext(MatrixClientContext); const context = useContext(DevtoolsContext); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f61c21b028..c1aeeab2de 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -902,6 +902,12 @@ "Room information": "Room information", "Room members": "Room members", "Back to thread": "Back to thread", + "None": "None", + "Bold": "Bold", + "Grey": "Grey", + "Red": "Red", + "Unsent": "Unsent", + "unknown": "unknown", "Change notification settings": "Change notification settings", "Messaging": "Messaging", "Profile": "Profile", @@ -1582,7 +1588,6 @@ "Error removing ignored user/server": "Error removing ignored user/server", "Error unsubscribing from list": "Error unsubscribing from list", "Please try again or view your console for hints.": "Please try again or view your console for hints.", - "None": "None", "Ban list rules - %(roomName)s": "Ban list rules - %(roomName)s", "Server rules": "Server rules", "User rules": "User rules", @@ -1942,7 +1947,6 @@ "Poll": "Poll", "Hide formatting": "Hide formatting", "Show formatting": "Show formatting", - "Bold": "Bold", "Italics": "Italics", "Strikethrough": "Strikethrough", "Code block": "Code block", @@ -2773,6 +2777,7 @@ "Explore room state": "Explore room state", "Explore room account data": "Explore room account data", "View servers in room": "View servers in room", + "Notifications debug": "Notifications debug", "Verification explorer": "Verification explorer", "Active Widgets": "Active Widgets", "Explore account data": "Explore account data", @@ -3152,6 +3157,25 @@ "Event Content": "Event Content", "Filter results": "Filter results", "No results found": "No results found", + "Room status": "Room status", + "Room unread status: ": "Room unread status: ", + ", count:": ", count:", + "Notification state is": "Notification state is", + "Room is ": "Room is ", + "encrypted ✅": "encrypted ✅", + "not encrypted 🚨": "not encrypted 🚨", + "Main timeline": "Main timeline", + "Total: ": "Total: ", + "Highlight: ": "Highlight: ", + "Dot: ": "Dot: ", + "User read up to: ": "User read up to: ", + "No receipt found": "No receipt found", + "Last event:": "Last event:", + "ID: ": "ID: ", + "Type: ": "Type: ", + "Sender: ": "Sender: ", + "Threads timeline": "Threads timeline", + "Thread Id: ": "Thread Id: ", "<%(count)s spaces>|other": "<%(count)s spaces>", "<%(count)s spaces>|one": "", "<%(count)s spaces>|zero": "", @@ -3182,7 +3206,6 @@ "Value": "Value", "Value in this room": "Value in this room", "Edit setting": "Edit setting", - "Unsent": "Unsent", "Requested": "Requested", "Ready": "Ready", "Started": "Started", diff --git a/src/stores/notifications/NotificationColor.ts b/src/stores/notifications/NotificationColor.ts index 58737866df..f89bb1728d 100644 --- a/src/stores/notifications/NotificationColor.ts +++ b/src/stores/notifications/NotificationColor.ts @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { _t } from "../../languageHandler"; + export enum NotificationColor { // Inverted (None -> Red) because we do integer comparisons on this None, // nothing special @@ -23,3 +25,20 @@ export enum NotificationColor { Red, // unread pings Unsent, // some messages failed to send } + +export function humanReadableNotificationColor(color: NotificationColor): string { + switch (color) { + case NotificationColor.None: + return _t("None"); + case NotificationColor.Bold: + return _t("Bold"); + case NotificationColor.Grey: + return _t("Grey"); + case NotificationColor.Red: + return _t("Red"); + case NotificationColor.Unsent: + return _t("Unsent"); + default: + return _t("unknown"); + } +} diff --git a/test/components/views/dialogs/__snapshots__/DevtoolsDialog-test.tsx.snap b/test/components/views/dialogs/__snapshots__/DevtoolsDialog-test.tsx.snap index 62014e2d2b..5e859a6c63 100644 --- a/test/components/views/dialogs/__snapshots__/DevtoolsDialog-test.tsx.snap +++ b/test/components/views/dialogs/__snapshots__/DevtoolsDialog-test.tsx.snap @@ -75,6 +75,11 @@ exports[`DevtoolsDialog renders the devtools dialog 1`] = ` > View servers in room + + + +`; diff --git a/test/stores/notifications/NotificationColor-test.ts b/test/stores/notifications/NotificationColor-test.ts new file mode 100644 index 0000000000..1125c47bff --- /dev/null +++ b/test/stores/notifications/NotificationColor-test.ts @@ -0,0 +1,31 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { humanReadableNotificationColor, NotificationColor } from "../../../src/stores/notifications/NotificationColor"; + +describe("NotificationColor", () => { + describe("humanReadableNotificationColor", () => { + it.each([ + [NotificationColor.None, "None"], + [NotificationColor.Bold, "Bold"], + [NotificationColor.Grey, "Grey"], + [NotificationColor.Red, "Red"], + [NotificationColor.Unsent, "Unsent"], + ])("correctly maps the output", (color, output) => { + expect(humanReadableNotificationColor(color)).toBe(output); + }); + }); +});