From 6e492917af8b36dc48b322b967612d2e452b7e43 Mon Sep 17 00:00:00 2001 From: Germain Date: Thu, 6 Oct 2022 10:44:04 +0100 Subject: [PATCH 01/33] Add feature detection for thread notifications --- src/Lifecycle.ts | 3 +++ src/stores/notifications/RoomNotificationState.ts | 10 +++------- .../notifications/RoomNotificationStateStore.ts | 14 ++++++++++++-- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 1e7fae8136e..886cee57751 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -62,6 +62,7 @@ import { DialogOpener } from "./utils/DialogOpener"; import { Action } from "./dispatcher/actions"; import AbstractLocalStorageSettingsHandler from "./settings/handlers/AbstractLocalStorageSettingsHandler"; import { OverwriteLoginPayload } from "./dispatcher/payloads/OverwriteLoginPayload"; +import { RoomNotificationStateStore } from './stores/notifications/RoomNotificationStateStore'; const HOMESERVER_URL_KEY = "mx_hs_url"; const ID_SERVER_URL_KEY = "mx_is_url"; @@ -824,6 +825,8 @@ async function startMatrixClient(startSyncing = true): Promise { await MatrixClientPeg.assign(); } + await RoomNotificationStateStore.checkThreadNotificationsSupport(); + // Run the migrations after the MatrixClientPeg has been assigned SettingsStore.runMigrations(); diff --git a/src/stores/notifications/RoomNotificationState.ts b/src/stores/notifications/RoomNotificationState.ts index c4c803483df..a2131aa884a 100644 --- a/src/stores/notifications/RoomNotificationState.ts +++ b/src/stores/notifications/RoomNotificationState.ts @@ -30,16 +30,14 @@ import { getUnsentMessages } from "../../components/structures/RoomStatusBar"; import { ThreadsRoomNotificationState } from "./ThreadsRoomNotificationState"; export class RoomNotificationState extends NotificationState implements IDestroyable { - constructor(public readonly room: Room, private readonly threadsState?: ThreadsRoomNotificationState) { + constructor(public readonly room: Room, private readonly threadsState: ThreadsRoomNotificationState) { super(); this.room.on(RoomEvent.Receipt, this.handleReadReceipt); this.room.on(RoomEvent.Timeline, this.handleRoomEventUpdate); this.room.on(RoomEvent.Redaction, this.handleRoomEventUpdate); this.room.on(RoomEvent.MyMembership, this.handleMembershipUpdate); this.room.on(RoomEvent.LocalEchoUpdated, this.handleLocalEchoUpdated); - if (threadsState) { - threadsState.on(NotificationStateEvents.Update, this.handleThreadsUpdate); - } + this.threadsState.on(NotificationStateEvents.Update, this.handleThreadsUpdate); MatrixClientPeg.get().on(MatrixEventEvent.Decrypted, this.onEventDecrypted); MatrixClientPeg.get().on(ClientEvent.AccountData, this.handleAccountDataUpdate); this.updateNotificationState(); @@ -56,9 +54,7 @@ export class RoomNotificationState extends NotificationState implements IDestroy this.room.removeListener(RoomEvent.Redaction, this.handleRoomEventUpdate); this.room.removeListener(RoomEvent.MyMembership, this.handleMembershipUpdate); this.room.removeListener(RoomEvent.LocalEchoUpdated, this.handleLocalEchoUpdated); - if (this.threadsState) { - this.threadsState.removeListener(NotificationStateEvents.Update, this.handleThreadsUpdate); - } + this.threadsState.removeListener(NotificationStateEvents.Update, this.handleThreadsUpdate); if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted); MatrixClientPeg.get().removeListener(ClientEvent.AccountData, this.handleAccountDataUpdate); diff --git a/src/stores/notifications/RoomNotificationStateStore.ts b/src/stores/notifications/RoomNotificationStateStore.ts index 48aa7e7c20f..8c5801d0cbc 100644 --- a/src/stores/notifications/RoomNotificationStateStore.ts +++ b/src/stores/notifications/RoomNotificationStateStore.ts @@ -28,6 +28,7 @@ import { SummarizedNotificationState } from "./SummarizedNotificationState"; import { ThreadsRoomNotificationState } from "./ThreadsRoomNotificationState"; import { VisibilityProvider } from "../room-list/filters/VisibilityProvider"; import { PosthogAnalytics } from "../../PosthogAnalytics"; +import { MatrixClientPeg } from "../../MatrixClientPeg"; interface IState {} @@ -39,9 +40,9 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient { instance.start(); return instance; })(); - private roomMap = new Map(); - private roomThreadsMap = new Map(); + + private roomThreadsMap: Map = new Map(); private listMap = new Map(); private _globalState = new SummarizedNotificationState(); @@ -49,6 +50,15 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient { super(defaultDispatcher, {}); } + // Optimistically setting to true + public static hasThreadNotificationSupport = true; + public static checkThreadNotificationsSupport = async (): Promise => { + const cli = MatrixClientPeg.get(); + const unstableFeature = await cli.doesServerSupportUnstableFeature("org.matrix.msc3773"); + const stableSupport = await cli.isVersionSupported("v1.4"); + this.hasThreadNotificationSupport = unstableFeature || stableSupport; + }; + /** * Gets a snapshot of notification state for all visible rooms. The number of states recorded * on the SummarizedNotificationState is equivalent to rooms. From d220ffa90cbe225280980545cbc8e793a22776da Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 12 Oct 2022 15:42:02 +0100 Subject: [PATCH 02/33] Extra logic from UI for NotificationBadge --- src/Lifecycle.ts | 2 - .../RoomStatusBarUnsentMessages.tsx | 2 +- .../views/avatars/DecoratedRoomAvatar.tsx | 2 +- .../views/dialogs/ForwardDialog.tsx | 2 +- .../dialogs/spotlight/SpotlightDialog.tsx | 2 +- src/components/views/rooms/EventTile.tsx | 2 +- src/components/views/rooms/ExtraTile.tsx | 2 +- .../NotificationBadge.tsx | 135 ++++++++++-------- src/components/views/rooms/RoomSublist.tsx | 2 +- src/components/views/rooms/RoomTile.tsx | 2 +- .../views/rooms/VoiceRecordComposerTile.tsx | 2 +- .../views/spaces/SpaceTreeLevel.tsx | 2 +- 12 files changed, 89 insertions(+), 68 deletions(-) rename src/components/views/rooms/{ => NotificationBadge}/NotificationBadge.tsx (55%) diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 886cee57751..56f71ad4084 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -825,8 +825,6 @@ async function startMatrixClient(startSyncing = true): Promise { await MatrixClientPeg.assign(); } - await RoomNotificationStateStore.checkThreadNotificationsSupport(); - // Run the migrations after the MatrixClientPeg has been assigned SettingsStore.runMigrations(); diff --git a/src/components/structures/RoomStatusBarUnsentMessages.tsx b/src/components/structures/RoomStatusBarUnsentMessages.tsx index 4c8f9fe35b5..c092bf0231a 100644 --- a/src/components/structures/RoomStatusBarUnsentMessages.tsx +++ b/src/components/structures/RoomStatusBarUnsentMessages.tsx @@ -17,7 +17,7 @@ limitations under the License. import React, { ReactElement } from "react"; import { StaticNotificationState } from "../../stores/notifications/StaticNotificationState"; -import NotificationBadge from "../views/rooms/NotificationBadge"; +import NotificationBadge from "../views/rooms/NotificationBadge/NotificationBadge"; interface RoomStatusBarUnsentMessagesProps { title: string; diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx index 06ee20cc08e..971ffae6921 100644 --- a/src/components/views/avatars/DecoratedRoomAvatar.tsx +++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx @@ -24,7 +24,7 @@ import { JoinRule } from "matrix-js-sdk/src/@types/partials"; import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; import RoomAvatar from "./RoomAvatar"; -import NotificationBadge from '../rooms/NotificationBadge'; +import NotificationBadge from '../rooms/NotificationBadge/NotificationBadge'; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { NotificationState } from "../../../stores/notifications/NotificationState"; import { isPresenceEnabled } from "../../../utils/presence"; diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index 8e23e3bd950..c9ee094e638 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -39,7 +39,7 @@ import { Alignment } from '../elements/Tooltip'; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; -import NotificationBadge from "../rooms/NotificationBadge"; +import NotificationBadge from "../rooms/NotificationBadge/NotificationBadge"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; import QueryMatcher from "../../../autocomplete/QueryMatcher"; diff --git a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx index b04299869c1..00ff17f1468 100644 --- a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx +++ b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx @@ -81,7 +81,7 @@ import { NetworkDropdown } from "../../directory/NetworkDropdown"; import AccessibleButton from "../../elements/AccessibleButton"; import LabelledCheckbox from "../../elements/LabelledCheckbox"; import Spinner from "../../elements/Spinner"; -import NotificationBadge from "../../rooms/NotificationBadge"; +import NotificationBadge from "../../rooms/NotificationBadge/NotificationBadge"; import BaseDialog from "../BaseDialog"; import FeedbackDialog from "../FeedbackDialog"; import { IDialogProps } from "../IDialogProps"; diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 8b88a35670c..89eada8afba 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -46,7 +46,7 @@ import Tooltip, { Alignment } from "../elements/Tooltip"; import EditorStateTransfer from "../../../utils/EditorStateTransfer"; import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; -import NotificationBadge from "./NotificationBadge"; +import NotificationBadge from "./NotificationBadge/NotificationBadge"; import LegacyCallEventGrouper from "../../structures/LegacyCallEventGrouper"; import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; import { Action } from '../../../dispatcher/actions'; diff --git a/src/components/views/rooms/ExtraTile.tsx b/src/components/views/rooms/ExtraTile.tsx index 313ae28b1f8..c7a6d2d30f7 100644 --- a/src/components/views/rooms/ExtraTile.tsx +++ b/src/components/views/rooms/ExtraTile.tsx @@ -21,7 +21,7 @@ import { RovingAccessibleButton, RovingAccessibleTooltipButton, } from "../../../accessibility/RovingTabIndex"; -import NotificationBadge from "./NotificationBadge"; +import NotificationBadge from "./NotificationBadge/NotificationBadge"; import { NotificationState } from "../../../stores/notifications/NotificationState"; import { ButtonEvent } from "../elements/AccessibleButton"; diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge/NotificationBadge.tsx similarity index 55% rename from src/components/views/rooms/NotificationBadge.tsx rename to src/components/views/rooms/NotificationBadge/NotificationBadge.tsx index 51745209aae..8768f6925c4 100644 --- a/src/components/views/rooms/NotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge/NotificationBadge.tsx @@ -17,14 +17,14 @@ limitations under the License. import React, { MouseEvent } from "react"; import classNames from "classnames"; -import { formatCount } from "../../../utils/FormattingUtils"; -import SettingsStore from "../../../settings/SettingsStore"; -import AccessibleButton from "../elements/AccessibleButton"; -import { XOR } from "../../../@types/common"; -import { NotificationState, NotificationStateEvents } from "../../../stores/notifications/NotificationState"; -import Tooltip from "../elements/Tooltip"; -import { _t } from "../../../languageHandler"; -import { NotificationColor } from "../../../stores/notifications/NotificationColor"; +import { formatCount } from "../../../../utils/FormattingUtils"; +import SettingsStore from "../../../../settings/SettingsStore"; +import AccessibleButton from "../../elements/AccessibleButton"; +import { XOR } from "../../../../@types/common"; +import { NotificationState, NotificationStateEvents } from "../../../../stores/notifications/NotificationState"; +import Tooltip from "../../elements/Tooltip"; +import { _t } from "../../../../languageHandler"; +import { NotificationColor } from "../../../../stores/notifications/NotificationColor"; interface IProps { notification: NotificationState; @@ -113,61 +113,84 @@ export default class NotificationBadge extends React.PureComponent 0; - let isEmptyBadge = !hasAnySymbol || !notification.hasUnreadCount; - if (forceCount) { - isEmptyBadge = false; - if (!notification.hasUnreadCount) return null; // Can't render a badge + const { notification, showUnsentTooltip, onClick } = this.props; + + let label; + let tooltip; + if (showUnsentTooltip && this.state.showTooltip && notification.color === NotificationColor.Unsent) { + label = _t("Message didn't send. Click for info."); + tooltip = ; } - let symbol = notification.symbol || formatCount(notification.count); - if (isEmptyBadge) symbol = ""; + return + { tooltip } + ; + } +} - const classes = classNames({ - 'mx_NotificationBadge': true, - 'mx_NotificationBadge_visible': isEmptyBadge ? true : notification.hasUnreadCount, - 'mx_NotificationBadge_highlighted': notification.hasMentions, - 'mx_NotificationBadge_dot': isEmptyBadge, - 'mx_NotificationBadge_2char': symbol.length > 0 && symbol.length < 3, - 'mx_NotificationBadge_3char': symbol.length > 2, - }); +interface Props { + symbol: string | null; + count: number; + color: NotificationColor; + onClick?: (ev: MouseEvent) => void; + onMouseOver?: (ev: MouseEvent) => void; + onMouseLeave?: (ev: MouseEvent) => void; + children?: React.ReactChildren; + label?: string; +} - if (onClick) { - let label: string; - let tooltip: JSX.Element; - if (showUnsentTooltip && this.state.showTooltip && notification.color === NotificationColor.Unsent) { - label = _t("Message didn't send. Click for info."); - tooltip = ; - } - - return ( - - { symbol } - { tooltip } - - ); - } +export function StatelessNotificationBadge({ + symbol, + count, + color, + ...props }: Props) { + // Don't show a badge if we don't need to + if (color === NotificationColor.None) return null; + const hasUnreadCount = color >= NotificationColor.Grey && (!!count || !!symbol); + + const isEmptyBadge = symbol === null && count === 0; + + if (symbol === null && count > 0) { + symbol = formatCount(count); + } + + const classes = classNames({ + 'mx_NotificationBadge': true, + 'mx_NotificationBadge_visible': isEmptyBadge ? true : hasUnreadCount, + 'mx_NotificationBadge_highlighted': color === NotificationColor.Red, + 'mx_NotificationBadge_dot': isEmptyBadge, + 'mx_NotificationBadge_2char': symbol?.length > 0 && symbol?.length < 3, + 'mx_NotificationBadge_3char': symbol?.length > 2, + }); + + if (props.onClick) { return ( -
+ { symbol } -
+ { props.children } + ); } + + return ( +
+ { symbol } +
+ ); } diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index 9e890e9c219..62e28260afa 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -54,7 +54,7 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import ExtraTile from "./ExtraTile"; import SettingsStore from "../../../settings/SettingsStore"; import { SlidingSyncManager } from "../../../SlidingSyncManager"; -import NotificationBadge from "./NotificationBadge"; +import NotificationBadge from "./NotificationBadge/NotificationBadge"; import RoomTile from "./RoomTile"; const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 4e80303aa05..e770caa3f47 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -32,7 +32,7 @@ import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import { RoomNotifState } from "../../../RoomNotifs"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { RoomNotificationContextMenu } from "../context_menus/RoomNotificationContextMenu"; -import NotificationBadge from "./NotificationBadge"; +import NotificationBadge from "./NotificationBadge/NotificationBadge"; import { ActionPayload } from "../../../dispatcher/payloads"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { NotificationState, NotificationStateEvents } from "../../../stores/notifications/NotificationState"; diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index c3fe0762c7c..ef6c145b49e 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -32,7 +32,7 @@ import RecordingPlayback, { PlaybackLayout } from "../audio_messages/RecordingPl import Modal from "../../../Modal"; import ErrorDialog from "../dialogs/ErrorDialog"; import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../MediaDeviceHandler"; -import NotificationBadge from "./NotificationBadge"; +import NotificationBadge from "./NotificationBadge/NotificationBadge"; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; import { NotificationColor } from "../../../stores/notifications/NotificationColor"; import InlineSpinner from "../elements/InlineSpinner"; diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index cab7bc3c76b..9e949b8bbad 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -23,7 +23,7 @@ import RoomAvatar from "../avatars/RoomAvatar"; import SpaceStore from "../../../stores/spaces/SpaceStore"; import { SpaceKey } from "../../../stores/spaces"; import SpaceTreeLevelLayoutStore from "../../../stores/spaces/SpaceTreeLevelLayoutStore"; -import NotificationBadge from "../rooms/NotificationBadge"; +import NotificationBadge from "../rooms/NotificationBadge/NotificationBadge"; import { _t } from "../../../languageHandler"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; From 37a49a6e3554ccd47e0ba7b45ece8a419b2f4697 Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 12 Oct 2022 15:47:14 +0100 Subject: [PATCH 03/33] Create UnreadNotificationBadge to display thread notifications --- src/RoomNotifs.ts | 16 +++- src/Unread.ts | 14 ++- src/components/structures/RoomStatusBar.tsx | 6 +- .../UnreadNotificationBadge.tsx | 36 +++++++ src/hooks/useUnreadNotifications.ts | 93 +++++++++++++++++++ 5 files changed, 157 insertions(+), 8 deletions(-) create mode 100644 src/components/views/rooms/NotificationBadge/UnreadNotificationBadge.tsx create mode 100644 src/hooks/useUnreadNotifications.ts diff --git a/src/RoomNotifs.ts b/src/RoomNotifs.ts index 08c15970c56..ee0f9f8f083 100644 --- a/src/RoomNotifs.ts +++ b/src/RoomNotifs.ts @@ -78,8 +78,14 @@ export function setRoomNotifsState(roomId: string, newState: RoomNotifState): Pr } } -export function getUnreadNotificationCount(room: Room, type: NotificationCountType = null): number { - let notificationCount = room.getUnreadNotificationCount(type); +export function getUnreadNotificationCount( + room: Room, + type: NotificationCountType | null = null, + threadId?: string, +): number { + let notificationCount = (!!threadId + ? room.getThreadUnreadNotificationCount(threadId, type) + : room.getUnreadNotificationCount(type)) ?? 0; // Check notification counts in the old room just in case there's some lost // there. We only go one level down to avoid performance issues, and theory @@ -89,11 +95,15 @@ export function getUnreadNotificationCount(room: Room, type: NotificationCountTy const oldRoomId = createEvent.getContent()['predecessor']['room_id']; const oldRoom = MatrixClientPeg.get().getRoom(oldRoomId); if (oldRoom) { + const oldNotificationCount = (!!threadId + ? oldRoom.getThreadUnreadNotificationCount(threadId, NotificationCountType.Highlight) + : oldRoom.getUnreadNotificationCount(NotificationCountType.Highlight)) ?? 0; + // We only ever care if there's highlights in the old room. No point in // notifying the user for unread messages because they would have extreme // difficulty changing their notification preferences away from "All Messages" // and "Noisy". - notificationCount += oldRoom.getUnreadNotificationCount(NotificationCountType.Highlight); + notificationCount += oldNotificationCount; } } diff --git a/src/Unread.ts b/src/Unread.ts index b9b3409c66c..f0908736cf8 100644 --- a/src/Unread.ts +++ b/src/Unread.ts @@ -18,6 +18,7 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { EventType } from "matrix-js-sdk/src/@types/event"; import { M_BEACON } from "matrix-js-sdk/src/@types/beacon"; +import { Feature, ServerSupport } from "matrix-js-sdk/src/feature"; import { MatrixClientPeg } from "./MatrixClientPeg"; import shouldHideEvent from './shouldHideEvent'; @@ -79,9 +80,16 @@ export function doesRoomHaveUnreadMessages(room: Room): boolean { return false; } } else { - const threadState = RoomNotificationStateStore.instance.getThreadsRoomState(room); - if (threadState.color > 0) { - return true; + const cli = room.client; + if (cli.canSupport.get(Feature.ThreadUnreadNotifications) === ServerSupport.Unsupported) { + const threadState = RoomNotificationStateStore.instance.getThreadsRoomState(room); + if (threadState.color > 0) { + return true; + } + } else { + if (room.hasThreadUnreadNotification()) { + return true; + } } } diff --git a/src/components/structures/RoomStatusBar.tsx b/src/components/structures/RoomStatusBar.tsx index d46ad12b50d..0346e560b6c 100644 --- a/src/components/structures/RoomStatusBar.tsx +++ b/src/components/structures/RoomStatusBar.tsx @@ -34,10 +34,12 @@ const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_EXPANDED = 1; const STATUS_BAR_EXPANDED_LARGE = 2; -export function getUnsentMessages(room: Room): MatrixEvent[] { +export function getUnsentMessages(room: Room, threadId?: string): MatrixEvent[] { if (!room) { return []; } return room.getPendingEvents().filter(function(ev) { - return ev.status === EventStatus.NOT_SENT; + const isNotSent = ev.status === EventStatus.NOT_SENT; + const belongsToTheThread = threadId === ev.threadRootId; + return isNotSent && (threadId && belongsToTheThread); }); } diff --git a/src/components/views/rooms/NotificationBadge/UnreadNotificationBadge.tsx b/src/components/views/rooms/NotificationBadge/UnreadNotificationBadge.tsx new file mode 100644 index 00000000000..b44f817dff8 --- /dev/null +++ b/src/components/views/rooms/NotificationBadge/UnreadNotificationBadge.tsx @@ -0,0 +1,36 @@ +/* +Copyright 2022 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 { Room } from "matrix-js-sdk/src/models/room"; +import React from "react"; + +import { useUnreadNotifications } from "../../../../hooks/useUnreadNotifications"; +import { StatelessNotificationBadge } from "./NotificationBadge"; + +interface Props { + room: Room; + threadId?: string; +} + +export function UnreadNotificationBadge({ room, threadId }: Props) { + const { symbol, count, color } = useUnreadNotifications(room, threadId); + + return ; +} diff --git a/src/hooks/useUnreadNotifications.ts b/src/hooks/useUnreadNotifications.ts new file mode 100644 index 00000000000..6ee315114f7 --- /dev/null +++ b/src/hooks/useUnreadNotifications.ts @@ -0,0 +1,93 @@ +/* +Copyright 2022 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 { NotificationCount, NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room"; +import { useCallback, useEffect, useState } from "react"; + +import { getUnsentMessages } from "../components/structures/RoomStatusBar"; +import { getRoomNotifsState, getUnreadNotificationCount, RoomNotifState } from "../RoomNotifs"; +import { NotificationColor } from "../stores/notifications/NotificationColor"; +import { doesRoomHaveUnreadMessages } from "../Unread"; +import { EffectiveMembership, getEffectiveMembership } from "../utils/membership"; +import { useEventEmitter } from "./useEventEmitter"; + +export const useUnreadNotifications = (room: Room, threadId?: string): { + symbol: string | null; + count: number; + color: NotificationColor; +} => { + const [symbol, setSymbol] = useState(null); + const [count, setCount] = useState(0); + const [color, setColor] = useState(0); + + useEventEmitter(room, RoomEvent.UnreadNotifications, + (unreadNotifications: NotificationCount, evtThreadId?: string) => { + // Discarding all events not related to the thread if one has been setup + if (threadId && threadId !== evtThreadId) return; + updateNotificationState(); + }, + ); + useEventEmitter(room, RoomEvent.Receipt, () => updateNotificationState()); + useEventEmitter(room, RoomEvent.Timeline, () => updateNotificationState()); + useEventEmitter(room, RoomEvent.Redaction, () => updateNotificationState()); + useEventEmitter(room, RoomEvent.LocalEchoUpdated, () => updateNotificationState()); + useEventEmitter(room, RoomEvent.MyMembership, () => updateNotificationState()); + + const updateNotificationState = useCallback(() => { + if (getUnsentMessages(room, threadId).length > 0) { + setSymbol("!"); + setCount(1); + setColor(NotificationColor.Unsent); + } else if (getEffectiveMembership(room.getMyMembership()) === EffectiveMembership.Invite) { + setSymbol("!"); + setCount(1); + setColor(NotificationColor.Red); + } else if (getRoomNotifsState(room.roomId) === RoomNotifState.Mute) { + setSymbol(null); + setCount(0); + setColor(NotificationColor.None); + } else { + const redNotifs = getUnreadNotificationCount(room, NotificationCountType.Highlight, threadId) ?? 0; + const greyNotifs = getUnreadNotificationCount(room, NotificationCountType.Total, threadId) ?? 0; + + const trueCount = greyNotifs || redNotifs; + setCount(trueCount); + setSymbol(null); + if (redNotifs > 0) { + setColor(NotificationColor.Red); + } else if (greyNotifs > 0) { + setColor(NotificationColor.Grey); + } else if (!threadId) { + // TODO: No support for `Bold` on threads at the moment + + // We don't have any notified messages, but we might have unread messages. Let's + // find out. + const hasUnread = doesRoomHaveUnreadMessages(room); + setColor(hasUnread ? NotificationColor.Bold : NotificationColor.None); + } + } + }, [room, threadId]); + + useEffect(() => { + updateNotificationState(); + }, [updateNotificationState]); + + return { + symbol, + count, + color, + }; +}; From 2b19d77fb25820f0a1b86cc31d325bfa5ecffc94 Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 12 Oct 2022 16:27:36 +0100 Subject: [PATCH 04/33] Hook room header to server unread thread notifications --- .../views/right_panel/RoomHeaderButtons.tsx | 48 +++++++++++++++---- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/src/components/views/right_panel/RoomHeaderButtons.tsx b/src/components/views/right_panel/RoomHeaderButtons.tsx index d950177e06b..bc2f23b9afb 100644 --- a/src/components/views/right_panel/RoomHeaderButtons.tsx +++ b/src/components/views/right_panel/RoomHeaderButtons.tsx @@ -20,7 +20,8 @@ limitations under the License. import React from "react"; import classNames from "classnames"; -import { Room } from "matrix-js-sdk/src/models/room"; +import { NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room"; +import { Feature, ServerSupport } from "matrix-js-sdk/src/feature"; import { _t } from '../../../languageHandler'; import HeaderButton from './HeaderButton'; @@ -43,6 +44,7 @@ import { SummarizedNotificationState } from "../../../stores/notifications/Summa import { NotificationStateEvents } from "../../../stores/notifications/NotificationState"; import PosthogTrackers from "../../../PosthogTrackers"; import { ButtonEvent } from "../elements/AccessibleButton"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; const ROOM_INFO_PHASES = [ RightPanelPhases.RoomSummary, @@ -136,29 +138,59 @@ export default class RoomHeaderButtons extends HeaderButtons { private threadNotificationState: ThreadsRoomNotificationState; private globalNotificationState: SummarizedNotificationState; + private get supportsThreadNotifications(): boolean { + const client = MatrixClientPeg.get(); + return client.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported; + } + constructor(props: IProps) { super(props, HeaderKind.Room); - this.threadNotificationState = RoomNotificationStateStore.instance.getThreadsRoomState(this.props.room); + if (!this.supportsThreadNotifications) { + this.threadNotificationState = RoomNotificationStateStore.instance.getThreadsRoomState(this.props.room); + } this.globalNotificationState = RoomNotificationStateStore.instance.globalState; } public componentDidMount(): void { super.componentDidMount(); - this.threadNotificationState.on(NotificationStateEvents.Update, this.onThreadNotification); + if (!this.supportsThreadNotifications) { + this.threadNotificationState?.on(NotificationStateEvents.Update, this.onNotificationUpdate); + } else { + this.props.room?.off(RoomEvent.UnreadNotifications, this.onNotificationUpdate); + } RoomNotificationStateStore.instance.on(UPDATE_STATUS_INDICATOR, this.onUpdateStatus); } public componentWillUnmount(): void { super.componentWillUnmount(); - this.threadNotificationState.off(NotificationStateEvents.Update, this.onThreadNotification); + if (!this.supportsThreadNotifications) { + this.threadNotificationState?.off(NotificationStateEvents.Update, this.onNotificationUpdate); + } else { + this.props.room?.off(RoomEvent.UnreadNotifications, this.onNotificationUpdate); + } RoomNotificationStateStore.instance.off(UPDATE_STATUS_INDICATOR, this.onUpdateStatus); } - private onThreadNotification = (): void => { + private onNotificationUpdate = (): void => { + let threadNotificationColor: NotificationColor; + if (!this.supportsThreadNotifications) { + threadNotificationColor = this.threadNotificationState.color; + } else { + switch (this.props.room.getThreadsAggregateNotificationType()) { + case NotificationCountType.Highlight: + threadNotificationColor = NotificationColor.Red; + break; + case NotificationCountType.Total: + threadNotificationColor = NotificationColor.Grey; + break; + default: + threadNotificationColor = NotificationColor.None; + } + } // XXX: why don't we read from this.state.threadNotificationColor in the render methods? this.setState({ - threadNotificationColor: this.threadNotificationState.color, + threadNotificationColor, }); }; @@ -258,9 +290,9 @@ export default class RoomHeaderButtons extends HeaderButtons { title={_t("Threads")} onClick={this.onThreadsPanelClicked} isHighlighted={this.isPhase(RoomHeaderButtons.THREAD_PHASES)} - isUnread={this.threadNotificationState.color > 0} + isUnread={this.threadNotificationState?.color > 0} > - + : null, ); From db20700eed0da783f02f7a6b99d830305304adda Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 12 Oct 2022 16:37:00 +0100 Subject: [PATCH 05/33] hook unread thread notifications to thread list UI --- res/css/views/rooms/_EventTile.pcss | 20 +++++-- src/components/views/rooms/EventTile.tsx | 57 ++++++++++++------- .../notifications/RoomNotificationState.ts | 21 ++++--- .../RoomNotificationStateStore.ts | 32 +++++------ 4 files changed, 78 insertions(+), 52 deletions(-) diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index 35cd87b1364..ecd141df3e3 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -426,8 +426,8 @@ $left-gutter: 64px; } &.mx_EventTile_selected .mx_EventTile_line { - // TODO: check if this would be necessary - padding-inline-start: calc(var(--EventTile_group_line-spacing-inline-start) + 20px); + // TODO: check if this would be necessary;TODOTODOTODOTODOTODOTODO + padding-inline-start: calc(var(--EventTile_group_line-spacing-inline-start) + 20px)padding-inline-startpadding-inline-startpadding-inline-startpadding-inline-startpadding-inline-start } } @@ -894,13 +894,20 @@ $left-gutter: 64px; } /* Display notification dot */ - &[data-notification]::before { + &[data-notification]::before, + .mx_NotificationBadge { + position: absolute; $notification-inset-block-start: 14px; /* 14px: align the dot with the timestamp row */ - width: $notification-dot-size; - height: $notification-dot-size; + width: $notification-dot-size !important; + height: $notification-dot-size !important; border-radius: 50%; inset: $notification-inset-block-start $spacing-8 auto auto; + + } + + .mx_NotificationBadge_count { + display: none; } &[data-notification="total"]::before { @@ -1301,7 +1308,8 @@ $left-gutter: 64px; } } - &[data-shape="ThreadsList"][data-notification]::before { + &[data-shape="ThreadsList"][data-notification]::before, + .mx_NotificationBadge { /* stylelint-disable-next-line declaration-colon-space-after */ inset-block-start: calc($notification-inset-block-start - var(--MatrixChat_useCompactLayout_group-padding-top)); diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 89eada8afba..2889cff19bb 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -27,6 +27,7 @@ import { NotificationCountType, Room, RoomEvent } from 'matrix-js-sdk/src/models import { CallErrorCode } from "matrix-js-sdk/src/webrtc/call"; import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import { UserTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning'; +import { Feature, ServerSupport } from 'matrix-js-sdk/src/feature'; import { Icon as LinkIcon } from '../../../../res/img/element-icons/link.svg'; import { Icon as ViewInRoomIcon } from '../../../../res/img/element-icons/view-in-room.svg'; @@ -84,6 +85,7 @@ import { useTooltip } from "../../../utils/useTooltip"; import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload"; import { isLocalRoom } from '../../../utils/localRoom/isLocalRoom'; import { ElementCall } from "../../../models/Call"; +import { UnreadNotificationBadge } from './NotificationBadge/UnreadNotificationBadge'; export type GetRelationsForEvent = (eventId: string, relationType: string, eventType: string) => Relations; @@ -394,7 +396,7 @@ export class UnwrappedEventTile extends React.Component { if (SettingsStore.getValue("feature_thread")) { this.props.mxEvent.on(ThreadEvent.Update, this.updateThread); - if (this.thread) { + if (this.thread && !this.supportsThreadNotifications) { this.setupNotificationListener(this.thread); } } @@ -405,33 +407,40 @@ export class UnwrappedEventTile extends React.Component { room?.on(ThreadEvent.New, this.onNewThread); } - private setupNotificationListener(thread: Thread): void { - const notifications = RoomNotificationStateStore.instance.getThreadsRoomState(thread.room); - - this.threadState = notifications.getThreadRoomState(thread); + private get supportsThreadNotifications(): boolean { + const client = MatrixClientPeg.get(); + return client.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported + } - this.threadState.on(NotificationStateEvents.Update, this.onThreadStateUpdate); - this.onThreadStateUpdate(); + private setupNotificationListener(thread: Thread): void { + if (!this.supportsThreadNotifications) { + const notifications = RoomNotificationStateStore.instance.getThreadsRoomState(thread.room); + this.threadState = notifications.getThreadRoomState(thread); + this.threadState.on(NotificationStateEvents.Update, this.onThreadStateUpdate); + this.onThreadStateUpdate(); + } } private onThreadStateUpdate = (): void => { - let threadNotification = null; - switch (this.threadState?.color) { - case NotificationColor.Grey: - threadNotification = NotificationCountType.Total; - break; - case NotificationColor.Red: - threadNotification = NotificationCountType.Highlight; - break; - } + if (!this.supportsThreadNotifications) { + let threadNotification = null; + switch (this.threadState?.color) { + case NotificationColor.Grey: + threadNotification = NotificationCountType.Total; + break; + case NotificationColor.Red: + threadNotification = NotificationCountType.Highlight; + break; + } - this.setState({ - threadNotification, - }); + this.setState({ + threadNotification, + }); + } }; private updateThread = (thread: Thread) => { - if (thread !== this.state.thread) { + if (thread !== this.state.thread && !this.supportsThreadNotifications) { if (this.threadState) { this.threadState.off(NotificationStateEvents.Update, this.onThreadStateUpdate); } @@ -1345,6 +1354,7 @@ export class UnwrappedEventTile extends React.Component { ]); } case TimelineRenderingType.ThreadsList: { + const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers return ( React.createElement(this.props.as || "li", { @@ -1358,7 +1368,9 @@ export class UnwrappedEventTile extends React.Component { "data-shape": this.context.timelineRenderingType, "data-self": isOwnEvent, "data-has-reply": !!replyChain, - "data-notification": this.state.threadNotification, + "data-notification": !this.supportsThreadNotifications + ? this.state.threadNotification + : undefined, "onMouseEnter": () => this.setState({ hover: true }), "onMouseLeave": () => this.setState({ hover: false }), "onClick": (ev: MouseEvent) => { @@ -1406,6 +1418,9 @@ export class UnwrappedEventTile extends React.Component { { msgOption } + ) ); } diff --git a/src/stores/notifications/RoomNotificationState.ts b/src/stores/notifications/RoomNotificationState.ts index a2131aa884a..f21998f9d3a 100644 --- a/src/stores/notifications/RoomNotificationState.ts +++ b/src/stores/notifications/RoomNotificationState.ts @@ -17,6 +17,7 @@ limitations under the License. import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event"; import { NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room"; import { ClientEvent } from "matrix-js-sdk/src/client"; +import { Feature, ServerSupport } from "matrix-js-sdk/src/feature"; import { NotificationColor } from "./NotificationColor"; import { IDestroyable } from "../../utils/IDestroyable"; @@ -30,16 +31,19 @@ import { getUnsentMessages } from "../../components/structures/RoomStatusBar"; import { ThreadsRoomNotificationState } from "./ThreadsRoomNotificationState"; export class RoomNotificationState extends NotificationState implements IDestroyable { - constructor(public readonly room: Room, private readonly threadsState: ThreadsRoomNotificationState) { + constructor(public readonly room: Room, private readonly threadsState?: ThreadsRoomNotificationState) { super(); + const cli = this.room.client; this.room.on(RoomEvent.Receipt, this.handleReadReceipt); this.room.on(RoomEvent.Timeline, this.handleRoomEventUpdate); this.room.on(RoomEvent.Redaction, this.handleRoomEventUpdate); this.room.on(RoomEvent.MyMembership, this.handleMembershipUpdate); this.room.on(RoomEvent.LocalEchoUpdated, this.handleLocalEchoUpdated); - this.threadsState.on(NotificationStateEvents.Update, this.handleThreadsUpdate); - MatrixClientPeg.get().on(MatrixEventEvent.Decrypted, this.onEventDecrypted); - MatrixClientPeg.get().on(ClientEvent.AccountData, this.handleAccountDataUpdate); + if (cli.canSupport.get(Feature.ThreadUnreadNotifications) === ServerSupport.Unsupported) { + this.threadsState.on(NotificationStateEvents.Update, this.handleThreadsUpdate); + } + cli.on(MatrixEventEvent.Decrypted, this.onEventDecrypted); + cli.on(ClientEvent.AccountData, this.handleAccountDataUpdate); this.updateNotificationState(); } @@ -49,16 +53,17 @@ export class RoomNotificationState extends NotificationState implements IDestroy public destroy(): void { super.destroy(); + const cli = this.room.client; this.room.removeListener(RoomEvent.Receipt, this.handleReadReceipt); this.room.removeListener(RoomEvent.Timeline, this.handleRoomEventUpdate); this.room.removeListener(RoomEvent.Redaction, this.handleRoomEventUpdate); this.room.removeListener(RoomEvent.MyMembership, this.handleMembershipUpdate); this.room.removeListener(RoomEvent.LocalEchoUpdated, this.handleLocalEchoUpdated); - this.threadsState.removeListener(NotificationStateEvents.Update, this.handleThreadsUpdate); - if (MatrixClientPeg.get()) { - MatrixClientPeg.get().removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted); - MatrixClientPeg.get().removeListener(ClientEvent.AccountData, this.handleAccountDataUpdate); + if (cli.canSupport.get(Feature.ThreadUnreadNotifications) === ServerSupport.Unsupported) { + this.threadsState.removeListener(NotificationStateEvents.Update, this.handleThreadsUpdate); } + cli.removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted); + cli.removeListener(ClientEvent.AccountData, this.handleAccountDataUpdate); } private handleThreadsUpdate = () => { diff --git a/src/stores/notifications/RoomNotificationStateStore.ts b/src/stores/notifications/RoomNotificationStateStore.ts index 8c5801d0cbc..ad9bd9f98d6 100644 --- a/src/stores/notifications/RoomNotificationStateStore.ts +++ b/src/stores/notifications/RoomNotificationStateStore.ts @@ -17,6 +17,7 @@ limitations under the License. import { Room } from "matrix-js-sdk/src/models/room"; import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync"; import { ClientEvent } from "matrix-js-sdk/src/client"; +import { Feature, ServerSupport } from "matrix-js-sdk/src/feature"; import { ActionPayload } from "../../dispatcher/payloads"; import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; @@ -28,7 +29,6 @@ import { SummarizedNotificationState } from "./SummarizedNotificationState"; import { ThreadsRoomNotificationState } from "./ThreadsRoomNotificationState"; import { VisibilityProvider } from "../room-list/filters/VisibilityProvider"; import { PosthogAnalytics } from "../../PosthogAnalytics"; -import { MatrixClientPeg } from "../../MatrixClientPeg"; interface IState {} @@ -50,15 +50,6 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient { super(defaultDispatcher, {}); } - // Optimistically setting to true - public static hasThreadNotificationSupport = true; - public static checkThreadNotificationsSupport = async (): Promise => { - const cli = MatrixClientPeg.get(); - const unstableFeature = await cli.doesServerSupportUnstableFeature("org.matrix.msc3773"); - const stableSupport = await cli.isVersionSupported("v1.4"); - this.hasThreadNotificationSupport = unstableFeature || stableSupport; - }; - /** * Gets a snapshot of notification state for all visible rooms. The number of states recorded * on the SummarizedNotificationState is equivalent to rooms. @@ -96,18 +87,25 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient { */ public getRoomState(room: Room): RoomNotificationState { if (!this.roomMap.has(room)) { - // Not very elegant, but that way we ensure that we start tracking - // threads notification at the same time at rooms. - // There are multiple entry points, and it's unclear which one gets - // called first - const threadState = new ThreadsRoomNotificationState(room); - this.roomThreadsMap.set(room, threadState); + let threadState; + if (room.client.canSupport.get(Feature.ThreadUnreadNotifications) === ServerSupport.Unsupported) { + // Not very elegant, but that way we ensure that we start tracking + // threads notification at the same time at rooms. + // There are multiple entry points, and it's unclear which one gets + // called first + const threadState = new ThreadsRoomNotificationState(room); + this.roomThreadsMap.set(room, threadState); + } this.roomMap.set(room, new RoomNotificationState(room, threadState)); } return this.roomMap.get(room); } - public getThreadsRoomState(room: Room): ThreadsRoomNotificationState { + public getThreadsRoomState(room: Room): ThreadsRoomNotificationState | null { + if (room.client.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported) { + return null; + } + if (!this.roomThreadsMap.has(room)) { this.roomThreadsMap.set(room, new ThreadsRoomNotificationState(room)); } From 0d8aa314b95e93d0988da60f83a55bf21e0b43d8 Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 12 Oct 2022 16:39:19 +0100 Subject: [PATCH 06/33] remove unused imports --- src/Lifecycle.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 56f71ad4084..1e7fae8136e 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -62,7 +62,6 @@ import { DialogOpener } from "./utils/DialogOpener"; import { Action } from "./dispatcher/actions"; import AbstractLocalStorageSettingsHandler from "./settings/handlers/AbstractLocalStorageSettingsHandler"; import { OverwriteLoginPayload } from "./dispatcher/payloads/OverwriteLoginPayload"; -import { RoomNotificationStateStore } from './stores/notifications/RoomNotificationStateStore'; const HOMESERVER_URL_KEY = "mx_hs_url"; const ID_SERVER_URL_KEY = "mx_is_url"; From c9bdfcff34d5656c818193f7b09b2112a239268d Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 12 Oct 2022 16:49:53 +0100 Subject: [PATCH 07/33] fix i18n --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4b759ca21ec..a263f61d5dd 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1874,7 +1874,6 @@ "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites.": "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites.", "Enable encryption in settings.": "Enable encryption in settings.", "End-to-end encryption isn't enabled": "End-to-end encryption isn't enabled", - "Message didn't send. Click for info.": "Message didn't send. Click for info.", "Unpin": "Unpin", "View message": "View message", "%(duration)ss": "%(duration)ss", @@ -2057,6 +2056,7 @@ "No microphone found": "No microphone found", "We didn't find a microphone on your device. Please check your settings and try again.": "We didn't find a microphone on your device. Please check your settings and try again.", "Stop recording": "Stop recording", + "Message didn't send. Click for info.": "Message didn't send. Click for info.", "Error updating main address": "Error updating main address", "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.", "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.", From 377ffd3840f6f2083094f8f78e87d38a3871ba4a Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 12 Oct 2022 16:56:35 +0100 Subject: [PATCH 08/33] fix css linting --- res/css/views/rooms/_EventTile.pcss | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index ecd141df3e3..127e61cb5f1 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -426,8 +426,8 @@ $left-gutter: 64px; } &.mx_EventTile_selected .mx_EventTile_line { - // TODO: check if this would be necessary;TODOTODOTODOTODOTODOTODO - padding-inline-start: calc(var(--EventTile_group_line-spacing-inline-start) + 20px)padding-inline-startpadding-inline-startpadding-inline-startpadding-inline-startpadding-inline-start + /* TODO: check if this would be necessary; */ + padding-inline-start: calc(var(--EventTile_group_line-spacing-inline-start) + 20px); } } @@ -903,7 +903,6 @@ $left-gutter: 64px; height: $notification-dot-size !important; border-radius: 50%; inset: $notification-inset-block-start $spacing-8 auto auto; - } .mx_NotificationBadge_count { From 5d154c8966436903a7c5d096e65ff6c11ce8e269 Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 12 Oct 2022 17:13:56 +0100 Subject: [PATCH 09/33] Add mock for canSupport --- src/components/views/rooms/EventTile.tsx | 2 +- test/test-utils/client.ts | 6 ++++++ test/test-utils/test-utils.ts | 6 ++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 2889cff19bb..e2d87843b71 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -409,7 +409,7 @@ export class UnwrappedEventTile extends React.Component { private get supportsThreadNotifications(): boolean { const client = MatrixClientPeg.get(); - return client.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported + return client.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported; } private setupNotificationListener(thread: Thread): void { diff --git a/test/test-utils/client.ts b/test/test-utils/client.ts index d6ec3f2fd33..6478743458c 100644 --- a/test/test-utils/client.ts +++ b/test/test-utils/client.ts @@ -16,6 +16,7 @@ limitations under the License. import EventEmitter from "events"; import { MethodKeysOf, mocked, MockedObject, PropertyKeysOf } from "jest-mock"; +import { Feature, ServerSupport } from "matrix-js-sdk/src/feature"; import { MatrixClient, User } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from "../../src/MatrixClientPeg"; @@ -50,6 +51,11 @@ export const getMockClientWithEventEmitter = ( const mock = mocked(new MockClientWithEventEmitter(mockProperties) as unknown as MatrixClient); jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(mock); + + mock.canSupport = new Map(); + Object.keys(Feature).forEach(feature => { + mock.canSupport.set(feature as Feature, ServerSupport.Stable); + }); return mock; }; diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 2e01f3a4d8a..95958c64d07 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -35,6 +35,7 @@ import { import { normalize } from "matrix-js-sdk/src/utils"; import { ReEmitter } from "matrix-js-sdk/src/ReEmitter"; import { MediaHandler } from "matrix-js-sdk/src/webrtc/mediaHandler"; +import { Feature, ServerSupport } from "matrix-js-sdk/src/feature"; import { MatrixClientPeg as peg } from '../../src/MatrixClientPeg'; import { makeType } from "../../src/utils/TypeUtils"; @@ -186,6 +187,11 @@ export function createTestClient(): MatrixClient { client.reEmitter = new ReEmitter(client); + client.canSupport = new Map(); + Object.keys(Feature).forEach(feature => { + client.canSupport.set(feature as Feature, ServerSupport.Stable); + }); + Object.defineProperty(client, "pollingTurnServers", { configurable: true, get: () => true, From 65620b143dd5176edb78b56d5127f465b73ba21d Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 12 Oct 2022 20:52:05 +0100 Subject: [PATCH 10/33] Add basic notificationbadge tests --- .../UnreadNotificationBadge-test.tsx | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx diff --git a/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx b/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx new file mode 100644 index 00000000000..671fabd564c --- /dev/null +++ b/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx @@ -0,0 +1,90 @@ +/* +Copyright 2022 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 { act, render } from "@testing-library/react"; +import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client"; +import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room"; +import { mocked } from "jest-mock"; + +import { + UnreadNotificationBadge, +} from "../../../../../src/components/views/rooms/NotificationBadge/UnreadNotificationBadge"; +import { stubClient } from "../../../../test-utils/test-utils"; +import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; + +const ROOM_ID = "!roomId:example.org"; +let THREAD_ID; + +describe("UnreadNotificationBadge", () => { + let mockClient: MatrixClient; + let room: Room; + + function getComponent(threadId?: string) { + return ; + } + + beforeEach(() => { + jest.clearAllMocks(); + + stubClient(); + mockClient = mocked(MatrixClientPeg.get()); + + room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", { + pendingEventOrdering: PendingEventOrdering.Detached, + }); + room.setUnreadNotificationCount(NotificationCountType.Total, 1); + room.setUnreadNotificationCount(NotificationCountType.Highlight, 0); + + room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 1); + room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0); + }); + + it("renders unread notification badge", () => { + const { container } = render(getComponent()); + + expect(container.querySelector(".mx_NotificationBadge_visible")).toBeTruthy(); + expect(container.querySelector(".mx_NotificationBadge_highlighted")).toBeFalsy(); + + act(() => { + room.setUnreadNotificationCount(NotificationCountType.Highlight, 1); + }); + + expect(container.querySelector(".mx_NotificationBadge_highlighted")).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(); + + act(() => { + room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 1); + }); + + expect(container.querySelector(".mx_NotificationBadge_highlighted")).toBeTruthy(); + }); + + it("hides unread notification badge", () => { + act(() => { + room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 0); + room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0); + const { container } = render(getComponent(THREAD_ID)); + expect(container.querySelector(".mx_NotificationBadge_visible")).toBeFalsy(); + }); + }); +}); From f76fb71ed2f3358899a6bfe52a92c07abac73f47 Mon Sep 17 00:00:00 2001 From: Germain Date: Thu, 13 Oct 2022 09:07:36 +0100 Subject: [PATCH 11/33] Migrate tests from Enzyme to RTL --- .../views/settings/KeyboardShortcut-test.tsx | 29 +- .../KeyboardShortcut-test.tsx.snap | 111 +- .../user/KeyboardUserSettingsTab-test.tsx | 17 +- .../KeyboardUserSettingsTab-test.tsx.snap | 1170 ++++++++++++++--- 4 files changed, 1059 insertions(+), 268 deletions(-) diff --git a/test/components/views/settings/KeyboardShortcut-test.tsx b/test/components/views/settings/KeyboardShortcut-test.tsx index 5fcb65ddba3..d26c0dd1e98 100644 --- a/test/components/views/settings/KeyboardShortcut-test.tsx +++ b/test/components/views/settings/KeyboardShortcut-test.tsx @@ -16,17 +16,14 @@ limitations under the License. */ import React from "react"; -// eslint-disable-next-line deprecate/import -import { mount, ReactWrapper } from "enzyme"; +import { render } from "@testing-library/react"; import { Key } from "../../../../src/Keyboard"; import { mockPlatformPeg, unmockPlatformPeg } from "../../../test-utils/platform"; +import { KeyboardKey, KeyboardShortcut } from "../../../../src/components/views/settings/KeyboardShortcut"; -const PATH_TO_COMPONENT = "../../../../src/components/views/settings/KeyboardShortcut.tsx"; - -const renderKeyboardShortcut = async (component, props?): Promise => { - const Component = (await import(PATH_TO_COMPONENT))[component]; - return mount(); +const renderKeyboardShortcut = (Component, props?) => { + return render().container; }; describe("KeyboardShortcut", () => { @@ -35,24 +32,24 @@ describe("KeyboardShortcut", () => { unmockPlatformPeg(); }); - it("renders key icon", async () => { - const body = await renderKeyboardShortcut("KeyboardKey", { name: Key.ARROW_DOWN }); + it("renders key icon", () => { + const body = renderKeyboardShortcut(KeyboardKey, { name: Key.ARROW_DOWN }); expect(body).toMatchSnapshot(); }); - it("renders alternative key name", async () => { - const body = await renderKeyboardShortcut("KeyboardKey", { name: Key.PAGE_DOWN }); + it("renders alternative key name", () => { + const body = renderKeyboardShortcut(KeyboardKey, { name: Key.PAGE_DOWN }); expect(body).toMatchSnapshot(); }); - it("doesn't render + if last", async () => { - const body = await renderKeyboardShortcut("KeyboardKey", { name: Key.A, last: true }); + it("doesn't render + if last", () => { + const body = renderKeyboardShortcut(KeyboardKey, { name: Key.A, last: true }); expect(body).toMatchSnapshot(); }); - it("doesn't render same modifier twice", async () => { + it("doesn't render same modifier twice", () => { mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) }); - const body1 = await renderKeyboardShortcut("KeyboardShortcut", { + const body1 = renderKeyboardShortcut(KeyboardShortcut, { value: { key: Key.A, ctrlOrCmdKey: true, @@ -61,7 +58,7 @@ describe("KeyboardShortcut", () => { }); expect(body1).toMatchSnapshot(); - const body2 = await renderKeyboardShortcut("KeyboardShortcut", { + const body2 = renderKeyboardShortcut(KeyboardShortcut, { value: { key: Key.A, ctrlOrCmdKey: true, diff --git a/test/components/views/settings/__snapshots__/KeyboardShortcut-test.tsx.snap b/test/components/views/settings/__snapshots__/KeyboardShortcut-test.tsx.snap index 23062d79809..e452b0a47ae 100644 --- a/test/components/views/settings/__snapshots__/KeyboardShortcut-test.tsx.snap +++ b/test/components/views/settings/__snapshots__/KeyboardShortcut-test.tsx.snap @@ -1,116 +1,73 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`KeyboardShortcut doesn't render + if last 1`] = ` - +
a - +
`; exports[`KeyboardShortcut doesn't render same modifier twice 1`] = ` - +
- - - - Ctrl - - - + - - - - - a - - - + + + Ctrl + + + + + + + a + +
- +
`; exports[`KeyboardShortcut doesn't render same modifier twice 2`] = ` - +
- - - - Ctrl - - - + - - - - - a - - - + + + Ctrl + + + + + + + a + +
- +
`; exports[`KeyboardShortcut renders alternative key name 1`] = ` - +
Page Down + - +
`; exports[`KeyboardShortcut renders key icon 1`] = ` - +
+ - +
`; diff --git a/test/components/views/settings/tabs/user/KeyboardUserSettingsTab-test.tsx b/test/components/views/settings/tabs/user/KeyboardUserSettingsTab-test.tsx index 57295a96fe7..a96b3a65337 100644 --- a/test/components/views/settings/tabs/user/KeyboardUserSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/user/KeyboardUserSettingsTab-test.tsx @@ -15,15 +15,16 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { render } from "@testing-library/react"; import React from "react"; -// eslint-disable-next-line deprecate/import -import { mount, ReactWrapper } from "enzyme"; +import KeyboardUserSettingsTab from + "../../../../../../src/components/views/settings/tabs/user/KeyboardUserSettingsTab"; import { Key } from "../../../../../../src/Keyboard"; +import { mockPlatformPeg } from "../../../../../test-utils/platform"; const PATH_TO_KEYBOARD_SHORTCUTS = "../../../../../../src/accessibility/KeyboardShortcuts"; const PATH_TO_KEYBOARD_SHORTCUT_UTILS = "../../../../../../src/accessibility/KeyboardShortcutUtils"; -const PATH_TO_COMPONENT = "../../../../../../src/components/views/settings/tabs/user/KeyboardUserSettingsTab"; const mockKeyboardShortcuts = (override) => { jest.doMock(PATH_TO_KEYBOARD_SHORTCUTS, () => { @@ -45,17 +46,17 @@ const mockKeyboardShortcutUtils = (override) => { }); }; -const renderKeyboardUserSettingsTab = async (component): Promise => { - const Component = (await import(PATH_TO_COMPONENT))[component]; - return mount(); +const renderKeyboardUserSettingsTab = () => { + return render().container; }; describe("KeyboardUserSettingsTab", () => { beforeEach(() => { jest.resetModules(); + mockPlatformPeg(); }); - it("renders list of keyboard shortcuts", async () => { + it("renders list of keyboard shortcuts", () => { mockKeyboardShortcuts({ "CATEGORIES": { "Composer": { @@ -101,7 +102,7 @@ describe("KeyboardUserSettingsTab", () => { }, }); - const body = await renderKeyboardUserSettingsTab("default"); + const body = renderKeyboardUserSettingsTab(); expect(body).toMatchSnapshot(); }); }); diff --git a/test/components/views/settings/tabs/user/__snapshots__/KeyboardUserSettingsTab-test.tsx.snap b/test/components/views/settings/tabs/user/__snapshots__/KeyboardUserSettingsTab-test.tsx.snap index 71bab7f6129..6151e4b959e 100644 --- a/test/components/views/settings/tabs/user/__snapshots__/KeyboardUserSettingsTab-test.tsx.snap +++ b/test/components/views/settings/tabs/user/__snapshots__/KeyboardUserSettingsTab-test.tsx.snap @@ -1,190 +1,1026 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = ` - +
Keyboard
-
+ Composer +
+
+ +
+ Send message +
+ + + Enter + + +
+
+
+ New line +
+ + + Shift + + + + + + + Enter + + +
+
+
+ Toggle Bold +
+ + + Ctrl + + + + + + + b + + +
+
+
+ Toggle Italics +
+ + + Ctrl + + + + + + + i + + +
+
- Composer -
-
- - -
- Cancel replying to a message - -
- - - - Ctrl - - - + - - - - - a - - - -
-
-
-
- -
- Toggle Bold - -
- - - - Ctrl - - - + - - - - - b - - - -
-
-
-
- + class="mx_KeyboardShortcut_shortcutRow" + > + Toggle Quote +
+ + + Ctrl + + + + + + + Shift + + + + + + + > + + +
+
+ Toggle Link +
+ + + Ctrl + + + + + + + Shift + + + + + + + l + + +
+
+
+ Toggle Code Block +
+ + + Ctrl + + + + + + + e + + +
+
+
+ Undo edit +
+ + + Ctrl + + + + + + + z + + +
+
+
+ Redo edit +
+ + + Ctrl + + + + + + + y + + +
+
+
+ Jump to start of the composer +
+ + + Ctrl + + + + + + + Home + + +
+
+
+ Jump to end of the composer +
+ + + Ctrl + + + + + + + End + + +
+
+
+ Cancel replying to a message +
+ + + Esc + + +
+
+
+ Navigate to next message to edit +
+ + + ↓ + + +
+
+
+ Navigate to previous message to edit +
+ + + ↑ + + +
+
+
+ Navigate to next message in composer history +
+ + + Ctrl + + + + + + + Alt + + + + + + + ↓ + + +
+
+
+ Navigate to previous message in composer history +
+ + + Ctrl + + + + + + + Alt + + + + + + + ↑ + + +
+
+
+ Send a sticker +
+ + + Ctrl + + + + + + + ; + + +
+
+
-
- +
+ Calls +
+
+ +
+ Toggle microphone mute +
+ + + Ctrl + + + + + + + d + + +
+
+
+ Toggle webcam on/off +
+ + + Ctrl + + + + + + + e + + +
+
+ +
+
+
+
+ Room +
+
+ +
+ Search (must be enabled) +
+ + + Ctrl + + + + + + + f + + +
+
+
+ Upload a file +
+ + + Ctrl + + + + + + + Shift + + + + + + + u + + +
+
- Navigation -
-
- - -
- Select room from the room list - -
- - - - Enter - - - -
-
-
-
- + class="mx_KeyboardShortcut_shortcutRow" + > + Dismiss read marker and jump to bottom +
+ + + Esc + + +
+
+ Jump to oldest unread message +
+ + + Shift + + + + + + + Page Up + + +
+
+
+ Scroll up in the timeline +
+ + + Page Up + + +
+
+
+ Scroll down in the timeline +
+ + + Page Down + + +
+
+
+ Jump to first message +
+ + + Ctrl + + + + + + + Home + + +
+
+
+ Jump to last message +
+ + + Ctrl + + + + + + + End + + +
+
+
- +
+
+
+ Room List +
+
+ +
+ Select room from the room list +
+ + + Enter + + +
+
+
+ Collapse room list section +
+ + + ← + + +
+
+
+ Expand room list section +
+ + + → + + +
+
+
+ Navigate down in the room list +
+ + + ↓ + + +
+
+
+ Navigate up in the room list +
+ + + ↑ + + +
+
+ +
+
+
+
+ Accessibility +
+
+ +
+ Close dialog or context menu +
+ + + Esc + + +
+
+
+ Activate selected button +
+ + + Enter + + +
+
+ +
+
+
+
+ Navigation +
+
+ +
+ Toggle the top left menu +
+ + + Ctrl + + + + + + + \` + + +
+
+
+ Toggle right panel +
+ + + Ctrl + + + + + + + . + + +
+
+
+ Toggle space panel +
+ + + Ctrl + + + + + + + Shift + + + + + + + d + + +
+
+
+ Open this settings tab +
+ + + Ctrl + + + + + + + / + + +
+
+
+ Go to Home View +
+ + + Ctrl + + + + + + + Alt + + + + + + + h + + +
+
+
+ Jump to room search +
+ + + Ctrl + + + + + + + k + + +
+
+
+ Next unread room or DM +
+ + + Alt + + + + + + + Shift + + + + + + + ↓ + + +
+
+
+ Previous unread room or DM +
+ + + Alt + + + + + + + Shift + + + + + + + ↑ + + +
+
+
+ Next room or DM +
+ + + Alt + + + + + + + ↓ + + +
+
+
+ Previous room or DM +
+ + + Alt + + + + + + + ↑ + + +
+
+ +
+
+
+
+ Autocomplete +
+
+ +
+ Cancel autocomplete +
+ + + Esc + + +
+
+
+ Next autocomplete suggestion +
+ + + ↓ + + +
+
+
+ Previous autocomplete suggestion +
+ + + ↑ + + +
+
+
+ Complete +
+ + + Enter + + +
+
+
+ Force complete +
+ + + Tab + + +
+
+ +
+
- +
`; From da5525709b6287c059e1142251cf668cf6067cb6 Mon Sep 17 00:00:00 2001 From: Germain Date: Thu, 13 Oct 2022 10:00:24 +0100 Subject: [PATCH 12/33] Do not create account data event for guests --- src/utils/notifications.ts | 3 +++ test/utils/notifications-test.ts | 29 +++++++++++++++++++---------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/utils/notifications.ts b/src/utils/notifications.ts index 0064eaf2bcd..32296d62e6e 100644 --- a/src/utils/notifications.ts +++ b/src/utils/notifications.ts @@ -31,6 +31,9 @@ export function getLocalNotificationAccountDataEventType(deviceId: string): stri } export async function createLocalNotificationSettingsIfNeeded(cli: MatrixClient): Promise { + if (cli.isGuest()) { + return; + } const eventType = getLocalNotificationAccountDataEventType(cli.deviceId); const event = cli.getAccountData(eventType); // New sessions will create an account data event to signify they support diff --git a/test/utils/notifications-test.ts b/test/utils/notifications-test.ts index c44b496608c..dde76b71cf9 100644 --- a/test/utils/notifications-test.ts +++ b/test/utils/notifications-test.ts @@ -30,20 +30,22 @@ jest.mock("../../src/settings/SettingsStore"); describe('notifications', () => { let accountDataStore = {}; - const mockClient = getMockClientWithEventEmitter({ - isGuest: jest.fn().mockReturnValue(false), - getAccountData: jest.fn().mockImplementation(eventType => accountDataStore[eventType]), - setAccountData: jest.fn().mockImplementation((eventType, content) => { - accountDataStore[eventType] = new MatrixEvent({ - type: eventType, - content, - }); - }), - }); + let mockClient; const accountDataEventKey = getLocalNotificationAccountDataEventType(mockClient.deviceId); beforeEach(() => { + jest.clearAllMocks(); + mockClient = getMockClientWithEventEmitter({ + isGuest: jest.fn().mockReturnValue(false), + getAccountData: jest.fn().mockImplementation(eventType => accountDataStore[eventType]), + setAccountData: jest.fn().mockImplementation((eventType, content) => { + accountDataStore[eventType] = new MatrixEvent({ + type: eventType, + content, + }); + }), + }); accountDataStore = {}; mocked(SettingsStore).getValue.mockReturnValue(false); }); @@ -55,6 +57,13 @@ describe('notifications', () => { expect(event?.getContent().is_silenced).toBe(true); }); + it('does not do anything for guests', async () => { + mockClient.isGuest.mockReset().mockReturnValue(true); + await createLocalNotificationSettingsIfNeeded(mockClient); + const event = mockClient.getAccountData(accountDataEventKey); + expect(event).toBeFalsy(); + }); + it.each(deviceNotificationSettingsKeys)( 'unsilenced for existing sessions when %s setting is truthy', async (settingKey) => { From 2201bb72540d422ec8e7e35edb65bbc0bba93f81 Mon Sep 17 00:00:00 2001 From: Germain Date: Thu, 13 Oct 2022 11:04:51 +0100 Subject: [PATCH 13/33] fix test --- test/utils/notifications-test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/utils/notifications-test.ts b/test/utils/notifications-test.ts index dde76b71cf9..9848d7e486a 100644 --- a/test/utils/notifications-test.ts +++ b/test/utils/notifications-test.ts @@ -31,8 +31,7 @@ jest.mock("../../src/settings/SettingsStore"); describe('notifications', () => { let accountDataStore = {}; let mockClient; - - const accountDataEventKey = getLocalNotificationAccountDataEventType(mockClient.deviceId); + let accountDataEventKey; beforeEach(() => { jest.clearAllMocks(); @@ -47,6 +46,7 @@ describe('notifications', () => { }), }); accountDataStore = {}; + accountDataEventKey = getLocalNotificationAccountDataEventType(mockClient.deviceId); mocked(SettingsStore).getValue.mockReturnValue(false); }); From 10d71f2a0ed7dca467261d506d8af6494e70a475 Mon Sep 17 00:00:00 2001 From: Germain Date: Thu, 13 Oct 2022 13:54:39 +0100 Subject: [PATCH 14/33] Add RoomNotifs getUnreadNotificationCount tests --- test/RoomNotifs-test.ts | 79 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/test/RoomNotifs-test.ts b/test/RoomNotifs-test.ts index 3f486205dfc..8ab37e69450 100644 --- a/test/RoomNotifs-test.ts +++ b/test/RoomNotifs-test.ts @@ -16,10 +16,15 @@ limitations under the License. import { mocked } from 'jest-mock'; import { ConditionKind, PushRuleActionName, TweakName } from "matrix-js-sdk/src/@types/PushRules"; +import { NotificationCountType, Room } from 'matrix-js-sdk/src/models/room'; -import { stubClient } from "./test-utils"; +import { mkEvent, stubClient } from "./test-utils"; import { MatrixClientPeg } from "../src/MatrixClientPeg"; -import { getRoomNotifsState, RoomNotifState } from "../src/RoomNotifs"; +import { + getRoomNotifsState, + RoomNotifState, + getUnreadNotificationCount, +} from "../src/RoomNotifs"; describe("RoomNotifs test", () => { beforeEach(() => { @@ -83,4 +88,74 @@ describe("RoomNotifs test", () => { }); expect(getRoomNotifsState("!roomId:server")).toBe(RoomNotifState.AllMessagesLoud); }); + + describe("getUnreadNotificationCount", () => { + const ROOM_ID = "!roomId:example.org"; + const THREAD_ID = "$threadId"; + + let cli; + let room: Room; + beforeEach(() => { + cli = MatrixClientPeg.get(); + room = new Room(ROOM_ID, cli, cli.getUserId()); + }); + + it("counts room notification type", () => { + expect(getUnreadNotificationCount(room, NotificationCountType.Total)).toBe(0); + expect(getUnreadNotificationCount(room, NotificationCountType.Highlight)).toBe(0); + }); + + it("counts notifications type", () => { + room.setUnreadNotificationCount(NotificationCountType.Total, 2); + room.setUnreadNotificationCount(NotificationCountType.Highlight, 1); + + expect(getUnreadNotificationCount(room, NotificationCountType.Total)).toBe(2); + expect(getUnreadNotificationCount(room, NotificationCountType.Highlight)).toBe(1); + }); + + it("counts predecessor highlight", () => { + room.setUnreadNotificationCount(NotificationCountType.Total, 2); + room.setUnreadNotificationCount(NotificationCountType.Highlight, 1); + + const OLD_ROOM_ID = "!oldRoomId:example.org"; + const oldRoom = new Room(OLD_ROOM_ID, cli, cli.getUserId()); + oldRoom.setUnreadNotificationCount(NotificationCountType.Total, 10); + oldRoom.setUnreadNotificationCount(NotificationCountType.Highlight, 6); + + cli.getRoom.mockReset().mockReturnValue(oldRoom); + + const predecessorEvent = mkEvent({ + event: true, + type: "m.room.create", + room: ROOM_ID, + user: cli.getUserId(), + content: { + creator: cli.getUserId(), + room_version: "5", + predecessor: { + room_id: OLD_ROOM_ID, + event_id: "$someevent", + }, + }, + ts: Date.now(), + }); + room.addLiveEvents([predecessorEvent]); + + expect(getUnreadNotificationCount(room, NotificationCountType.Total)).toBe(8); + expect(getUnreadNotificationCount(room, NotificationCountType.Highlight)).toBe(7); + }); + + it("counts thread notification type", () => { + expect(getUnreadNotificationCount(room, NotificationCountType.Total, THREAD_ID)).toBe(0); + expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, THREAD_ID)).toBe(0); + }); + + it("counts notifications type", () => { + room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 2); + room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 1); + + expect(getUnreadNotificationCount(room, NotificationCountType.Total, THREAD_ID)).toBe(2); + expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, THREAD_ID)).toBe(1); + }); + }); }); From da538eaae499741395552da887ce89f81ccbc700 Mon Sep 17 00:00:00 2001 From: Germain Date: Thu, 13 Oct 2022 15:40:21 +0100 Subject: [PATCH 15/33] Add RoomNotificationStateStore tests --- .../notifications/RoomNotificationState.ts | 2 +- .../RoomNotificationStateStore-test.ts | 60 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 test/stores/notifications/RoomNotificationStateStore-test.ts diff --git a/src/stores/notifications/RoomNotificationState.ts b/src/stores/notifications/RoomNotificationState.ts index f21998f9d3a..6e8f9d7335c 100644 --- a/src/stores/notifications/RoomNotificationState.ts +++ b/src/stores/notifications/RoomNotificationState.ts @@ -40,7 +40,7 @@ export class RoomNotificationState extends NotificationState implements IDestroy this.room.on(RoomEvent.MyMembership, this.handleMembershipUpdate); this.room.on(RoomEvent.LocalEchoUpdated, this.handleLocalEchoUpdated); if (cli.canSupport.get(Feature.ThreadUnreadNotifications) === ServerSupport.Unsupported) { - this.threadsState.on(NotificationStateEvents.Update, this.handleThreadsUpdate); + this.threadsState?.on(NotificationStateEvents.Update, this.handleThreadsUpdate); } cli.on(MatrixEventEvent.Decrypted, this.onEventDecrypted); cli.on(ClientEvent.AccountData, this.handleAccountDataUpdate); diff --git a/test/stores/notifications/RoomNotificationStateStore-test.ts b/test/stores/notifications/RoomNotificationStateStore-test.ts new file mode 100644 index 00000000000..e5d24881aef --- /dev/null +++ b/test/stores/notifications/RoomNotificationStateStore-test.ts @@ -0,0 +1,60 @@ +/* +Copyright 2022 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 { PendingEventOrdering } from "matrix-js-sdk/src/client"; +import { Feature, ServerSupport } from "matrix-js-sdk/src/feature"; +import { Room } from "matrix-js-sdk/src/models/room"; + +import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; +import { RoomNotificationStateStore } from "../../../src/stores/notifications/RoomNotificationStateStore"; +import { stubClient } from "../../test-utils"; + +describe("RoomNotificationStateStore", () => { + const ROOM_ID = "!roomId:example.org"; + + let room; + let client; + + beforeEach(() => { + stubClient(); + client = MatrixClientPeg.get(); + room = new Room(ROOM_ID, client, client.getUserId(), { + pendingEventOrdering: PendingEventOrdering.Detached, + }); + }); + + it("does not use legacy thread notification store", () => { + client.canSupport.set(Feature.ThreadUnreadNotifications, ServerSupport.Stable); + expect(RoomNotificationStateStore.instance.getThreadsRoomState(room)).toBeNull(); + }); + + it("use legacy thread notification store", () => { + client.canSupport.set(Feature.ThreadUnreadNotifications, ServerSupport.Unsupported); + expect(RoomNotificationStateStore.instance.getThreadsRoomState(room)).not.toBeNull(); + }); + + it("does not use legacy thread notification store", () => { + client.canSupport.set(Feature.ThreadUnreadNotifications, ServerSupport.Stable); + RoomNotificationStateStore.instance.getRoomState(room); + expect(RoomNotificationStateStore.instance.getThreadsRoomState(room)).toBeNull(); + }); + + it("use legacy thread notification store", () => { + client.canSupport.set(Feature.ThreadUnreadNotifications, ServerSupport.Unsupported); + RoomNotificationStateStore.instance.getRoomState(room); + expect(RoomNotificationStateStore.instance.getThreadsRoomState(room)).not.toBeNull(); + }); +}); From e272fa2ab429114d2759176c8b9031ff3f54feef Mon Sep 17 00:00:00 2001 From: Germain Date: Fri, 14 Oct 2022 15:14:54 +0100 Subject: [PATCH 16/33] Add room header tests --- .../views/right_panel/RoomHeaderButtons.tsx | 32 +++--- .../right_panel/RoomHeaderButtons-test.tsx | 97 +++++++++++++++++++ 2 files changed, 116 insertions(+), 13 deletions(-) create mode 100644 test/components/views/right_panel/RoomHeaderButtons-test.tsx diff --git a/src/components/views/right_panel/RoomHeaderButtons.tsx b/src/components/views/right_panel/RoomHeaderButtons.tsx index 80f93ffe1af..c6e012fff4c 100644 --- a/src/components/views/right_panel/RoomHeaderButtons.tsx +++ b/src/components/views/right_panel/RoomHeaderButtons.tsx @@ -157,8 +157,9 @@ export default class RoomHeaderButtons extends HeaderButtons { if (!this.supportsThreadNotifications) { this.threadNotificationState?.on(NotificationStateEvents.Update, this.onNotificationUpdate); } else { - this.props.room?.off(RoomEvent.UnreadNotifications, this.onNotificationUpdate); + this.props.room?.on(RoomEvent.UnreadNotifications, this.onNotificationUpdate); } + this.onNotificationUpdate(); RoomNotificationStateStore.instance.on(UPDATE_STATUS_INDICATOR, this.onUpdateStatus); } @@ -177,23 +178,27 @@ export default class RoomHeaderButtons extends HeaderButtons { if (!this.supportsThreadNotifications) { threadNotificationColor = this.threadNotificationState.color; } else { - switch (this.props.room.getThreadsAggregateNotificationType()) { - case NotificationCountType.Highlight: - threadNotificationColor = NotificationColor.Red; - break; - case NotificationCountType.Total: - threadNotificationColor = NotificationColor.Grey; - break; - default: - threadNotificationColor = NotificationColor.None; - } + threadNotificationColor = this.notificationColor; } + + // console.log // XXX: why don't we read from this.state.threadNotificationColor in the render methods? this.setState({ threadNotificationColor, }); }; + private get notificationColor(): NotificationColor { + switch (this.props.room.threadsAggregateNotificationType) { + case NotificationCountType.Highlight: + return NotificationColor.Red; + case NotificationCountType.Total: + return NotificationColor.Grey; + default: + return NotificationColor.None; + } + } + private onUpdateStatus = (notificationState: SummarizedNotificationState): void => { // XXX: why don't we read from this.state.globalNotificationCount in the render methods? this.globalNotificationState = notificationState; @@ -287,12 +292,13 @@ export default class RoomHeaderButtons extends HeaderButtons { ? 0} + isUnread={this.state.threadNotificationColor > 0} > - + : null, ); diff --git a/test/components/views/right_panel/RoomHeaderButtons-test.tsx b/test/components/views/right_panel/RoomHeaderButtons-test.tsx new file mode 100644 index 00000000000..5d873f4b869 --- /dev/null +++ b/test/components/views/right_panel/RoomHeaderButtons-test.tsx @@ -0,0 +1,97 @@ +/* +Copyright 2022 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 { render } from "@testing-library/react"; +import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client"; +import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room"; +import React from "react"; + +import RoomHeaderButtons from "../../../../src/components/views/right_panel/RoomHeaderButtons"; +import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; +import SettingsStore from "../../../../src/settings/SettingsStore"; +import { stubClient } from "../../../test-utils"; + +describe("RoomHeaderButtons-test.tsx", function() { + const ROOM_ID = "!roomId:example.org"; + let room: Room; + let client: MatrixClient; + + beforeEach(() => { + jest.clearAllMocks(); + + stubClient(); + client = MatrixClientPeg.get(); + room = new Room(ROOM_ID, client, client.getUserId(), { + pendingEventOrdering: PendingEventOrdering.Detached, + }); + + jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => { + if (name === "feature_thread") return true; + }); + }); + + function getComponent(room: Room) { + return render(); + } + + function getThreadButton(container) { + return container.querySelector(".mx_RightPanel_threadsButton"); + } + + function isIndicatorOfType(container, type: "red" | "gray") { + return container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator") + .className + .includes(type); + } + + it("shows the thread button", () => { + const { container } = getComponent(room); + expect(getThreadButton(container)).not.toBeNull(); + }); + + it("hides the thread button", () => { + jest.spyOn(SettingsStore, "getValue").mockReset().mockReturnValue(false); + const { container } = getComponent(room); + expect(getThreadButton(container)).toBeNull(); + }); + + it("room wide notification does not change the thread button", () => { + room.setUnreadNotificationCount(NotificationCountType.Highlight, 1); + room.setUnreadNotificationCount(NotificationCountType.Total, 1); + + const { container } = getComponent(room); + + expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull(); + }); + + it("room wide notification does not change the thread button", () => { + const { container } = getComponent(room); + + room.setThreadUnreadNotificationCount("$123", NotificationCountType.Total, 1); + expect(isIndicatorOfType(container, "gray")).toBe(true); + + room.setThreadUnreadNotificationCount("$123", NotificationCountType.Highlight, 1); + expect(isIndicatorOfType(container, "red")).toBe(true); + + room.setThreadUnreadNotificationCount("$123", NotificationCountType.Total, 0); + room.setThreadUnreadNotificationCount("$123", NotificationCountType.Highlight, 0); + + expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull(); + }); +}); From ac1567dfe2d4d721155fed5f03e7177e98f2e7ff Mon Sep 17 00:00:00 2001 From: Germain Date: Fri, 14 Oct 2022 15:43:19 +0100 Subject: [PATCH 17/33] Remove uneeded code --- src/Unread.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/Unread.ts b/src/Unread.ts index f0908736cf8..da358f642bc 100644 --- a/src/Unread.ts +++ b/src/Unread.ts @@ -18,13 +18,11 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { EventType } from "matrix-js-sdk/src/@types/event"; import { M_BEACON } from "matrix-js-sdk/src/@types/beacon"; -import { Feature, ServerSupport } from "matrix-js-sdk/src/feature"; import { MatrixClientPeg } from "./MatrixClientPeg"; import shouldHideEvent from './shouldHideEvent'; import { haveRendererForEvent } from "./events/EventTileFactory"; import SettingsStore from "./settings/SettingsStore"; -import { RoomNotificationStateStore } from "./stores/notifications/RoomNotificationStateStore"; /** * Returns true if this event arriving in a room should affect the room's @@ -79,18 +77,6 @@ export function doesRoomHaveUnreadMessages(room: Room): boolean { if (room.timeline.length && room.timeline[room.timeline.length - 1].getSender() === myUserId) { return false; } - } else { - const cli = room.client; - if (cli.canSupport.get(Feature.ThreadUnreadNotifications) === ServerSupport.Unsupported) { - const threadState = RoomNotificationStateStore.instance.getThreadsRoomState(room); - if (threadState.color > 0) { - return true; - } - } else { - if (room.hasThreadUnreadNotification()) { - return true; - } - } } // if the read receipt relates to an event is that part of a thread From 598bd85a85c71e26f8a951951eeae2381a925e0c Mon Sep 17 00:00:00 2001 From: Germain Date: Tue, 18 Oct 2022 10:15:39 +0100 Subject: [PATCH 18/33] Fix getUnsentMessages Co-authored-by: Janne Mareike Koschinski --- src/components/structures/RoomStatusBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/RoomStatusBar.tsx b/src/components/structures/RoomStatusBar.tsx index 0346e560b6c..e7032525460 100644 --- a/src/components/structures/RoomStatusBar.tsx +++ b/src/components/structures/RoomStatusBar.tsx @@ -39,7 +39,7 @@ export function getUnsentMessages(room: Room, threadId?: string): MatrixEvent[] return room.getPendingEvents().filter(function(ev) { const isNotSent = ev.status === EventStatus.NOT_SENT; const belongsToTheThread = threadId === ev.threadRootId; - return isNotSent && (threadId && belongsToTheThread); + return isNotSent && (!threadId || belongsToTheThread); }); } From 1e41e2f058655fd0bb73d0de007fd01175a56490 Mon Sep 17 00:00:00 2001 From: Germain Date: Fri, 21 Oct 2022 08:39:49 +0100 Subject: [PATCH 19/33] fix per PR comments --- res/css/views/rooms/_EventTile.pcss | 1 + .../views/rooms/NotificationBadge/NotificationBadge.tsx | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index 127e61cb5f1..55702c787bf 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -899,6 +899,7 @@ $left-gutter: 64px; position: absolute; $notification-inset-block-start: 14px; /* 14px: align the dot with the timestamp row */ + /* !important to fix overly specific CSS selector applied on mx_NotificationBadge */ width: $notification-dot-size !important; height: $notification-dot-size !important; border-radius: 50%; diff --git a/src/components/views/rooms/NotificationBadge/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge/NotificationBadge.tsx index 8768f6925c4..39328fd7154 100644 --- a/src/components/views/rooms/NotificationBadge/NotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge/NotificationBadge.tsx @@ -115,8 +115,8 @@ export default class NotificationBadge extends React.PureComponent; @@ -143,7 +143,7 @@ interface Props { onClick?: (ev: MouseEvent) => void; onMouseOver?: (ev: MouseEvent) => void; onMouseLeave?: (ev: MouseEvent) => void; - children?: React.ReactChildren; + children?: React.ReactChildren | JSX.Element; label?: string; } From de23f99dab4129b12c53af9150cbcd3d80b2a882 Mon Sep 17 00:00:00 2001 From: Germain Date: Fri, 21 Oct 2022 09:26:15 +0100 Subject: [PATCH 20/33] Add RoomStatusBar test --- .../structures/RoomStatusBar-test.tsx | 91 +++++++++++++++++++ test/test-utils/threads.ts | 4 +- 2 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 test/components/structures/RoomStatusBar-test.tsx diff --git a/test/components/structures/RoomStatusBar-test.tsx b/test/components/structures/RoomStatusBar-test.tsx new file mode 100644 index 00000000000..db8b0e03ffd --- /dev/null +++ b/test/components/structures/RoomStatusBar-test.tsx @@ -0,0 +1,91 @@ +/* +Copyright 2022 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 { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client"; +import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { Room } from "matrix-js-sdk/src/models/room"; + +import { getUnsentMessages } from "../../../src/components/structures/RoomStatusBar"; +import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; +import { mkEvent, stubClient } from "../../test-utils/test-utils"; +import { mkThread } from "../../test-utils/threads"; + +describe("RoomStatusBar", () => { + const ROOM_ID = "!roomId:example.org"; + let room: Room; + let client: MatrixClient; + let event: MatrixEvent; + + beforeEach(() => { + jest.clearAllMocks(); + + stubClient(); + client = MatrixClientPeg.get(); + room = new Room(ROOM_ID, client, client.getUserId(), { + pendingEventOrdering: PendingEventOrdering.Detached, + }); + event = mkEvent({ + event: true, + type: "m.room.message", + user: "@user1:server", + room: "!room1:server", + content: {}, + }); + event.status = EventStatus.NOT_SENT; + }); + + describe("getUnsentMessages", () => { + it("returns no unsent messages", () => { + expect(getUnsentMessages(room)).toHaveLength(0); + }); + + it("checks the event status", () => { + room.addPendingEvent(event, "123"); + + expect(getUnsentMessages(room)).toHaveLength(1); + event.status = EventStatus.SENT; + + expect(getUnsentMessages(room)).toHaveLength(0); + }); + + it("only returns events related to a thread", () => { + room.addPendingEvent(event, "123"); + + const { rootEvent, events } = mkThread({ + room, + client, + authorId: "@alice:example.org", + participantUserIds: ["@alice:example.org"], + length: 2, + }); + rootEvent.status = EventStatus.NOT_SENT; + room.addPendingEvent(rootEvent, rootEvent.getId()); + for (const event of events) { + event.status = EventStatus.NOT_SENT; + room.addPendingEvent(event, Date.now() + Math.random() + ""); + } + + const pendingEvents = getUnsentMessages(room, rootEvent.getId()); + + expect(pendingEvents[0].threadRootId).toBe(rootEvent.getId()); + expect(pendingEvents[1].threadRootId).toBe(rootEvent.getId()); + expect(pendingEvents[2].threadRootId).toBe(rootEvent.getId()); + + // Filters out the non thread events + expect(pendingEvents.every(ev => ev.getId() !== event.getId())).toBe(true); + }); + }); +}); diff --git a/test/test-utils/threads.ts b/test/test-utils/threads.ts index 419b09b2b85..2259527178a 100644 --- a/test/test-utils/threads.ts +++ b/test/test-utils/threads.ts @@ -106,7 +106,7 @@ export const mkThread = ({ participantUserIds, length = 2, ts = 1, -}: MakeThreadProps): { thread: Thread, rootEvent: MatrixEvent } => { +}: MakeThreadProps): { thread: Thread, rootEvent: MatrixEvent, events: MatrixEvent[] } => { const { rootEvent, events } = makeThreadEvents({ roomId: room.roomId, authorId, @@ -120,5 +120,5 @@ export const mkThread = ({ // So that we do not have to mock the thread loading thread.initialEventsFetched = true; - return { thread, rootEvent }; + return { thread, rootEvent, events }; }; From 62943be46ad8f7f819949bdff66cd8edebb9dc5d Mon Sep 17 00:00:00 2001 From: Germain Date: Fri, 21 Oct 2022 10:17:13 +0100 Subject: [PATCH 21/33] Add EventTile test to assert notification badge presence --- src/components/views/rooms/EventTile.tsx | 16 +-- .../NotificationBadge/NotificationBadge.tsx | 2 +- .../components/views/rooms/EventTile-test.tsx | 112 ++++++++++++++++++ 3 files changed, 121 insertions(+), 9 deletions(-) create mode 100644 test/components/views/rooms/EventTile-test.tsx diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index a5805d617e0..b943420430f 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -115,7 +115,7 @@ export interface IEventTileType extends React.Component { getEventTileOps?(): IEventTileOps; } -interface IProps { +export interface EventTileProps { // the MatrixEvent to show mxEvent: MatrixEvent; @@ -250,7 +250,7 @@ interface IState { } // MUST be rendered within a RoomContext with a set timelineRenderingType -export class UnwrappedEventTile extends React.Component { +export class UnwrappedEventTile extends React.Component { private suppressReadReceiptAnimation: boolean; private isListeningForReceipts: boolean; private tile = React.createRef(); @@ -269,7 +269,7 @@ export class UnwrappedEventTile extends React.Component { static contextType = RoomContext; public context!: React.ContextType; - constructor(props: IProps, context: React.ContextType) { + constructor(props: EventTileProps, context: React.ContextType) { super(props, context); const thread = this.thread; @@ -453,7 +453,7 @@ export class UnwrappedEventTile extends React.Component { // TODO: [REACT-WARNING] Replace with appropriate lifecycle event // eslint-disable-next-line - UNSAFE_componentWillReceiveProps(nextProps: IProps) { + UNSAFE_componentWillReceiveProps(nextProps: EventTileProps) { // re-check the sender verification as outgoing events progress through // the send process. if (nextProps.eventSendStatus !== this.props.eventSendStatus) { @@ -461,7 +461,7 @@ export class UnwrappedEventTile extends React.Component { } } - shouldComponentUpdate(nextProps: IProps, nextState: IState): boolean { + shouldComponentUpdate(nextProps: EventTileProps, nextState: IState): boolean { if (objectHasDiff(this.state, nextState)) { return true; } @@ -490,7 +490,7 @@ export class UnwrappedEventTile extends React.Component { } } - componentDidUpdate(prevProps: IProps, prevState: IState, snapshot) { + componentDidUpdate() { // If we're not listening for receipts and expect to be, register a listener. if (!this.isListeningForReceipts && (this.shouldShowSentReceipt || this.shouldShowSendingReceipt)) { MatrixClientPeg.get().on(RoomEvent.Receipt, this.onRoomReceipt); @@ -676,7 +676,7 @@ export class UnwrappedEventTile extends React.Component { }, this.props.onHeightChanged); // Decryption may have caused a change in size } - private propsEqual(objA: IProps, objB: IProps): boolean { + private propsEqual(objA: EventTileProps, objB: EventTileProps): boolean { const keysA = Object.keys(objA); const keysB = Object.keys(objB); @@ -1527,7 +1527,7 @@ export class UnwrappedEventTile extends React.Component { } // Wrap all event tiles with the tile error boundary so that any throws even during construction are captured -const SafeEventTile = forwardRef((props: IProps, ref: RefObject) => { +const SafeEventTile = forwardRef((props: EventTileProps, ref: RefObject) => { return ; diff --git a/src/components/views/rooms/NotificationBadge/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge/NotificationBadge.tsx index 39328fd7154..931d2a3bcc3 100644 --- a/src/components/views/rooms/NotificationBadge/NotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge/NotificationBadge.tsx @@ -189,7 +189,7 @@ export function StatelessNotificationBadge({ } return ( -
+
{ symbol }
); diff --git a/test/components/views/rooms/EventTile-test.tsx b/test/components/views/rooms/EventTile-test.tsx new file mode 100644 index 00000000000..6de3a262cdc --- /dev/null +++ b/test/components/views/rooms/EventTile-test.tsx @@ -0,0 +1,112 @@ +/* +Copyright 2022 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 { act, render } from "@testing-library/react"; +import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room"; +import React from "react"; + +import EventTile, { EventTileProps } from "../../../../src/components/views/rooms/EventTile"; +import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext"; +import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; +import { getRoomContext, mkMessage, stubClient } from "../../../test-utils"; +import { mkThread } from "../../../test-utils/threads"; + +describe("EventTile", () => { + const ROOM_ID = "!roomId:example.org"; + let mxEvent: MatrixEvent; + let room: Room; + let client: MatrixClient; + // let changeEvent: (event: MatrixEvent) => void; + + function TestEventTile(props: Partial) { + // const [event] = useState(mxEvent); + // Give a way for a test to update the event prop. + // changeEvent = setEvent; + + return ; + } + + function getComponent( + overrides: Partial = {}, + renderingType: TimelineRenderingType = TimelineRenderingType.Room, + ) { + const context = getRoomContext(room, { + timelineRenderingType: renderingType, + }); + return render( + + + , + ); + } + + beforeEach(() => { + jest.clearAllMocks(); + + stubClient(); + client = MatrixClientPeg.get(); + + room = new Room(ROOM_ID, client, client.getUserId(), { + pendingEventOrdering: PendingEventOrdering.Detached, + }); + + jest.spyOn(client, "getRoom").mockReturnValue(room); + + mxEvent = mkMessage({ + room: room.roomId, + user: "@alice:example.org", + msg: "Hello world!", + event: true, + }); + }); + + describe("EventTile renderingType: ThreadsList", () => { + beforeEach(() => { + const { rootEvent } = mkThread({ + room, + client, + authorId: "@alice:example.org", + participantUserIds: ["@alice:example.org"], + }); + mxEvent = rootEvent; + }); + + it("shows an unread notification bage", () => { + const { container } = getComponent({}, TimelineRenderingType.ThreadsList); + + expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(0); + + act(() => { + room.setThreadUnreadNotificationCount(mxEvent.getId(), NotificationCountType.Total, 3); + }); + + expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(1); + expect(container.getElementsByClassName("mx_NotificationBadge_highlighted")).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); + }); + }); +}); From 77745f8c69ad79ebbf339c56262ed1ea92a57c8b Mon Sep 17 00:00:00 2001 From: Germain Date: Fri, 21 Oct 2022 10:47:20 +0100 Subject: [PATCH 22/33] remove data test id --- .../views/rooms/NotificationBadge/NotificationBadge.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/NotificationBadge/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge/NotificationBadge.tsx index 931d2a3bcc3..39328fd7154 100644 --- a/src/components/views/rooms/NotificationBadge/NotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge/NotificationBadge.tsx @@ -189,7 +189,7 @@ export function StatelessNotificationBadge({ } return ( -
+
{ symbol }
); From 5e5c2c8a250dc3f34821f73eba48c214c724afee Mon Sep 17 00:00:00 2001 From: Germain Date: Fri, 21 Oct 2022 11:39:40 +0100 Subject: [PATCH 23/33] Add UnreadNotificationBadgeTest --- src/hooks/useUnreadNotifications.ts | 4 +- .../UnreadNotificationBadge-test.tsx | 48 +++++++++++++++++-- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/hooks/useUnreadNotifications.ts b/src/hooks/useUnreadNotifications.ts index 6ee315114f7..32621372749 100644 --- a/src/hooks/useUnreadNotifications.ts +++ b/src/hooks/useUnreadNotifications.ts @@ -60,8 +60,8 @@ export const useUnreadNotifications = (room: Room, threadId?: string): { setCount(0); setColor(NotificationColor.None); } else { - const redNotifs = getUnreadNotificationCount(room, NotificationCountType.Highlight, threadId) ?? 0; - const greyNotifs = getUnreadNotificationCount(room, NotificationCountType.Total, threadId) ?? 0; + const redNotifs = getUnreadNotificationCount(room, NotificationCountType.Highlight, threadId); + const greyNotifs = getUnreadNotificationCount(room, NotificationCountType.Total, threadId); const trueCount = greyNotifs || redNotifs; setCount(trueCount); diff --git a/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx b/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx index 671fabd564c..80fe550b473 100644 --- a/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx +++ b/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx @@ -15,16 +15,25 @@ limitations under the License. */ import React from "react"; -import { act, render } from "@testing-library/react"; +import "jest-mock"; +import { screen, act, render } from "@testing-library/react"; import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client"; import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room"; -import { mocked } from "jest-mock"; +import { mocked, MockedFunction } from "jest-mock"; +import { EventStatus } from "matrix-js-sdk/src/models/event-status"; import { UnreadNotificationBadge, } from "../../../../../src/components/views/rooms/NotificationBadge/UnreadNotificationBadge"; -import { stubClient } from "../../../../test-utils/test-utils"; +import { mkMessage, stubClient } from "../../../../test-utils/test-utils"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; +import * as RoomNotifs from "../../../../../src/RoomNotifs"; + +jest.mock("../../../../../src/RoomNotifs"); +jest.mock('../../../../../src/RoomNotifs', () => ({ + ...(jest.requireActual('../../../../../src/RoomNotifs')), + getRoomNotifsState: jest.fn(), +})); const ROOM_ID = "!roomId:example.org"; let THREAD_ID; @@ -51,6 +60,8 @@ describe("UnreadNotificationBadge", () => { room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 1); room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0); + + jest.spyOn(RoomNotifs, "getRoomNotifsState").mockReturnValue(RoomNotifs.RoomNotifState.AllMessages); }); it("renders unread notification badge", () => { @@ -87,4 +98,35 @@ describe("UnreadNotificationBadge", () => { expect(container.querySelector(".mx_NotificationBadge_visible")).toBeFalsy(); }); }); + + it("adds a warning for unsent messages", () => { + const evt = mkMessage({ + room: room.roomId, + user: "@alice:example.org", + msg: "Hello world!", + event: true, + }); + evt.status = EventStatus.NOT_SENT; + + room.addPendingEvent(evt, "123"); + + render(getComponent()); + + expect(screen.queryByText("!")).not.toBeNull(); + }); + + it("adds a warning for invites", () => { + jest.spyOn(room, "getMyMembership").mockReturnValue("invite"); + render(getComponent()); + expect(screen.queryByText("!")).not.toBeNull(); + }); + + it("hides counter for muted rooms", () => { + jest.spyOn(RoomNotifs, "getRoomNotifsState") + .mockReset() + .mockReturnValue(RoomNotifs.RoomNotifState.Mute); + + const { container } = render(getComponent()); + expect(container.querySelector(".mx_NotificationBadge")).toBeNull(); + }); }); From bd7ef15f866d7a349cd6eace079702a457adb9b7 Mon Sep 17 00:00:00 2001 From: Germain Date: Fri, 21 Oct 2022 11:47:14 +0100 Subject: [PATCH 24/33] lint --- .../rooms/NotificationBadge/UnreadNotificationBadge-test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx b/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx index 80fe550b473..20289dc6b91 100644 --- a/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx +++ b/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx @@ -19,7 +19,7 @@ import "jest-mock"; import { screen, act, render } from "@testing-library/react"; import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client"; import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room"; -import { mocked, MockedFunction } from "jest-mock"; +import { mocked } from "jest-mock"; import { EventStatus } from "matrix-js-sdk/src/models/event-status"; import { @@ -31,7 +31,7 @@ import * as RoomNotifs from "../../../../../src/RoomNotifs"; jest.mock("../../../../../src/RoomNotifs"); jest.mock('../../../../../src/RoomNotifs', () => ({ - ...(jest.requireActual('../../../../../src/RoomNotifs')), + ...(jest.requireActual('../../../../../src/RoomNotifs') as Object), getRoomNotifsState: jest.fn(), })); From 3e71f381500bd19ba501ac922d7f8a09e329d4d4 Mon Sep 17 00:00:00 2001 From: Germain Date: Fri, 21 Oct 2022 14:08:16 +0100 Subject: [PATCH 25/33] Remove unneeded code clauses --- src/RoomNotifs.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/RoomNotifs.ts b/src/RoomNotifs.ts index ee0f9f8f083..3dd147f91a7 100644 --- a/src/RoomNotifs.ts +++ b/src/RoomNotifs.ts @@ -80,12 +80,12 @@ export function setRoomNotifsState(roomId: string, newState: RoomNotifState): Pr export function getUnreadNotificationCount( room: Room, - type: NotificationCountType | null = null, + type: NotificationCountType, threadId?: string, ): number { let notificationCount = (!!threadId ? room.getThreadUnreadNotificationCount(threadId, type) - : room.getUnreadNotificationCount(type)) ?? 0; + : room.getUnreadNotificationCount(type)); // Check notification counts in the old room just in case there's some lost // there. We only go one level down to avoid performance issues, and theory @@ -97,7 +97,7 @@ export function getUnreadNotificationCount( if (oldRoom) { const oldNotificationCount = (!!threadId ? oldRoom.getThreadUnreadNotificationCount(threadId, NotificationCountType.Highlight) - : oldRoom.getUnreadNotificationCount(NotificationCountType.Highlight)) ?? 0; + : oldRoom.getUnreadNotificationCount(NotificationCountType.Highlight)); // We only ever care if there's highlights in the old room. No point in // notifying the user for unread messages because they would have extreme From 97e0d3607bb126e424c57e052a1b7f51cafed266 Mon Sep 17 00:00:00 2001 From: Germain Date: Fri, 21 Oct 2022 15:05:40 +0100 Subject: [PATCH 26/33] remove clause that can not happen --- src/RoomNotifs.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/RoomNotifs.ts b/src/RoomNotifs.ts index 3dd147f91a7..6c1e07e66b2 100644 --- a/src/RoomNotifs.ts +++ b/src/RoomNotifs.ts @@ -91,19 +91,17 @@ export function getUnreadNotificationCount( // there. We only go one level down to avoid performance issues, and theory // is that 1st generation rooms will have already been read by the 3rd generation. const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, ""); - if (createEvent && createEvent.getContent()['predecessor']) { - const oldRoomId = createEvent.getContent()['predecessor']['room_id']; + const predecessor = createEvent?.getContent().predecessor; + // Exclude threadId, as the same thread can't continue over a room upgrade + if (!threadId && predecessor) { + const oldRoomId = predecessor.room_id; const oldRoom = MatrixClientPeg.get().getRoom(oldRoomId); if (oldRoom) { - const oldNotificationCount = (!!threadId - ? oldRoom.getThreadUnreadNotificationCount(threadId, NotificationCountType.Highlight) - : oldRoom.getUnreadNotificationCount(NotificationCountType.Highlight)); - // We only ever care if there's highlights in the old room. No point in // notifying the user for unread messages because they would have extreme // difficulty changing their notification preferences away from "All Messages" // and "Noisy". - notificationCount += oldNotificationCount; + notificationCount += oldRoom.getUnreadNotificationCount(NotificationCountType.Highlight); } } From bde88be84314165bc81f3038815beb865a1b441d Mon Sep 17 00:00:00 2001 From: Germain Date: Fri, 21 Oct 2022 15:26:23 +0100 Subject: [PATCH 27/33] coverage for destroy --- .../RoomNotificationState-test.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/test/stores/notifications/RoomNotificationState-test.ts b/test/stores/notifications/RoomNotificationState-test.ts index 904e0689092..c9ee6dd4974 100644 --- a/test/stores/notifications/RoomNotificationState-test.ts +++ b/test/stores/notifications/RoomNotificationState-test.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { Room } from "matrix-js-sdk/src/models/room"; -import { MatrixEventEvent, MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { MatrixEventEvent, MatrixEvent, MatrixClient } from "matrix-js-sdk/src/matrix"; import { stubClient } from "../../test-utils"; import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; @@ -24,12 +24,16 @@ import * as testUtils from "../../test-utils"; import { NotificationStateEvents } from "../../../src/stores/notifications/NotificationState"; describe("RoomNotificationState", () => { - stubClient(); - const client = MatrixClientPeg.get(); + let testRoom: Room; + let client: MatrixClient; - it("Updates on event decryption", () => { - const testRoom = testUtils.mkStubRoom("$aroomid", "Test room", client); + beforeEach(() => { + stubClient(); + client = MatrixClientPeg.get(); + testRoom = testUtils.mkStubRoom("$aroomid", "Test room", client); + }); + it("Updates on event decryption", () => { const roomNotifState = new RoomNotificationState(testRoom as any as Room); const listener = jest.fn(); roomNotifState.addListener(NotificationStateEvents.Update, listener); @@ -40,4 +44,9 @@ describe("RoomNotificationState", () => { client.emit(MatrixEventEvent.Decrypted, testEvent); expect(listener).toHaveBeenCalled(); }); + + it("removes listeners", () => { + const roomNotifState = new RoomNotificationState(testRoom as any as Room); + expect(() => roomNotifState.destroy()).not.toThrow(); + }); }); From 6922dad1b5ff11476e1bbad1cfa35f450127ed6c Mon Sep 17 00:00:00 2001 From: Germain Date: Fri, 21 Oct 2022 17:05:44 +0100 Subject: [PATCH 28/33] Fix sliding sync test --- cypress/e2e/sliding-sync/sliding-sync.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/sliding-sync/sliding-sync.ts b/cypress/e2e/sliding-sync/sliding-sync.ts index e0e7c974a77..4aafcb2b30f 100644 --- a/cypress/e2e/sliding-sync/sliding-sync.ts +++ b/cypress/e2e/sliding-sync/sliding-sync.ts @@ -212,7 +212,7 @@ describe("Sliding Sync", () => { cy.contains(".mx_RoomTile", "Test Room").should("not.have.class", "mx_NotificationBadge_count"); }); - it("should not show unread indicators", () => { // TODO: for now. Later we should. + it.only("should not show unread indicators", () => { // TODO: for now. Later we should. createAndJoinBob(); // disable notifs in this room (TODO: CS API call?) @@ -235,7 +235,7 @@ describe("Sliding Sync", () => { "Test Room", "Dummy", ]); - cy.contains(".mx_RoomTile", "Test Room").get(".mx_NotificationBadge").should("not.exist"); + cy.contains(".mx_RoomTile", "Test Room").get(".mx_NotificationBadge").should("not.be.visible"); }); it("should update user settings promptly", () => { From 2c2caf239b2d22f45f174a07386489e49932399d Mon Sep 17 00:00:00 2001 From: Germain Date: Fri, 21 Oct 2022 17:17:57 +0100 Subject: [PATCH 29/33] add cb tests --- .../NotificationBadge-test.tsx | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 test/components/views/rooms/NotificationBadge/NotificationBadge-test.tsx diff --git a/test/components/views/rooms/NotificationBadge/NotificationBadge-test.tsx b/test/components/views/rooms/NotificationBadge/NotificationBadge-test.tsx new file mode 100644 index 00000000000..c71fae36105 --- /dev/null +++ b/test/components/views/rooms/NotificationBadge/NotificationBadge-test.tsx @@ -0,0 +1,49 @@ +/* +Copyright 2022 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 { fireEvent, render } from "@testing-library/react"; +import React from "react"; + +import { + StatelessNotificationBadge, +} from "../../../../../src/components/views/rooms/NotificationBadge/NotificationBadge"; +import { NotificationColor } from "../../../../../src/stores/notifications/NotificationColor"; + +describe("NotificationBadge", () => { + describe("StatelessNotificationBadge", () => { + it("lets you click it", () => { + const cb = jest.fn(); + + const { container } = render(); + + fireEvent.click(container.firstChild); + expect(cb).toHaveBeenCalledTimes(1); + + fireEvent.mouseEnter(container.firstChild); + expect(cb).toHaveBeenCalledTimes(2); + + fireEvent.mouseLeave(container.firstChild); + expect(cb).toHaveBeenCalledTimes(3); + }); + }); +}); From c2928c67f9113d290eab2e6056646485afb627a9 Mon Sep 17 00:00:00 2001 From: Germain Date: Fri, 21 Oct 2022 17:23:08 +0100 Subject: [PATCH 30/33] Fix it.only --- cypress/e2e/sliding-sync/sliding-sync.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/e2e/sliding-sync/sliding-sync.ts b/cypress/e2e/sliding-sync/sliding-sync.ts index 4aafcb2b30f..ebc90443f34 100644 --- a/cypress/e2e/sliding-sync/sliding-sync.ts +++ b/cypress/e2e/sliding-sync/sliding-sync.ts @@ -212,7 +212,7 @@ describe("Sliding Sync", () => { cy.contains(".mx_RoomTile", "Test Room").should("not.have.class", "mx_NotificationBadge_count"); }); - it.only("should not show unread indicators", () => { // TODO: for now. Later we should. + it("should not show unread indicators", () => { // TODO: for now. Later we should. createAndJoinBob(); // disable notifs in this room (TODO: CS API call?) From 7b8c60aeb428d6af298578e70f20087d88ffbbfc Mon Sep 17 00:00:00 2001 From: Germain Date: Fri, 21 Oct 2022 19:30:28 +0100 Subject: [PATCH 31/33] undo unneeded changes --- .../RoomStatusBarUnsentMessages.tsx | 2 +- .../views/avatars/DecoratedRoomAvatar.tsx | 2 +- .../views/dialogs/ForwardDialog.tsx | 2 +- .../dialogs/spotlight/SpotlightDialog.tsx | 2 +- src/components/views/rooms/EventTile.tsx | 2 +- src/components/views/rooms/ExtraTile.tsx | 2 +- .../NotificationBadge.tsx | 75 ++--------------- .../StatelessNotificationBadge.tsx | 81 +++++++++++++++++++ .../UnreadNotificationBadge.tsx | 2 +- src/components/views/rooms/RoomSublist.tsx | 2 +- src/components/views/rooms/RoomTile.tsx | 2 +- .../views/rooms/VoiceRecordComposerTile.tsx | 2 +- .../views/spaces/SpaceTreeLevel.tsx | 2 +- .../NotificationBadge-test.tsx | 2 +- 14 files changed, 100 insertions(+), 80 deletions(-) rename src/components/views/rooms/{NotificationBadge => }/NotificationBadge.tsx (64%) create mode 100644 src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx diff --git a/src/components/structures/RoomStatusBarUnsentMessages.tsx b/src/components/structures/RoomStatusBarUnsentMessages.tsx index c092bf0231a..4c8f9fe35b5 100644 --- a/src/components/structures/RoomStatusBarUnsentMessages.tsx +++ b/src/components/structures/RoomStatusBarUnsentMessages.tsx @@ -17,7 +17,7 @@ limitations under the License. import React, { ReactElement } from "react"; import { StaticNotificationState } from "../../stores/notifications/StaticNotificationState"; -import NotificationBadge from "../views/rooms/NotificationBadge/NotificationBadge"; +import NotificationBadge from "../views/rooms/NotificationBadge"; interface RoomStatusBarUnsentMessagesProps { title: string; diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx index 971ffae6921..06ee20cc08e 100644 --- a/src/components/views/avatars/DecoratedRoomAvatar.tsx +++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx @@ -24,7 +24,7 @@ import { JoinRule } from "matrix-js-sdk/src/@types/partials"; import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; import RoomAvatar from "./RoomAvatar"; -import NotificationBadge from '../rooms/NotificationBadge/NotificationBadge'; +import NotificationBadge from '../rooms/NotificationBadge'; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { NotificationState } from "../../../stores/notifications/NotificationState"; import { isPresenceEnabled } from "../../../utils/presence"; diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index c9ee094e638..8e23e3bd950 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -39,7 +39,7 @@ import { Alignment } from '../elements/Tooltip'; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; -import NotificationBadge from "../rooms/NotificationBadge/NotificationBadge"; +import NotificationBadge from "../rooms/NotificationBadge"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; import QueryMatcher from "../../../autocomplete/QueryMatcher"; diff --git a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx index 5318bf61929..dfec2ab5097 100644 --- a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx +++ b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx @@ -81,7 +81,7 @@ import { NetworkDropdown } from "../../directory/NetworkDropdown"; import AccessibleButton from "../../elements/AccessibleButton"; import LabelledCheckbox from "../../elements/LabelledCheckbox"; import Spinner from "../../elements/Spinner"; -import NotificationBadge from "../../rooms/NotificationBadge/NotificationBadge"; +import NotificationBadge from "../../rooms/NotificationBadge"; import BaseDialog from "../BaseDialog"; import FeedbackDialog from "../FeedbackDialog"; import { IDialogProps } from "../IDialogProps"; diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index b943420430f..670a291a422 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -47,7 +47,7 @@ import Tooltip, { Alignment } from "../elements/Tooltip"; import EditorStateTransfer from "../../../utils/EditorStateTransfer"; import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; -import NotificationBadge from "./NotificationBadge/NotificationBadge"; +import NotificationBadge from "./NotificationBadge"; import LegacyCallEventGrouper from "../../structures/LegacyCallEventGrouper"; import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; import { Action } from '../../../dispatcher/actions'; diff --git a/src/components/views/rooms/ExtraTile.tsx b/src/components/views/rooms/ExtraTile.tsx index c7a6d2d30f7..313ae28b1f8 100644 --- a/src/components/views/rooms/ExtraTile.tsx +++ b/src/components/views/rooms/ExtraTile.tsx @@ -21,7 +21,7 @@ import { RovingAccessibleButton, RovingAccessibleTooltipButton, } from "../../../accessibility/RovingTabIndex"; -import NotificationBadge from "./NotificationBadge/NotificationBadge"; +import NotificationBadge from "./NotificationBadge"; import { NotificationState } from "../../../stores/notifications/NotificationState"; import { ButtonEvent } from "../elements/AccessibleButton"; diff --git a/src/components/views/rooms/NotificationBadge/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx similarity index 64% rename from src/components/views/rooms/NotificationBadge/NotificationBadge.tsx rename to src/components/views/rooms/NotificationBadge.tsx index 39328fd7154..3555582298b 100644 --- a/src/components/views/rooms/NotificationBadge/NotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge.tsx @@ -15,16 +15,14 @@ limitations under the License. */ import React, { MouseEvent } from "react"; -import classNames from "classnames"; -import { formatCount } from "../../../../utils/FormattingUtils"; -import SettingsStore from "../../../../settings/SettingsStore"; -import AccessibleButton from "../../elements/AccessibleButton"; -import { XOR } from "../../../../@types/common"; -import { NotificationState, NotificationStateEvents } from "../../../../stores/notifications/NotificationState"; -import Tooltip from "../../elements/Tooltip"; -import { _t } from "../../../../languageHandler"; -import { NotificationColor } from "../../../../stores/notifications/NotificationColor"; +import SettingsStore from "../../../settings/SettingsStore"; +import { XOR } from "../../../@types/common"; +import { NotificationState, NotificationStateEvents } from "../../../stores/notifications/NotificationState"; +import Tooltip from "../elements/Tooltip"; +import { _t } from "../../../languageHandler"; +import { NotificationColor } from "../../../stores/notifications/NotificationColor"; +import { StatelessNotificationBadge } from "./NotificationBadge/StatelessNotificationBadge"; interface IProps { notification: NotificationState; @@ -135,62 +133,3 @@ export default class NotificationBadge extends React.PureComponent; } } - -interface Props { - symbol: string | null; - count: number; - color: NotificationColor; - onClick?: (ev: MouseEvent) => void; - onMouseOver?: (ev: MouseEvent) => void; - onMouseLeave?: (ev: MouseEvent) => void; - children?: React.ReactChildren | JSX.Element; - label?: string; -} - -export function StatelessNotificationBadge({ - symbol, - count, - color, - ...props }: Props) { - // Don't show a badge if we don't need to - if (color === NotificationColor.None) return null; - - const hasUnreadCount = color >= NotificationColor.Grey && (!!count || !!symbol); - - const isEmptyBadge = symbol === null && count === 0; - - if (symbol === null && count > 0) { - symbol = formatCount(count); - } - - const classes = classNames({ - 'mx_NotificationBadge': true, - 'mx_NotificationBadge_visible': isEmptyBadge ? true : hasUnreadCount, - 'mx_NotificationBadge_highlighted': color === NotificationColor.Red, - 'mx_NotificationBadge_dot': isEmptyBadge, - 'mx_NotificationBadge_2char': symbol?.length > 0 && symbol?.length < 3, - 'mx_NotificationBadge_3char': symbol?.length > 2, - }); - - if (props.onClick) { - return ( - - { symbol } - { props.children } - - ); - } - - return ( -
- { symbol } -
- ); -} diff --git a/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx b/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx new file mode 100644 index 00000000000..e8cf054e565 --- /dev/null +++ b/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx @@ -0,0 +1,81 @@ +/* +Copyright 2020 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, { MouseEvent } from "react"; +import classNames from "classnames"; + +import { formatCount } from "../../../../utils/FormattingUtils"; +import AccessibleButton from "../../elements/AccessibleButton"; +import { NotificationColor } from "../../../../stores/notifications/NotificationColor"; + +interface Props { + symbol: string | null; + count: number; + color: NotificationColor; + onClick?: (ev: MouseEvent) => void; + onMouseOver?: (ev: MouseEvent) => void; + onMouseLeave?: (ev: MouseEvent) => void; + children?: React.ReactChildren | JSX.Element; + label?: string; +} + +export function StatelessNotificationBadge({ + symbol, + count, + color, + ...props }: Props) { + // Don't show a badge if we don't need to + if (color === NotificationColor.None) return null; + + const hasUnreadCount = color >= NotificationColor.Grey && (!!count || !!symbol); + + const isEmptyBadge = symbol === null && count === 0; + + if (symbol === null && count > 0) { + symbol = formatCount(count); + } + + const classes = classNames({ + 'mx_NotificationBadge': true, + 'mx_NotificationBadge_visible': isEmptyBadge ? true : hasUnreadCount, + 'mx_NotificationBadge_highlighted': color === NotificationColor.Red, + 'mx_NotificationBadge_dot': isEmptyBadge, + 'mx_NotificationBadge_2char': symbol?.length > 0 && symbol?.length < 3, + 'mx_NotificationBadge_3char': symbol?.length > 2, + }); + + if (props.onClick) { + return ( + + { symbol } + { props.children } + + ); + } + + return ( +
+ { symbol } +
+ ); +} diff --git a/src/components/views/rooms/NotificationBadge/UnreadNotificationBadge.tsx b/src/components/views/rooms/NotificationBadge/UnreadNotificationBadge.tsx index b44f817dff8..a623daa716e 100644 --- a/src/components/views/rooms/NotificationBadge/UnreadNotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge/UnreadNotificationBadge.tsx @@ -18,7 +18,7 @@ import { Room } from "matrix-js-sdk/src/models/room"; import React from "react"; import { useUnreadNotifications } from "../../../../hooks/useUnreadNotifications"; -import { StatelessNotificationBadge } from "./NotificationBadge"; +import { StatelessNotificationBadge } from "./StatelessNotificationBadge"; interface Props { room: Room; diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index 62e28260afa..9e890e9c219 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -54,7 +54,7 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import ExtraTile from "./ExtraTile"; import SettingsStore from "../../../settings/SettingsStore"; import { SlidingSyncManager } from "../../../SlidingSyncManager"; -import NotificationBadge from "./NotificationBadge/NotificationBadge"; +import NotificationBadge from "./NotificationBadge"; import RoomTile from "./RoomTile"; const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 76000befbb7..68f4dfe4de2 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -32,7 +32,7 @@ import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import { RoomNotifState } from "../../../RoomNotifs"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { RoomNotificationContextMenu } from "../context_menus/RoomNotificationContextMenu"; -import NotificationBadge from "./NotificationBadge/NotificationBadge"; +import NotificationBadge from "./NotificationBadge"; import { ActionPayload } from "../../../dispatcher/payloads"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { NotificationState, NotificationStateEvents } from "../../../stores/notifications/NotificationState"; diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index ef6c145b49e..c3fe0762c7c 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -32,7 +32,7 @@ import RecordingPlayback, { PlaybackLayout } from "../audio_messages/RecordingPl import Modal from "../../../Modal"; import ErrorDialog from "../dialogs/ErrorDialog"; import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../MediaDeviceHandler"; -import NotificationBadge from "./NotificationBadge/NotificationBadge"; +import NotificationBadge from "./NotificationBadge"; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; import { NotificationColor } from "../../../stores/notifications/NotificationColor"; import InlineSpinner from "../elements/InlineSpinner"; diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 844b4ff975f..5952e877d9f 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -32,7 +32,7 @@ import RoomAvatar from "../avatars/RoomAvatar"; import SpaceStore from "../../../stores/spaces/SpaceStore"; import { SpaceKey } from "../../../stores/spaces"; import SpaceTreeLevelLayoutStore from "../../../stores/spaces/SpaceTreeLevelLayoutStore"; -import NotificationBadge from "../rooms/NotificationBadge/NotificationBadge"; +import NotificationBadge from "../rooms/NotificationBadge"; import { _t } from "../../../languageHandler"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; diff --git a/test/components/views/rooms/NotificationBadge/NotificationBadge-test.tsx b/test/components/views/rooms/NotificationBadge/NotificationBadge-test.tsx index c71fae36105..95d598a704b 100644 --- a/test/components/views/rooms/NotificationBadge/NotificationBadge-test.tsx +++ b/test/components/views/rooms/NotificationBadge/NotificationBadge-test.tsx @@ -19,7 +19,7 @@ import React from "react"; import { StatelessNotificationBadge, -} from "../../../../../src/components/views/rooms/NotificationBadge/NotificationBadge"; +} from "../../../../../src/components/views/rooms/NotificationBadge/StatelessNotificationBadge"; import { NotificationColor } from "../../../../../src/stores/notifications/NotificationColor"; describe("NotificationBadge", () => { From 49cb9f8473ab5527958d7fc1c58865204d5a3924 Mon Sep 17 00:00:00 2001 From: Germain Date: Fri, 21 Oct 2022 19:31:32 +0100 Subject: [PATCH 32/33] copyright year --- .../rooms/NotificationBadge/StatelessNotificationBadge.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx b/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx index e8cf054e565..868df3216fc 100644 --- a/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2022 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. From 27a3fbdb7e6dd2c64dfe80ae71b4b4a5b4b14bda Mon Sep 17 00:00:00 2001 From: Germain Date: Fri, 21 Oct 2022 19:40:07 +0100 Subject: [PATCH 33/33] i18n --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b76a486e887..f40e1658040 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1892,6 +1892,7 @@ "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites.": "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites.", "Enable encryption in settings.": "Enable encryption in settings.", "End-to-end encryption isn't enabled": "End-to-end encryption isn't enabled", + "Message didn't send. Click for info.": "Message didn't send. Click for info.", "Unpin": "Unpin", "View message": "View message", "%(duration)ss": "%(duration)ss", @@ -2076,7 +2077,6 @@ "Stop recording": "Stop recording", "Italic": "Italic", "Underline": "Underline", - "Message didn't send. Click for info.": "Message didn't send. Click for info.", "Error updating main address": "Error updating main address", "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.", "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.",