From 889b0cebb2ad799bec5251156e9e23faa7366e03 Mon Sep 17 00:00:00 2001 From: Kerry Date: Thu, 24 Feb 2022 09:44:34 +0100 Subject: [PATCH] Fix 'my threads' filtering to include participated threads (#7882) * move js utils into directory Signed-off-by: Kerry Archibald * typescripterize js test-utils Signed-off-by: Kerry Archibald * move test utils to directory Signed-off-by: Kerry Archibald * move remaining mock functions to directory Signed-off-by: Kerry Archibald * update imports Signed-off-by: Kerry Archibald * missed copyright Signed-off-by: Kerry Archibald * threads test helpers Signed-off-by: Kerry Archibald * forgotten copyright Signed-off-by: Kerry Archibald * comments Signed-off-by: Kerry Archibald * fix threads helper unsigned Signed-off-by: Kerry Archibald * test filter creation when thread capabilities enabled Signed-off-by: Kerry Archibald --- src/components/structures/ThreadPanel.tsx | 8 +- .../structures/ThreadPanel-test.tsx | 120 ++++++++++++++++++ test/test-utils/threads.ts | 21 +-- 3 files changed, 135 insertions(+), 14 deletions(-) diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index d10b7c8fca..ec8d4c9cae 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -39,7 +39,7 @@ import { Layout } from '../../settings/enums/Layout'; import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks'; import Measured from '../views/elements/Measured'; -async function getThreadTimelineSet( +export async function getThreadTimelineSet( client: MatrixClient, room: Room, filterType = ThreadFilterType.All, @@ -89,14 +89,12 @@ async function getThreadTimelineSet( Array.from(room.threads) .sort(([, threadA], [, threadB]) => threadA.replyToEvent.getTs() - threadB.replyToEvent.getTs()) .forEach(([, thread]) => { - const isOwnEvent = thread.rootEvent.getSender() === client.getUserId(); - if (filterType !== ThreadFilterType.My || isOwnEvent) { + const currentUserParticipated = thread.events.some(event => event.getSender() === client.getUserId()); + if (filterType !== ThreadFilterType.My || currentUserParticipated) { timelineSet.getLiveTimeline().addEvent(thread.rootEvent, false); } }); - // for (const [, thread] of room.threads) { - // } return timelineSet; } } diff --git a/test/components/structures/ThreadPanel-test.tsx b/test/components/structures/ThreadPanel-test.tsx index 7dc50ebf24..ebdbaf1bc5 100644 --- a/test/components/structures/ThreadPanel-test.tsx +++ b/test/components/structures/ThreadPanel-test.tsx @@ -16,22 +16,33 @@ limitations under the License. import React from 'react'; import { shallow, mount } from "enzyme"; +import { + MatrixClient, + RelationType, + Room, + UNSTABLE_FILTER_RELATION_SENDERS, + UNSTABLE_FILTER_RELATION_TYPES, +} from 'matrix-js-sdk'; +import { mocked } from 'jest-mock'; import '../../skinned-sdk'; import { ThreadFilterType, ThreadPanelHeader, ThreadPanelHeaderFilterOptionItem, + getThreadTimelineSet, } from '../../../src/components/structures/ThreadPanel'; import { ContextMenuButton } from '../../../src/accessibility/context_menu/ContextMenuButton'; import ContextMenu from '../../../src/components/structures/ContextMenu'; import { _t } from '../../../src/languageHandler'; +import { makeThread } from '../../test-utils/threads'; describe('ThreadPanel', () => { describe('Header', () => { it('expect that All filter for ThreadPanelHeader properly renders Show: All threads', () => { const wrapper = shallow( undefined} />, ); @@ -41,6 +52,7 @@ describe('ThreadPanel', () => { it('expect that My filter for ThreadPanelHeader properly renders Show: My threads', () => { const wrapper = shallow( undefined} />, ); @@ -50,6 +62,7 @@ describe('ThreadPanel', () => { it('expect that ThreadPanelHeader properly opens a context menu when clicked on the button', () => { const wrapper = mount( undefined} />, ); @@ -64,6 +77,7 @@ describe('ThreadPanel', () => { it('expect that ThreadPanelHeader has the correct option selected in the context menu', () => { const wrapper = mount( undefined} />, ); @@ -75,4 +89,110 @@ describe('ThreadPanel', () => { expect(foundButton).toMatchSnapshot(); }); }); + + describe('getThreadTimelineSet()', () => { + const filterId = '123'; + const client = { + getUserId: jest.fn(), + getCapabilities: jest.fn().mockResolvedValue({}), + decryptEventIfNeeded: jest.fn().mockResolvedValue(undefined), + getOrCreateFilter: jest.fn().mockResolvedValue(filterId), + paginateEventTimeline: jest.fn().mockResolvedValue(undefined), + } as unknown as MatrixClient; + + const aliceId = '@alice:server.org'; + const bobId = '@bob:server.org'; + const charlieId = '@charlie:server.org'; + const room = new Room('!room1:server.org', client, aliceId); + + const roomWithThreads = new Room('!room2:server.org', client, aliceId); + const aliceAndBobThread = makeThread(client, roomWithThreads, { + authorId: aliceId, + participantUserIds: [aliceId, bobId], + roomId: roomWithThreads.roomId, + }); + const justBobThread = makeThread(client, roomWithThreads, { + authorId: bobId, + participantUserIds: [bobId], + roomId: roomWithThreads.roomId, + }); + const everyoneThread = makeThread(client, roomWithThreads, { + authorId: charlieId, + participantUserIds: [aliceId, bobId, charlieId], + length: 5, + roomId: roomWithThreads.roomId, + }); + roomWithThreads.threads.set(aliceAndBobThread.id, aliceAndBobThread); + roomWithThreads.threads.set(justBobThread.id, justBobThread); + roomWithThreads.threads.set(everyoneThread.id, everyoneThread); + + beforeEach(() => { + mocked(client.getUserId).mockReturnValue(aliceId); + mocked(client.getCapabilities).mockResolvedValue({}); + }); + + describe('when extra capabilities are not enabled on server', () => { + it('returns an empty timelineset when room has no threads', async () => { + const result = await getThreadTimelineSet(client, room); + + expect(result.getLiveTimeline().getEvents()).toEqual([]); + }); + + it('returns a timelineset with thread root events for room when filter is All', async () => { + const result = await getThreadTimelineSet(client, roomWithThreads); + + const resultEvents = result.getLiveTimeline().getEvents(); + expect(resultEvents.length).toEqual(3); + expect(resultEvents).toEqual(expect.arrayContaining([ + justBobThread.rootEvent, + aliceAndBobThread.rootEvent, + everyoneThread.rootEvent, + ])); + }); + + it('returns a timelineset with threads user has participated in when filter is My', async () => { + // current user is alice + mocked(client).getUserId.mockReturnValue(aliceId); + + const result = await getThreadTimelineSet(client, roomWithThreads, ThreadFilterType.My); + const resultEvents = result.getLiveTimeline().getEvents(); + expect(resultEvents).toEqual(expect.arrayContaining([ + // alice authored root event + aliceAndBobThread.rootEvent, + // alive replied to this thread + everyoneThread.rootEvent, + ])); + }); + }); + + describe('when extra capabilities are enabled on server', () => { + beforeEach(() => { + jest.clearAllMocks(); + mocked(client.getCapabilities).mockResolvedValue({ + ['io.element.thread']: { enabled: true }, + }); + }); + + it('creates a filter with correct definition when filterType is All', async () => { + await getThreadTimelineSet(client, room); + + const [filterKey, filter] = mocked(client).getOrCreateFilter.mock.calls[0]; + expect(filterKey).toEqual(`THREAD_PANEL_${room.roomId}_${ThreadFilterType.All}`); + expect(filter.getDefinition().room.timeline).toEqual({ + [UNSTABLE_FILTER_RELATION_TYPES.name]: [RelationType.Thread], + }); + }); + + it('creates a filter with correct definition when filterType is My', async () => { + await getThreadTimelineSet(client, room, ThreadFilterType.My); + + const [filterKey, filter] = mocked(client).getOrCreateFilter.mock.calls[0]; + expect(filterKey).toEqual(`THREAD_PANEL_${room.roomId}_${ThreadFilterType.My}`); + expect(filter.getDefinition().room.timeline).toEqual({ + [UNSTABLE_FILTER_RELATION_TYPES.name]: [RelationType.Thread], + [UNSTABLE_FILTER_RELATION_SENDERS.name]: [aliceId], + }); + }); + }); + }); }); diff --git a/test/test-utils/threads.ts b/test/test-utils/threads.ts index 1d250d07e9..1fa2e3a70b 100644 --- a/test/test-utils/threads.ts +++ b/test/test-utils/threads.ts @@ -1,4 +1,3 @@ - /* Copyright 2022 The Matrix.org Foundation C.I.C. @@ -15,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IUnsigned, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk"; +import { MatrixClient, MatrixEvent, RelationType, Room } from "matrix-js-sdk"; import { Thread } from "matrix-js-sdk/src/models/thread"; import { mkMessage, MessageEventProps } from "./test-utils"; @@ -26,7 +25,7 @@ export const makeThreadEvent = ({ rootEventId, replyToEventId, ...props }: Messa ...props, relatesTo: { event_id: rootEventId, - rel_type: "io.element.thread", + rel_type: RelationType.Thread, ['m.in_reply_to']: { event_id: replyToEventId, }, @@ -50,12 +49,12 @@ type MakeThreadEventsProps = { export const makeThreadEvents = ({ roomId, authorId, participantUserIds, length = 2, ts = 1, currentUserId, -}): { rootEvent: MatrixEvent, events: MatrixEvent[] } => { +}: MakeThreadEventsProps): { rootEvent: MatrixEvent, events: MatrixEvent[] } => { const rootEvent = mkMessage({ user: authorId, event: true, room: roomId, - msg: 'root event message', + msg: 'root event message ' + Math.random(), ts, }); @@ -79,10 +78,14 @@ export const makeThreadEvents = ({ } rootEvent.setUnsigned({ - latest_event: events[events.length - 1], - count: length, - current_user_participated: [...participantUserIds, authorId].includes(currentUserId), - } as IUnsigned); + "m.relations": { + [RelationType.Thread]: { + latest_event: events[events.length - 1], + count: length, + current_user_participated: [...participantUserIds, authorId].includes(currentUserId), + }, + }, + }); return { rootEvent, events }; };