fix: Bug - In direct voice call the status is displayed as offline

Resolve direct-call participant join state and DM peer status across user identity aliases so call UI no longer shows participants as disconnected when they are in the call.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-13 21:31:03 +02:00
co-authored by Cursor
parent baa350e90a
commit 924d4bbb1d
6 changed files with 268 additions and 22 deletions
@@ -16,6 +16,7 @@ import {
} from '../../../../infrastructure/mobile';
import { initializeAppI18nForTests, provideAppI18nForTests } from '../../../../core/i18n/app-i18n.testing';
import { ViewportService } from '../../../../core/platform';
import { RealtimeSessionFacade } from '../../../../core/realtime';
import {
VoiceActivityService,
VoiceConnectionFacade,
@@ -109,6 +110,47 @@ describe('DirectCallService', () => {
expect(context.directMessages.createGroupConversation).not.toHaveBeenCalled();
});
it('marks a remote join against the session participant alias stored locally', async () => {
const aliceForeign = createUser('alice-foreign', 'Alice');
const bobForeign = createUser('bob-foreign', 'Bob');
const bobHome = { ...bobForeign, id: 'bob-home', oderId: 'bob-home' };
const context = createServiceContext({ currentUser: aliceForeign, allUsers: [aliceForeign, bobForeign] });
context.directCallEvents.next({
type: 'direct-call',
directCall: {
action: 'ring',
callId: 'dm-alice-foreign--bob-foreign',
conversationId: 'dm-alice-foreign--bob-foreign',
createdAt: 10,
sender: toParticipant(bobForeign),
participantIds: ['alice-foreign', 'bob-foreign'],
participants: [toParticipant(aliceForeign), toParticipant(bobForeign)]
}
});
await vi.waitFor(() => expect(context.service.sessionById('dm-alice-foreign--bob-foreign')).not.toBeNull());
context.directCallEvents.next({
type: 'direct-call',
directCall: {
action: 'join',
callId: 'dm-alice-foreign--bob-foreign',
conversationId: 'dm-alice-foreign--bob-foreign',
createdAt: 10,
sender: toParticipant(bobHome),
participantIds: ['alice-foreign', 'bob-foreign'],
participants: [toParticipant(aliceForeign), toParticipant(bobForeign)]
}
});
await vi.waitFor(() => {
const session = context.service.sessionById('dm-alice-foreign--bob-foreign');
expect(session?.participants['bob-foreign']?.joined).toBe(true);
});
});
it('answers an incoming call from the modal action', async () => {
const context = createServiceContext({ currentUser: bob, allUsers: [alice, bob] });
@@ -573,9 +615,17 @@ function createServiceContext(options: ServiceContextOptions): ServiceContext {
{
provide: MobileMediaService,
useValue: {
ensureVoiceCapturePermissions: vi.fn(async () => true),
setSpeakerphoneEnabled: vi.fn(async () => undefined)
}
},
{
provide: RealtimeSessionFacade,
useValue: {
getClientInstanceId: vi.fn(() => 'test-client'),
requestVoiceClientTakeover: vi.fn()
}
},
...provideAppI18nForTests()
]
});
@@ -1,4 +1,4 @@
/* eslint-disable @typescript-eslint/member-ordering */
import {
Injectable,
computed,
@@ -33,6 +33,11 @@ import {
User
} from '../../../../shared-kernel';
import { DirectCallSession, participantToUser } from '../../domain/models/direct-call.model';
import {
findDirectCallParticipantEntry,
findDirectCallParticipantEntryForUser,
isDirectCallParticipantJoined
} from '../../domain/logic/direct-call-participant-identity.rules';
import { toDirectMessageParticipant } from '../../../direct-message';
@Injectable({ providedIn: 'root' })
@@ -772,13 +777,20 @@ export class DirectCallService {
private preserveJoinedParticipants(previousSession: DirectCallSession, nextSession: DirectCallSession): DirectCallSession {
return {
...nextSession,
participants: Object.fromEntries(Object.values(nextSession.participants).map((participant) => [
participant.userId,
{
...participant,
joined: previousSession.participants[participant.userId]?.joined ?? participant.joined
}
]))
participants: Object.fromEntries(Object.values(nextSession.participants).map((participant) => {
const previousEntry = findDirectCallParticipantEntryForUser(previousSession, {
id: participant.userId,
oderId: participant.profile.userId
});
return [
participant.userId,
{
...participant,
joined: previousEntry?.participant.joined ?? participant.joined
}
];
}))
};
}
@@ -865,7 +877,14 @@ export class DirectCallService {
joined: boolean,
status: DirectCallSession['status']
): DirectCallSession {
const participant = session.participants[participantId];
const knownUser = this.users().find((user) =>
user.id === participantId || user.oderId === participantId || user.peerId === participantId
);
const entry = knownUser
? findDirectCallParticipantEntryForUser(session, knownUser, [participantId])
: findDirectCallParticipantEntry(session, participantId);
const key = entry?.key ?? participantId;
const participant = entry?.participant ?? session.participants[participantId];
return {
...session,
@@ -874,7 +893,7 @@ export class DirectCallService {
...session.participants,
...(participant
? {
[participantId]: {
[key]: {
...participant,
joined
}
@@ -916,9 +935,9 @@ export class DirectCallService {
}
private isCurrentUserJoined(session: DirectCallSession): boolean {
const meId = this.currentUserId();
const user = this.currentUser();
return !!meId && !!session.participants[meId]?.joined;
return !!user && isDirectCallParticipantJoined(session, user);
}
private stopLocalMedia(session: DirectCallSession): void {
@@ -955,8 +974,13 @@ export class DirectCallService {
}
private markRemoteVoiceState(userId: string, session: DirectCallSession, connected: boolean): void {
const knownUser = this.users().find((user) =>
user.id === userId || user.oderId === userId || user.peerId === userId
);
const resolvedUserId = knownUser?.id ?? userId;
this.store.dispatch(UsersActions.updateVoiceState({
userId,
userId: resolvedUserId,
voiceState: {
isConnected: connected,
isMuted: false,