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

Commit 306a244

Browse files
Germaint3chguy
andauthored
Show thread notification if thread timeline is closed (#9495)
* Show thread notification if thread timeline is closed * Simplify isViewingEventTimeline statement Co-authored-by: Michael Telatynski <[email protected]> * Fix show desktop notifications * Add RoomViewStore thread id assertions * Add Notifier tests * fix lint * Remove it.only Co-authored-by: Michael Telatynski <[email protected]>
1 parent d273441 commit 306a244

File tree

7 files changed

+178
-21
lines changed

7 files changed

+178
-21
lines changed

src/Notifier.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,16 @@ export const Notifier = {
435435
if (actions?.notify) {
436436
this._performCustomEventHandling(ev);
437437

438-
if (SdkContextClass.instance.roomViewStore.getRoomId() === room.roomId &&
438+
const store = SdkContextClass.instance.roomViewStore;
439+
const isViewingRoom = store.getRoomId() === room.roomId;
440+
const threadId: string | undefined = ev.getId() !== ev.threadRootId
441+
? ev.threadRootId
442+
: undefined;
443+
const isViewingThread = store.getThreadId() === threadId;
444+
445+
const isViewingEventTimeline = isViewingRoom && (!threadId || isViewingThread);
446+
447+
if (isViewingEventTimeline &&
439448
UserActivity.sharedInstance().userActiveRecently() &&
440449
!Modal.hasDialogs()
441450
) {

src/components/structures/ThreadView.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import Spinner from "../views/elements/Spinner";
5555
import { ComposerInsertPayload, ComposerType } from "../../dispatcher/payloads/ComposerInsertPayload";
5656
import Heading from '../views/typography/Heading';
5757
import { SdkContextClass } from '../../contexts/SDKContext';
58+
import { ThreadPayload } from '../../dispatcher/payloads/ThreadPayload';
5859

5960
interface IProps {
6061
room: Room;
@@ -132,6 +133,11 @@ export default class ThreadView extends React.Component<IProps, IState> {
132133
metricsTrigger: undefined, // room doesn't change
133134
});
134135
}
136+
137+
dis.dispatch<ThreadPayload>({
138+
action: Action.ViewThread,
139+
thread_id: null,
140+
});
135141
}
136142

137143
public componentDidUpdate(prevProps) {
@@ -225,6 +231,10 @@ export default class ThreadView extends React.Component<IProps, IState> {
225231
};
226232

227233
private async postThreadUpdate(thread: Thread): Promise<void> {
234+
dis.dispatch<ThreadPayload>({
235+
action: Action.ViewThread,
236+
thread_id: thread.id,
237+
});
228238
thread.emit(ThreadEvent.ViewThread);
229239
await thread.fetchInitialEvents();
230240
this.updateThreadRelation();

src/dispatcher/actions.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ export enum Action {
116116
*/
117117
ViewRoom = "view_room",
118118

119+
/**
120+
* Changes thread based on payload parameters. Should be used with ThreadPayload.
121+
*/
122+
ViewThread = "view_thread",
123+
119124
/**
120125
* Changes room based on room list order and payload parameters. Should be used with ViewRoomDeltaPayload.
121126
*/
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { ActionPayload } from "../payloads";
18+
import { Action } from "../actions";
19+
20+
/* eslint-disable camelcase */
21+
export interface ThreadPayload extends Pick<ActionPayload, "action"> {
22+
action: Action.ViewThread;
23+
24+
thread_id: string | null;
25+
}
26+
/* eslint-enable camelcase */

src/stores/RoomViewStore.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import { awaitRoomDownSync } from "../utils/RoomUpgrade";
5050
import { UPDATE_EVENT } from "./AsyncStore";
5151
import { SdkContextClass } from "../contexts/SDKContext";
5252
import { CallStore } from "./CallStore";
53+
import { ThreadPayload } from "../dispatcher/payloads/ThreadPayload";
5354

5455
const NUM_JOIN_RETRY = 5;
5556

@@ -66,6 +67,10 @@ interface State {
6667
* The ID of the room currently being viewed
6768
*/
6869
roomId: string | null;
70+
/**
71+
* The ID of the thread currently being viewed
72+
*/
73+
threadId: string | null;
6974
/**
7075
* The ID of the room being subscribed to (in Sliding Sync)
7176
*/
@@ -109,6 +114,7 @@ const INITIAL_STATE: State = {
109114
joining: false,
110115
joinError: null,
111116
roomId: null,
117+
threadId: null,
112118
subscribingRoomId: null,
113119
initialEventId: null,
114120
initialEventPixelOffset: null,
@@ -200,6 +206,9 @@ export class RoomViewStore extends EventEmitter {
200206
case Action.ViewRoom:
201207
this.viewRoom(payload);
202208
break;
209+
case Action.ViewThread:
210+
this.viewThread(payload);
211+
break;
203212
// for these events blank out the roomId as we are no longer in the RoomView
204213
case 'view_welcome_page':
205214
case Action.ViewHomePage:
@@ -430,6 +439,12 @@ export class RoomViewStore extends EventEmitter {
430439
}
431440
}
432441

442+
private viewThread(payload: ThreadPayload): void {
443+
this.setState({
444+
threadId: payload.thread_id,
445+
});
446+
}
447+
433448
private viewRoomError(payload: ViewRoomErrorPayload): void {
434449
this.setState({
435450
roomId: payload.room_id,
@@ -550,6 +565,10 @@ export class RoomViewStore extends EventEmitter {
550565
return this.state.roomId;
551566
}
552567

568+
public getThreadId(): Optional<string> {
569+
return this.state.threadId;
570+
}
571+
553572
// The event to scroll to when the room is first viewed
554573
public getInitialEventId(): Optional<string> {
555574
return this.state.initialEventId;

test/Notifier-test.ts

Lines changed: 98 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
1919
import { Room } from "matrix-js-sdk/src/models/room";
2020
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
2121
import { SyncState } from "matrix-js-sdk/src/sync";
22+
import { waitFor } from "@testing-library/react";
2223

2324
import BasePlatform from "../src/BasePlatform";
2425
import { ElementCall } from "../src/models/Call";
@@ -29,8 +30,15 @@ import {
2930
createLocalNotificationSettingsIfNeeded,
3031
getLocalNotificationAccountDataEventType,
3132
} from "../src/utils/notifications";
32-
import { getMockClientWithEventEmitter, mkEvent, mkRoom, mockClientMethodsUser, mockPlatformPeg } from "./test-utils";
33+
import { getMockClientWithEventEmitter, mkEvent, mockClientMethodsUser, mockPlatformPeg } from "./test-utils";
3334
import { IncomingCallToast } from "../src/toasts/IncomingCallToast";
35+
import { SdkContextClass } from "../src/contexts/SDKContext";
36+
import UserActivity from "../src/UserActivity";
37+
import Modal from "../src/Modal";
38+
import { mkThread } from "./test-utils/threads";
39+
import dis from "../src/dispatcher/dispatcher";
40+
import { ThreadPayload } from "../src/dispatcher/payloads/ThreadPayload";
41+
import { Action } from "../src/dispatcher/actions";
3442

3543
jest.mock("../src/utils/notifications", () => ({
3644
// @ts-ignore
@@ -50,10 +58,12 @@ describe("Notifier", () => {
5058

5159
let MockPlatform: MockedObject<BasePlatform>;
5260
let mockClient: MockedObject<MatrixClient>;
53-
let testRoom: MockedObject<Room>;
61+
let testRoom: Room;
5462
let accountDataEventKey: string;
5563
let accountDataStore = {};
5664

65+
let mockSettings: Record<string, boolean> = {};
66+
5767
const userId = "@bob:example.org";
5868

5969
beforeEach(() => {
@@ -78,7 +88,7 @@ describe("Notifier", () => {
7888
};
7989
accountDataEventKey = getLocalNotificationAccountDataEventType(mockClient.deviceId);
8090

81-
testRoom = mkRoom(mockClient, roomId);
91+
testRoom = new Room(roomId, mockClient, mockClient.getUserId());
8292

8393
MockPlatform = mockPlatformPeg({
8494
supportsNotifications: jest.fn().mockReturnValue(true),
@@ -89,7 +99,9 @@ describe("Notifier", () => {
8999

90100
Notifier.isBodyEnabled = jest.fn().mockReturnValue(true);
91101

92-
mockClient.getRoom.mockReturnValue(testRoom);
102+
mockClient.getRoom.mockImplementation(id => {
103+
return id === roomId ? testRoom : new Room(id, mockClient, mockClient.getUserId());
104+
});
93105
});
94106

95107
describe('triggering notification from events', () => {
@@ -121,13 +133,14 @@ describe("Notifier", () => {
121133
},
122134
});
123135

124-
const enabledSettings = [
125-
'notificationsEnabled',
126-
'audioNotificationsEnabled',
127-
];
136+
mockSettings = {
137+
'notificationsEnabled': true,
138+
'audioNotificationsEnabled': true,
139+
};
140+
128141
// enable notifications by default
129-
jest.spyOn(SettingsStore, "getValue").mockImplementation(
130-
settingName => enabledSettings.includes(settingName),
142+
jest.spyOn(SettingsStore, "getValue").mockReset().mockImplementation(
143+
settingName => mockSettings[settingName] ?? false,
131144
);
132145
});
133146

@@ -253,16 +266,13 @@ describe("Notifier", () => {
253266
});
254267

255268
const callOnEvent = (type?: string) => {
256-
const callEvent = {
257-
getContent: () => { },
258-
getRoomId: () => roomId,
259-
isBeingDecrypted: () => false,
260-
isDecryptionFailure: () => false,
261-
getSender: () => "@alice:foo",
262-
getType: () => type ?? ElementCall.CALL_EVENT_TYPE.name,
263-
getStateKey: () => "state_key",
264-
} as unknown as MatrixEvent;
265-
269+
const callEvent = mkEvent({
270+
type: type ?? ElementCall.CALL_EVENT_TYPE.name,
271+
user: "@alice:foo",
272+
room: roomId,
273+
content: {},
274+
event: true,
275+
});
266276
Notifier.onEvent(callEvent);
267277
return callEvent;
268278
};
@@ -345,4 +355,72 @@ describe("Notifier", () => {
345355
expect(createLocalNotificationSettingsIfNeededMock).toHaveBeenCalled();
346356
});
347357
});
358+
359+
describe('_evaluateEvent', () => {
360+
beforeEach(() => {
361+
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId")
362+
.mockReturnValue(testRoom.roomId);
363+
364+
jest.spyOn(UserActivity.sharedInstance(), "userActiveRecently")
365+
.mockReturnValue(true);
366+
367+
jest.spyOn(Modal, "hasDialogs").mockReturnValue(false);
368+
369+
jest.spyOn(Notifier, "_displayPopupNotification").mockReset();
370+
jest.spyOn(Notifier, "isEnabled").mockReturnValue(true);
371+
372+
mockClient.getPushActionsForEvent.mockReturnValue({
373+
notify: true,
374+
tweaks: {
375+
sound: true,
376+
},
377+
});
378+
});
379+
380+
it("should show a pop-up", () => {
381+
expect(Notifier._displayPopupNotification).toHaveBeenCalledTimes(0);
382+
Notifier._evaluateEvent(testEvent);
383+
expect(Notifier._displayPopupNotification).toHaveBeenCalledTimes(0);
384+
385+
const eventFromOtherRoom = mkEvent({
386+
event: true,
387+
type: "m.room.message",
388+
user: "@user1:server",
389+
room: "!otherroom:example.org",
390+
content: {},
391+
});
392+
393+
Notifier._evaluateEvent(eventFromOtherRoom);
394+
expect(Notifier._displayPopupNotification).toHaveBeenCalledTimes(1);
395+
});
396+
397+
it("should a pop-up for thread event", async () => {
398+
const { events, rootEvent } = mkThread({
399+
room: testRoom,
400+
client: mockClient,
401+
authorId: "@bob:example.org",
402+
participantUserIds: ["@bob:example.org"],
403+
});
404+
405+
expect(Notifier._displayPopupNotification).toHaveBeenCalledTimes(0);
406+
407+
Notifier._evaluateEvent(rootEvent);
408+
expect(Notifier._displayPopupNotification).toHaveBeenCalledTimes(0);
409+
410+
Notifier._evaluateEvent(events[1]);
411+
expect(Notifier._displayPopupNotification).toHaveBeenCalledTimes(1);
412+
413+
dis.dispatch<ThreadPayload>({
414+
action: Action.ViewThread,
415+
thread_id: rootEvent.getId(),
416+
});
417+
418+
await waitFor(() =>
419+
expect(SdkContextClass.instance.roomViewStore.getThreadId()).toBe(rootEvent.getId()),
420+
);
421+
422+
Notifier._evaluateEvent(events[1]);
423+
expect(Notifier._displayPopupNotification).toHaveBeenCalledTimes(1);
424+
});
425+
});
348426
});

test/components/structures/ThreadView-test.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { act } from "react-dom/test-utils";
2828
import ThreadView from "../../../src/components/structures/ThreadView";
2929
import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
3030
import RoomContext from "../../../src/contexts/RoomContext";
31+
import { SdkContextClass } from "../../../src/contexts/SDKContext";
3132
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
3233
import DMRoomMap from "../../../src/utils/DMRoomMap";
3334
import ResizeNotifier from "../../../src/utils/ResizeNotifier";
@@ -155,4 +156,13 @@ describe("ThreadView", () => {
155156
ROOM_ID, rootEvent2.getId(), expectedMessageBody(rootEvent2, "yolo"),
156157
);
157158
});
159+
160+
it("sets the correct thread in the room view store", async () => {
161+
// expect(SdkContextClass.instance.roomViewStore.getThreadId()).toBeNull();
162+
const { unmount } = await getComponent();
163+
expect(SdkContextClass.instance.roomViewStore.getThreadId()).toBe(rootEvent.getId());
164+
165+
unmount();
166+
await waitFor(() => expect(SdkContextClass.instance.roomViewStore.getThreadId()).toBeNull());
167+
});
158168
});

0 commit comments

Comments
 (0)