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

Commit

Permalink
Add thread notification with server assistance (MSC3773) (#9400)
Browse files Browse the repository at this point in the history
Co-authored-by: Janne Mareike Koschinski <[email protected]>
  • Loading branch information
Germain and justjanne authored Oct 24, 2022
1 parent d4f1c57 commit 9eb4f8d
Show file tree
Hide file tree
Showing 22 changed files with 1,014 additions and 142 deletions.
2 changes: 1 addition & 1 deletion cypress/e2e/sliding-sync/sliding-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
18 changes: 13 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,22 @@ $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;
/* !important to fix overly specific CSS selector applied on mx_NotificationBadge */
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 {
background-color: $room-icon-unread-color;
}
Expand Down Expand Up @@ -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));
Expand Down
16 changes: 12 additions & 4 deletions src/RoomNotifs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,23 @@ 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,
threadId?: string,
): number {
let notificationCount = (!!threadId
? room.getThreadUnreadNotificationCount(threadId, type)
: 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
// 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) {
// We only ever care if there's highlights in the old room. No point in
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 @@ -77,11 +76,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);
});
}

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
Loading

0 comments on commit 9eb4f8d

Please sign in to comment.