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 <1389908+richvdh@users.noreply.github.com> * 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 <1389908+richvdh@users.noreply.github.com> * 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 <1389908+richvdh@users.noreply.github.com> * 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 <1389908+richvdh@users.noreply.github.com>
This commit is contained in:
parent
a8341c0e95
commit
a5ed97b903
20 changed files with 458 additions and 33 deletions
|
@ -30,7 +30,7 @@ import { NotificationLevel } from "../../../stores/notifications/NotificationLev
|
|||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import { clearRoomNotification } from "../../../utils/notifications";
|
||||
import { clearRoomNotification, setMarkedUnreadState } from "../../../utils/notifications";
|
||||
import { IProps as IContextMenuProps } from "../../structures/ContextMenu";
|
||||
import IconizedContextMenu, {
|
||||
IconizedContextMenuCheckbox,
|
||||
|
@ -45,13 +45,60 @@ import { useSettingValue } from "../../../hooks/useSettings";
|
|||
|
||||
export interface RoomGeneralContextMenuProps extends IContextMenuProps {
|
||||
room: Room;
|
||||
/**
|
||||
* Called when the 'favourite' option is selected, after the menu has processed
|
||||
* the mouse or keyboard event.
|
||||
* @param event The event that caused the option to be selected.
|
||||
*/
|
||||
onPostFavoriteClick?: (event: ButtonEvent) => void;
|
||||
/**
|
||||
* Called when the 'low priority' option is selected, after the menu has processed
|
||||
* the mouse or keyboard event.
|
||||
* @param event The event that caused the option to be selected.
|
||||
*/
|
||||
onPostLowPriorityClick?: (event: ButtonEvent) => void;
|
||||
/**
|
||||
* Called when the 'invite' option is selected, after the menu has processed
|
||||
* the mouse or keyboard event.
|
||||
* @param event The event that caused the option to be selected.
|
||||
*/
|
||||
onPostInviteClick?: (event: ButtonEvent) => void;
|
||||
/**
|
||||
* Called when the 'copy link' option is selected, after the menu has processed
|
||||
* the mouse or keyboard event.
|
||||
* @param event The event that caused the option to be selected.
|
||||
*/
|
||||
onPostCopyLinkClick?: (event: ButtonEvent) => void;
|
||||
/**
|
||||
* Called when the 'settings' option is selected, after the menu has processed
|
||||
* the mouse or keyboard event.
|
||||
* @param event The event that caused the option to be selected.
|
||||
*/
|
||||
onPostSettingsClick?: (event: ButtonEvent) => void;
|
||||
/**
|
||||
* Called when the 'forget room' option is selected, after the menu has processed
|
||||
* the mouse or keyboard event.
|
||||
* @param event The event that caused the option to be selected.
|
||||
*/
|
||||
onPostForgetClick?: (event: ButtonEvent) => void;
|
||||
/**
|
||||
* Called when the 'leave' option is selected, after the menu has processed
|
||||
* the mouse or keyboard event.
|
||||
* @param event The event that caused the option to be selected.
|
||||
*/
|
||||
onPostLeaveClick?: (event: ButtonEvent) => void;
|
||||
/**
|
||||
* Called when the 'mark as read' option is selected, after the menu has processed
|
||||
* the mouse or keyboard event.
|
||||
* @param event The event that caused the option to be selected.
|
||||
*/
|
||||
onPostMarkAsReadClick?: (event: ButtonEvent) => void;
|
||||
/**
|
||||
* Called when the 'mark as unread' option is selected, after the menu has processed
|
||||
* the mouse or keyboard event.
|
||||
* @param event The event that caused the option to be selected.
|
||||
*/
|
||||
onPostMarkAsUnreadClick?: (event: ButtonEvent) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -67,6 +114,8 @@ export const RoomGeneralContextMenu: React.FC<RoomGeneralContextMenuProps> = ({
|
|||
onPostSettingsClick,
|
||||
onPostLeaveClick,
|
||||
onPostForgetClick,
|
||||
onPostMarkAsReadClick,
|
||||
onPostMarkAsUnreadClick,
|
||||
...props
|
||||
}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
@ -213,18 +262,33 @@ export const RoomGeneralContextMenu: React.FC<RoomGeneralContextMenuProps> = ({
|
|||
}
|
||||
|
||||
const { level } = useUnreadNotifications(room);
|
||||
const markAsReadOption: JSX.Element | null =
|
||||
level > NotificationLevel.None ? (
|
||||
<IconizedContextMenuCheckbox
|
||||
onClick={() => {
|
||||
clearRoomNotification(room, cli);
|
||||
onFinished?.();
|
||||
}}
|
||||
active={false}
|
||||
label={_t("room|context_menu|mark_read")}
|
||||
iconClassName="mx_RoomGeneralContextMenu_iconMarkAsRead"
|
||||
/>
|
||||
) : null;
|
||||
const markAsReadOption: JSX.Element | null = (() => {
|
||||
if (level > NotificationLevel.None) {
|
||||
return (
|
||||
<IconizedContextMenuOption
|
||||
onClick={wrapHandler(() => {
|
||||
clearRoomNotification(room, cli);
|
||||
onFinished?.();
|
||||
}, onPostMarkAsReadClick)}
|
||||
label={_t("room|context_menu|mark_read")}
|
||||
iconClassName="mx_RoomGeneralContextMenu_iconMarkAsRead"
|
||||
/>
|
||||
);
|
||||
} else if (!roomTags.includes(DefaultTagID.Archived)) {
|
||||
return (
|
||||
<IconizedContextMenuOption
|
||||
onClick={wrapHandler(() => {
|
||||
setMarkedUnreadState(room, cli, true);
|
||||
onFinished?.();
|
||||
}, onPostMarkAsUnreadClick)}
|
||||
label={_t("room|context_menu|mark_unread")}
|
||||
iconClassName="mx_RoomGeneralContextMenu_iconMarkAsUnread"
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
const developerModeEnabled = useSettingValue<boolean>("developerMode");
|
||||
const developerToolsOption = developerModeEnabled ? (
|
||||
|
|
|
@ -102,7 +102,7 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
|
|||
if (notification.isIdle && !notification.knocked) return null;
|
||||
if (hideIfDot && notification.level < NotificationLevel.Notification) {
|
||||
// This would just be a dot and we've been told not to show dots, so don't show it
|
||||
if (!notification.hasUnreadCount) return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
const commonProps: React.ComponentProps<typeof StatelessNotificationBadge> = {
|
||||
|
|
|
@ -70,6 +70,16 @@ export const StatelessNotificationBadge = forwardRef<HTMLDivElement, XOR<Props,
|
|||
symbol = formatCount(count);
|
||||
}
|
||||
|
||||
// We show a dot if either:
|
||||
// * The props force us to, or
|
||||
// * It's just an activity-level notification or (in theory) lower and the room isn't knocked
|
||||
const badgeType =
|
||||
forceDot || (level <= NotificationLevel.Activity && !knocked)
|
||||
? "dot"
|
||||
: !symbol || symbol.length < 3
|
||||
? "badge_2char"
|
||||
: "badge_3char";
|
||||
|
||||
const classes = classNames({
|
||||
mx_NotificationBadge: true,
|
||||
mx_NotificationBadge_visible: isEmptyBadge || knocked ? true : hasUnreadCount,
|
||||
|
@ -77,10 +87,10 @@ export const StatelessNotificationBadge = forwardRef<HTMLDivElement, XOR<Props,
|
|||
mx_NotificationBadge_level_highlight: level >= NotificationLevel.Highlight,
|
||||
mx_NotificationBadge_knocked: knocked,
|
||||
|
||||
// At most one of mx_NotificationBadge_dot, mx_NotificationBadge_2char, mx_NotificationBadge_3char
|
||||
mx_NotificationBadge_dot: (isEmptyBadge && !knocked) || forceDot,
|
||||
mx_NotificationBadge_2char: !forceDot && symbol && symbol.length > 0 && symbol.length < 3,
|
||||
mx_NotificationBadge_3char: !forceDot && symbol && symbol.length > 2,
|
||||
// Exactly one of mx_NotificationBadge_dot, mx_NotificationBadge_2char, mx_NotificationBadge_3char
|
||||
mx_NotificationBadge_dot: badgeType === "dot",
|
||||
mx_NotificationBadge_2char: badgeType === "badge_2char",
|
||||
mx_NotificationBadge_3char: badgeType === "badge_3char",
|
||||
});
|
||||
|
||||
if (props.onClick) {
|
||||
|
|
|
@ -362,6 +362,12 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
|
|||
onPostLeaveClick={(ev: ButtonEvent) =>
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuLeaveItem", ev)
|
||||
}
|
||||
onPostMarkAsReadClick={(ev: ButtonEvent) =>
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuMarkRead", ev)
|
||||
}
|
||||
onPostMarkAsUnreadClick={(ev: ButtonEvent) =>
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuMarkUnread", ev)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue