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

Pop out of Threads Activity Centre #12136

Merged
merged 75 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from 74 commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
b317093
Add `Thread Activity centre` labs flag
florianduros Jan 3, 2024
a07196b
Rename translation string
florianduros Jan 4, 2024
8f633bd
WIP Thread Activity Centre
florianduros Jan 11, 2024
c241a1a
Merge branch 'develop' into florianduros/threads/pop-out-panel
florianduros Jan 11, 2024
927eeb6
Merge branch 'develop' into florianduros/threads/labs-flag
florianduros Jan 11, 2024
007c4f5
Update supportedLevels
florianduros Jan 12, 2024
7f9a9d9
Merge branch 'develop' into florianduros/threads/labs-flag
florianduros Jan 12, 2024
2d40d2c
Merge branch 'florianduros/threads/labs-flag' into florianduros/threa…
florianduros Jan 12, 2024
f04f591
css lint
florianduros Jan 12, 2024
6115607
i18n lint
florianduros Jan 12, 2024
c83559b
Fix labs subsection test
florianduros Jan 12, 2024
531e10a
Merge branch 'florianduros/threads/labs-flag' into florianduros/threa…
florianduros Jan 12, 2024
b226a08
Update Threads Activity Centre label
florianduros Jan 15, 2024
94ace6f
Merge branch 'florianduros/threads/labs-flag' into florianduros/threa…
florianduros Jan 15, 2024
3734924
Merge branch 'develop' into florianduros/threads/labs-flag
florianduros Jan 15, 2024
aefe979
Rename Thread Activity Centre to Threads Activity Centre
florianduros Jan 15, 2024
b7f43af
Merge branch 'develop' into florianduros/threads/pop-out-panel
florianduros Jan 15, 2024
8a5d875
Use compound `MenuItem` instead of custom button
florianduros Jan 16, 2024
84d371f
Color thread icon when hovered
florianduros Jan 16, 2024
b120df6
Make the pop-up scrollable and add a max height
florianduros Jan 17, 2024
27d21b6
Remove Math.random in key
florianduros Jan 17, 2024
0943a1c
Merge branch 'develop' into florianduros/threads/pop-out-panel
florianduros Jan 17, 2024
c06a6ca
Remove unused class
florianduros Jan 17, 2024
eca98b8
Change add comments on `mx_ThreadsActivityRows` and `mx_ThreadsActivi…
florianduros Jan 17, 2024
5b854dc
Make threads activity centre labs flag split out unread counts
dbkr Jan 18, 2024
ba9ad07
Fix tests
dbkr Jan 18, 2024
f2dc6f9
Simpler fix
dbkr Jan 18, 2024
141cb35
Open thread panel when thread clicke in Threads Activity Centre
dbkr Jan 19, 2024
662c946
Merge pull request #12161 from matrix-org/dbkr/open_threadpanel
dbkr Jan 19, 2024
dc954c5
Dynamic state of room
florianduros Jan 23, 2024
1e4b378
Merge pull request #12156 from matrix-org/dbkr/split_unread_counts
dbkr Jan 23, 2024
7a72dd8
Merge branch 'develop' into florianduros/threads/labs-flag
florianduros Jan 23, 2024
f30e62d
Merge branch 'florianduros/threads/labs-flag' into florianduros/threa…
florianduros Jan 23, 2024
3478500
Merge branch 'develop' into florianduros/threads/pop-out-panel
dbkr Jan 23, 2024
c66da31
Merge branch 'develop' into florianduros/threads/labs-flag
florianduros Jan 23, 2024
4a9619b
Add doc
florianduros Jan 23, 2024
9db452c
Use the StatelessNotificationBadge component in ThreadsActivityCentre
dbkr Jan 23, 2024
8d803b3
Remove unused style
dbkr Jan 23, 2024
09d2691
Merge pull request #12167 from matrix-org/dbkr/use_stateless_notifica…
dbkr Jan 23, 2024
3279050
Merge branch 'develop' into florianduros/threads/labs-flag
florianduros Jan 24, 2024
777e7f2
Add room sorting
florianduros Jan 24, 2024
d33ffbf
Fix `ThreadsActivityRow` props doc
florianduros Jan 24, 2024
c6683d2
Pass in & cache the status of the TAC labs flag
dbkr Jan 24, 2024
67550c1
Pass includeThreads as setting to doesRoomHaveUnreadMessages too
dbkr Jan 24, 2024
c9eccdc
Fix tests
dbkr Jan 24, 2024
d56b703
Merge branch 'develop' into florianduros/threads/pop-out-panel
dbkr Jan 25, 2024
2802f7a
Add analytics to the TAC (#12179)
florianduros Jan 25, 2024
4e5ad83
Merge branch 'florianduros/threads/labs-flag' into florianduros/threa…
dbkr Jan 26, 2024
ac236a6
Merge branch 'develop' into florianduros/threads/pop-out-panel
florianduros Jan 29, 2024
b893eb7
Merge branch 'develop' into florianduros/threads/pop-out-panel
florianduros Jan 29, 2024
49b4aee
Update TAC label (#12186)
florianduros Jan 30, 2024
ccda3bc
Add `IndicatorIcon` to the TAC button (#12182)
florianduros Jan 30, 2024
1ea0291
Threads don't have activity if the room is muted
dbkr Jan 30, 2024
daebac6
Merge pull request #12192 from matrix-org/dbkr/honour_muted_unread
dbkr Jan 31, 2024
41d392f
Re-use doesRoomHaveUnreadThreads for useRoomThreadNotifications
dbkr Jan 31, 2024
6872cc4
Merge pull request #12195 from matrix-org/dbkr/reuse_unread_threads
dbkr Jan 31, 2024
872af63
Add TAC description in labs (#12197)
florianduros Jan 31, 2024
7e94ab0
Fox position & size of dot on the tac button
dbkr Jan 31, 2024
97c92dd
Merge branch 'develop' into florianduros/threads/pop-out-panel
florianduros Jan 31, 2024
19c9fcf
Merge pull request #12199 from matrix-org/dbkr/fix_tac_dot_position
dbkr Jan 31, 2024
48b4a04
TAC: Utils tests (#12200)
florianduros Feb 1, 2024
a5d9c33
Add test for the ThreadsActivityCentre component
dbkr Feb 1, 2024
6b0e759
Add snapshot test
dbkr Feb 2, 2024
3dc60a6
Fix narrow hover background on TAC button
dbkr Feb 2, 2024
7fb8349
Merge pull request #12212 from matrix-org/dbkr/tac_fix_narrow_hover
dbkr Feb 2, 2024
f9608c0
Merge pull request #12207 from matrix-org/dbkr/tac_test
dbkr Feb 2, 2024
a5f2876
Merge branch 'develop' into florianduros/threads/pop-out-panel
florianduros Feb 2, 2024
b934950
Add caption for empty TAC
dbkr Feb 5, 2024
f144a8f
s/tac/threads_activity_centre/
dbkr Feb 5, 2024
9c024c1
Fix i18n & add tests
dbkr Feb 5, 2024
f63abc3
Merge pull request #12226 from matrix-org/dbkr/tac_empty_caption
dbkr Feb 6, 2024
5b70511
Merge branch 'develop' into florianduros/threads/pop-out-panel
florianduros Feb 6, 2024
c97e9e9
Merge branch 'develop' into florianduros/threads/pop-out-panel
florianduros Feb 7, 2024
2ad891b
Add playwright tests for the TAC (#12227)
florianduros Feb 7, 2024
d8d7217
Fox comments
florianduros Feb 7, 2024
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
},
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/analytics-events": "^0.9.0",
"@matrix-org/analytics-events": "^0.10.0",
"@matrix-org/emojibase-bindings": "^1.1.2",
"@matrix-org/matrix-wysiwyg": "2.17.0",
"@matrix-org/olm": "3.2.15",
Expand Down
355 changes: 355 additions & 0 deletions playwright/e2e/spaces/threads-activity-centre/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,355 @@
/*
Copyright 2024 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 { JSHandle, Locator, Page } from "@playwright/test";

import type { MatrixEvent, IContent, Room } from "matrix-js-sdk/src/matrix";
import { test as base, expect } from "../../../element-web-test";
import { Bot } from "../../../pages/bot";
import { Client } from "../../../pages/client";
import { ElementAppPage } from "../../../pages/ElementAppPage";

/**
* Set up for a read receipt test:
* - Create a user with the supplied name
* - As that user, create two rooms with the supplied names
* - Create a bot with the supplied name
* - Invite the bot to both rooms and ensure that it has joined
*/
export const test = base.extend<{
roomAlphaName?: string;
roomAlpha: { name: string; roomId: string };
roomBetaName?: string;
roomBeta: { name: string; roomId: string };
msg: MessageBuilder;
util: Helpers;
}>({
displayName: "Mae",
botCreateOpts: { displayName: "Other User" },

roomAlphaName: "Room Alpha",
roomAlpha: async ({ roomAlphaName: name, app, user, bot }, use) => {
const roomId = await app.client.createRoom({ name, invite: [bot.credentials.userId] });
await use({ name, roomId });
},
roomBetaName: "Room Beta",
roomBeta: async ({ roomBetaName: name, app, user, bot }, use) => {
const roomId = await app.client.createRoom({ name, invite: [bot.credentials.userId] });
await use({ name, roomId });
},
msg: async ({ page, app, util }, use) => {
await use(new MessageBuilder(page, app, util));
},
util: async ({ roomAlpha, roomBeta, page, app, bot }, use) => {
await use(new Helpers(page, app, bot));
},
});

/**
* A utility that is able to find messages based on their content, by looking
* inside the `timeline` objects in the object model.
*
* Crucially, we hold on to references to events that have been edited or
* redacted, so we can still look them up by their old content.
*
* Provides utilities that build on the ability to find messages, e.g. replyTo,
* which finds a message and then constructs a reply to it.
*/
export class MessageBuilder {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is basically a lighter version of the read-receipt MessageBuilder.
Copy paste, added mention in thread and removed all the unused parts.

constructor(
private page: Page,
private app: ElementAppPage,
private helpers: Helpers,
) {}

/**
* Map of message content -> event.
*/
messages = new Map<String, Promise<JSHandle<MatrixEvent>>>();

/**
* Utility to find a MatrixEvent by its body content
* @param room - the room to search for the event in
* @param message - the body of the event to search for
* @param includeThreads - whether to search within threads too
*/
async getMessage(room: JSHandle<Room>, message: string, includeThreads = false): Promise<JSHandle<MatrixEvent>> {
const cached = this.messages.get(message);
if (cached) {
return cached;
}

const promise = room.evaluateHandle(
async (room, { message, includeThreads }) => {
let ev = room.timeline.find((e) => e.getContent().body === message);
if (!ev && includeThreads) {
for (const thread of room.getThreads()) {
ev = thread.timeline.find((e) => e.getContent().body === message);
if (ev) break;
}
}

if (ev) return ev;

return new Promise<MatrixEvent>((resolve) => {
room.on("Room.timeline" as any, (ev: MatrixEvent) => {
if (ev.getContent().body === message) {
resolve(ev);
}
});
});
},
{ message, includeThreads },
);

this.messages.set(message, promise);
return promise;
}

/**
* MessageContentSpec to send a threaded response into a room
* @param rootMessage - the body of the thread root message to send a response to
* @param newMessage - the message body to send into the thread response or an object with the message content
*/
threadedOff(rootMessage: string, newMessage: string | IContent): MessageContentSpec {
return new (class extends MessageContentSpec {
public async getContent(room: JSHandle<Room>): Promise<Record<string, unknown>> {
const ev = await this.messageFinder.getMessage(room, rootMessage);
return ev.evaluate((ev, newMessage) => {
if (typeof newMessage === "string") {
return {
"msgtype": "m.text",
"body": newMessage,
"m.relates_to": {
event_id: ev.getId(),
is_falling_back: true,
rel_type: "m.thread",
},
};
} else {
return {
"msgtype": "m.text",
"m.relates_to": {
event_id: ev.getId(),
is_falling_back: true,
rel_type: "m.thread",
},
...newMessage,
};
}
}, newMessage);
}
})(this);
}
}

/**
* Something that can provide the content of a message.
*
* For example, we return and instance of this from {@link
* MessageBuilder.replyTo} which creates a reply based on a previous message.
*/
export abstract class MessageContentSpec {
messageFinder: MessageBuilder | null;

constructor(messageFinder: MessageBuilder = null) {
this.messageFinder = messageFinder;
}

public abstract getContent(room: JSHandle<Room>): Promise<Record<string, unknown>>;
}

/**
* Something that we will turn into a message or event when we pass it in to
* e.g. receiveMessages.
*/
export type Message = string | MessageContentSpec;

export class Helpers {
constructor(
private page: Page,
private app: ElementAppPage,
private bot: Bot,
) {}

/**
* Use the supplied client to send messages or perform actions as specified by
* the supplied {@link Message} items.
*/
async sendMessageAsClient(cli: Client, roomName: string | { name: string }, messages: Message[]) {
const room = await this.findRoomByName(typeof roomName === "string" ? roomName : roomName.name);
const roomId = await room.evaluate((room) => room.roomId);

for (const message of messages) {
if (typeof message === "string") {
await cli.sendMessage(roomId, { body: message, msgtype: "m.text" });
} else if (message instanceof MessageContentSpec) {
await cli.sendMessage(roomId, await message.getContent(room));
}
// TODO: without this wait, some tests that send lots of messages flake
// from time to time. I (andyb) have done some investigation, but it
// needs more work to figure out. The messages do arrive over sync, but
// they never appear in the timeline, and they never fire a
// Room.timeline event. I think this only happens with events that refer
// to other events (e.g. replies), so it might be caused by the
// referring event arriving before the referred-to event.
await this.page.waitForTimeout(100);
}
}

/**
* Open the room with the supplied name.
*/
async goTo(room: string | { name: string }) {
await this.app.viewRoomByName(typeof room === "string" ? room : room.name);
}

/**
* Click the thread with the supplied content in the thread root to open it in
* the Threads panel.
*/
async openThread(rootMessage: string) {
const tile = this.page.locator(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", { hasText: rootMessage });
await tile.hover();
await tile.getByRole("button", { name: "Reply in thread" }).click();
await expect(this.page.locator(".mx_ThreadView_timelinePanelWrapper")).toBeVisible();
}

async findRoomByName(roomName: string): Promise<JSHandle<Room>> {
return this.app.client.evaluateHandle((cli, roomName) => {
return cli.getRooms().find((r) => r.name === roomName);
}, roomName);
}

/**
* Sends messages into given room as a bot
* @param room - the name of the room to send messages into
* @param messages - the list of messages to send, these can be strings or implementations of MessageSpec like `editOf`
*/
async receiveMessages(room: string | { name: string }, messages: Message[]) {
await this.sendMessageAsClient(this.bot, room, messages);
}

/**
* Get the threads activity centre button
* @private
*/
private getTacButton(): Locator {
return this.page.getByRole("navigation", { name: "Spaces" }).getByLabel("Threads");
}

/**
* Return the threads activity centre panel
*/
getTacPanel() {
return this.page.getByRole("menu", { name: "Threads" });
}

/**
* Open the Threads Activity Centre
*/
openTac() {
return this.getTacButton().click();
}

/**
* Click on a room in the Threads Activity Centre
* @param name - room name
*/
clickRoomInTac(name: string) {
return this.getTacPanel().getByRole("menuitem", { name }).click();
}

/**
* Assert that the threads activity centre button has no indicator
*/
assertNoTacIndicator() {
return expect(this.getTacButton()).toMatchScreenshot("tac-no-indicator.png");
}

/**
* Assert that the threads activity centre button has a notification indicator
*/
assertNotificationTac() {
return expect(this.getTacButton().locator("[data-indicator='success']")).toBeVisible();
}

/**
* Assert that the threads activity centre button has a highlight indicator
*/
assertHighlightIndicator() {
return expect(this.getTacButton().locator("[data-indicator='critical']")).toBeVisible();
}

/**
* Assert that the threads activity centre panel has the expected rooms
* @param content - the expected rooms and their notification levels
*/
async assertRoomsInTac(content: Array<{ room: string; notificationLevel: "highlight" | "notification" }>) {
const getBadgeClass = (notificationLevel: "highlight" | "notification") =>
notificationLevel === "highlight"
? "mx_NotificationBadge_level_highlight"
: "mx_NotificationBadge_level_notification";

// Ensure that we have the right number of rooms
await expect(this.getTacPanel().getByRole("menuitem")).toHaveCount(content.length);

// Ensure that each room is present in the correct order and has the correct notification level
const roomsLocator = this.getTacPanel().getByRole("menuitem");
for (const [index, { room, notificationLevel }] of content.entries()) {
const roomLocator = roomsLocator.nth(index);
// Ensure that the room name are correct
await expect(roomLocator).toHaveText(new RegExp(room));
// There is no accessibility marker for the StatelessNotificationBadge
await expect(roomLocator.locator(`.${getBadgeClass(notificationLevel)}`)).toBeVisible();
}
}

/**
* Assert that the thread panel is opened
*/
assertThreadPanelIsOpened() {
return expect(this.page.locator(".mx_ThreadPanel")).toBeVisible();
}

/**
* Populate the rooms with messages and threads
* @param room1
* @param room2
* @param msg - MessageBuilder
*/
async populateThreads(
room1: { name: string; roomId: string },
room2: { name: string; roomId: string },
msg: MessageBuilder,
) {
await this.receiveMessages(room2, [
"Msg1",
msg.threadedOff("Msg1", {
"body": "User",
"format": "org.matrix.custom.html",
"formatted_body": "<a href='https://matrix.to/#/@user:localhost'>User</a>",
"m.mentions": {
user_ids: ["@user:localhost"],
},
}),
]);
await this.receiveMessages(room2, ["Msg2", msg.threadedOff("Msg2", "Resp2")]);
await this.receiveMessages(room1, ["Msg3", msg.threadedOff("Msg3", "Resp3")]);
}
}

export { expect };
Loading
Loading