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

@@ -37,6 +37,69 @@ interface AvatarFields {
avatarUpdatedAt?: number;
}
interface ProfileFields {
displayName: string;
description?: string;
profileUpdatedAt?: number;
}
function hasOwnProperty(object: object, key: string): boolean {
return Object.prototype.hasOwnProperty.call(object, key);
}
function normalizeProfileUpdatedAt(value: unknown): number | undefined {
return typeof value === 'number' && Number.isFinite(value) && value > 0
? value
: undefined;
}
function normalizeDisplayName(value: unknown): string | undefined {
if (typeof value !== 'string') {
return undefined;
}
const normalized = value.trim().replace(/\s+/g, ' ');
return normalized || undefined;
}
function normalizeDescription(value: unknown): string | undefined {
if (typeof value !== 'string') {
return undefined;
}
const normalized = value.trim();
return normalized || undefined;
}
function mergeProfileFields(
existingValue: Partial<ProfileFields> | undefined,
incomingValue: Partial<ProfileFields>,
preferIncomingFallback = true
): ProfileFields {
const existingUpdatedAt = normalizeProfileUpdatedAt(existingValue?.profileUpdatedAt) ?? 0;
const incomingUpdatedAt = normalizeProfileUpdatedAt(incomingValue.profileUpdatedAt) ?? 0;
const preferIncoming = incomingUpdatedAt === existingUpdatedAt
? preferIncomingFallback
: incomingUpdatedAt > existingUpdatedAt;
const existingDisplayName = normalizeDisplayName(existingValue?.displayName);
const incomingDisplayName = normalizeDisplayName(incomingValue.displayName);
const existingDescription = normalizeDescription(existingValue?.description);
const incomingHasDescription = hasOwnProperty(incomingValue, 'description');
const incomingDescription = normalizeDescription(incomingValue.description);
return {
displayName: preferIncoming
? (incomingDisplayName || existingDisplayName || 'User')
: (existingDisplayName || incomingDisplayName || 'User'),
description: preferIncoming
? (incomingHasDescription ? incomingDescription : existingDescription)
: existingDescription,
profileUpdatedAt: Math.max(existingUpdatedAt, incomingUpdatedAt) || undefined
};
}
function mergeAvatarFields(
existingValue: AvatarFields | undefined,
incomingValue: AvatarFields,
@@ -112,10 +175,12 @@ function buildPresenceAwareUser(existingUser: User | undefined, incomingUser: Us
? incomingUser.status
: (existingUser?.status && existingUser.status !== 'offline' ? existingUser.status : 'online'))
: 'offline';
const profileFields = mergeProfileFields(existingUser, incomingUser, true);
return {
...existingUser,
...incomingUser,
...profileFields,
...mergeAvatarFields(existingUser, incomingUser, true),
presenceServerIds,
isOnline,
@@ -128,17 +193,21 @@ function buildAvatarUser(existingUser: User | undefined, incomingUser: {
oderId: string;
username: string;
displayName: string;
avatarUrl: string;
description?: string;
profileUpdatedAt?: number;
avatarUrl?: string;
avatarHash?: string;
avatarMime?: string;
avatarUpdatedAt?: number;
}): User {
const profileFields = mergeProfileFields(existingUser, incomingUser, true);
return {
...existingUser,
id: incomingUser.id,
oderId: incomingUser.oderId,
username: incomingUser.username || existingUser?.username || 'user',
displayName: incomingUser.displayName || existingUser?.displayName || 'User',
...profileFields,
status: existingUser?.status ?? 'offline',
role: existingUser?.role ?? 'member',
joinedAt: existingUser?.joinedAt ?? Date.now(),
@@ -230,6 +299,18 @@ export const usersReducer = createReducer(
state
);
}),
on(UsersActions.updateCurrentUserProfile, (state, { profile }) => {
if (!state.currentUserId)
return state;
return usersAdapter.updateOne(
{
id: state.currentUserId,
changes: mergeProfileFields(state.entities[state.currentUserId], profile, true)
},
state
);
}),
on(UsersActions.updateCurrentUserAvatar, (state, { avatar }) => {
if (!state.currentUserId)
return state;