Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
a7859c3
First stage
dbkr Jun 12, 2026
2e2792f
Working version
dbkr Jun 16, 2026
0bc1bcc
Commit the set status view component
dbkr Jun 16, 2026
c736ca3
Change to use dropdown with custom trigger
dbkr Jun 18, 2026
c9bbcb4
Show full set status component in user menu
dbkr Jun 23, 2026
0a8dc76
In user menu, clicking takes you to settings
dbkr Jun 23, 2026
4fb3691
Add the view model
dbkr Jun 23, 2026
5417081
Merge branch 'develop' into dbkr/set_userstatus
dbkr Jun 23, 2026
859715b
oxfmt
dbkr Jun 23, 2026
fb10213
Fix type import
dbkr Jun 24, 2026
32faa0a
Pass set status viewmodel in story
dbkr Jun 24, 2026
0a26480
Bump compound-web
dbkr Jun 24, 2026
968267a
Snapshot
dbkr Jun 24, 2026
da9dc16
Add explicit aria label
dbkr Jun 24, 2026
423eef2
snapshot again
dbkr Jun 24, 2026
b97060b
Only show user status if feature flag enabled
dbkr Jun 24, 2026
0c1c149
Make status btton view not a button
dbkr Jun 24, 2026
0a369c4
Merge branch 'develop' into dbkr/set_userstatus
dbkr Jun 24, 2026
eb6d5a2
Screenshots
dbkr Jun 24, 2026
113e5d7
Disable in settings if feature flag off
dbkr Jun 25, 2026
ef5cd1e
Merge branch 'develop' into dbkr/set_userstatus
dbkr Jun 25, 2026
79f70d6
Snapshot
dbkr Jun 25, 2026
244b4ec
Snapshot again
dbkr Jun 25, 2026
1bdb968
Update screenshots
dbkr Jun 26, 2026
32b2017
Fix size & positioning of the status button
dbkr Jun 26, 2026
be20575
Update screenshots
dbkr Jun 26, 2026
34f97aa
Screenshots
dbkr Jun 26, 2026
305b94c
Merge remote-tracking branch 'origin/develop' into dbkr/set_userstatus
dbkr Jun 29, 2026
2b343f1
Merge branch 'develop' into dbkr/set_userstatus
dbkr Jun 29, 2026
ddbf6f9
Merge branch 'develop' into dbkr/set_userstatus
dbkr Jun 29, 2026
716b78b
Merge remote-tracking branch 'origin/develop' into dbkr/set_userstatus
dbkr Jun 30, 2026
57e9e3b
Merge branch 'develop' into dbkr/set_userstatus
dbkr Jul 1, 2026
e730b6a
Show user status in DM room list, DM room header, member list & user …
langleyd Jul 3, 2026
b9a0d1c
Merge remote-tracking branch 'origin/develop' into langleyd/user-stat…
langleyd Jul 3, 2026
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 12 additions & 3 deletions apps/web/res/css/views/messages/_DisambiguatedProfile.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,28 @@ Please see LICENSE files in the repository root for full details.

/** Disambiguated profile needs to have a different layout in the member tile */
.mx_MemberTileView .mx_DisambiguatedProfile {
display: flex;
flex-direction: column;
/* Grid so that the name and the user status emoji share the first row
(with the name truncating and the emoji always visible), while the
disambiguated MXID gets a full row below. */
display: grid;
grid-template-columns: minmax(0, max-content) auto;
align-items: center;

.mx_DisambiguatedProfile_mxid {
grid-column: 1 / -1;
margin-inline-start: 0;
font: var(--cpd-font-body-sm-regular);
text-overflow: ellipsis;
overflow: hidden;
}

.mx_DisambiguatedProfile_userStatus {
margin-inline-start: var(--cpd-space-0-5x);
}

span:not(.mx_DisambiguatedProfile_mxid) {
/**
In a member tile, this span element is a flex child and so
In a member tile, this span element is a grid child and so
we need the following for text overflow to work.
**/
overflow: hidden;
Expand Down
5 changes: 5 additions & 0 deletions apps/web/res/css/views/right_panel/_UserInfo.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ Please see LICENSE files in the repository root for full details.
height: 20px;
}

.mx_UserInfo_userStatusMessage {
justify-content: center;
max-width: 100%;
}

.mx_UserInfo_timezone {
height: 20px;
margin: 0;
Expand Down
6 changes: 6 additions & 0 deletions apps/web/res/css/views/rooms/_RoomHeader.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ Please see LICENSE files in the repository root for full details.
padding: var(--cpd-space-1x);
}

.mx_RoomHeader_userStatus {
/* With the heading's own 4px gap this makes up the 8px gap from the design */
margin-inline-start: var(--cpd-space-1x);
min-width: 0;
}

.mx_RoomHeader .mx_FacePile {
color: $secondary-content;
background: $background;
Expand Down
10 changes: 5 additions & 5 deletions apps/web/res/css/views/settings/_AvatarSetting.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ Please see LICENSE files in the repository root for full details.
*/

.mx_AvatarSetting_avatar {
width: 90px;
min-width: 90px; /* so it doesn't get crushed by the flexbox in languages with longer words */
height: 90px;
width: 120px;
min-width: 120px; /* so it doesn't get crushed by the flexbox in languages with longer words */
height: 120px;
margin-top: 8px;
position: relative;

Expand All @@ -31,9 +31,9 @@ Please see LICENSE files in the repository root for full details.

& > img {
display: block;
height: 90px;
height: 120px;
width: inherit;
border-radius: 90px;
border-radius: 120px;
cursor: pointer;
object-fit: cover;
}
Expand Down
6 changes: 5 additions & 1 deletion apps/web/res/css/views/settings/_UserProfileSettings.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@ Please see LICENSE files in the repository root for full details.
gap: 16px;

.mx_UserProfileSettings_profile_displayName {
flex-grow: 1;
flex-grow: 0;
width: 100%;
gap: 0;
}
.mx_UserProfileSettings_profile_nameAndStatus {
width: 100%;
gap: 16px;
}
}

.mx_UserProfileSettings_profile_controls {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ Please see LICENSE files in the repository root for full details.
import React, { type JSX } from "react";
import { type User, type RoomMember } from "matrix-js-sdk/src/matrix";
import { Heading, Tooltip, Text } from "@vector-im/compound-web";
import { Flex } from "@element-hq/web-shared-components";
import { Flex, UserStatusMessageView } from "@element-hq/web-shared-components";

import { useUserfoHeaderViewModel } from "../../../viewmodels/right_panel/user_info/UserInfoHeaderViewModel";
import MemberAvatar from "../../avatars/MemberAvatar";
import { Container, type Member, type IDevice } from "../UserInfo";
import PresenceLabel from "../../rooms/PresenceLabel";
import CopyableText from "../../elements/CopyableText";
import { UserInfoHeaderVerificationView } from "./UserInfoHeaderVerificationView";
import { useUserStatus } from "../../../../hooks/useUserStatus";

export interface UserInfoHeaderViewProps {
member: Member;
Expand All @@ -33,6 +34,7 @@ export const UserInfoHeaderView: React.FC<UserInfoHeaderViewProps> = ({
const vm = useUserfoHeaderViewModel({ member, roomId });
const avatarUrl = (member as User).avatarUrl;
const displayName = (member as RoomMember).rawDisplayName;
const userStatus = useUserStatus(member.userId);

let presenceLabel: JSX.Element | undefined;

Expand Down Expand Up @@ -73,6 +75,9 @@ export const UserInfoHeaderView: React.FC<UserInfoHeaderViewProps> = ({
{displayName}
</Flex>
</Heading>
{userStatus && (
<UserStatusMessageView status={userStatus} className="mx_UserInfo_userStatusMessage" />
)}
{presenceLabel}
{vm.timezoneInfo && (
<Tooltip label={vm.timezoneInfo?.timezone ?? ""}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { MemberTileView } from "./common/MemberTileView";
import { InvitedIconView } from "./common/InvitedIconView";
import { type MemberWithSeparator } from "../../../../viewmodels/memberlist/MemberListViewModel";
import { DisambiguatedProfileViewModel } from "../../../../../viewmodels/room/timeline/event-tile/DisambiguatedProfileViewModel";
import { useUserStatus } from "../../../../../hooks/useUserStatus";

interface IProps {
/**
Expand Down Expand Up @@ -47,17 +48,22 @@ export function RoomMemberTileView(props: IProps): JSX.Element {
/>
);
const name = vm.name;
const userStatus = useUserStatus(member.userId);
const disambiguatedProfileVM = useCreateAutoDisposedViewModel(
() =>
new DisambiguatedProfileViewModel({
fallbackName: name,
member,
withTooltip: true,
userStatus,
}),
);
useEffect(() => {
disambiguatedProfileVM.setMember(name, member);
}, [disambiguatedProfileVM, member, name]);
useEffect(() => {
disambiguatedProfileVM.setUserStatus(userStatus);
}, [disambiguatedProfileVM, userStatus]);
const nameJSX = <DisambiguatedProfileView vm={disambiguatedProfileVM} className="mx_DisambiguatedProfile" />;

const presenceState = member.presenceState;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
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, UserStatusMessageView } 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";

Expand Down Expand Up @@ -57,6 +57,7 @@
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,
Expand Down Expand Up @@ -448,6 +449,7 @@
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");
Expand Down Expand Up @@ -496,6 +498,10 @@
>
<span className="mx_RoomHeader_truncated mx_lineClamp">{roomName}</span>

{isDirectMessage && dmUserStatus && (

Check warning on line 501 in apps/web/src/components/views/rooms/RoomHeader/RoomHeader.tsx

View workflow job for this annotation

GitHub Actions / Tests

Uncovered Line

Line 501 is not covered by tests
<UserStatusMessageView status={dmUserStatus} className="mx_RoomHeader_userStatus" />
)}

{!isDirectMessage && joinRule === JoinRule.Public && (
<Tooltip label={_t("common|public_room")} placement="right">
<PublicIcon
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/views/settings/AvatarSetting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ const AvatarSetting: React.FC<IProps> = ({
<BaseAvatar
idName={placeholderId}
name={placeholderName}
size="90px"
size="120px"
url={avatarURL}
altText={avatarAccessibleName}
/>
Expand Down
48 changes: 32 additions & 16 deletions apps/web/src/components/views/settings/UserProfileSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import { logger } from "matrix-js-sdk/src/logger";
import { EditInPlace, Alert, ErrorMessage } from "@vector-im/compound-web";
import PopOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/pop-out";
import SignOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/sign-out";
import { Flex, useToastContext } from "@element-hq/web-shared-components";
import {
Flex,
SetStatusView,
useCreateAutoDisposedViewModel,
useToastContext,
} from "@element-hq/web-shared-components";

import { _t } from "../../../languageHandler";
import { OwnProfileStore } from "../../../stores/OwnProfileStore";
Expand All @@ -27,6 +32,8 @@ import LogoutDialog, { shouldShowLogoutDialog } from "../dialogs/LogoutDialog";
import Modal from "../../../Modal";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { SettingsSection } from "./shared/SettingsSection.tsx";
import { SetStatusViewModel } from "../../../viewmodels/status/SetStatusViewModel.ts";
import SettingsStore from "../../../settings/SettingsStore.ts";

const SpinnerToast: React.FC<{ children?: ReactNode }> = ({ children }) => (
<>
Expand Down Expand Up @@ -131,6 +138,9 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({
})();
}, [client]);

const userStatusEnabled = SettingsStore.getValue("feature_user_status");
const setStatusVM = useCreateAutoDisposedViewModel(() => new SetStatusViewModel({ client }));

const onAvatarRemove = useCallback(async () => {
const removeToast = toastRack.displayToast(
<SpinnerToast>{_t("settings|general|avatar_remove_progress")}</SpinnerToast>,
Expand Down Expand Up @@ -212,21 +222,27 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({
placeholderId={client.getUserId() ?? ""}
disabled={!canSetAvatar}
/>
<EditInPlace
className="mx_UserProfileSettings_profile_displayName"
label={_t("settings|general|display_name")}
value={displayName}
saveButtonLabel={_t("common|save")}
cancelButtonLabel={_t("common|cancel")}
savedLabel={_t("common|saved")}
savingLabel={_t("common|updating")}
onChange={onDisplayNameChanged}
onCancel={onDisplayNameCancel}
onSave={onDisplayNameSave}
disabled={!canSetDisplayName}
>
{displayNameError && <ErrorMessage>{_t("settings|general|display_name_error")}</ErrorMessage>}
</EditInPlace>

<Flex direction="column" className="mx_UserProfileSettings_profile_nameAndStatus">
<EditInPlace
className="mx_UserProfileSettings_profile_displayName"
label={_t("settings|general|display_name")}
value={displayName}
saveButtonLabel={_t("common|save")}
cancelButtonLabel={_t("common|cancel")}
savedLabel={_t("common|saved")}
savingLabel={_t("common|updating")}
onChange={onDisplayNameChanged}
onCancel={onDisplayNameCancel}
onSave={onDisplayNameSave}
disabled={!canSetDisplayName}
>
{displayNameError && (
<ErrorMessage>{_t("settings|general|display_name_error")}</ErrorMessage>
)}
</EditInPlace>
{userStatusEnabled && <SetStatusView vm={setStatusVM} />}
</Flex>
</div>
{avatarError && (
<Alert title={_t("settings|general|avatar_upload_error_title")} type="critical">
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/views/spaces/SpacePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ const SpacePanel: React.FC = () => {
ref={ref}
aria-label={_t("common|spaces")}
>
<UserMenu vm={userMenuVm} className="mx_UserMenu" />
<UserMenu vm={userMenuVm} setStatusVm={userMenuVm.setStatusVm} className="mx_UserMenu" />
<AccessibleButton
className={classNames("mx_SpacePanel_toggleCollapse", {
expanded: !isPanelCollapsed,
Expand Down
30 changes: 7 additions & 23 deletions apps/web/src/hooks/useUserStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,13 @@ Please see LICENSE files in the repository root for full details.
*/

import { useEffect, useState } from "react";
import { ClientEvent, MatrixError } from "matrix-js-sdk/src/matrix";
import { logger as rootLogger } from "matrix-js-sdk/src/logger";
import { ClientEvent } from "matrix-js-sdk/src/matrix";
import { type UserStatus } from "@element-hq/web-shared-components";

import { useMatrixClientContext } from "../contexts/MatrixClientContext";
import { useTypedEventEmitter } from "./useEventEmitter";
import { useFeatureEnabled } from "./useSettings";
import { validateUserStatus } from "../utils/userStatus";

const logger = rootLogger.getChild("useUserStatus");
import { fetchUserStatus, validateUserStatus } from "../utils/userStatus";

/**
* Hook to get the MSC4426 user status for a given user ID. Returns undefined if the feature is disabled,
Expand All @@ -27,43 +24,30 @@ const logger = rootLogger.getChild("useUserStatus");
export function useUserStatus(userId: string | undefined): UserStatus | undefined {
const isEnabled = useFeatureEnabled("feature_user_status");
const matrixClient = useMatrixClientContext();
const [rawUserStatus, setRawUserStatus] = useState<unknown>();
const [userStatus, setUserStatus] = useState<UserStatus | undefined>();

useTypedEventEmitter(matrixClient, ClientEvent.UserProfileUpdate, (syncedUserId, syncProfile) => {
if (syncedUserId !== userId) {
return;
}

setRawUserStatus(syncProfile["org.matrix.msc4426.status"]);
setUserStatus(validateUserStatus(syncProfile["org.matrix.msc4426.status"]));
});
useEffect(() => {
(async () => {
if (!isEnabled) {
return;
}
if (!userId) {
setRawUserStatus(undefined);
return;
}
if ((await matrixClient.doesServerSupportExtendedProfiles()) === false) {
setRawUserStatus(undefined);
setUserStatus(undefined);
return;
}
try {
const result = await matrixClient.getExtendedProfileProperty(userId, "org.matrix.msc4426.status");
setRawUserStatus(result);
} catch (ex) {
if (ex instanceof MatrixError && ex.errcode === "M_NOT_FOUND") {
setRawUserStatus(undefined);
} else {
logger.warn(`Failed to get userStatus for ${userId}`, ex);
}
}
setUserStatus(await fetchUserStatus(matrixClient, userId));
})();
}, [isEnabled, userId, matrixClient]);
if (!isEnabled) {
return;
}

return validateUserStatus(rawUserStatus);
return userStatus;
}
Loading
Loading