Reconnection when signal server is not active and minor changes

This commit is contained in:
2026-03-18 20:45:00 +01:00
parent eb987ac672
commit 141de64767
15 changed files with 229 additions and 27 deletions

View File

@@ -563,10 +563,10 @@ export class ServerDirectoryService {
);
}
/** Notify the directory that a user has left a server. */
notifyLeave(serverId: string, userId: string): Observable<void> {
/** Remove a user's remembered membership after leaving a server. */
notifyLeave(serverId: string, userId: string, selector?: ServerSourceSelector): Observable<void> {
return this.http
.post<void>(`${this.buildApiBaseUrl()}/servers/${serverId}/leave`, { userId })
.post<void>(`${this.buildApiBaseUrl(selector)}/servers/${serverId}/leave`, { userId })
.pipe(
catchError((error) => {
console.error('Failed to notify leave:', error);

View File

@@ -2,7 +2,9 @@
<div class="mx-auto flex min-h-[calc(100vh-8rem)] max-w-4xl items-center justify-center">
<div class="w-full overflow-hidden rounded-3xl border border-border bg-card/90 shadow-2xl backdrop-blur">
<div class="border-b border-border bg-gradient-to-br from-primary/20 via-transparent to-blue-500/10 px-6 py-8 sm:px-10">
<div class="inline-flex items-center rounded-full border border-border bg-secondary/70 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.25em] text-muted-foreground">
<div
class="inline-flex items-center rounded-full border border-border bg-secondary/70 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.25em] text-muted-foreground"
>
Invite link
</div>
<h1 class="mt-4 text-3xl font-semibold tracking-tight text-foreground sm:text-4xl">
@@ -51,9 +53,7 @@
@if (invite()!.server.hasPassword) {
<span class="rounded-full bg-secondary px-2.5 py-1 text-muted-foreground">Password bypassed by invite</span>
}
<span class="rounded-full bg-primary/10 px-2.5 py-1 text-primary">
Expires {{ invite()!.expiresAt | date:'medium' }}
</span>
<span class="rounded-full bg-primary/10 px-2.5 py-1 text-primary"> Expires {{ invite()!.expiresAt | date: 'medium' }} </span>
</div>
</div>
}

View File

@@ -165,9 +165,7 @@
Owner: <span class="text-foreground/80">{{ server.ownerName || server.ownerId || 'Unknown' }}</span>
</div>
@if (server.hasPassword && !server.isPrivate && !isServerMarkedBanned(server)) {
<div class="text-muted-foreground">
Access: <span class="text-foreground/80">Password required</span>
</div>
<div class="text-muted-foreground">Access: <span class="text-foreground/80">Password required</span></div>
}
</div>
</button>
@@ -327,9 +325,7 @@
id="create-server-password"
class="w-full px-3 py-2 bg-secondary rounded-lg border border-border text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
/>
<p class="mt-1 text-xs text-muted-foreground">
Users who already joined keep access even if you change the password later.
</p>
<p class="mt-1 text-xs text-muted-foreground">Users who already joined keep access even if you change the password later.</p>
</div>
</div>

View File

@@ -123,6 +123,7 @@ export class ServersRailComponent {
this.prepareVoiceContext(room);
if (this.webrtc.hasJoinedServer(room.id) && roomWsUrl === currentWsUrl) {
this.store.dispatch(RoomsActions.setSignalServerReconnecting({ isReconnecting: false }));
this.store.dispatch(RoomsActions.viewServer({ room }));
} else {
await this.attemptJoinRoom(room);
@@ -301,10 +302,31 @@ export class ServersRailComponent {
if (errorCode === 'BANNED') {
this.bannedServerName.set(room.name);
this.showBannedDialog.set(true);
return;
}
if (this.shouldFallbackToOfflineView(error)) {
this.closePasswordDialog();
this.store.dispatch(RoomsActions.setSignalServerReconnecting({ isReconnecting: true }));
this.store.dispatch(RoomsActions.viewServer({ room }));
}
}
}
private shouldFallbackToOfflineView(error: unknown): boolean {
const serverError = error as {
error?: { errorCode?: string };
status?: number;
};
const errorCode = serverError?.error?.errorCode;
const status = serverError?.status;
return errorCode === 'SERVER_NOT_FOUND'
|| status === 0
|| status === 404
|| (typeof status === 'number' && status >= 500);
}
private toServerInfo(room: Room) {
return {
id: room.id,

View File

@@ -13,6 +13,16 @@
/>
<span class="text-sm font-semibold text-foreground truncate">{{ roomName() }}</span>
@if (showRoomReconnectNotice()) {
<span class="inline-flex items-center gap-1 rounded bg-destructive/15 px-2 py-0.5 text-xs text-destructive">
<ng-icon
name="lucideRefreshCw"
class="h-3.5 w-3.5 animate-spin"
/>
Reconnecting to signal server…
</span>
}
@if (roomDescription()) {
<span class="hidden md:inline text-sm text-muted-foreground border-l border-border pl-2 truncate">
{{ roomDescription() }}

View File

@@ -15,10 +15,14 @@ import {
lucideX,
lucideChevronLeft,
lucideHash,
lucideMenu
lucideMenu,
lucideRefreshCw
} from '@ng-icons/lucide';
import { Router } from '@angular/router';
import { selectCurrentRoom } 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';
@@ -52,7 +56,8 @@ type ElectronWindow = Window & {
lucideX,
lucideChevronLeft,
lucideHash,
lucideMenu })
lucideMenu,
lucideRefreshCw })
],
templateUrl: './title-bar.component.html'
})
@@ -80,9 +85,17 @@ export class TitleBarComponent {
isReconnecting = computed(() => !this.webrtc.isConnected() && this.webrtc.hasEverConnected());
isAuthed = computed(() => !!this.currentUser());
currentRoom = this.store.selectSignal(selectCurrentRoom);
isSignalServerReconnecting = this.store.selectSignal(selectIsSignalServerReconnecting);
inRoom = computed(() => !!this.currentRoom());
roomName = computed(() => this.currentRoom()?.name || '');
roomDescription = computed(() => this.currentRoom()?.description || '');
showRoomReconnectNotice = computed(() =>
this.inRoom() && (
this.isSignalServerReconnecting()
|| this.webrtc.shouldShowConnectionError()
|| this.isReconnecting()
)
);
private _showMenu = signal(false);
showMenu = computed(() => this._showMenu());
showLeaveConfirm = signal(false);

View File

@@ -67,6 +67,7 @@ export const RoomsActions = createActionGroup({
'Rename Channel': props<{ channelId: string; name: string }>(),
'Clear Search Results': emptyProps(),
'Set Connecting': props<{ isConnecting: boolean }>()
'Set Connecting': props<{ isConnecting: boolean }>(),
'Set Signal Server Reconnecting': props<{ isReconnecting: boolean }>()
}
});

View File

@@ -95,6 +95,7 @@ function isWrongServer(
interface RoomPresenceSignalingMessage {
type: string;
reason?: string;
serverId?: string;
users?: { oderId: string; displayName: string }[];
oderId?: string;
@@ -471,6 +472,15 @@ export class RoomsEffects {
});
}
if (currentUser && room) {
this.serverDirectory.notifyLeave(roomId, currentUser.id, {
sourceId: room.sourceId,
sourceUrl: room.sourceUrl
}).subscribe({
error: () => {}
});
}
// Delete from local DB
this.db.deleteRoom(roomId);
@@ -764,7 +774,11 @@ export class RoomsEffects {
})
);
return [UsersActions.clearUsers(), ...joinActions];
return [
RoomsActions.setSignalServerReconnecting({ isReconnecting: false }),
UsersActions.clearUsers(),
...joinActions
];
}
case 'user_joined': {
@@ -780,6 +794,7 @@ export class RoomsEffects {
};
return [
RoomsActions.setSignalServerReconnecting({ isReconnecting: false }),
UsersActions.userJoined({
user: buildSignalingUser(joinedUser, buildKnownUserExtras(currentRoom, joinedUser.oderId))
})
@@ -794,7 +809,20 @@ export class RoomsEffects {
return EMPTY;
this.knownVoiceUsers.delete(signalingMessage.oderId);
return [UsersActions.userLeft({ userId: signalingMessage.oderId })];
return [
RoomsActions.setSignalServerReconnecting({ isReconnecting: false }),
UsersActions.userLeft({ userId: signalingMessage.oderId })
];
}
case 'access_denied': {
if (isWrongServer(signalingMessage.serverId, viewedServerId))
return EMPTY;
if (signalingMessage.reason !== 'SERVER_NOT_FOUND')
return EMPTY;
return [RoomsActions.setSignalServerReconnecting({ isReconnecting: true })];
}
default:

View File

@@ -82,6 +82,8 @@ export interface RoomsState {
isConnecting: boolean;
/** Whether the user is connected to a room. */
isConnected: boolean;
/** Whether the current room is using locally cached data while reconnecting. */
isSignalServerReconnecting: boolean;
/** Whether rooms are being loaded from local storage. */
loading: boolean;
/** Most recent error message, if any. */
@@ -98,6 +100,7 @@ export const initialState: RoomsState = {
isSearching: false,
isConnecting: false,
isConnected: false,
isSignalServerReconnecting: false,
loading: false,
error: null,
activeChannelId: 'general'
@@ -159,6 +162,7 @@ export const roomsReducer = createReducer(
currentRoom: enriched,
savedRooms: upsertRoom(state.savedRooms, enriched),
isConnecting: false,
isSignalServerReconnecting: false,
isConnected: true,
activeChannelId: 'general'
};
@@ -185,6 +189,7 @@ export const roomsReducer = createReducer(
currentRoom: enriched,
savedRooms: upsertRoom(state.savedRooms, enriched),
isConnecting: false,
isSignalServerReconnecting: false,
isConnected: true,
activeChannelId: 'general'
};
@@ -206,6 +211,7 @@ export const roomsReducer = createReducer(
...state,
currentRoom: null,
roomSettings: null,
isSignalServerReconnecting: false,
isConnecting: false,
isConnected: false
})),
@@ -279,6 +285,7 @@ export const roomsReducer = createReducer(
// Delete room
on(RoomsActions.deleteRoomSuccess, (state, { roomId }) => ({
...state,
isSignalServerReconnecting: state.currentRoom?.id === roomId ? false : state.isSignalServerReconnecting,
savedRooms: state.savedRooms.filter((room) => room.id !== roomId),
currentRoom: state.currentRoom?.id === roomId ? null : state.currentRoom
})),
@@ -286,6 +293,7 @@ export const roomsReducer = createReducer(
// Forget room (local only)
on(RoomsActions.forgetRoomSuccess, (state, { roomId }) => ({
...state,
isSignalServerReconnecting: state.currentRoom?.id === roomId ? false : state.isSignalServerReconnecting,
savedRooms: state.savedRooms.filter((room) => room.id !== roomId),
currentRoom: state.currentRoom?.id === roomId ? null : state.currentRoom
})),
@@ -295,6 +303,7 @@ export const roomsReducer = createReducer(
...state,
currentRoom: enrichRoom(room),
savedRooms: upsertRoom(state.savedRooms, room),
isSignalServerReconnecting: false,
isConnected: true
})),
@@ -303,6 +312,7 @@ export const roomsReducer = createReducer(
...state,
currentRoom: null,
roomSettings: null,
isSignalServerReconnecting: false,
isConnected: false
})),
@@ -367,6 +377,11 @@ export const roomsReducer = createReducer(
isConnecting
})),
on(RoomsActions.setSignalServerReconnecting, (state, { isReconnecting }) => ({
...state,
isSignalServerReconnecting: isReconnecting
})),
// Channel management
on(RoomsActions.selectChannel, (state, { channelId }) => ({
...state,

View File

@@ -26,6 +26,10 @@ export const selectIsConnected = createSelector(
selectRoomsState,
(state) => state.isConnected
);
export const selectIsSignalServerReconnecting = createSelector(
selectRoomsState,
(state) => state.isSignalServerReconnecting
);
export const selectRoomsError = createSelector(
selectRoomsState,
(state) => state.error