diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 9eca8c8636..0b887ecc77 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -139,3 +139,16 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} repository: vectorim/element-web + + - name: Repository Dispatch + uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3 + if: github.event_name != 'pull_request' + with: + repository: element-hq/element-web-pro + token: ${{ secrets.ELEMENT_BOT_TOKEN }} + event-type: image-built + # Stable way to determine the :version + client-payload: |- + { + "base-ref": "${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}" + } diff --git a/CHANGELOG.md b/CHANGELOG.md index e578d777f9..36dea524a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +Changes in [1.11.108](https://github.com/element-hq/element-web/releases/tag/v1.11.108) (2025-07-30) +==================================================================================================== +## šŸ› Bug Fixes + +* [Backport staging] Fix downloaded attachments not being decrypted ([#30434](https://github.com/element-hq/element-web/pull/30434)). Contributed by @RiotRobot. + + Changes in [1.11.107](https://github.com/element-hq/element-web/releases/tag/v1.11.107) (2025-07-29) ==================================================================================================== ## ✨ Features diff --git a/package.json b/package.json index e48d36d004..675120f452 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "element-web", - "version": "1.11.107", + "version": "1.11.108", "description": "Element: the future of secure communication", "author": "New Vector Ltd.", "repository": { @@ -128,9 +128,9 @@ "jsrsasign": "^11.0.0", "jszip": "^3.7.0", "katex": "^0.16.0", - "linkify-react": "4.3.1", - "linkify-string": "4.3.1", - "linkifyjs": "4.3.1", + "linkify-react": "4.3.2", + "linkify-string": "4.3.2", + "linkifyjs": "4.3.2", "lodash": "^4.17.21", "maplibre-gl": "^5.0.0", "matrix-encrypt-attachment": "^1.0.3", @@ -187,7 +187,7 @@ "@babel/runtime": "^7.12.5", "@casualbot/jest-sonar-reporter": "2.2.7", "@element-hq/element-call-embedded": "0.13.1", - "@element-hq/element-web-playwright-common": "^1.4.3", + "@element-hq/element-web-playwright-common": "^1.4.4", "@peculiar/webcrypto": "^1.4.3", "@playwright/test": "^1.50.1", "@principalstudio/html-webpack-inject-preload": "^1.2.7", diff --git a/playwright/e2e/audio-player/audio-player.spec.ts b/playwright/e2e/audio-player/audio-player.spec.ts index a8cb15a5da..8532362f6b 100644 --- a/playwright/e2e/audio-player/audio-player.spec.ts +++ b/playwright/e2e/audio-player/audio-player.spec.ts @@ -19,6 +19,7 @@ const clickButtonReply = async (tile: Locator) => { await tile.hover(); await tile.getByRole("button", { name: "Reply", exact: true }).click(); }).toPass(); + await expect(tile.page().getByText("Replying", { exact: true })).toBeVisible(); }; test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => { diff --git a/playwright/e2e/left-panel/room-list-panel/room-list-header.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list-header.spec.ts index daa8d3869f..ed7a24fff8 100644 --- a/playwright/e2e/left-panel/room-list-panel/room-list-header.spec.ts +++ b/playwright/e2e/left-panel/room-list-panel/room-list-header.spec.ts @@ -35,8 +35,8 @@ test.describe("Header section of the room list", () => { await expect(page.getByRole("menu")).toMatchScreenshot("room-list-header-compose-menu.png"); - // New message should open the direct messages dialog - await page.getByRole("menuitem", { name: "New message" }).click(); + // Start chat should open the direct messages dialog + await page.getByRole("menuitem", { name: "Start chat" }).click(); await expect(page.getByRole("heading", { name: "Direct Messages" })).toBeVisible(); await app.closeDialog(); diff --git a/playwright/e2e/right-panel/memberlist.spec.ts b/playwright/e2e/right-panel/memberlist.spec.ts index cd22626575..71a4a3cada 100644 --- a/playwright/e2e/right-panel/memberlist.spec.ts +++ b/playwright/e2e/right-panel/memberlist.spec.ts @@ -11,6 +11,32 @@ import { Bot } from "../../pages/bot"; const ROOM_NAME = "Test room"; const NAME = "Alice"; +async function setupRoomWithMembers( + app: any, + page: any, + homeserver: any, + roomName: string, + memberNames: string[], +): Promise { + const visibility = await page.evaluate(() => (window as any).matrixcs.Visibility.Public); + const id = await app.client.createRoom({ name: roomName, visibility }); + const bots: Bot[] = []; + + for (let i = 0; i < memberNames.length; i++) { + const displayName = memberNames[i]; + const bot = new Bot(page, homeserver, { displayName, startClient: false, autoAcceptInvites: false }); + if (displayName === "Susan") { + await bot.prepareClient(); + await app.client.inviteUser(id, bot.credentials?.userId); + } else { + await bot.joinRoom(id); + } + bots.push(bot); + } + + return id; +} + test.use({ synapseConfig: { presence: { @@ -25,17 +51,8 @@ test.use({ test.describe("Memberlist", () => { test.beforeEach(async ({ app, user, page, homeserver }, testInfo) => { testInfo.setTimeout(testInfo.timeout + 30_000); - const id = await app.client.createRoom({ name: ROOM_NAME }); - const newBots: Bot[] = []; const names = ["Bob", "Bob", "Susan"]; - for (let i = 0; i < 3; i++) { - const displayName = names[i]; - const autoAcceptInvites = displayName !== "Susan"; - const bot = new Bot(page, homeserver, { displayName, startClient: true, autoAcceptInvites }); - await bot.prepareClient(); - await app.client.inviteUser(id, bot.credentials?.userId); - newBots.push(bot); - } + await setupRoomWithMembers(app, page, homeserver, ROOM_NAME, names); }); test("Renders correctly", { tag: "@screenshot" }, async ({ page, app }) => { @@ -45,4 +62,37 @@ test.describe("Memberlist", () => { await expect(memberlist.getByText("Invited")).toHaveCount(1); await expect(page.locator(".mx_MemberListView")).toMatchScreenshot("with-four-members.png"); }); + + test("should handle scroll and click to view member profile", async ({ page, app, homeserver }) => { + // Create a room with many members to enable scrolling + const memberNames = Array.from({ length: 15 }, (_, i) => `Member${i.toString()}`); + await setupRoomWithMembers(app, page, homeserver, "Large Room", memberNames); + + // Navigate to the room and open member list + await app.viewRoomByName("Large Room"); + + const memberlist = await app.toggleMemberlistPanel(); + + // Get the scrollable container + const memberListContainer = memberlist.locator(".mx_AutoHideScrollbar"); + + // Scroll down to the bottom of the member list + await app.scrollListToBottom(memberListContainer); + + // Wait for the target member to be visible after scrolling + const targetName = "Member14"; + const targetMember = memberlist.locator(".mx_MemberTileView_name").filter({ hasText: targetName }); + await targetMember.waitFor({ state: "visible" }); + + // Verify Alice is not visible at this point + await expect(memberlist.locator(".mx_MemberTileView_name").filter({ hasText: "Alice" })).toHaveCount(0); + + // Click on a member near the bottom of the list + await expect(targetMember).toBeVisible(); + await targetMember.click(); + + // Verify that the user info screen is shown and hasn't scrolled back to top + await expect(page.locator(".mx_UserInfo")).toBeVisible(); + await expect(page.locator(".mx_UserInfo_profile").getByText(targetName)).toBeVisible(); + }); }); diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/default-empty-room-list-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/default-empty-room-list-linux.png index 45d2a775ea..d47d04e9a6 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/default-empty-room-list-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/default-empty-room-list-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/room-panel-empty-room-list-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/room-panel-empty-room-list-linux.png index 250712308d..ac3f26e529 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/room-panel-empty-room-list-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/room-panel-empty-room-list-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-compose-menu-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-compose-menu-linux.png index 9f501a58d4..202a83c23a 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-compose-menu-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-compose-menu-linux.png differ diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index 7af8ae47f1..6c8962abf2 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers"; -const TAG = "develop@sha256:8c2d9a93dd209a79d3e5e50cd18addfe52d80bea0ffe48a5d3e15836032eeb9d"; +const TAG = "develop@sha256:2f6fff14ff23f356705abdbf2ed62c3dd6ca2103cef4ae813714ddc199bbd76a"; /** * SynapseContainer which freezes the docker digest to stabilise tests, diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index e61713ca69..39c0147da0 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -141,6 +141,7 @@ import { type OpenForwardDialogPayload } from "../../dispatcher/payloads/OpenFor import { ShareFormat, type SharePayload } from "../../dispatcher/payloads/SharePayload"; import Markdown from "../../Markdown"; import { sanitizeHtmlParams } from "../../Linkify"; +import { isOnlyAdmin } from "../../utils/membership"; // legacy export export { default as Views } from "../../Views"; @@ -1255,29 +1256,22 @@ export default class MatrixChat extends React.PureComponent { const client = MatrixClientPeg.get(); if (client && roomToLeave) { - const plEvent = roomToLeave.currentState.getStateEvents(EventType.RoomPowerLevels, ""); - const plContent = plEvent ? plEvent.getContent() : {}; - const userLevels = plContent.users || {}; - const currentUserLevel = userLevels[client.getUserId()!]; - const userLevelValues = Object.values(userLevels); - if (userLevelValues.every((x) => typeof x === "number")) { + // If the user is the only user with highest power level + if (isOnlyAdmin(roomToLeave)) { + const userLevelValues = roomToLeave.getJoinedMembers().map((m) => m.powerLevel); + const maxUserLevel = Math.max(...(userLevelValues as number[])); - // If the user is the only user with highest power level - if ( - maxUserLevel === currentUserLevel && - userLevelValues.lastIndexOf(maxUserLevel) == userLevelValues.indexOf(maxUserLevel) - ) { - const warning = - maxUserLevel >= 100 - ? _t("leave_room_dialog|room_leave_admin_warning") - : _t("leave_room_dialog|room_leave_mod_warning"); - warnings.push( - - {" " /* Whitespace, otherwise the sentences get smashed together */} - {warning} - , - ); - } + + const warning = + maxUserLevel >= 100 + ? _t("leave_room_dialog|room_leave_admin_warning") + : _t("leave_room_dialog|room_leave_mod_warning"); + warnings.push( + + {" " /* Whitespace, otherwise the sentences get smashed together */} + {warning} + , + ); } } diff --git a/src/components/utils/ListView.tsx b/src/components/utils/ListView.tsx index ee46327fd9..051d0a25e8 100644 --- a/src/components/utils/ListView.tsx +++ b/src/components/utils/ListView.tsx @@ -36,7 +36,12 @@ export interface IListViewProps * @param context - The context object containing the focused key and any additional data * @returns JSX element representing the rendered item */ - getItemComponent: (index: number, item: Item, context: ListContext) => JSX.Element; + getItemComponent: ( + index: number, + item: Item, + context: ListContext, + onFocus: (e: React.FocusEvent) => void, + ) => JSX.Element; /** * Optional additional context data to pass to each rendered item. @@ -207,6 +212,20 @@ export function ListView(props: IListViewProps): JSX.Element => { + const onFocus = (e: React.FocusEvent): void => { + // If one of the item components has been focused directly, set the focused and tabIndex state + // and stop propagation so the ListViews onFocus doesn't also handle it. + const key = getItemKey(item); + setIsFocused(true); + setTabIndexKey(key); + e.stopPropagation(); + }; + return getItemComponent(index, item, context, onFocus); + }, + [getItemComponent, getItemKey], + ); /** * Handles focus events on the list. * Sets the focused state and scrolls to the focused item if it is not currently visible. @@ -259,7 +278,7 @@ export function ListView(props: IListViewProps ); diff --git a/src/components/views/avatars/RoomAvatarView.tsx b/src/components/views/avatars/RoomAvatarView.tsx index 1f85ee9512..408690fa1d 100644 --- a/src/components/views/avatars/RoomAvatarView.tsx +++ b/src/components/views/avatars/RoomAvatarView.tsx @@ -14,6 +14,7 @@ import OnlineOrUnavailableIcon from "@vector-im/compound-design-tokens/assets/we import OfflineIcon from "@vector-im/compound-design-tokens/assets/web/icons/presence-outline-8x8"; import BusyIcon from "@vector-im/compound-design-tokens/assets/web/icons/presence-strikethrough-8x8"; import classNames from "classnames"; +import { Tooltip } from "@vector-im/compound-web"; import RoomAvatar from "./RoomAvatar"; import { AvatarBadgeDecoration, useRoomAvatarViewModel } from "../../viewmodels/avatars/RoomAvatarViewModel"; @@ -37,6 +38,7 @@ export function RoomAvatarView({ room }: RoomAvatarViewProps): JSX.Element { if (!vm.badgeDecoration) return ; const icon = getAvatarDecoration(vm.badgeDecoration, vm.presence); + const label = getDecorationLabel(vm.badgeDecoration, vm.presence); // Presence indicator and video/public icons don't have the same size // We use different masks @@ -48,22 +50,15 @@ export function RoomAvatarView({ room }: RoomAvatarViewProps): JSX.Element { return (
- {icon} + {label ? {icon} : icon}
); } -type PresenceDecorationProps = { - /** - * The presence of the user in the DM room. - */ - presence: NonNullable; -}; - /** - * Component to display the presence of a user in a DM room. + * Get the decoration for the avatar based on the presence. */ -function PresenceDecoration({ presence }: PresenceDecorationProps): JSX.Element { +function getPresenceDecoration(presence: Presence): JSX.Element { switch (presence) { case Presence.Online: return ( @@ -72,7 +67,7 @@ function PresenceDecoration({ presence }: PresenceDecorationProps): JSX.Element height="8px" className="mx_RoomAvatarView_PresenceDecoration" color="var(--cpd-color-icon-accent-primary)" - aria-label={_t("presence|online")} + aria-label={getPresenceLabel(presence)} /> ); case Presence.Away: @@ -82,7 +77,7 @@ function PresenceDecoration({ presence }: PresenceDecorationProps): JSX.Element height="8px" className="mx_RoomAvatarView_PresenceDecoration" color="var(--cpd-color-icon-quaternary)" - aria-label={_t("presence|away")} + aria-label={getPresenceLabel(presence)} /> ); case Presence.Offline: @@ -92,7 +87,7 @@ function PresenceDecoration({ presence }: PresenceDecorationProps): JSX.Element height="8px" className="mx_RoomAvatarView_PresenceDecoration" color="var(--cpd-color-icon-tertiary)" - aria-label={_t("presence|offline")} + aria-label={getPresenceLabel(presence)} /> ); case Presence.Busy: @@ -102,7 +97,7 @@ function PresenceDecoration({ presence }: PresenceDecorationProps): JSX.Element height="8px" className="mx_RoomAvatarView_PresenceDecoration" color="var(--cpd-color-icon-tertiary)" - aria-label={_t("presence|busy")} + aria-label={getPresenceLabel(presence)} /> ); } @@ -116,7 +111,7 @@ function getAvatarDecoration(decoration: AvatarBadgeDecoration, presence: Presen height="16px" className="mx_RoomAvatarView_icon" color="var(--cpd-color-icon-tertiary)" - aria-label={_t("room|room_is_low_priority")} + aria-label={getDecorationLabel(decoration, presence)} /> ); } else if (decoration === AvatarBadgeDecoration.VideoRoom) { @@ -126,7 +121,7 @@ function getAvatarDecoration(decoration: AvatarBadgeDecoration, presence: Presen height="16px" className="mx_RoomAvatarView_icon" color="var(--cpd-color-icon-tertiary)" - aria-label={_t("room|video_room")} + aria-label={getDecorationLabel(decoration, presence)} /> ); } else if (decoration === AvatarBadgeDecoration.PublicRoom) { @@ -136,10 +131,44 @@ function getAvatarDecoration(decoration: AvatarBadgeDecoration, presence: Presen height="16px" className="mx_RoomAvatarView_icon" color="var(--cpd-color-icon-info-primary)" - aria-label={_t("room|header|room_is_public")} + aria-label={getDecorationLabel(decoration, presence)} /> ); } else if (decoration === AvatarBadgeDecoration.Presence) { - return ; + return getPresenceDecoration(presence!); + } +} + +/** + * Get the label for the avatar decoration. + * This is used for the tooltip and a11y label. + */ +function getDecorationLabel(decoration: AvatarBadgeDecoration, presence: Presence | null): string | undefined { + switch (decoration) { + case AvatarBadgeDecoration.LowPriority: + return _t("room|room_is_low_priority"); + case AvatarBadgeDecoration.VideoRoom: + return _t("room|video_room"); + case AvatarBadgeDecoration.PublicRoom: + return _t("room|header|room_is_public"); + case AvatarBadgeDecoration.Presence: + return getPresenceLabel(presence!); + } +} + +/** + * Get the label for the presence. + * This is used for the tooltip and a11y label. + */ +function getPresenceLabel(presence: Presence): string { + switch (presence) { + case Presence.Online: + return _t("presence|online"); + case Presence.Away: + return _t("presence|away"); + case Presence.Offline: + return _t("presence|offline"); + case Presence.Busy: + return _t("presence|busy"); } } diff --git a/src/components/views/dialogs/LeaveSpaceDialog.tsx b/src/components/views/dialogs/LeaveSpaceDialog.tsx index e81606db79..7796bf4f61 100644 --- a/src/components/views/dialogs/LeaveSpaceDialog.tsx +++ b/src/components/views/dialogs/LeaveSpaceDialog.tsx @@ -15,23 +15,13 @@ import BaseDialog from "../dialogs/BaseDialog"; import SpaceStore from "../../../stores/spaces/SpaceStore"; import SpaceChildrenPicker from "../spaces/SpaceChildrenPicker"; import { filterBoolean } from "../../../utils/arrays"; +import { isOnlyAdmin } from "../../../utils/membership"; interface IProps { space: Room; onFinished(leave: boolean, rooms?: Room[]): void; } -const isOnlyAdmin = (room: Room): boolean => { - const userId = room.client.getSafeUserId(); - if (room.getMember(userId)?.powerLevelNorm !== 100) { - return false; // user is not an admin - } - return room.getJoinedMembers().every((member) => { - // return true if every other member has a lower power level (we are highest) - return member.userId === userId || member.powerLevelNorm < 100; - }); -}; - const LeaveSpaceDialog: React.FC = ({ space, onFinished }) => { const spaceChildren = useMemo(() => { const roomSet = new Set(SpaceStore.instance.getSpaceFilteredRoomIds(space.roomId)); diff --git a/src/components/views/messages/DownloadActionButton.tsx b/src/components/views/messages/DownloadActionButton.tsx index 479e792fca..0072dd42a0 100644 --- a/src/components/views/messages/DownloadActionButton.tsx +++ b/src/components/views/messages/DownloadActionButton.tsx @@ -26,6 +26,12 @@ interface IProps { mediaEventHelperGet: () => MediaEventHelper | undefined; } +function useButtonTitle(loading: boolean, isEncrypted: boolean): string { + if (!loading) return _t("action|download"); + + return isEncrypted ? _t("timeline|download_action_decrypting") : _t("timeline|download_action_downloading"); +} + export default function DownloadActionButton({ mxEvent, mediaEventHelperGet }: IProps): ReactElement | null { const mediaEventHelper = useMemo(() => mediaEventHelperGet(), [mediaEventHelperGet]); const downloadUrl = mediaEventHelper?.media.srcHttp ?? ""; @@ -33,6 +39,8 @@ export default function DownloadActionButton({ mxEvent, mediaEventHelperGet }: I const { download, loading, canDownload } = useDownloadMedia(downloadUrl, fileName, mxEvent); + const buttonTitle = useButtonTitle(loading, mediaEventHelper?.media.isEncrypted ?? false); + if (!canDownload) return null; const spinner = loading ? : undefined; @@ -45,7 +53,7 @@ export default function DownloadActionButton({ mxEvent, mediaEventHelperGet }: I return ( = (props: IProps) => { }, []); const getItemComponent = useCallback( - (index: number, item: MemberWithSeparator, context: ListContext): JSX.Element => { + ( + index: number, + item: MemberWithSeparator, + context: ListContext, + onFocus: (e: React.FocusEvent) => void, + ): JSX.Element => { const itemKey = getItemKey(item); const isRovingItem = itemKey === context.tabIndexKey; const focused = isRovingItem && context.focused; @@ -56,6 +61,7 @@ const MemberListView: React.FC = (props: IProps) => { tabIndex={isRovingItem ? 0 : -1} index={index} memberCount={memberCount} + onFocus={onFocus} /> ); } else { @@ -66,6 +72,7 @@ const MemberListView: React.FC = (props: IProps) => { tabIndex={isRovingItem ? 0 : -1} memberIndex={index - 1} // Adjust as invites are below the separator memberCount={memberCount} + onFocus={onFocus} /> ); } @@ -73,19 +80,6 @@ const MemberListView: React.FC = (props: IProps) => { [isPresenceEnabled, getItemKey, memberCount], ); - const handleSelectItem = useCallback( - (item: MemberWithSeparator): void => { - if (item !== SEPARATOR) { - if (item.member) { - onClickMember(item.member); - } else { - onClickMember(item.threePidInvite); - } - } - }, - [onClickMember], - ); - const isItemFocusable = useCallback((item: MemberWithSeparator): boolean => { return item !== SEPARATOR; }, []); diff --git a/src/components/views/rooms/MemberList/tiles/RoomMemberTileView.tsx b/src/components/views/rooms/MemberList/tiles/RoomMemberTileView.tsx index 4837972da3..99a15893af 100644 --- a/src/components/views/rooms/MemberList/tiles/RoomMemberTileView.tsx +++ b/src/components/views/rooms/MemberList/tiles/RoomMemberTileView.tsx @@ -24,6 +24,7 @@ interface IProps { showPresence?: boolean; focused?: boolean; tabIndex?: number; + onFocus: (e: React.FocusEvent) => void; } export function RoomMemberTileView(props: IProps): JSX.Element { @@ -59,6 +60,7 @@ export function RoomMemberTileView(props: IProps): JSX.Element { return ( void; } export function ThreePidInviteTileView(props: Props): JSX.Element { @@ -39,6 +40,7 @@ export function ThreePidInviteTileView(props: Props): JSX.Element { iconJsx={iconJsx} focused={props.focused} tabIndex={props.tabIndex} + onFocus={props.onFocus} /> ); } diff --git a/src/components/views/rooms/MemberList/tiles/common/MemberTileView.tsx b/src/components/views/rooms/MemberList/tiles/common/MemberTileView.tsx index 3f97e2b48e..cb0cec74d9 100644 --- a/src/components/views/rooms/MemberList/tiles/common/MemberTileView.tsx +++ b/src/components/views/rooms/MemberList/tiles/common/MemberTileView.tsx @@ -13,6 +13,7 @@ interface Props { avatarJsx: JSX.Element; nameJsx: JSX.Element | string; onClick: () => void; + onFocus: (e: React.FocusEvent) => void; memberIndex: number; memberCount: number; ariaLabel?: string; @@ -41,6 +42,7 @@ export function MemberTileView(props: Props): JSX.Element { ref={ref} className="mx_MemberTileView" onClick={props.onClick} + onFocus={props.onFocus} aria-label={props?.ariaLabel} tabIndex={props.tabIndex} role="option" diff --git a/src/components/views/rooms/RoomListPanel/EmptyRoomList.tsx b/src/components/views/rooms/RoomListPanel/EmptyRoomList.tsx index 031e63ac22..66d881cfdb 100644 --- a/src/components/views/rooms/RoomListPanel/EmptyRoomList.tsx +++ b/src/components/views/rooms/RoomListPanel/EmptyRoomList.tsx @@ -7,7 +7,7 @@ import React, { type JSX, type PropsWithChildren } from "react"; import { Button } from "@vector-im/compound-web"; -import UserAddIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-add"; +import ChatIcon from "@vector-im/compound-design-tokens/assets/web/icons/chat"; import RoomIcon from "@vector-im/compound-design-tokens/assets/web/icons/room"; import type { RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel"; @@ -148,8 +148,8 @@ function DefaultPlaceholder({ vm }: DefaultPlaceholderProps): JSX.Element { direction="column" gap="var(--cpd-space-4x)" > - {vm.canCreateRoom && ( @@ -247,10 +247,10 @@ exports[` should render the default placeholder when there is n xmlns="http://www.w3.org/2000/svg" > - New message + Start chat