mirror of
https://github.com/element-hq/element-web
synced 2024-11-25 18:55:58 +03:00
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 <7t3chguy@gmail.com> * Fix show desktop notifications * Add RoomViewStore thread id assertions * Add Notifier tests * fix lint * Remove it.only Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
d273441596
commit
306a2449e5
7 changed files with 178 additions and 21 deletions
|
@ -435,7 +435,16 @@ export const Notifier = {
|
|||
if (actions?.notify) {
|
||||
this._performCustomEventHandling(ev);
|
||||
|
||||
if (SdkContextClass.instance.roomViewStore.getRoomId() === room.roomId &&
|
||||
const store = SdkContextClass.instance.roomViewStore;
|
||||
const isViewingRoom = store.getRoomId() === room.roomId;
|
||||
const threadId: string | undefined = ev.getId() !== ev.threadRootId
|
||||
? ev.threadRootId
|
||||
: undefined;
|
||||
const isViewingThread = store.getThreadId() === threadId;
|
||||
|
||||
const isViewingEventTimeline = isViewingRoom && (!threadId || isViewingThread);
|
||||
|
||||
if (isViewingEventTimeline &&
|
||||
UserActivity.sharedInstance().userActiveRecently() &&
|
||||
!Modal.hasDialogs()
|
||||
) {
|
||||
|
|
|
@ -55,6 +55,7 @@ import Spinner from "../views/elements/Spinner";
|
|||
import { ComposerInsertPayload, ComposerType } from "../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import Heading from '../views/typography/Heading';
|
||||
import { SdkContextClass } from '../../contexts/SDKContext';
|
||||
import { ThreadPayload } from '../../dispatcher/payloads/ThreadPayload';
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -132,6 +133,11 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
metricsTrigger: undefined, // room doesn't change
|
||||
});
|
||||
}
|
||||
|
||||
dis.dispatch<ThreadPayload>({
|
||||
action: Action.ViewThread,
|
||||
thread_id: null,
|
||||
});
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps) {
|
||||
|
@ -225,6 +231,10 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private async postThreadUpdate(thread: Thread): Promise<void> {
|
||||
dis.dispatch<ThreadPayload>({
|
||||
action: Action.ViewThread,
|
||||
thread_id: thread.id,
|
||||
});
|
||||
thread.emit(ThreadEvent.ViewThread);
|
||||
await thread.fetchInitialEvents();
|
||||
this.updateThreadRelation();
|
||||
|
|
|
@ -116,6 +116,11 @@ export enum Action {
|
|||
*/
|
||||
ViewRoom = "view_room",
|
||||
|
||||
/**
|
||||
* Changes thread based on payload parameters. Should be used with ThreadPayload.
|
||||
*/
|
||||
ViewThread = "view_thread",
|
||||
|
||||
/**
|
||||
* Changes room based on room list order and payload parameters. Should be used with ViewRoomDeltaPayload.
|
||||
*/
|
||||
|
|
26
src/dispatcher/payloads/ThreadPayload.ts
Normal file
26
src/dispatcher/payloads/ThreadPayload.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
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 { ActionPayload } from "../payloads";
|
||||
import { Action } from "../actions";
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
export interface ThreadPayload extends Pick<ActionPayload, "action"> {
|
||||
action: Action.ViewThread;
|
||||
|
||||
thread_id: string | null;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
|
@ -50,6 +50,7 @@ import { awaitRoomDownSync } from "../utils/RoomUpgrade";
|
|||
import { UPDATE_EVENT } from "./AsyncStore";
|
||||
import { SdkContextClass } from "../contexts/SDKContext";
|
||||
import { CallStore } from "./CallStore";
|
||||
import { ThreadPayload } from "../dispatcher/payloads/ThreadPayload";
|
||||
|
||||
const NUM_JOIN_RETRY = 5;
|
||||
|
||||
|
@ -66,6 +67,10 @@ interface State {
|
|||
* The ID of the room currently being viewed
|
||||
*/
|
||||
roomId: string | null;
|
||||
/**
|
||||
* The ID of the thread currently being viewed
|
||||
*/
|
||||
threadId: string | null;
|
||||
/**
|
||||
* The ID of the room being subscribed to (in Sliding Sync)
|
||||
*/
|
||||
|
@ -109,6 +114,7 @@ const INITIAL_STATE: State = {
|
|||
joining: false,
|
||||
joinError: null,
|
||||
roomId: null,
|
||||
threadId: null,
|
||||
subscribingRoomId: null,
|
||||
initialEventId: null,
|
||||
initialEventPixelOffset: null,
|
||||
|
@ -200,6 +206,9 @@ export class RoomViewStore extends EventEmitter {
|
|||
case Action.ViewRoom:
|
||||
this.viewRoom(payload);
|
||||
break;
|
||||
case Action.ViewThread:
|
||||
this.viewThread(payload);
|
||||
break;
|
||||
// for these events blank out the roomId as we are no longer in the RoomView
|
||||
case 'view_welcome_page':
|
||||
case Action.ViewHomePage:
|
||||
|
@ -430,6 +439,12 @@ export class RoomViewStore extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
private viewThread(payload: ThreadPayload): void {
|
||||
this.setState({
|
||||
threadId: payload.thread_id,
|
||||
});
|
||||
}
|
||||
|
||||
private viewRoomError(payload: ViewRoomErrorPayload): void {
|
||||
this.setState({
|
||||
roomId: payload.room_id,
|
||||
|
@ -550,6 +565,10 @@ export class RoomViewStore extends EventEmitter {
|
|||
return this.state.roomId;
|
||||
}
|
||||
|
||||
public getThreadId(): Optional<string> {
|
||||
return this.state.threadId;
|
||||
}
|
||||
|
||||
// The event to scroll to when the room is first viewed
|
||||
public getInitialEventId(): Optional<string> {
|
||||
return this.state.initialEventId;
|
||||
|
|
|
@ -19,6 +19,7 @@ import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
|
|||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { SyncState } from "matrix-js-sdk/src/sync";
|
||||
import { waitFor } from "@testing-library/react";
|
||||
|
||||
import BasePlatform from "../src/BasePlatform";
|
||||
import { ElementCall } from "../src/models/Call";
|
||||
|
@ -29,8 +30,15 @@ import {
|
|||
createLocalNotificationSettingsIfNeeded,
|
||||
getLocalNotificationAccountDataEventType,
|
||||
} from "../src/utils/notifications";
|
||||
import { getMockClientWithEventEmitter, mkEvent, mkRoom, mockClientMethodsUser, mockPlatformPeg } from "./test-utils";
|
||||
import { getMockClientWithEventEmitter, mkEvent, mockClientMethodsUser, mockPlatformPeg } from "./test-utils";
|
||||
import { IncomingCallToast } from "../src/toasts/IncomingCallToast";
|
||||
import { SdkContextClass } from "../src/contexts/SDKContext";
|
||||
import UserActivity from "../src/UserActivity";
|
||||
import Modal from "../src/Modal";
|
||||
import { mkThread } from "./test-utils/threads";
|
||||
import dis from "../src/dispatcher/dispatcher";
|
||||
import { ThreadPayload } from "../src/dispatcher/payloads/ThreadPayload";
|
||||
import { Action } from "../src/dispatcher/actions";
|
||||
|
||||
jest.mock("../src/utils/notifications", () => ({
|
||||
// @ts-ignore
|
||||
|
@ -50,10 +58,12 @@ describe("Notifier", () => {
|
|||
|
||||
let MockPlatform: MockedObject<BasePlatform>;
|
||||
let mockClient: MockedObject<MatrixClient>;
|
||||
let testRoom: MockedObject<Room>;
|
||||
let testRoom: Room;
|
||||
let accountDataEventKey: string;
|
||||
let accountDataStore = {};
|
||||
|
||||
let mockSettings: Record<string, boolean> = {};
|
||||
|
||||
const userId = "@bob:example.org";
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -78,7 +88,7 @@ describe("Notifier", () => {
|
|||
};
|
||||
accountDataEventKey = getLocalNotificationAccountDataEventType(mockClient.deviceId);
|
||||
|
||||
testRoom = mkRoom(mockClient, roomId);
|
||||
testRoom = new Room(roomId, mockClient, mockClient.getUserId());
|
||||
|
||||
MockPlatform = mockPlatformPeg({
|
||||
supportsNotifications: jest.fn().mockReturnValue(true),
|
||||
|
@ -89,7 +99,9 @@ describe("Notifier", () => {
|
|||
|
||||
Notifier.isBodyEnabled = jest.fn().mockReturnValue(true);
|
||||
|
||||
mockClient.getRoom.mockReturnValue(testRoom);
|
||||
mockClient.getRoom.mockImplementation(id => {
|
||||
return id === roomId ? testRoom : new Room(id, mockClient, mockClient.getUserId());
|
||||
});
|
||||
});
|
||||
|
||||
describe('triggering notification from events', () => {
|
||||
|
@ -121,13 +133,14 @@ describe("Notifier", () => {
|
|||
},
|
||||
});
|
||||
|
||||
const enabledSettings = [
|
||||
'notificationsEnabled',
|
||||
'audioNotificationsEnabled',
|
||||
];
|
||||
mockSettings = {
|
||||
'notificationsEnabled': true,
|
||||
'audioNotificationsEnabled': true,
|
||||
};
|
||||
|
||||
// enable notifications by default
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation(
|
||||
settingName => enabledSettings.includes(settingName),
|
||||
jest.spyOn(SettingsStore, "getValue").mockReset().mockImplementation(
|
||||
settingName => mockSettings[settingName] ?? false,
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -253,16 +266,13 @@ describe("Notifier", () => {
|
|||
});
|
||||
|
||||
const callOnEvent = (type?: string) => {
|
||||
const callEvent = {
|
||||
getContent: () => { },
|
||||
getRoomId: () => roomId,
|
||||
isBeingDecrypted: () => false,
|
||||
isDecryptionFailure: () => false,
|
||||
getSender: () => "@alice:foo",
|
||||
getType: () => type ?? ElementCall.CALL_EVENT_TYPE.name,
|
||||
getStateKey: () => "state_key",
|
||||
} as unknown as MatrixEvent;
|
||||
|
||||
const callEvent = mkEvent({
|
||||
type: type ?? ElementCall.CALL_EVENT_TYPE.name,
|
||||
user: "@alice:foo",
|
||||
room: roomId,
|
||||
content: {},
|
||||
event: true,
|
||||
});
|
||||
Notifier.onEvent(callEvent);
|
||||
return callEvent;
|
||||
};
|
||||
|
@ -345,4 +355,72 @@ describe("Notifier", () => {
|
|||
expect(createLocalNotificationSettingsIfNeededMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('_evaluateEvent', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId")
|
||||
.mockReturnValue(testRoom.roomId);
|
||||
|
||||
jest.spyOn(UserActivity.sharedInstance(), "userActiveRecently")
|
||||
.mockReturnValue(true);
|
||||
|
||||
jest.spyOn(Modal, "hasDialogs").mockReturnValue(false);
|
||||
|
||||
jest.spyOn(Notifier, "_displayPopupNotification").mockReset();
|
||||
jest.spyOn(Notifier, "isEnabled").mockReturnValue(true);
|
||||
|
||||
mockClient.getPushActionsForEvent.mockReturnValue({
|
||||
notify: true,
|
||||
tweaks: {
|
||||
sound: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should show a pop-up", () => {
|
||||
expect(Notifier._displayPopupNotification).toHaveBeenCalledTimes(0);
|
||||
Notifier._evaluateEvent(testEvent);
|
||||
expect(Notifier._displayPopupNotification).toHaveBeenCalledTimes(0);
|
||||
|
||||
const eventFromOtherRoom = mkEvent({
|
||||
event: true,
|
||||
type: "m.room.message",
|
||||
user: "@user1:server",
|
||||
room: "!otherroom:example.org",
|
||||
content: {},
|
||||
});
|
||||
|
||||
Notifier._evaluateEvent(eventFromOtherRoom);
|
||||
expect(Notifier._displayPopupNotification).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should a pop-up for thread event", async () => {
|
||||
const { events, rootEvent } = mkThread({
|
||||
room: testRoom,
|
||||
client: mockClient,
|
||||
authorId: "@bob:example.org",
|
||||
participantUserIds: ["@bob:example.org"],
|
||||
});
|
||||
|
||||
expect(Notifier._displayPopupNotification).toHaveBeenCalledTimes(0);
|
||||
|
||||
Notifier._evaluateEvent(rootEvent);
|
||||
expect(Notifier._displayPopupNotification).toHaveBeenCalledTimes(0);
|
||||
|
||||
Notifier._evaluateEvent(events[1]);
|
||||
expect(Notifier._displayPopupNotification).toHaveBeenCalledTimes(1);
|
||||
|
||||
dis.dispatch<ThreadPayload>({
|
||||
action: Action.ViewThread,
|
||||
thread_id: rootEvent.getId(),
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(SdkContextClass.instance.roomViewStore.getThreadId()).toBe(rootEvent.getId()),
|
||||
);
|
||||
|
||||
Notifier._evaluateEvent(events[1]);
|
||||
expect(Notifier._displayPopupNotification).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -28,6 +28,7 @@ import { act } from "react-dom/test-utils";
|
|||
import ThreadView from "../../../src/components/structures/ThreadView";
|
||||
import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
|
||||
import RoomContext from "../../../src/contexts/RoomContext";
|
||||
import { SdkContextClass } from "../../../src/contexts/SDKContext";
|
||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||
import DMRoomMap from "../../../src/utils/DMRoomMap";
|
||||
import ResizeNotifier from "../../../src/utils/ResizeNotifier";
|
||||
|
@ -155,4 +156,13 @@ describe("ThreadView", () => {
|
|||
ROOM_ID, rootEvent2.getId(), expectedMessageBody(rootEvent2, "yolo"),
|
||||
);
|
||||
});
|
||||
|
||||
it("sets the correct thread in the room view store", async () => {
|
||||
// expect(SdkContextClass.instance.roomViewStore.getThreadId()).toBeNull();
|
||||
const { unmount } = await getComponent();
|
||||
expect(SdkContextClass.instance.roomViewStore.getThreadId()).toBe(rootEvent.getId());
|
||||
|
||||
unmount();
|
||||
await waitFor(() => expect(SdkContextClass.instance.roomViewStore.getThreadId()).toBeNull());
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue