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:
@@ -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) {
|
||||
|
||||
@@ -183,8 +183,12 @@ function mergeMembers(
|
||||
const avatarFields = mergeAvatarFields(normalizedExisting, normalizedIncoming, preferIncoming);
|
||||
|
||||
return {
|
||||
id: normalizedExisting.id || normalizedIncoming.id,
|
||||
oderId: normalizedIncoming.oderId || normalizedExisting.oderId,
|
||||
id: preferIncoming
|
||||
? (normalizedIncoming.id || normalizedExisting.id)
|
||||
: (normalizedExisting.id || normalizedIncoming.id),
|
||||
oderId: preferIncoming
|
||||
? (normalizedIncoming.oderId || normalizedExisting.oderId)
|
||||
: (normalizedExisting.oderId || normalizedIncoming.oderId),
|
||||
username: preferIncoming
|
||||
? (normalizedIncoming.username || normalizedExisting.username)
|
||||
: (normalizedExisting.username || normalizedIncoming.username),
|
||||
@@ -241,6 +245,48 @@ export function roomMemberFromUser(
|
||||
);
|
||||
}
|
||||
|
||||
function profileDedupKey(member: RoomMember): string {
|
||||
return `${member.displayName.trim().toLowerCase()}::${member.username.trim().toLowerCase()}`;
|
||||
}
|
||||
|
||||
function deduplicateMatchingProfiles(members: RoomMember[], now: number): RoomMember[] {
|
||||
const profiles = new Map<string, RoomMember>();
|
||||
|
||||
for (const member of members) {
|
||||
const profileKey = profileDedupKey(member);
|
||||
|
||||
profiles.set(profileKey, mergeMembers(profiles.get(profileKey), member, now));
|
||||
}
|
||||
|
||||
return Array.from(profiles.values());
|
||||
}
|
||||
|
||||
/** Remove every roster entry whose id or oderId matches one of the supplied ids. */
|
||||
export function removeRoomMembersMatchingIds(
|
||||
members: RoomMember[] = [],
|
||||
ids: ReadonlySet<string>
|
||||
): RoomMember[] {
|
||||
if (ids.size === 0) {
|
||||
return pruneRoomMembers(members);
|
||||
}
|
||||
|
||||
return pruneRoomMembers(members).filter(
|
||||
(member) => !ids.has(member.id) && !ids.has(member.oderId || '')
|
||||
);
|
||||
}
|
||||
|
||||
/** Replace stale self aliases with the canonical actor member for this room. */
|
||||
export function collapseSelfRoomMembers(
|
||||
members: RoomMember[] = [],
|
||||
selfIds: ReadonlySet<string>,
|
||||
canonicalMember: RoomMember,
|
||||
now = Date.now()
|
||||
): RoomMember[] {
|
||||
const withoutSelfAliases = removeRoomMembersMatchingIds(members, selfIds);
|
||||
|
||||
return upsertRoomMember(withoutSelfAliases, canonicalMember, now);
|
||||
}
|
||||
|
||||
/** Deduplicate, sanitize, sort, and prune stale room members. */
|
||||
export function pruneRoomMembers(
|
||||
members: RoomMember[] = [],
|
||||
@@ -266,7 +312,10 @@ export function pruneRoomMembers(
|
||||
);
|
||||
}
|
||||
|
||||
return Array.from(deduplicatedMembers.values()).sort(compareMembers);
|
||||
return deduplicateMatchingProfiles(
|
||||
Array.from(deduplicatedMembers.values()),
|
||||
now
|
||||
).sort(compareMembers);
|
||||
}
|
||||
|
||||
/** Upsert a member into a room roster while preserving the best known data. */
|
||||
|
||||
Reference in New Issue
Block a user