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

@@ -10,6 +10,8 @@ export async function handleSaveUser(command: SaveUserCommand, dataSource: DataS
oderId: user.oderId ?? null,
username: user.username ?? null,
displayName: user.displayName ?? null,
description: user.description ?? null,
profileUpdatedAt: user.profileUpdatedAt ?? null,
avatarUrl: user.avatarUrl ?? null,
avatarHash: user.avatarHash ?? null,
avatarMime: user.avatarMime ?? null,

View File

@@ -46,6 +46,8 @@ export function rowToUser(row: UserEntity) {
oderId: row.oderId ?? '',
username: row.username ?? '',
displayName: row.displayName ?? '',
description: row.description ?? undefined,
profileUpdatedAt: row.profileUpdatedAt ?? undefined,
avatarUrl: row.avatarUrl ?? undefined,
avatarHash: row.avatarHash ?? undefined,
avatarMime: row.avatarMime ?? undefined,

View File

@@ -66,6 +66,8 @@ export interface RoomMemberRecord {
oderId?: string;
username: string;
displayName: string;
description?: string;
profileUpdatedAt?: number;
avatarUrl?: string;
avatarHash?: string;
avatarMime?: string;
@@ -338,16 +340,18 @@ function normalizeRoomMember(rawMember: Record<string, unknown>, now: number): R
const { joinedAt, lastSeenAt } = resolveRoomMemberTimes(rawMember, now);
const username = trimmedString(rawMember, 'username');
const displayName = trimmedString(rawMember, 'displayName');
const description = trimmedString(rawMember, 'description');
const profileUpdatedAt = isFiniteNumber(rawMember['profileUpdatedAt']) ? rawMember['profileUpdatedAt'] : undefined;
const avatarUrl = trimmedString(rawMember, 'avatarUrl');
const avatarHash = trimmedString(rawMember, 'avatarHash');
const avatarMime = trimmedString(rawMember, 'avatarMime');
const avatarUpdatedAt = isFiniteNumber(rawMember['avatarUpdatedAt']) ? rawMember['avatarUpdatedAt'] : undefined;
return {
const member: RoomMemberRecord = {
id: normalizedId || normalizedKey,
oderId: normalizedOderId || undefined,
username: username || fallbackUsername({ id: normalizedId || normalizedKey, oderId: normalizedOderId || undefined, displayName }),
displayName: displayName || fallbackDisplayName({ id: normalizedId || normalizedKey, oderId: normalizedOderId || undefined, username }),
profileUpdatedAt,
avatarUrl: avatarUrl || undefined,
avatarHash: avatarHash || undefined,
avatarMime: avatarMime || undefined,
@@ -357,6 +361,12 @@ function normalizeRoomMember(rawMember: Record<string, unknown>, now: number): R
joinedAt,
lastSeenAt
};
if (Object.prototype.hasOwnProperty.call(rawMember, 'description')) {
member.description = description || undefined;
}
return member;
}
function mergeRoomMembers(existingMember: RoomMemberRecord | undefined, incomingMember: RoomMemberRecord): RoomMemberRecord {
@@ -365,6 +375,11 @@ function mergeRoomMembers(existingMember: RoomMemberRecord | undefined, incoming
}
const preferIncoming = incomingMember.lastSeenAt >= existingMember.lastSeenAt;
const existingProfileUpdatedAt = existingMember.profileUpdatedAt ?? 0;
const incomingProfileUpdatedAt = incomingMember.profileUpdatedAt ?? 0;
const preferIncomingProfile = incomingProfileUpdatedAt === existingProfileUpdatedAt
? preferIncoming
: incomingProfileUpdatedAt > existingProfileUpdatedAt;
const existingAvatarUpdatedAt = existingMember.avatarUpdatedAt ?? 0;
const incomingAvatarUpdatedAt = incomingMember.avatarUpdatedAt ?? 0;
const preferIncomingAvatar = incomingAvatarUpdatedAt === existingAvatarUpdatedAt
@@ -377,9 +392,13 @@ function mergeRoomMembers(existingMember: RoomMemberRecord | undefined, incoming
username: preferIncoming
? (incomingMember.username || existingMember.username)
: (existingMember.username || incomingMember.username),
displayName: preferIncoming
displayName: preferIncomingProfile
? (incomingMember.displayName || existingMember.displayName)
: (existingMember.displayName || incomingMember.displayName),
description: preferIncomingProfile
? (Object.prototype.hasOwnProperty.call(incomingMember, 'description') ? incomingMember.description : existingMember.description)
: existingMember.description,
profileUpdatedAt: Math.max(existingProfileUpdatedAt, incomingProfileUpdatedAt) || undefined,
avatarUrl: preferIncomingAvatar
? (incomingMember.avatarUrl || existingMember.avatarUrl)
: (existingMember.avatarUrl || incomingMember.avatarUrl),
@@ -780,6 +799,8 @@ export async function replaceRoomRelations(
oderId: member.oderId ?? null,
username: member.username,
displayName: member.displayName,
description: member.description ?? null,
profileUpdatedAt: member.profileUpdatedAt ?? null,
avatarUrl: member.avatarUrl ?? null,
avatarHash: member.avatarHash ?? null,
avatarMime: member.avatarMime ?? null,
@@ -930,6 +951,8 @@ export async function loadRoomRelationsMap(
oderId: row.oderId ?? undefined,
username: row.username,
displayName: row.displayName,
description: row.description ?? undefined,
profileUpdatedAt: row.profileUpdatedAt ?? undefined,
avatarUrl: row.avatarUrl ?? undefined,
avatarHash: row.avatarHash ?? undefined,
avatarMime: row.avatarMime ?? undefined,

View File

@@ -105,6 +105,8 @@ export interface UserPayload {
oderId?: string;
username?: string;
displayName?: string;
description?: string;
profileUpdatedAt?: number;
avatarUrl?: string;
avatarHash?: string;
avatarMime?: string;

View File

@@ -24,6 +24,12 @@ export class RoomMemberEntity {
@Column('text')
displayName!: string;
@Column('text', { nullable: true })
description!: string | null;
@Column('integer', { nullable: true })
profileUpdatedAt!: number | null;
@Column('text', { nullable: true })
avatarUrl!: string | null;

View File

@@ -18,6 +18,12 @@ export class UserEntity {
@Column('text', { nullable: true })
displayName!: string | null;
@Column('text', { nullable: true })
description!: string | null;
@Column('integer', { nullable: true })
profileUpdatedAt!: number | null;
@Column('text', { nullable: true })
avatarUrl!: string | null;

View File

@@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddUserProfileMetadata1000000000007 implements MigrationInterface {
name = 'AddUserProfileMetadata1000000000007';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" ADD COLUMN "description" TEXT`);
await queryRunner.query(`ALTER TABLE "users" ADD COLUMN "profileUpdatedAt" INTEGER`);
await queryRunner.query(`ALTER TABLE "room_members" ADD COLUMN "description" TEXT`);
await queryRunner.query(`ALTER TABLE "room_members" ADD COLUMN "profileUpdatedAt" INTEGER`);
}
public async down(): Promise<void> {
// SQLite column removal requires table rebuilds. Keep rollback no-op.
}
}