Devtools for stuck notifications (#10042)
This commit is contained in:
parent
469228f45e
commit
6dd578e5a7
9 changed files with 378 additions and 6 deletions
|
@ -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, string> = {
|
|||
[Category.Other]: _td("Other"),
|
||||
};
|
||||
|
||||
export type Tool = React.FC<IDevtoolsProps>;
|
||||
export type Tool = React.FC<IDevtoolsProps> | ((props: IDevtoolsProps) => JSX.Element);
|
||||
const Tools: Record<Category, [label: string, tool: Tool][]> = {
|
||||
[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],
|
||||
],
|
||||
|
|
180
src/components/views/dialogs/devtools/RoomNotifications.tsx
Normal file
180
src/components/views/dialogs/devtools/RoomNotifications.tsx
Normal file
|
@ -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 (
|
||||
<BaseTool onBack={onBack}>
|
||||
<section>
|
||||
<h2>{_t("Room status")}</h2>
|
||||
<ul>
|
||||
<li>
|
||||
{_t("Room unread status: ")}
|
||||
<strong>{humanReadableNotificationColor(color)}</strong>
|
||||
{count > 0 && (
|
||||
<>
|
||||
{_t(", count:")} <strong>{count}</strong>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{_t("Notification state is")} <strong>{notificationState}</strong>
|
||||
</li>
|
||||
<li>
|
||||
{_t("Room is ")}
|
||||
<strong>
|
||||
{cli.isRoomEncrypted(room.roomId!) ? _t("encrypted ✅") : _t("not encrypted 🚨")}
|
||||
</strong>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>{_t("Main timeline")}</h2>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
{_t("Total: ")} {room.getRoomUnreadNotificationCount(NotificationCountType.Total)}
|
||||
</li>
|
||||
<li>
|
||||
{_t("Highlight: ")} {room.getRoomUnreadNotificationCount(NotificationCountType.Highlight)}
|
||||
</li>
|
||||
<li>
|
||||
{_t("Dot: ")} {doesRoomOrThreadHaveUnreadMessages(room) + ""}
|
||||
</li>
|
||||
{roomHasUnread(room) && (
|
||||
<>
|
||||
<li>
|
||||
{_t("User read up to: ")}
|
||||
<strong>
|
||||
{room.getReadReceiptForUserId(cli.getSafeUserId())?.eventId ??
|
||||
_t("No receipt found")}
|
||||
</strong>
|
||||
</li>
|
||||
<li>
|
||||
{_t("Last event:")}
|
||||
<ul>
|
||||
<li>
|
||||
{_t("ID: ")} <strong>{room.timeline[room.timeline.length - 1].getId()}</strong>
|
||||
</li>
|
||||
<li>
|
||||
{_t("Type: ")}{" "}
|
||||
<strong>{room.timeline[room.timeline.length - 1].getType()}</strong>
|
||||
</li>
|
||||
<li>
|
||||
{_t("Sender: ")}{" "}
|
||||
<strong>{room.timeline[room.timeline.length - 1].getSender()}</strong>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>{_t("Threads timeline")}</h2>
|
||||
<ul>
|
||||
{room
|
||||
.getThreads()
|
||||
.filter((thread) => threadHasUnread(thread))
|
||||
.map((thread) => (
|
||||
<li key={thread.id}>
|
||||
{_t("Thread Id: ")} {thread.id}
|
||||
<ul>
|
||||
<li>
|
||||
{_t("Total: ")}
|
||||
<strong>
|
||||
{room.getThreadUnreadNotificationCount(
|
||||
thread.id,
|
||||
NotificationCountType.Total,
|
||||
)}
|
||||
</strong>
|
||||
</li>
|
||||
<li>
|
||||
{_t("Highlight: ")}
|
||||
<strong>
|
||||
{room.getThreadUnreadNotificationCount(
|
||||
thread.id,
|
||||
NotificationCountType.Highlight,
|
||||
)}
|
||||
</strong>
|
||||
</li>
|
||||
<li>
|
||||
{_t("Dot: ")} <strong>{doesRoomOrThreadHaveUnreadMessages(thread) + ""}</strong>
|
||||
</li>
|
||||
<li>
|
||||
{_t("User read up to: ")}
|
||||
<strong>
|
||||
{thread.getReadReceiptForUserId(cli.getSafeUserId())?.eventId ??
|
||||
_t("No receipt found")}
|
||||
</strong>
|
||||
</li>
|
||||
<li>
|
||||
{_t("Last event:")}
|
||||
<ul>
|
||||
<li>
|
||||
{_t("ID: ")} <strong>{thread.lastReply()?.getId()}</strong>
|
||||
</li>
|
||||
<li>
|
||||
{_t("Type: ")} <strong>{thread.lastReply()?.getType()}</strong>
|
||||
</li>
|
||||
<li>
|
||||
{_t("Sender: ")} <strong>{thread.lastReply()?.getSender()}</strong>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</BaseTool>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
|
@ -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<Phase, string> = {
|
||||
|
@ -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);
|
||||
|
||||
|
|
|
@ -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": "<space>",
|
||||
"<%(count)s spaces>|zero": "<empty string>",
|
||||
|
@ -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",
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,6 +75,11 @@ exports[`DevtoolsDialog renders the devtools dialog 1`] = `
|
|||
>
|
||||
View servers in room
|
||||
</button>
|
||||
<button
|
||||
class="mx_DevTools_button"
|
||||
>
|
||||
Notifications debug
|
||||
</button>
|
||||
<button
|
||||
class="mx_DevTools_button"
|
||||
>
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
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 React from "react";
|
||||
import { render } from "@testing-library/react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { PendingEventOrdering } from "matrix-js-sdk/src/client";
|
||||
|
||||
import RoomNotifications from "../../../../../src/components/views/dialogs/devtools/RoomNotifications";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import { stubClient } from "../../../../test-utils";
|
||||
import { DevtoolsContext } from "../../../../../src/components/views/dialogs/devtools/BaseTool";
|
||||
|
||||
describe("<RoomNotifications />", () => {
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const { asFragment } = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<DevtoolsContext.Provider
|
||||
value={{
|
||||
room: new Room("!roomId", cli, "@alice:example.com", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<RoomNotifications onBack={() => {}} setTool={() => {}} />
|
||||
</DevtoolsContext.Provider>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,62 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<RoomNotifications /> should render 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_DevTools_content"
|
||||
>
|
||||
<section>
|
||||
<h2>
|
||||
Room status
|
||||
</h2>
|
||||
<ul>
|
||||
<li>
|
||||
Room unread status:
|
||||
<strong>
|
||||
None
|
||||
</strong>
|
||||
</li>
|
||||
<li>
|
||||
Notification state is
|
||||
<strong />
|
||||
</li>
|
||||
<li>
|
||||
Room is
|
||||
<strong>
|
||||
not encrypted 🚨
|
||||
</strong>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
<h2>
|
||||
Main timeline
|
||||
</h2>
|
||||
<ul>
|
||||
<li>
|
||||
Total: 0
|
||||
</li>
|
||||
<li>
|
||||
Highlight: 0
|
||||
</li>
|
||||
<li>
|
||||
Dot: false
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
<h2>
|
||||
Threads timeline
|
||||
</h2>
|
||||
<ul />
|
||||
</section>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<button>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
31
test/stores/notifications/NotificationColor-test.ts
Normal file
31
test/stores/notifications/NotificationColor-test.ts
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue