feat: Add user metadata changing display name and description with sync
All checks were successful
Queue Release Build / prepare (push) Successful in 28s
Deploy Web Apps / deploy (push) Successful in 5m2s
Queue Release Build / build-windows (push) Successful in 16m44s
Queue Release Build / build-linux (push) Successful in 27m12s
Queue Release Build / finalize (push) Successful in 22s

This commit is contained in:
2026-04-17 22:04:18 +02:00
parent 3ba8a2c9eb
commit bd21568726
41 changed files with 1176 additions and 191 deletions

View File

@@ -5,15 +5,14 @@ import {
createEffect,
ofType
} from '@ngrx/effects';
import { Action } from '@ngrx/store';
import { Store } from '@ngrx/store';
import { Store, type Action } from '@ngrx/store';
import { EMPTY } from 'rxjs';
import {
mergeMap,
tap,
withLatestFrom
} from 'rxjs/operators';
import {
import type {
ChatEvent,
Room,
RoomMember,
@@ -394,7 +393,28 @@ export class RoomMembersSyncEffects {
);
}
return this.createRoomMemberUpdateActions(room, members);
const actions = this.createRoomMemberUpdateActions(room, members);
const currentUserId = currentUser?.oderId || currentUser?.id;
for (const member of members) {
const memberId = member.oderId || member.id;
if (!member.avatarUrl || !memberId || memberId === currentUserId) {
continue;
}
actions.push(UsersActions.upsertRemoteUserAvatar({
user: {
id: member.id,
oderId: memberId,
username: member.username,
displayName: member.displayName,
avatarUrl: member.avatarUrl
}
}));
}
return actions;
}
private handleMemberLeave(

View File

@@ -36,6 +36,51 @@ function normalizeAvatarUpdatedAt(value: unknown): number | undefined {
: undefined;
}
function normalizeProfileUpdatedAt(value: unknown): number | undefined {
return typeof value === 'number' && Number.isFinite(value) && value > 0
? value
: undefined;
}
function normalizeDescription(value: unknown): string | undefined {
if (typeof value !== 'string') {
return undefined;
}
const normalized = value.trim();
return normalized || undefined;
}
function hasOwnProperty(object: object, key: string): boolean {
return Object.prototype.hasOwnProperty.call(object, key);
}
function mergeProfileFields(
existingMember: Pick<RoomMember, 'displayName' | 'description' | 'profileUpdatedAt'>,
incomingMember: Pick<RoomMember, 'displayName' | 'description' | 'profileUpdatedAt'>,
preferIncomingFallback: boolean
): Pick<RoomMember, 'displayName' | 'description' | 'profileUpdatedAt'> {
const existingUpdatedAt = existingMember.profileUpdatedAt ?? 0;
const incomingUpdatedAt = incomingMember.profileUpdatedAt ?? 0;
const preferIncoming = incomingUpdatedAt === existingUpdatedAt
? preferIncomingFallback
: incomingUpdatedAt > existingUpdatedAt;
const incomingHasDescription = hasOwnProperty(incomingMember, 'description');
const incomingDescription = normalizeDescription(incomingMember.description);
const existingDescription = normalizeDescription(existingMember.description);
return {
displayName: preferIncoming
? (incomingMember.displayName || existingMember.displayName)
: (existingMember.displayName || incomingMember.displayName),
description: preferIncoming
? (incomingHasDescription ? incomingDescription : existingDescription)
: existingDescription,
profileUpdatedAt: Math.max(existingUpdatedAt, incomingUpdatedAt) || undefined
};
}
function mergeAvatarFields(
existingMember: Pick<RoomMember, 'avatarUrl' | 'avatarHash' | 'avatarMime' | 'avatarUpdatedAt'>,
incomingMember: Pick<RoomMember, 'avatarUrl' | 'avatarHash' | 'avatarMime' | 'avatarUpdatedAt'>,
@@ -73,12 +118,12 @@ function normalizeMember(member: RoomMember, now = Date.now()): RoomMember {
typeof member.joinedAt === 'number' && Number.isFinite(member.joinedAt)
? member.joinedAt
: lastSeenAt;
return {
const nextMember: RoomMember = {
id: member.id || key,
oderId: member.oderId || undefined,
username: member.username || fallbackUsername(member),
displayName: fallbackDisplayName(member),
profileUpdatedAt: normalizeProfileUpdatedAt(member.profileUpdatedAt),
avatarUrl: member.avatarUrl || undefined,
avatarHash: member.avatarHash || undefined,
avatarMime: member.avatarMime || undefined,
@@ -88,6 +133,12 @@ function normalizeMember(member: RoomMember, now = Date.now()): RoomMember {
joinedAt,
lastSeenAt
};
if (hasOwnProperty(member, 'description')) {
nextMember.description = normalizeDescription(member.description);
}
return nextMember;
}
function compareMembers(firstMember: RoomMember, secondMember: RoomMember): number {
@@ -128,6 +179,7 @@ function mergeMembers(
const normalizedExisting = normalizeMember(existingMember, now);
const preferIncoming = normalizedIncoming.lastSeenAt >= normalizedExisting.lastSeenAt;
const profileFields = mergeProfileFields(normalizedExisting, normalizedIncoming, preferIncoming);
const avatarFields = mergeAvatarFields(normalizedExisting, normalizedIncoming, preferIncoming);
return {
@@ -136,9 +188,7 @@ function mergeMembers(
username: preferIncoming
? (normalizedIncoming.username || normalizedExisting.username)
: (normalizedExisting.username || normalizedIncoming.username),
displayName: preferIncoming
? (normalizedIncoming.displayName || normalizedExisting.displayName)
: (normalizedExisting.displayName || normalizedIncoming.displayName),
...profileFields,
...avatarFields,
role: mergeRole(normalizedExisting.role, normalizedIncoming.role, preferIncoming),
roleIds: preferIncoming
@@ -177,6 +227,8 @@ export function roomMemberFromUser(
oderId: user.oderId || undefined,
username: user.username || '',
displayName: user.displayName || user.username || 'User',
description: user.description,
profileUpdatedAt: user.profileUpdatedAt,
avatarUrl: user.avatarUrl,
avatarHash: user.avatarHash,
avatarMime: user.avatarMime,

View File

@@ -1,6 +1,6 @@
import { Store } from '@ngrx/store';
import { firstValueFrom } from 'rxjs';
import { Room, User } from '../../shared-kernel';
import type { Room, User } from '../../shared-kernel';
import {
type RoomSignalSource,
type ServerSourceSelector,
@@ -353,6 +353,8 @@ export class RoomSignalingConnection {
const wsUrl = this.serverDirectory.getWebSocketUrl(selector);
const oderId = resolvedOderId || user?.oderId || this.webrtc.peerId();
const displayName = resolveUserDisplayName(user);
const description = user?.description;
const profileUpdatedAt = user?.profileUpdatedAt;
const sameSignalRooms = this.getRoomsForSignalingUrl(this.includeRoom(savedRooms, room), wsUrl);
const backgroundRooms = sameSignalRooms.filter((candidate) => candidate.id !== room.id);
const joinCurrentEndpointRooms = () => {
@@ -361,7 +363,10 @@ export class RoomSignalingConnection {
}
this.webrtc.setCurrentServer(room.id);
this.webrtc.identify(oderId, displayName, wsUrl);
this.webrtc.identify(oderId, displayName, wsUrl, {
description,
profileUpdatedAt
});
for (const backgroundRoom of backgroundRooms) {
this.webrtc.joinRoom(backgroundRoom.id, oderId, wsUrl);

View File

@@ -5,7 +5,7 @@ import {
createEffect,
ofType
} from '@ngrx/effects';
import { Action, Store } from '@ngrx/store';
import { Store, type Action } from '@ngrx/store';
import {
of,
from,
@@ -30,7 +30,7 @@ import {
import { RealtimeSessionFacade } from '../../core/realtime';
import { DatabaseService } from '../../infrastructure/persistence';
import { resolveRoomPermission } from '../../domains/access-control';
import {
import type {
ChatEvent,
Room,
RoomSettings,
@@ -50,9 +50,9 @@ import {
resolveRoom,
sanitizeRoomSnapshot,
normalizeIncomingBans,
getPersistedCurrentUserId,
RoomPresenceSignalingMessage
getPersistedCurrentUserId
} from './rooms.helpers';
import type { RoomPresenceSignalingMessage } from './rooms.helpers';
/**
* NgRx effects for real-time state synchronisation: signaling presence
@@ -113,6 +113,8 @@ export class RoomStateSyncEffects {
.map((user) =>
buildSignalingUser(user, {
...buildKnownUserExtras(room, user.oderId),
description: user.description,
profileUpdatedAt: user.profileUpdatedAt,
presenceServerIds: [signalingMessage.serverId],
...(user.status ? { status: user.status } : {})
})
@@ -141,12 +143,16 @@ export class RoomStateSyncEffects {
const joinedUser = {
oderId: signalingMessage.oderId,
displayName: signalingMessage.displayName,
description: signalingMessage.description,
profileUpdatedAt: signalingMessage.profileUpdatedAt,
status: signalingMessage.status
};
const actions: Action[] = [
UsersActions.userJoined({
user: buildSignalingUser(joinedUser, {
...buildKnownUserExtras(room, joinedUser.oderId),
description: joinedUser.description,
profileUpdatedAt: joinedUser.profileUpdatedAt,
presenceServerIds: [signalingMessage.serverId]
})
})

View File

@@ -48,6 +48,8 @@ export function buildKnownUserExtras(room: Room | null, identifier: string): Rec
return {
username: knownMember.username,
description: knownMember.description,
profileUpdatedAt: knownMember.profileUpdatedAt,
avatarUrl: knownMember.avatarUrl,
avatarHash: knownMember.avatarHash,
avatarMime: knownMember.avatarMime,
@@ -194,8 +196,10 @@ export interface RoomPresenceSignalingMessage {
reason?: string;
serverId?: string;
serverIds?: string[];
users?: { oderId: string; displayName: string; status?: string }[];
users?: { oderId: string; displayName: string; description?: string; profileUpdatedAt?: number; status?: string }[];
oderId?: string;
displayName?: string;
description?: string;
profileUpdatedAt?: number;
status?: string;
}