Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit a5ed97b

Browse files
dbkrrichvdh
andauthored
Mark as Unread (#12254)
* Support the mark as unread flag * Add mark as unread menu option and make clering notifications also clear the unread flag * Mark as read on viewing room * Tests * Remove random import * Don't show mark as unread for historical rooms * Fix tests & add test for menu option * Test RoomNotificationState updates on unread flag change * Test it doesn't update on other room account data * New icon for mark as unread * Add analytics events for mark as (un)read * Bump to new analytics-events package * Read from both stable & unstable prefixes * Cast to boolean before checking to avoid setting state unnecessarily * Typo Co-authored-by: Richard van der Hoff <[email protected]> * Doc external interface (and the rest at the same time) * Doc & rename unread market set function * Doc const exports * Remove listener on destroy * Add playwright test * Clearer language, hopefully * Move comment * Add reference to the MSC Co-authored-by: Richard van der Hoff <[email protected]> * Expand on function doc * Remove empty beforeEach * Rejig badge logic a little and add tests * Fix basdges to not display dots in room sublists again and hopefully rename the forceDot option to something that better indicates what it does, and add tests. * Remove duplicate license header (?) * Missing word (several times...) * Incorporate PR suggestion on badge type switch * Better description in doc comment Co-authored-by: Richard van der Hoff <[email protected]> * Update other doc comments in the same way * Remove duplicate quote * Use quotes consistently * Better test name * c+p fail --------- Co-authored-by: Richard van der Hoff <[email protected]>
1 parent a8341c0 commit a5ed97b

File tree

20 files changed

+458
-33
lines changed

20 files changed

+458
-33
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
},
6868
"dependencies": {
6969
"@babel/runtime": "^7.12.5",
70-
"@matrix-org/analytics-events": "^0.10.0",
70+
"@matrix-org/analytics-events": "^0.12.0",
7171
"@matrix-org/emojibase-bindings": "^1.1.2",
7272
"@matrix-org/matrix-wysiwyg": "2.17.0",
7373
"@matrix-org/olm": "3.2.15",
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
Copyright 2024 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { test, expect } from "../../element-web-test";
18+
19+
const TEST_ROOM_NAME = "The mark unread test room";
20+
21+
test.describe("Mark as Unread", () => {
22+
test.use({
23+
displayName: "Tom",
24+
botCreateOpts: {
25+
displayName: "BotBob",
26+
autoAcceptInvites: true,
27+
},
28+
});
29+
30+
test("should mark a room as unread", async ({ page, app, bot }) => {
31+
const roomId = await app.client.createRoom({
32+
name: TEST_ROOM_NAME,
33+
});
34+
const dummyRoomId = await app.client.createRoom({
35+
name: "Room of no consequence",
36+
});
37+
await app.client.inviteUser(roomId, bot.credentials.userId);
38+
await bot.joinRoom(roomId);
39+
await bot.sendMessage(roomId, "I am a robot. Beep.");
40+
41+
// Regular notification on new message
42+
await expect(page.getByLabel(TEST_ROOM_NAME + " 1 unread message.")).toBeVisible();
43+
await expect(page).toHaveTitle("Element [1]");
44+
45+
await page.goto("/#/room/" + roomId);
46+
47+
// should now be read, since we viewed the room (we have to assert the page title:
48+
// the room badge isn't visible since we're viewing the room)
49+
await expect(page).toHaveTitle("Element | " + TEST_ROOM_NAME);
50+
51+
// navigate away from the room again
52+
await page.goto("/#/room/" + dummyRoomId);
53+
54+
const roomTile = page.getByLabel(TEST_ROOM_NAME);
55+
await roomTile.focus();
56+
await roomTile.getByRole("button", { name: "Room options" }).click();
57+
await page.getByRole("menuitem", { name: "Mark as unread" }).click();
58+
59+
expect(page.getByLabel(TEST_ROOM_NAME + " Unread messages.")).toBeVisible();
60+
});
61+
});

res/css/views/context_menus/_RoomGeneralContextMenu.pcss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
mask-image: url("$(res)/img/element-icons/roomlist/mark-as-read.svg");
1111
}
1212

13+
.mx_RoomGeneralContextMenu_iconMarkAsUnread::before {
14+
mask-image: url("$(res)/img/element-icons/roomlist/mark-as-unread.svg");
15+
}
16+
1317
.mx_RoomGeneralContextMenu_iconNotificationsDefault::before {
1418
mask-image: url("$(res)/img/element-icons/notifications.svg");
1519
}
Lines changed: 4 additions & 0 deletions
Loading

src/RoomNotifs.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { getUnsentMessages } from "./components/structures/RoomStatusBar";
2929
import { doesRoomHaveUnreadMessages, doesRoomOrThreadHaveUnreadMessages } from "./Unread";
3030
import { EffectiveMembership, getEffectiveMembership, isKnockDenied } from "./utils/membership";
3131
import SettingsStore from "./settings/SettingsStore";
32+
import { getMarkedUnreadState } from "./utils/notifications";
3233

3334
export enum RoomNotifState {
3435
AllMessagesLoud = "all_messages_loud",
@@ -279,7 +280,8 @@ export function determineUnreadState(
279280
return { symbol: null, count: trueCount, level: NotificationLevel.Highlight };
280281
}
281282

282-
if (greyNotifs > 0) {
283+
const markedUnreadState = getMarkedUnreadState(room);
284+
if (greyNotifs > 0 || markedUnreadState) {
283285
return { symbol: null, count: trueCount, level: NotificationLevel.Notification };
284286
}
285287

src/components/views/context_menus/RoomGeneralContextMenu.tsx

Lines changed: 77 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { NotificationLevel } from "../../../stores/notifications/NotificationLev
3030
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
3131
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
3232
import DMRoomMap from "../../../utils/DMRoomMap";
33-
import { clearRoomNotification } from "../../../utils/notifications";
33+
import { clearRoomNotification, setMarkedUnreadState } from "../../../utils/notifications";
3434
import { IProps as IContextMenuProps } from "../../structures/ContextMenu";
3535
import IconizedContextMenu, {
3636
IconizedContextMenuCheckbox,
@@ -45,13 +45,60 @@ import { useSettingValue } from "../../../hooks/useSettings";
4545

4646
export interface RoomGeneralContextMenuProps extends IContextMenuProps {
4747
room: Room;
48+
/**
49+
* Called when the 'favourite' option is selected, after the menu has processed
50+
* the mouse or keyboard event.
51+
* @param event The event that caused the option to be selected.
52+
*/
4853
onPostFavoriteClick?: (event: ButtonEvent) => void;
54+
/**
55+
* Called when the 'low priority' option is selected, after the menu has processed
56+
* the mouse or keyboard event.
57+
* @param event The event that caused the option to be selected.
58+
*/
4959
onPostLowPriorityClick?: (event: ButtonEvent) => void;
60+
/**
61+
* Called when the 'invite' option is selected, after the menu has processed
62+
* the mouse or keyboard event.
63+
* @param event The event that caused the option to be selected.
64+
*/
5065
onPostInviteClick?: (event: ButtonEvent) => void;
66+
/**
67+
* Called when the 'copy link' option is selected, after the menu has processed
68+
* the mouse or keyboard event.
69+
* @param event The event that caused the option to be selected.
70+
*/
5171
onPostCopyLinkClick?: (event: ButtonEvent) => void;
72+
/**
73+
* Called when the 'settings' option is selected, after the menu has processed
74+
* the mouse or keyboard event.
75+
* @param event The event that caused the option to be selected.
76+
*/
5277
onPostSettingsClick?: (event: ButtonEvent) => void;
78+
/**
79+
* Called when the 'forget room' option is selected, after the menu has processed
80+
* the mouse or keyboard event.
81+
* @param event The event that caused the option to be selected.
82+
*/
5383
onPostForgetClick?: (event: ButtonEvent) => void;
84+
/**
85+
* Called when the 'leave' option is selected, after the menu has processed
86+
* the mouse or keyboard event.
87+
* @param event The event that caused the option to be selected.
88+
*/
5489
onPostLeaveClick?: (event: ButtonEvent) => void;
90+
/**
91+
* Called when the 'mark as read' option is selected, after the menu has processed
92+
* the mouse or keyboard event.
93+
* @param event The event that caused the option to be selected.
94+
*/
95+
onPostMarkAsReadClick?: (event: ButtonEvent) => void;
96+
/**
97+
* Called when the 'mark as unread' option is selected, after the menu has processed
98+
* the mouse or keyboard event.
99+
* @param event The event that caused the option to be selected.
100+
*/
101+
onPostMarkAsUnreadClick?: (event: ButtonEvent) => void;
55102
}
56103

57104
/**
@@ -67,6 +114,8 @@ export const RoomGeneralContextMenu: React.FC<RoomGeneralContextMenuProps> = ({
67114
onPostSettingsClick,
68115
onPostLeaveClick,
69116
onPostForgetClick,
117+
onPostMarkAsReadClick,
118+
onPostMarkAsUnreadClick,
70119
...props
71120
}) => {
72121
const cli = useContext(MatrixClientContext);
@@ -213,18 +262,33 @@ export const RoomGeneralContextMenu: React.FC<RoomGeneralContextMenuProps> = ({
213262
}
214263

215264
const { level } = useUnreadNotifications(room);
216-
const markAsReadOption: JSX.Element | null =
217-
level > NotificationLevel.None ? (
218-
<IconizedContextMenuCheckbox
219-
onClick={() => {
220-
clearRoomNotification(room, cli);
221-
onFinished?.();
222-
}}
223-
active={false}
224-
label={_t("room|context_menu|mark_read")}
225-
iconClassName="mx_RoomGeneralContextMenu_iconMarkAsRead"
226-
/>
227-
) : null;
265+
const markAsReadOption: JSX.Element | null = (() => {
266+
if (level > NotificationLevel.None) {
267+
return (
268+
<IconizedContextMenuOption
269+
onClick={wrapHandler(() => {
270+
clearRoomNotification(room, cli);
271+
onFinished?.();
272+
}, onPostMarkAsReadClick)}
273+
label={_t("room|context_menu|mark_read")}
274+
iconClassName="mx_RoomGeneralContextMenu_iconMarkAsRead"
275+
/>
276+
);
277+
} else if (!roomTags.includes(DefaultTagID.Archived)) {
278+
return (
279+
<IconizedContextMenuOption
280+
onClick={wrapHandler(() => {
281+
setMarkedUnreadState(room, cli, true);
282+
onFinished?.();
283+
}, onPostMarkAsUnreadClick)}
284+
label={_t("room|context_menu|mark_unread")}
285+
iconClassName="mx_RoomGeneralContextMenu_iconMarkAsUnread"
286+
/>
287+
);
288+
} else {
289+
return null;
290+
}
291+
})();
228292

229293
const developerModeEnabled = useSettingValue<boolean>("developerMode");
230294
const developerToolsOption = developerModeEnabled ? (

src/components/views/rooms/NotificationBadge.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
102102
if (notification.isIdle && !notification.knocked) return null;
103103
if (hideIfDot && notification.level < NotificationLevel.Notification) {
104104
// This would just be a dot and we've been told not to show dots, so don't show it
105-
if (!notification.hasUnreadCount) return null;
105+
return null;
106106
}
107107

108108
const commonProps: React.ComponentProps<typeof StatelessNotificationBadge> = {

src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,17 +70,27 @@ export const StatelessNotificationBadge = forwardRef<HTMLDivElement, XOR<Props,
7070
symbol = formatCount(count);
7171
}
7272

73+
// We show a dot if either:
74+
// * The props force us to, or
75+
// * It's just an activity-level notification or (in theory) lower and the room isn't knocked
76+
const badgeType =
77+
forceDot || (level <= NotificationLevel.Activity && !knocked)
78+
? "dot"
79+
: !symbol || symbol.length < 3
80+
? "badge_2char"
81+
: "badge_3char";
82+
7383
const classes = classNames({
7484
mx_NotificationBadge: true,
7585
mx_NotificationBadge_visible: isEmptyBadge || knocked ? true : hasUnreadCount,
7686
mx_NotificationBadge_level_notification: level == NotificationLevel.Notification,
7787
mx_NotificationBadge_level_highlight: level >= NotificationLevel.Highlight,
7888
mx_NotificationBadge_knocked: knocked,
7989

80-
// At most one of mx_NotificationBadge_dot, mx_NotificationBadge_2char, mx_NotificationBadge_3char
81-
mx_NotificationBadge_dot: (isEmptyBadge && !knocked) || forceDot,
82-
mx_NotificationBadge_2char: !forceDot && symbol && symbol.length > 0 && symbol.length < 3,
83-
mx_NotificationBadge_3char: !forceDot && symbol && symbol.length > 2,
90+
// Exactly one of mx_NotificationBadge_dot, mx_NotificationBadge_2char, mx_NotificationBadge_3char
91+
mx_NotificationBadge_dot: badgeType === "dot",
92+
mx_NotificationBadge_2char: badgeType === "badge_2char",
93+
mx_NotificationBadge_3char: badgeType === "badge_3char",
8494
});
8595

8696
if (props.onClick) {

src/components/views/rooms/RoomTile.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,12 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
362362
onPostLeaveClick={(ev: ButtonEvent) =>
363363
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuLeaveItem", ev)
364364
}
365+
onPostMarkAsReadClick={(ev: ButtonEvent) =>
366+
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuMarkRead", ev)
367+
}
368+
onPostMarkAsUnreadClick={(ev: ButtonEvent) =>
369+
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuMarkUnread", ev)
370+
}
365371
/>
366372
)}
367373
</React.Fragment>

src/i18n/strings/en_EN.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1892,6 +1892,7 @@
18921892
"forget": "Forget Room",
18931893
"low_priority": "Low Priority",
18941894
"mark_read": "Mark as read",
1895+
"mark_unread": "Mark as unread",
18951896
"mentions_only": "Mentions only",
18961897
"notifications_default": "Match default setting",
18971898
"notifications_mute": "Mute room",

0 commit comments

Comments
 (0)