This repository has been archived by the owner on Sep 11, 2024. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 828
Pop out of Threads Activity Centre #12136
Merged
Merged
Changes from all commits
Commits
Show all changes
75 commits
Select commit
Hold shift + click to select a range
b317093
Add `Thread Activity centre` labs flag
florianduros a07196b
Rename translation string
florianduros 8f633bd
WIP Thread Activity Centre
florianduros c241a1a
Merge branch 'develop' into florianduros/threads/pop-out-panel
florianduros 927eeb6
Merge branch 'develop' into florianduros/threads/labs-flag
florianduros 007c4f5
Update supportedLevels
florianduros 7f9a9d9
Merge branch 'develop' into florianduros/threads/labs-flag
florianduros 2d40d2c
Merge branch 'florianduros/threads/labs-flag' into florianduros/threa…
florianduros f04f591
css lint
florianduros 6115607
i18n lint
florianduros c83559b
Fix labs subsection test
florianduros 531e10a
Merge branch 'florianduros/threads/labs-flag' into florianduros/threa…
florianduros b226a08
Update Threads Activity Centre label
florianduros 94ace6f
Merge branch 'florianduros/threads/labs-flag' into florianduros/threa…
florianduros 3734924
Merge branch 'develop' into florianduros/threads/labs-flag
florianduros aefe979
Rename Thread Activity Centre to Threads Activity Centre
florianduros b7f43af
Merge branch 'develop' into florianduros/threads/pop-out-panel
florianduros 8a5d875
Use compound `MenuItem` instead of custom button
florianduros 84d371f
Color thread icon when hovered
florianduros b120df6
Make the pop-up scrollable and add a max height
florianduros 27d21b6
Remove Math.random in key
florianduros 0943a1c
Merge branch 'develop' into florianduros/threads/pop-out-panel
florianduros c06a6ca
Remove unused class
florianduros eca98b8
Change add comments on `mx_ThreadsActivityRows` and `mx_ThreadsActivi…
florianduros 5b854dc
Make threads activity centre labs flag split out unread counts
dbkr ba9ad07
Fix tests
dbkr f2dc6f9
Simpler fix
dbkr 141cb35
Open thread panel when thread clicke in Threads Activity Centre
dbkr 662c946
Merge pull request #12161 from matrix-org/dbkr/open_threadpanel
dbkr dc954c5
Dynamic state of room
florianduros 1e4b378
Merge pull request #12156 from matrix-org/dbkr/split_unread_counts
dbkr 7a72dd8
Merge branch 'develop' into florianduros/threads/labs-flag
florianduros f30e62d
Merge branch 'florianduros/threads/labs-flag' into florianduros/threa…
florianduros 3478500
Merge branch 'develop' into florianduros/threads/pop-out-panel
dbkr c66da31
Merge branch 'develop' into florianduros/threads/labs-flag
florianduros 4a9619b
Add doc
florianduros 9db452c
Use the StatelessNotificationBadge component in ThreadsActivityCentre
dbkr 8d803b3
Remove unused style
dbkr 09d2691
Merge pull request #12167 from matrix-org/dbkr/use_stateless_notifica…
dbkr 3279050
Merge branch 'develop' into florianduros/threads/labs-flag
florianduros 777e7f2
Add room sorting
florianduros d33ffbf
Fix `ThreadsActivityRow` props doc
florianduros c6683d2
Pass in & cache the status of the TAC labs flag
dbkr 67550c1
Pass includeThreads as setting to doesRoomHaveUnreadMessages too
dbkr c9eccdc
Fix tests
dbkr d56b703
Merge branch 'develop' into florianduros/threads/pop-out-panel
dbkr 2802f7a
Add analytics to the TAC (#12179)
florianduros 4e5ad83
Merge branch 'florianduros/threads/labs-flag' into florianduros/threa…
dbkr ac236a6
Merge branch 'develop' into florianduros/threads/pop-out-panel
florianduros b893eb7
Merge branch 'develop' into florianduros/threads/pop-out-panel
florianduros 49b4aee
Update TAC label (#12186)
florianduros ccda3bc
Add `IndicatorIcon` to the TAC button (#12182)
florianduros 1ea0291
Threads don't have activity if the room is muted
dbkr daebac6
Merge pull request #12192 from matrix-org/dbkr/honour_muted_unread
dbkr 41d392f
Re-use doesRoomHaveUnreadThreads for useRoomThreadNotifications
dbkr 6872cc4
Merge pull request #12195 from matrix-org/dbkr/reuse_unread_threads
dbkr 872af63
Add TAC description in labs (#12197)
florianduros 7e94ab0
Fox position & size of dot on the tac button
dbkr 97c92dd
Merge branch 'develop' into florianduros/threads/pop-out-panel
florianduros 19c9fcf
Merge pull request #12199 from matrix-org/dbkr/fix_tac_dot_position
dbkr 48b4a04
TAC: Utils tests (#12200)
florianduros a5d9c33
Add test for the ThreadsActivityCentre component
dbkr 6b0e759
Add snapshot test
dbkr 3dc60a6
Fix narrow hover background on TAC button
dbkr 7fb8349
Merge pull request #12212 from matrix-org/dbkr/tac_fix_narrow_hover
dbkr f9608c0
Merge pull request #12207 from matrix-org/dbkr/tac_test
dbkr a5f2876
Merge branch 'develop' into florianduros/threads/pop-out-panel
florianduros b934950
Add caption for empty TAC
dbkr f144a8f
s/tac/threads_activity_centre/
dbkr 9c024c1
Fix i18n & add tests
dbkr f63abc3
Merge pull request #12226 from matrix-org/dbkr/tac_empty_caption
dbkr 5b70511
Merge branch 'develop' into florianduros/threads/pop-out-panel
florianduros c97e9e9
Merge branch 'develop' into florianduros/threads/pop-out-panel
florianduros 2ad891b
Add playwright tests for the TAC (#12227)
florianduros d8d7217
Fox comments
florianduros File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { | ||
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 }; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.