diff --git a/apps/web/playwright/snapshots/invite/invite-dialog.spec.ts/send-your-first-message-view-linux.png b/apps/web/playwright/snapshots/invite/invite-dialog.spec.ts/send-your-first-message-view-linux.png index b598dd9b68..18fb646765 100644 Binary files a/apps/web/playwright/snapshots/invite/invite-dialog.spec.ts/send-your-first-message-view-linux.png and b/apps/web/playwright/snapshots/invite/invite-dialog.spec.ts/send-your-first-message-view-linux.png differ diff --git a/apps/web/playwright/snapshots/room/room-header.spec.ts/room-header-with-icon-linux.png b/apps/web/playwright/snapshots/room/room-header.spec.ts/room-header-with-icon-linux.png index 8d41007913..9ef3eb221d 100644 Binary files a/apps/web/playwright/snapshots/room/room-header.spec.ts/room-header-with-icon-linux.png and b/apps/web/playwright/snapshots/room/room-header.spec.ts/room-header-with-icon-linux.png differ diff --git a/apps/web/res/css/views/rooms/_RoomHeader.pcss b/apps/web/res/css/views/rooms/_RoomHeader.pcss index a42811fae2..353120fd36 100644 --- a/apps/web/res/css/views/rooms/_RoomHeader.pcss +++ b/apps/web/res/css/views/rooms/_RoomHeader.pcss @@ -59,13 +59,12 @@ Please see LICENSE files in the repository root for full details. .mx_RoomHeader_heading { display: flex; - gap: var(--cpd-space-1x); + gap: var(--cpd-space-1-5x); align-items: center; } .mx_RoomHeader_icon { flex-shrink: 0; - padding: var(--cpd-space-1x); } .mx_RoomHeader .mx_FacePile { diff --git a/apps/web/src/components/views/rooms/RoomHeader/RoomHeader.tsx b/apps/web/src/components/views/rooms/RoomHeader/RoomHeader.tsx index 4f7d0b9c1e..c1a4f8d810 100644 --- a/apps/web/src/components/views/rooms/RoomHeader/RoomHeader.tsx +++ b/apps/web/src/components/views/rooms/RoomHeader/RoomHeader.tsx @@ -20,7 +20,7 @@ import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error- import PublicIcon from "@vector-im/compound-design-tokens/assets/web/icons/public"; import { HistoryVisibility, JoinRule, type Room } from "matrix-js-sdk/src/matrix"; import { type ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle"; -import { Flex, Box } from "@element-hq/web-shared-components"; +import { Flex, Box, StatusTextView } from "@element-hq/web-shared-components"; import { CallType } from "matrix-js-sdk/src/webrtc/call"; import { HistoryIcon, UserProfileSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; @@ -57,6 +57,7 @@ import { ToggleableIcon } from "./toggle/ToggleableIcon.tsx"; import { CurrentRightPanelPhaseContextProvider } from "../../../../contexts/CurrentRightPanelPhaseContext.tsx"; import { LocalRoom } from "../../../../models/LocalRoom.ts"; import { useIsEncrypted } from "../../../../hooks/useIsEncrypted.ts"; +import { useUserStatus } from "../../../../hooks/useUserStatus.ts"; function RoomHeaderButtons({ room, @@ -448,6 +449,7 @@ export default function RoomHeader({ const historyVisibility = useRoomState(room, (state) => state.getHistoryVisibility()); const dmMember = useDmMember(room); const isDirectMessage = !!dmMember; + const dmUserStatus = useUserStatus(dmMember?.userId); const isRoomEncrypted = useIsEncrypted(client, room); const e2eStatus = useEncryptionStatus(client, room); const askToJoinEnabled = useFeatureEnabled("feature_ask_to_join"); @@ -496,6 +498,10 @@ export default function RoomHeader({ > {roomName} + {isDirectMessage && dmUserStatus && ( + + )} + {!isDirectMessage && joinRule === JoinRule.Public && ( { }); }); + it("does not show a user status for non-DM rooms", async () => { + SettingsStore.setValue("feature_user_status", null, SettingLevel.DEVICE, true); + client.doesServerSupportExtendedProfiles = jest.fn().mockResolvedValue(true); + mocked(client.getExtendedProfileProperty).mockResolvedValue({ emoji: "🐎", text: "on a horse" }); + + render(, getWrapper()); + + expect(screen.queryByText("on a horse")).not.toBeInTheDocument(); + expect(client.doesServerSupportExtendedProfiles).not.toHaveBeenCalled(); + }); + it("shows a history icon if the room is encrypted and has shared history", async () => { mocked(client.getCrypto()!).isEncryptionEnabledInRoom.mockResolvedValue(true); await room.addLiveEvents( @@ -811,6 +823,42 @@ describe("RoomHeader", () => { await waitFor(() => expect(getByLabelText(document.body, expectedLabel)).toBeInTheDocument()); }); + it("shows the user status", async () => { + SettingsStore.setValue("feature_user_status", null, SettingLevel.DEVICE, true); + client.doesServerSupportExtendedProfiles = jest.fn().mockResolvedValue(true); + mocked(client.getExtendedProfileProperty).mockResolvedValue({ emoji: "🐎", text: "on a horse" }); + + render(, getWrapper()); + + await waitFor(() => expect(screen.getByText("on a horse")).toBeInTheDocument()); + expect(screen.getByText("🐎")).toBeInTheDocument(); + }); + + it("updates user status when it changes", async () => { + SettingsStore.setValue("feature_user_status", null, SettingLevel.DEVICE, true); + client.doesServerSupportExtendedProfiles = jest.fn().mockResolvedValue(true); + mocked(client.getExtendedProfileProperty).mockResolvedValue({ emoji: "🐎", text: "on a horse" }); + + render(, getWrapper()); + + mocked(client.getExtendedProfileProperty).mockResolvedValue({ emoji: "🐴", text: "is a horse" }); + client.emit(ClientEvent.UserProfileUpdate, "@bob:example.org", { emoji: "🐴", text: "is a horse" }); + + await waitFor(() => expect(screen.getByText("is a horse")).toBeInTheDocument()); + expect(screen.getByText("🐴")).toBeInTheDocument(); + }); + + it("does not show the user status when the feature is disabled", async () => { + SettingsStore.setValue("feature_user_status", null, SettingLevel.DEVICE, false); + client.doesServerSupportExtendedProfiles = jest.fn().mockResolvedValue(true); + mocked(client.getExtendedProfileProperty).mockResolvedValue({ emoji: "🐎", text: "on a horse" }); + + render(, getWrapper()); + + expect(screen.queryByText("on a horse")).not.toBeInTheDocument(); + expect(client.doesServerSupportExtendedProfiles).not.toHaveBeenCalled(); + }); + it("does not show the face pile for DMs", () => { SdkConfig.put({ features: {