fix: Bug - Users appear as both online and offline

Align chat message sender ids with per-server presence identities so profile cards opened from message authors resolve the same live user state as the members panel.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-13 20:55:13 +02:00
parent c3c2f01cc6
commit b2a2d9d770
11 changed files with 274 additions and 26 deletions

View File

@@ -0,0 +1,49 @@
import type { User } from '../../shared-kernel';
import {
buildUserIdentityLookup,
findUserEntityByIdentity,
resolveUserByIdentity
} from './user-identity-lookup.rules';
function createUser(overrides: Partial<User> = {}): User {
return {
id: 'server-user-1',
oderId: 'server-user-1',
username: 'alice',
displayName: 'Alice',
status: 'online',
role: 'member',
joinedAt: 1,
presenceServerIds: ['room-1'],
isOnline: true,
...overrides
};
}
describe('user-identity-lookup.rules', () => {
it('indexes id, oderId, and peerId aliases in buildUserIdentityLookup', () => {
const user = createUser({
id: 'server-user-1',
oderId: 'oder-1',
peerId: 'peer-1'
});
const lookup = buildUserIdentityLookup([user]);
expect(resolveUserByIdentity(lookup, 'server-user-1')).toBe(user);
expect(resolveUserByIdentity(lookup, 'oder-1')).toBe(user);
expect(resolveUserByIdentity(lookup, 'peer-1')).toBe(user);
});
it('findUserEntityByIdentity resolves users keyed by a different entity id', () => {
const user = createUser({
id: 'server-user-1',
oderId: 'oder-1'
});
const entities = {
'server-user-1': user
};
expect(findUserEntityByIdentity(entities, 'oder-1')).toBe(user);
expect(findUserEntityByIdentity(entities, 'home-user-1')).toBeUndefined();
});
});

View File

@@ -0,0 +1,68 @@
import type { User } from '../../shared-kernel';
type UserIdentityFields = Pick<User, 'id' | 'oderId' | 'peerId'>;
function collectUserIdentityKeys(user: UserIdentityFields): string[] {
const keys: string[] = [];
if (user.id?.trim()) {
keys.push(user.id.trim());
}
if (user.oderId?.trim() && user.oderId !== user.id) {
keys.push(user.oderId.trim());
}
if (user.peerId?.trim() && user.peerId !== user.id && user.peerId !== user.oderId) {
keys.push(user.peerId.trim());
}
return keys;
}
/** Build a lookup map keyed by every known identity alias for each user. */
export function buildUserIdentityLookup(users: readonly User[]): ReadonlyMap<string, User> {
const lookup = new Map<string, User>();
for (const user of users) {
for (const key of collectUserIdentityKeys(user)) {
lookup.set(key, user);
}
}
return lookup;
}
/** Resolve a user from a pre-built identity lookup map. */
export function resolveUserByIdentity(
lookup: ReadonlyMap<string, User>,
identity: string | undefined
): User | undefined {
if (!identity?.trim()) {
return undefined;
}
return lookup.get(identity.trim());
}
/** Resolve a user entity when NgRx entity keys may not match the queried identity. */
export function findUserEntityByIdentity(
entities: Record<string, User | undefined>,
identity: string | undefined
): User | undefined {
if (!identity?.trim()) {
return undefined;
}
const trimmed = identity.trim();
const direct = entities[trimmed];
if (direct) {
return direct;
}
return Object.values(entities).find((user): user is User =>
!!user
&& (user.id === trimmed || user.oderId === trimmed || user.peerId === trimmed)
);
}