Fix syncing issues
Some checks failed
Deploy Web Apps / deploy (push) Has been cancelled
Queue Release Build / prepare (push) Has been cancelled
Queue Release Build / build-linux (push) Has been cancelled
Queue Release Build / build-windows (push) Has been cancelled
Queue Release Build / finalize (push) Has been cancelled

This commit is contained in:
2026-03-18 23:11:48 +01:00
parent 1cdd1c5d2b
commit 4faa62864d
8 changed files with 124 additions and 93 deletions

View File

@@ -5,10 +5,7 @@ import {
signal,
effect
} from '@angular/core';
import {
NavigationEnd,
Router
} from '@angular/router';
import { NavigationEnd, Router } from '@angular/router';
import { take } from 'rxjs';
import { v4 as uuidv4 } from 'uuid';
import { WebRTCService } from './webrtc.service';

View File

@@ -218,7 +218,6 @@ export class DebuggingService {
const rawMessage = args.map((arg) => this.stringifyPreview(arg)).join(' ')
.trim() || '(empty console call)';
// Use only string args for label/message extraction so that
// stringified object payloads don't pollute the parsed message.
// Object payloads are captured separately via extractConsolePayload.
@@ -226,7 +225,6 @@ export class DebuggingService {
.filter((arg): arg is string => typeof arg === 'string')
.join(' ')
.trim() || rawMessage;
const consoleMetadata = this.extractConsoleMetadata(metadataSource);
const payload = this.extractConsolePayload(args);
const payloadText = payload === undefined

View File

@@ -19,10 +19,7 @@ import {
lucideRefreshCw
} from '@ng-icons/lucide';
import { Router } from '@angular/router';
import {
selectCurrentRoom,
selectIsSignalServerReconnecting
} from '../../store/rooms/rooms.selectors';
import { selectCurrentRoom, selectIsSignalServerReconnecting } from '../../store/rooms/rooms.selectors';
import { RoomsActions } from '../../store/rooms/rooms.actions';
import { selectCurrentUser } from '../../store/users/users.selectors';
import { ServerDirectoryService } from '../../core/services/server-directory.service';
@@ -56,8 +53,8 @@ type ElectronWindow = Window & {
lucideX,
lucideChevronLeft,
lucideHash,
lucideMenu,
lucideRefreshCw })
lucideMenu,
lucideRefreshCw })
],
templateUrl: './title-bar.component.html'
})

View File

@@ -58,6 +58,7 @@ export class VoicePlaybackService {
this.temporaryOutputDeviceId = this.webrtc.forceDefaultRemotePlaybackOutput()
? 'default'
: null;
void this.applyEffectiveOutputDeviceToAllPipelines();
});

View File

@@ -84,6 +84,17 @@ export class DebugConsoleToolbarComponent {
readonly tabs: ('logs' | 'network')[] = ['logs', 'network'];
@HostListener('document:click', ['$event'])
onDocumentClick(event: MouseEvent): void {
if (!this.exportMenuOpen())
return;
const target = event.target as HTMLElement;
if (!target.closest('[data-export-menu]'))
this.closeExportMenu();
}
setActiveTab(tab: 'logs' | 'network'): void {
this.activeTabChange.emit(tab);
}
@@ -138,17 +149,6 @@ export class DebugConsoleToolbarComponent {
this.closeExportMenu();
}
@HostListener('document:click', ['$event'])
onDocumentClick(event: MouseEvent): void {
if (!this.exportMenuOpen())
return;
const target = event.target as HTMLElement;
if (!target.closest('[data-export-menu]'))
this.closeExportMenu();
}
getDetachLabel(): string {
return this.detached() ? 'Dock' : 'Detach';
}

View File

@@ -24,10 +24,16 @@
<header class="border-b border-border p-5">
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div>
<h2 id="screen-share-source-picker-title" class="text-lg font-semibold text-foreground">
<h2
id="screen-share-source-picker-title"
class="text-lg font-semibold text-foreground"
>
Choose what to share
</h2>
<p id="screen-share-source-picker-description" class="mt-1 text-sm text-muted-foreground">
<p
id="screen-share-source-picker-description"
class="mt-1 text-sm text-muted-foreground"
>
Select a screen or window to start sharing.
</p>
</div>
@@ -55,7 +61,11 @@
</label>
</div>
<div class="mt-4 flex flex-wrap gap-2" role="tablist" aria-label="Share source type">
<div
class="mt-4 flex flex-wrap gap-2"
role="tablist"
aria-label="Share source type"
>
<button
type="button"
class="inline-flex items-center gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-60"
@@ -129,7 +139,11 @@
<div class="flex items-start justify-between gap-3">
<div class="min-w-0 flex-1">
<span class="screen-share-source-picker__preview">
<img [ngSrc]="source.thumbnail" [alt]="source.name" fill />
<img
[ngSrc]="source.thumbnail"
[alt]="source.name"
fill
/>
</span>
<p class="mt-3 truncate font-medium">{{ source.name }}</p>
@@ -156,13 +170,13 @@
} @else {
<div class="flex min-h-52 items-center justify-center px-5 py-8 text-center">
<div>
<p class="text-sm font-medium text-foreground">
No {{ activeTab() === 'screen' ? 'screens' : 'windows' }} available
</p>
<p class="text-sm font-medium text-foreground">No {{ activeTab() === 'screen' ? 'screens' : 'windows' }} available</p>
<p class="mt-1 text-sm text-muted-foreground">
{{ activeTab() === 'screen'
? 'No displays were reported by Electron right now.'
: 'Restore the window you want to share and try again.' }}
{{
activeTab() === 'screen'
? 'No displays were reported by Electron right now.'
: 'Restore the window you want to share and try again.'
}}
</p>
</div>
</div>

View File

@@ -2,7 +2,7 @@
* Sync-lifecycle effects for the messages store slice.
*
* These effects manage the periodic sync polling, peer-connect
* handshakes, and join-room kickoff that keep message databases
* handshakes, and room-activation kickoff that keep message databases
* in sync across peers.
*
* Extracted from the monolithic MessagesEffects to keep each
@@ -33,7 +33,7 @@ import {
exhaustMap,
switchMap,
repeat,
takeUntil
startWith
} from 'rxjs/operators';
import { MessagesActions } from './messages.actions';
import { RoomsActions } from '../rooms/rooms.actions';
@@ -103,13 +103,13 @@ export class MessagesSyncEffects {
);
/**
* When the user joins a room, sends a summary and inventory
* When the user joins or views a room, sends a summary and inventory
* request to every already-connected peer.
*/
joinRoomSyncKickoff$ = createEffect(
roomActivationSyncKickoff$ = createEffect(
() =>
this.actions$.pipe(
ofType(RoomsActions.joinRoomSuccess),
ofType(RoomsActions.joinRoomSuccess, RoomsActions.viewServerSuccess),
withLatestFrom(this.store.select(selectCurrentRoom)),
mergeMap(([{ room }, currentRoom]) => {
const activeRoom = currentRoom || room;
@@ -152,63 +152,83 @@ export class MessagesSyncEffects {
{ dispatch: false }
);
/**
* Reset the polling cadence when the active room changes so the next
* room does not inherit a stale slow-poll delay.
*/
resetPeriodicSyncOnRoomActivation$ = createEffect(
() =>
this.actions$.pipe(
ofType(RoomsActions.joinRoomSuccess, RoomsActions.viewServerSuccess),
tap(() => {
this.lastSyncClean = false;
this.syncReset$.next();
})
),
{ dispatch: false }
);
/**
* Alternates between fast (10 s) and slow (15 min) sync intervals.
* Sends inventory requests to all connected peers.
* Sends inventory requests to all connected peers for the active room.
*/
periodicSyncPoll$ = createEffect(() =>
timer(SYNC_POLL_FAST_MS).pipe(
repeat({
delay: () =>
timer(
this.lastSyncClean ? SYNC_POLL_SLOW_MS : SYNC_POLL_FAST_MS
)
}),
takeUntil(this.syncReset$),
withLatestFrom(this.store.select(selectCurrentRoom)),
filter(
([, room]) =>
!!room && this.webrtc.getConnectedPeers().length > 0
),
exhaustMap(([, room]) => {
const peers = this.webrtc.getConnectedPeers();
this.syncReset$.pipe(
startWith(undefined),
switchMap(() =>
timer(SYNC_POLL_FAST_MS).pipe(
repeat({
delay: () =>
timer(
this.lastSyncClean ? SYNC_POLL_SLOW_MS : SYNC_POLL_FAST_MS
)
}),
withLatestFrom(this.store.select(selectCurrentRoom)),
filter(
([, room]) =>
!!room && this.webrtc.getConnectedPeers().length > 0
),
exhaustMap(([, room]) => {
const peers = this.webrtc.getConnectedPeers();
if (!room || peers.length === 0) {
return of(MessagesActions.syncComplete());
}
return from(
this.db.getMessages(room.id, INVENTORY_LIMIT, 0)
).pipe(
map(() => {
for (const pid of peers) {
try {
this.webrtc.sendToPeer(pid, {
type: 'chat-inventory-request',
roomId: room.id
});
} catch (error) {
this.debugging.warn('messages', 'Failed to request peer inventory during sync poll', {
error,
peerId: pid,
roomId: room.id
});
}
if (!room || peers.length === 0) {
return of(MessagesActions.syncComplete());
}
return MessagesActions.startSync();
}),
catchError((error) => {
this.lastSyncClean = false;
this.debugging.warn('messages', 'Periodic sync poll failed', {
error,
roomId: room.id
});
return from(
this.db.getMessages(room.id, INVENTORY_LIMIT, 0)
).pipe(
map(() => {
for (const pid of peers) {
try {
this.webrtc.sendToPeer(pid, {
type: 'chat-inventory-request',
roomId: room.id
});
} catch (error) {
this.debugging.warn('messages', 'Failed to request peer inventory during sync poll', {
error,
peerId: pid,
roomId: room.id
});
}
}
return of(MessagesActions.syncComplete());
return MessagesActions.startSync();
}),
catchError((error) => {
this.lastSyncClean = false;
this.debugging.warn('messages', 'Periodic sync poll failed', {
error,
roomId: room.id
});
return of(MessagesActions.syncComplete());
})
);
})
);
})
)
)
)
);

View File

@@ -348,7 +348,11 @@ export class RoomsEffects {
this.actions$.pipe(
ofType(RoomsActions.createRoomSuccess, RoomsActions.joinRoomSuccess),
withLatestFrom(this.store.select(selectCurrentUser), this.store.select(selectSavedRooms)),
tap(([{ room }, user, savedRooms]) => {
tap(([
{ room },
user,
savedRooms
]) => {
this.connectToRoomSignaling(room, user ?? null, undefined, savedRooms);
this.router.navigate(['/room', room.id]);
@@ -362,7 +366,11 @@ export class RoomsEffects {
this.actions$.pipe(
ofType(RoomsActions.viewServer),
withLatestFrom(this.store.select(selectCurrentUser), this.store.select(selectSavedRooms)),
switchMap(([{ room }, user, savedRooms]) => {
switchMap(([
{ room },
user,
savedRooms
]) => {
if (!user) {
return of(RoomsActions.joinRoomFailure({ error: 'Not logged in' }));
}
@@ -573,7 +581,6 @@ export class RoomsEffects {
hasPassword: nextHasPassword,
maxUsers: settings.maxUsers ?? room.maxUsers
};
const localRoomUpdates: Partial<Room> = {
...updatedSettings,
password: hasPasswordUpdate ? (normalizedPassword || undefined) : room.password,
@@ -837,10 +844,7 @@ export class RoomsEffects {
return EMPTY;
this.knownVoiceUsers.delete(signalingMessage.oderId);
return [
RoomsActions.setSignalServerReconnecting({ isReconnecting: false }),
UsersActions.userLeft({ userId: signalingMessage.oderId })
];
return [RoomsActions.setSignalServerReconnecting({ isReconnecting: false }), UsersActions.userLeft({ userId: signalingMessage.oderId })];
}
case 'access_denied': {