feat(presence): live stream badges, honest ring delivery, cross-signal DMs

- Camera and screen-share badges are decided by `shouldShowStreamIndicator`,
  so a live stream is visible from outside the voice channel and clicking the
  badge joins the streamer's channel before focusing the stream.
- `ringParticipants` reports whether a `direct-call` ring reached anyone; a
  call that reached nobody shows `call.errors.ringUndelivered` instead of
  sitting in "calling" as if it were live.
- Direct messages resolve every local identity alias, so a conversation
  opened from a foreign roster entry lands in the same thread.
This commit is contained in:
2026-08-14 03:19:29 +02:00
parent 92c2f578e2
commit 3266581d3c
16 changed files with 1180 additions and 118 deletions
@@ -0,0 +1,130 @@
import { expect, type Page } from '@playwright/test';
import { test, type Client } from '../../fixtures/multi-client';
import { installDeterministicVoiceSettings } from '../../helpers/voice-session';
import {
dumpRtcDiagnostics,
installAutoResumeAudioContext,
installWebRTCTracking,
waitForAudioStatsPresent,
waitForConnectedPeerCount,
waitForInboundVideoFlow,
waitForOutboundVideoFlow
} from '../../helpers/webrtc-helpers';
import { ChatRoomPage } from '../../pages/chat-room.page';
import { RegisterPage } from '../../pages/register.page';
import { ServerSearchPage } from '../../pages/server-search.page';
const USER_PASSWORD = 'TestPass123!';
const VOICE_CHANNEL = 'General';
/**
* Screen share is pull-based: the sharer only attaches tracks to peers that asked
* for them. This covers the case the request model has to get right - a viewer who
* arrives after the share already started.
*/
test.describe('Late joiner screen share', () => {
test('a user who joins voice mid-share still receives the screen', async ({ createClient }) => {
test.setTimeout(240_000);
const serverName = `Late Share ${Date.now()}`;
const sharer = await createVoiceClient(createClient, 'sharer');
const viewer = await createVoiceClient(createClient, 'viewer');
await test.step('Both users register and join the server', async () => {
await new ServerSearchPage(sharer.page).createServer(serverName, {
description: 'Late joiner screen share test'
});
await expect(sharer.page).toHaveURL(/\/room\//, { timeout: 20_000 });
await new ServerSearchPage(viewer.page).joinServerFromSearch(serverName);
await expect(viewer.page).toHaveURL(/\/room\//, { timeout: 20_000 });
});
await test.step('The sharer starts sharing while alone in voice', async () => {
const room = new ChatRoomPage(sharer.page);
await room.ensureVoiceChannelExists(VOICE_CHANNEL);
await room.joinVoiceChannel(VOICE_CHANNEL);
await expect(room.voiceControls).toBeVisible({ timeout: 20_000 });
await openVoiceWorkspace(sharer.page);
await room.startScreenShare();
await expect(room.isScreenShareActive).toBeVisible({ timeout: 15_000 });
});
await test.step('The viewer joins voice after the share is already running', async () => {
const room = new ChatRoomPage(viewer.page);
await room.joinVoiceChannel(VOICE_CHANNEL);
await expect(room.voiceControls).toBeVisible({ timeout: 20_000 });
await waitForConnectedPeerCount(viewer.page, 1, 90_000);
await waitForConnectedPeerCount(sharer.page, 1, 90_000);
await waitForAudioStatsPresent(viewer.page, 30_000);
await openVoiceWorkspace(viewer.page);
});
await test.step('The in-progress screen reaches the late joiner', async () => {
try {
const outbound = await waitForOutboundVideoFlow(sharer.page, 60_000);
const inbound = await waitForInboundVideoFlow(viewer.page, 60_000);
expect(
outbound.outboundBytesDelta > 0 || outbound.outboundPacketsDelta > 0,
'The sharer never sent screen video to the late joiner'
).toBe(true);
expect(
inbound.inboundBytesDelta > 0 || inbound.inboundPacketsDelta > 0,
'The late joiner never received the in-progress screen share'
).toBe(true);
} catch (error) {
console.log(`[sharer RTC]\n${await dumpRtcDiagnostics(sharer.page)}`);
console.log(`[viewer RTC]\n${await dumpRtcDiagnostics(viewer.page)}`);
throw error;
}
});
await test.step('The late joiner renders a remote screen tile', async () => {
await expect(viewer.page.locator('app-voice-workspace-stream-tile').first())
.toBeVisible({ timeout: 30_000 });
});
});
});
/** Expand the voice workspace, which is what turns on remote screen-share requests. */
async function openVoiceWorkspace(page: Page): Promise<void> {
const viewButton = page.locator('app-rooms-side-panel')
.getByRole('button', { name: /view/i })
.first();
await expect(viewButton).toBeVisible({ timeout: 20_000 });
await viewButton.click();
await expect(page.locator('app-voice-workspace')).toBeVisible({ timeout: 20_000 });
}
async function createVoiceClient(
createClient: () => Promise<Client>,
role: string
): Promise<Client> {
const client = await createClient();
await installDeterministicVoiceSettings(client.page);
await installWebRTCTracking(client.page);
await installAutoResumeAudioContext(client.page);
const registerPage = new RegisterPage(client.page);
await registerPage.goto();
await registerPage.register(
`late_share_${role}_${Date.now()}`,
`Late Share ${role}`,
USER_PASSWORD
);
await expect(client.page).toHaveURL(/\/dashboard/, { timeout: 20_000 });
return client;
}