feat: Add profile images

This commit is contained in:
2026-04-17 03:05:47 +02:00
parent 35b616fb77
commit 17738ec484
49 changed files with 2622 additions and 89 deletions

View File

@@ -30,6 +30,39 @@ function mergePresenceServerIds(
return normalizePresenceServerIds([...(existingServerIds ?? []), ...(incomingServerIds ?? [])]);
}
interface AvatarFields {
avatarUrl?: string;
avatarHash?: string;
avatarMime?: string;
avatarUpdatedAt?: number;
}
function mergeAvatarFields(
existingValue: AvatarFields | undefined,
incomingValue: AvatarFields,
preferIncomingFallback = true
): AvatarFields {
const existingUpdatedAt = existingValue?.avatarUpdatedAt ?? 0;
const incomingUpdatedAt = incomingValue.avatarUpdatedAt ?? 0;
const preferIncoming = incomingUpdatedAt === existingUpdatedAt
? preferIncomingFallback
: incomingUpdatedAt > existingUpdatedAt;
return {
avatarUrl: preferIncoming
? (incomingValue.avatarUrl || existingValue?.avatarUrl)
: (existingValue?.avatarUrl || incomingValue.avatarUrl),
avatarHash: preferIncoming
? (incomingValue.avatarHash || existingValue?.avatarHash)
: (existingValue?.avatarHash || incomingValue.avatarHash),
avatarMime: preferIncoming
? (incomingValue.avatarMime || existingValue?.avatarMime)
: (existingValue?.avatarMime || incomingValue.avatarMime),
avatarUpdatedAt: Math.max(existingUpdatedAt, incomingUpdatedAt) || undefined
};
}
function buildDisconnectedVoiceState(user: User): User['voiceState'] {
if (!user.voiceState) {
return undefined;
@@ -83,12 +116,36 @@ function buildPresenceAwareUser(existingUser: User | undefined, incomingUser: Us
return {
...existingUser,
...incomingUser,
...mergeAvatarFields(existingUser, incomingUser, true),
presenceServerIds,
isOnline,
status
};
}
function buildAvatarUser(existingUser: User | undefined, incomingUser: {
id: string;
oderId: string;
username: string;
displayName: string;
avatarUrl: string;
avatarHash?: string;
avatarMime?: string;
avatarUpdatedAt?: number;
}): User {
return {
...existingUser,
id: incomingUser.id,
oderId: incomingUser.oderId,
username: incomingUser.username || existingUser?.username || 'user',
displayName: incomingUser.displayName || existingUser?.displayName || 'User',
status: existingUser?.status ?? 'offline',
role: existingUser?.role ?? 'member',
joinedAt: existingUser?.joinedAt ?? Date.now(),
...mergeAvatarFields(existingUser, incomingUser, true)
};
}
function buildPresenceRemovalChanges(
user: User,
update: { serverId?: string; serverIds?: readonly string[] }
@@ -173,6 +230,18 @@ export const usersReducer = createReducer(
state
);
}),
on(UsersActions.updateCurrentUserAvatar, (state, { avatar }) => {
if (!state.currentUserId)
return state;
return usersAdapter.updateOne(
{
id: state.currentUserId,
changes: mergeAvatarFields(state.entities[state.currentUserId], avatar, true)
},
state
);
}),
on(UsersActions.loadRoomUsers, (state) => ({
...state,
loading: true,
@@ -256,6 +325,9 @@ export const usersReducer = createReducer(
state
)
),
on(UsersActions.upsertRemoteUserAvatar, (state, { user }) =>
usersAdapter.upsertOne(buildAvatarUser(state.entities[user.id], user), state)
),
on(UsersActions.updateUserRole, (state, { userId, role }) =>
usersAdapter.updateOne(
{