fix: Bug - Same user logged in on multiple clients acts like 2 different users

Collapse home and signal-server actor aliases into one canonical room member so multi-device sessions no longer duplicate the local user in the members panel.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-12 00:52:59 +02:00
parent 07e91a0d09
commit e75b4a38ed
3 changed files with 140 additions and 21 deletions

View File

@@ -26,6 +26,7 @@ import { selectCurrentRoom, selectSavedRooms } from './rooms.selectors';
import { normalizeRoomAccessControl, resolveLegacyRole } from '../../domains/access-control';
import {
areRoomMembersEqual,
collapseSelfRoomMembers,
findRoomMember,
mergeRoomMembers,
pruneRoomMembers,
@@ -36,6 +37,7 @@ import {
updateRoomMemberRole,
upsertRoomMember
} from './room-members.helpers';
import { resolveRoomMemberActorIdentity } from './room-member-identity.rules';
import { SignalServerAuthService } from '../../domains/authentication/application/services/signal-server-auth.service';
import { isSelfPresenceUserId } from '../../domains/authentication/domain/logic/self-presence-identity.rules';
@@ -63,7 +65,7 @@ export class RoomMembersSyncEffects {
room.members ?? [],
this.buildCurrentUserMember(room, currentUser, true)
);
const actions = this.createRoomMemberUpdateActions(room, members);
const actions = this.createRoomMemberUpdateActions(room, members, currentUser, true);
return actions.length > 0 ? actions : EMPTY;
})
@@ -93,7 +95,7 @@ export class RoomMembersSyncEffects {
currentRoom.members ?? [],
this.buildCurrentUserMember(currentRoom, currentUser, true)
);
const actions = this.createRoomMemberUpdateActions(currentRoom, members);
const actions = this.createRoomMemberUpdateActions(currentRoom, members, currentUser, true);
return actions.length > 0 ? actions : EMPTY;
})
@@ -104,13 +106,20 @@ export class RoomMembersSyncEffects {
syncRoleChangesIntoCurrentRoom$ = createEffect(() =>
this.actions$.pipe(
ofType(UsersActions.updateUserRole),
withLatestFrom(this.store.select(selectCurrentRoom)),
mergeMap(([{ userId, role }, currentRoom]) => {
withLatestFrom(
this.store.select(selectCurrentRoom),
this.store.select(selectCurrentUser)
),
mergeMap(([
{ userId, role },
currentRoom,
currentUser
]) => {
if (!currentRoom)
return EMPTY;
const members = updateRoomMemberRole(currentRoom.members ?? [], userId, role);
const actions = this.createRoomMemberUpdateActions(currentRoom, members);
const actions = this.createRoomMemberUpdateActions(currentRoom, members, currentUser, true);
return actions.length > 0 ? actions : EMPTY;
})
@@ -194,7 +203,12 @@ export class RoomMembersSyncEffects {
members = upsertRoomMember(members, this.buildPresenceMember(room, user));
}
const actions = this.createRoomMemberUpdateActions(room, members);
const actions = this.createRoomMemberUpdateActions(
room,
members,
currentUser ?? null,
currentRoom?.id === room.id
);
return actions.length > 0 ? actions : EMPTY;
}
@@ -211,7 +225,12 @@ export class RoomMembersSyncEffects {
room.members ?? [],
this.buildPresenceMember(room, joinedUser)
);
const actions = this.createRoomMemberUpdateActions(room, members);
const actions = this.createRoomMemberUpdateActions(
room,
members,
currentUser ?? null,
currentRoom?.id === room.id
);
return actions.length > 0 ? actions : EMPTY;
}
@@ -221,7 +240,12 @@ export class RoomMembersSyncEffects {
return EMPTY;
const members = touchRoomMemberLastSeen(room.members ?? [], signalingMessage.oderId, Date.now());
const actions = this.createRoomMemberUpdateActions(room, members);
const actions = this.createRoomMemberUpdateActions(
room,
members,
currentUser ?? null,
currentRoom?.id === room.id
);
return actions.length > 0 ? actions : EMPTY;
}
@@ -339,15 +363,30 @@ export class RoomMembersSyncEffects {
}
private buildCurrentUserMember(room: Room, currentUser: User, isCurrentRoom: boolean): RoomMember {
const existingMember = findRoomMember(room.members ?? [], currentUser.oderId || currentUser.id);
const role = room.hostId === currentUser.id
const selfIds = this.signalServerAuth.resolveSelfPresenceUserIdsForRoom(currentUser, room.sourceUrl);
const identity = resolveRoomMemberActorIdentity(
room,
currentUser,
(serverUrl, fallback) => this.signalServerAuth.resolveActorUserIdForServer(serverUrl, fallback),
(user, roomSourceUrl) => this.signalServerAuth.resolveSelfPresenceUserIdsForRoom(user, roomSourceUrl)
);
const actorUserId = identity.actorUserId;
const existingMember = findRoomMember(room.members ?? [], actorUserId)
?? (room.members ?? []).find((member) =>
isSelfPresenceUserId(member.oderId, selfIds) || isSelfPresenceUserId(member.id, selfIds)
);
const isHost = room.hostId === currentUser.id
|| room.hostId === currentUser.oderId
|| room.hostId === actorUserId;
const role = isHost
? 'host'
: (isCurrentRoom ? currentUser.role : existingMember?.role ?? 'member');
const seenAt = existingMember?.lastSeenAt ?? currentUser.joinedAt ?? Date.now();
return {
...roomMemberFromUser(currentUser, seenAt, role),
id: existingMember?.id ?? currentUser.id,
id: identity.existingMemberId ?? actorUserId,
oderId: actorUserId,
joinedAt: existingMember?.joinedAt ?? currentUser.joinedAt ?? seenAt,
avatarUrl: currentUser.avatarUrl ?? existingMember?.avatarUrl,
role
@@ -375,10 +414,24 @@ export class RoomMembersSyncEffects {
};
}
private createRoomMemberUpdateActions(room: Room, members: RoomMember[]): Action[] {
return areRoomMembersEqual(room.members ?? [], members)
private createRoomMemberUpdateActions(
room: Room,
members: RoomMember[],
currentUser: User | null = null,
isCurrentRoom = false
): Action[] {
const normalizedMembers = currentUser
? collapseSelfRoomMembers(
members,
this.signalServerAuth.resolveSelfPresenceUserIdsForRoom(currentUser, room.sourceUrl),
this.buildCurrentUserMember(room, currentUser, isCurrentRoom),
Date.now()
)
: pruneRoomMembers(members);
return areRoomMembersEqual(room.members ?? [], normalizedMembers)
? []
: [RoomsActions.updateRoom({ roomId: room.id, changes: { members } })];
: [RoomsActions.updateRoom({ roomId: room.id, changes: { members: normalizedMembers } })];
}
private resolveRoleSyncRoom(
@@ -491,7 +544,7 @@ export class RoomMembersSyncEffects {
members
});
return this.createRoomMemberUpdateActions(room, members);
return this.createRoomMemberUpdateActions(room, members, currentUser, isCurrentRoom);
}
private handleMemberRoster(
@@ -514,7 +567,12 @@ export class RoomMembersSyncEffects {
);
}
const actions = this.createRoomMemberUpdateActions(room, members);
const actions = this.createRoomMemberUpdateActions(
room,
members,
currentUser,
currentRoom?.id === room.id
);
const currentUserId = currentUser?.oderId || currentUser?.id;
for (const member of members) {
@@ -551,7 +609,9 @@ export class RoomMembersSyncEffects {
const actions = this.createRoomMemberUpdateActions(
room,
removeRoomMember(room.members ?? [], event.targetUserId, event.oderId)
removeRoomMember(room.members ?? [], event.targetUserId, event.oderId),
null,
currentRoom?.id === room.id
);
const departedUserId = event.oderId ?? event.targetUserId;
@@ -652,7 +712,9 @@ export class RoomMembersSyncEffects {
const actions = this.createRoomMemberUpdateActions(
room,
updateRoomMemberRole(room.members ?? [], event.targetUserId, event.role)
updateRoomMemberRole(room.members ?? [], event.targetUserId, event.role),
null,
currentRoom?.id === room.id
);
if (currentRoom?.id === room.id) {