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

Add thread notification with server assistance (MSC3773) #9400

Merged
merged 42 commits into from
Oct 24, 2022
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
6e49291
Add feature detection for thread notifications
germain-gg Oct 6, 2022
d220ffa
Extra logic from UI for NotificationBadge
germain-gg Oct 12, 2022
37a49a6
Create UnreadNotificationBadge to display thread notifications
germain-gg Oct 12, 2022
2b19d77
Hook room header to server unread thread notifications
germain-gg Oct 12, 2022
db20700
hook unread thread notifications to thread list UI
germain-gg Oct 12, 2022
edb229f
Merge branch 'develop' into gsouquet/notifications-msc3773
germain-gg Oct 12, 2022
0d8aa31
remove unused imports
germain-gg Oct 12, 2022
c9bdfcf
fix i18n
germain-gg Oct 12, 2022
377ffd3
fix css linting
germain-gg Oct 12, 2022
5d154c8
Add mock for canSupport
germain-gg Oct 12, 2022
65620b1
Add basic notificationbadge tests
germain-gg Oct 12, 2022
bfd1a1f
Merge branch 'develop' into gsouquet/notifications-msc3773
germain-gg Oct 12, 2022
f76fb71
Migrate tests from Enzyme to RTL
germain-gg Oct 13, 2022
6323677
Merge branch 'gsouquet/fix-i18n-tests' into gsouquet/notifications-ms…
germain-gg Oct 13, 2022
da55257
Do not create account data event for guests
germain-gg Oct 13, 2022
2201bb7
fix test
germain-gg Oct 13, 2022
f48eff0
Merge branch 'gsouquet/fix-23479' into gsouquet/notifications-msc3773
germain-gg Oct 13, 2022
10d71f2
Add RoomNotifs getUnreadNotificationCount tests
germain-gg Oct 13, 2022
58e505b
Merge branch 'develop' into gsouquet/notifications-msc3773
germain-gg Oct 13, 2022
da538ea
Add RoomNotificationStateStore tests
germain-gg Oct 13, 2022
e272fa2
Add room header tests
germain-gg Oct 14, 2022
ac1567d
Remove uneeded code
germain-gg Oct 14, 2022
535ec10
Merge branch 'develop' into gsouquet/notifications-msc3773
germain-gg Oct 14, 2022
598bd85
Fix getUnsentMessages
Oct 18, 2022
fbd2218
Merge branch 'develop' into gsouquet/notifications-msc3773
germain-gg Oct 19, 2022
7c0368e
Merge branch 'develop' into gsouquet/notifications-msc3773
germain-gg Oct 21, 2022
1e41e2f
fix per PR comments
germain-gg Oct 21, 2022
de23f99
Add RoomStatusBar test
germain-gg Oct 21, 2022
62943be
Add EventTile test to assert notification badge presence
germain-gg Oct 21, 2022
77745f8
remove data test id
germain-gg Oct 21, 2022
5e5c2c8
Add UnreadNotificationBadgeTest
germain-gg Oct 21, 2022
bd7ef15
lint
germain-gg Oct 21, 2022
3e71f38
Remove unneeded code clauses
germain-gg Oct 21, 2022
97e0d36
remove clause that can not happen
germain-gg Oct 21, 2022
bde88be
coverage for destroy
germain-gg Oct 21, 2022
a396acd
Merge branch 'develop' into gsouquet/notifications-msc3773
germain-gg Oct 21, 2022
6922dad
Fix sliding sync test
germain-gg Oct 21, 2022
2c2caf2
add cb tests
germain-gg Oct 21, 2022
c2928c6
Fix it.only
germain-gg Oct 21, 2022
7b8c60a
undo unneeded changes
germain-gg Oct 21, 2022
49cb9f8
copyright year
germain-gg Oct 21, 2022
27a3fbd
i18n
germain-gg Oct 21, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions res/css/views/rooms/_EventTile.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ $left-gutter: 64px;
}

&.mx_EventTile_selected .mx_EventTile_line {
// TODO: check if this would be necessary
/* TODO: check if this would be necessary; */
padding-inline-start: calc(var(--EventTile_group_line-spacing-inline-start) + 20px);
}
}
Expand Down Expand Up @@ -894,15 +894,21 @@ $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;
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
border-radius: 50%;
inset: $notification-inset-block-start $spacing-8 auto auto;
}

.mx_NotificationBadge_count {
display: none;
}

&[data-notification="total"]::before {
background-color: $room-icon-unread-color;
}
Expand Down Expand Up @@ -1301,7 +1307,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));
Expand Down
16 changes: 13 additions & 3 deletions src/RoomNotifs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
}

Expand Down
6 changes: 0 additions & 6 deletions src/Unread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ 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
Expand Down Expand Up @@ -78,11 +77,6 @@ export function doesRoomHaveUnreadMessages(room: Room): boolean {
if (room.timeline.length && room.timeline[room.timeline.length - 1].getSender() === myUserId) {
return false;
}
} else {
const threadState = RoomNotificationStateStore.instance.getThreadsRoomState(room);
if (threadState.color > 0) {
return true;
}
}

// if the read receipt relates to an event is that part of a thread
Expand Down
6 changes: 4 additions & 2 deletions src/components/structures/RoomStatusBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
});
}

Expand Down
2 changes: 1 addition & 1 deletion src/components/structures/RoomStatusBarUnsentMessages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/avatars/DecoratedRoomAvatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/dialogs/ForwardDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/dialogs/spotlight/SpotlightDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
54 changes: 46 additions & 8 deletions src/components/views/right_panel/RoomHeaderButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -136,32 +138,67 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
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?.on(RoomEvent.UnreadNotifications, this.onNotificationUpdate);
}
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 {
threadNotificationColor = this.notificationColor;
}

// console.log
// XXX: why don't we read from this.state.threadNotificationColor in the render methods?
this.setState({
threadNotificationColor: this.threadNotificationState.color,
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;
Expand Down Expand Up @@ -255,12 +292,13 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
? <HeaderButton
key={RightPanelPhases.ThreadPanel}
name="threadsButton"
data-testid="threadsButton"
title={_t("Threads")}
onClick={this.onThreadsPanelClicked}
isHighlighted={this.isPhase(RoomHeaderButtons.THREAD_PHASES)}
isUnread={this.threadNotificationState.color > 0}
isUnread={this.state.threadNotificationColor > 0}
>
<UnreadIndicator color={this.threadNotificationState.color} />
<UnreadIndicator color={this.state.threadNotificationColor} />
</HeaderButton>
: null,
);
Expand Down
59 changes: 37 additions & 22 deletions src/components/views/rooms/EventTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -46,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";
import NotificationBadge from "./NotificationBadge/NotificationBadge";
import LegacyCallEventGrouper from "../../structures/LegacyCallEventGrouper";
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
import { Action } from '../../../dispatcher/actions';
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -394,7 +396,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
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);
}
}
Expand All @@ -405,33 +407,40 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
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);
}
Expand Down Expand Up @@ -1347,6 +1356,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
]);
}
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", {
Expand All @@ -1360,7 +1370,9 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
"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) => {
Expand Down Expand Up @@ -1408,6 +1420,9 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
</RovingAccessibleTooltipButton>
</Toolbar>
{ msgOption }
<UnreadNotificationBadge
room={room}
threadId={this.props.mxEvent.getId()} />
</>)
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/rooms/ExtraTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
Loading