feat: Add user statuses and cards

This commit is contained in:
2026-04-16 22:52:45 +02:00
parent b4ac0cdc92
commit 2927a86fbb
57 changed files with 1964 additions and 185 deletions

View File

@@ -113,7 +113,8 @@ export class RoomStateSyncEffects {
.map((user) =>
buildSignalingUser(user, {
...buildKnownUserExtras(room, user.oderId),
presenceServerIds: [signalingMessage.serverId]
presenceServerIds: [signalingMessage.serverId],
...(user.status ? { status: user.status } : {})
})
);
const actions: Action[] = [
@@ -139,7 +140,8 @@ export class RoomStateSyncEffects {
const joinedUser = {
oderId: signalingMessage.oderId,
displayName: signalingMessage.displayName
displayName: signalingMessage.displayName,
status: signalingMessage.status
};
const actions: Action[] = [
UsersActions.userJoined({
@@ -188,6 +190,34 @@ export class RoomStateSyncEffects {
return actions;
}
case 'status_update': {
if (!signalingMessage.oderId || !signalingMessage.status)
return EMPTY;
const validStatuses = [
'online',
'away',
'busy',
'offline'
];
if (!validStatuses.includes(signalingMessage.status))
return EMPTY;
// 'offline' from the server means the user chose Invisible;
// display them as disconnected to other users.
const mappedStatus = signalingMessage.status === 'offline'
? 'disconnected'
: signalingMessage.status as 'online' | 'away' | 'busy';
return [
UsersActions.updateRemoteUserStatus({
userId: signalingMessage.oderId,
status: mappedStatus
})
];
}
case 'access_denied': {
if (isWrongServer(signalingMessage.serverId, viewedServerId))
return EMPTY;

View File

@@ -0,0 +1,55 @@
import { buildSignalingUser } from './rooms.helpers';
describe('buildSignalingUser - status', () => {
it('defaults to online when no status provided', () => {
const user = buildSignalingUser({ oderId: 'u1', displayName: 'Alice' });
expect(user.status).toBe('online');
});
it('uses away status when provided', () => {
const user = buildSignalingUser({ oderId: 'u1', displayName: 'Alice', status: 'away' });
expect(user.status).toBe('away');
});
it('uses busy status when provided', () => {
const user = buildSignalingUser({ oderId: 'u1', displayName: 'Bob', status: 'busy' });
expect(user.status).toBe('busy');
});
it('ignores invalid status and defaults to online', () => {
const user = buildSignalingUser({ oderId: 'u1', displayName: 'Eve', status: 'invalid' });
expect(user.status).toBe('online');
});
it('maps offline status to disconnected', () => {
const user = buildSignalingUser({ oderId: 'u1', displayName: 'Ghost', status: 'offline' });
expect(user.status).toBe('disconnected');
});
it('allows extras to override status', () => {
const user = buildSignalingUser(
{ oderId: 'u1', displayName: 'Dave', status: 'away' },
{ status: 'busy' }
);
expect(user.status).toBe('busy');
});
it('preserves other fields', () => {
const user = buildSignalingUser(
{ oderId: 'u1', displayName: 'Alice', status: 'away' },
{ presenceServerIds: ['server-1'] }
);
expect(user.oderId).toBe('u1');
expect(user.id).toBe('u1');
expect(user.displayName).toBe('Alice');
expect(user.isOnline).toBe(true);
expect(user.role).toBe('member');
});
});

View File

@@ -10,17 +10,28 @@ import { ROOM_URL_PATTERN } from '../../core/constants';
/** Build a minimal User object from signaling payload. */
export function buildSignalingUser(
data: { oderId: string; displayName?: string },
data: { oderId: string; displayName?: string; status?: string },
extras: Record<string, unknown> = {}
) {
const displayName = data.displayName?.trim() || 'User';
const rawStatus = ([
'online',
'away',
'busy',
'offline'
] as const).includes(data.status as 'online')
? data.status as 'online' | 'away' | 'busy' | 'offline'
: 'online';
// 'offline' from the server means the user chose Invisible;
// display them as disconnected to other users.
const status = rawStatus === 'offline' ? 'disconnected' as const : rawStatus;
return {
oderId: data.oderId,
id: data.oderId,
username: displayName.toLowerCase().replace(/\s+/g, '_'),
displayName,
status: 'online' as const,
status,
isOnline: true,
role: 'member' as const,
joinedAt: Date.now(),
@@ -180,7 +191,8 @@ export interface RoomPresenceSignalingMessage {
reason?: string;
serverId?: string;
serverIds?: string[];
users?: { oderId: string; displayName: string }[];
users?: { oderId: string; displayName: string; status?: string }[];
oderId?: string;
displayName?: string;
status?: string;
}