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: {