Reconnection when signal server is not active and minor changes
This commit is contained in:
@@ -4,9 +4,24 @@ import { createWindow, getMainWindow } from '../window/create-window';
|
|||||||
|
|
||||||
const CUSTOM_PROTOCOL = 'toju';
|
const CUSTOM_PROTOCOL = 'toju';
|
||||||
const DEEP_LINK_PREFIX = `${CUSTOM_PROTOCOL}://`;
|
const DEEP_LINK_PREFIX = `${CUSTOM_PROTOCOL}://`;
|
||||||
|
const DEV_SINGLE_INSTANCE_EXIT_CODE_ENV = 'METOYOU_SINGLE_INSTANCE_EXIT_CODE';
|
||||||
|
|
||||||
let pendingDeepLink: string | null = null;
|
let pendingDeepLink: string | null = null;
|
||||||
|
|
||||||
|
function resolveDevSingleInstanceExitCode(): number | null {
|
||||||
|
const rawValue = process.env[DEV_SINGLE_INSTANCE_EXIT_CODE_ENV];
|
||||||
|
|
||||||
|
if (!rawValue) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedValue = Number.parseInt(rawValue, 10);
|
||||||
|
|
||||||
|
return Number.isInteger(parsedValue) && parsedValue > 0
|
||||||
|
? parsedValue
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
function extractDeepLink(argv: string[]): string | null {
|
function extractDeepLink(argv: string[]): string | null {
|
||||||
return argv.find((argument) => typeof argument === 'string' && argument.startsWith(DEEP_LINK_PREFIX)) || null;
|
return argv.find((argument) => typeof argument === 'string' && argument.startsWith(DEEP_LINK_PREFIX)) || null;
|
||||||
}
|
}
|
||||||
@@ -60,7 +75,14 @@ export function initializeDeepLinkHandling(): boolean {
|
|||||||
const hasSingleInstanceLock = app.requestSingleInstanceLock();
|
const hasSingleInstanceLock = app.requestSingleInstanceLock();
|
||||||
|
|
||||||
if (!hasSingleInstanceLock) {
|
if (!hasSingleInstanceLock) {
|
||||||
app.quit();
|
const devExitCode = resolveDevSingleInstanceExitCode();
|
||||||
|
|
||||||
|
if (devExitCode != null) {
|
||||||
|
app.exit(devExitCode);
|
||||||
|
} else {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ function buildOrigin(protocol: string, host: string): string {
|
|||||||
return `${protocol}://${host}`.replace(/\/+$/, '');
|
return `${protocol}://${host}`.replace(/\/+$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function originFromUrl(url: URL): string {
|
||||||
|
return buildOrigin(url.protocol.replace(':', ''), url.host);
|
||||||
|
}
|
||||||
|
|
||||||
export function getRequestOrigin(request: Request): string {
|
export function getRequestOrigin(request: Request): string {
|
||||||
const forwardedProtoHeader = request.get('x-forwarded-proto');
|
const forwardedProtoHeader = request.get('x-forwarded-proto');
|
||||||
const forwardedHostHeader = request.get('x-forwarded-host');
|
const forwardedHostHeader = request.get('x-forwarded-host');
|
||||||
@@ -15,18 +19,24 @@ export function getRequestOrigin(request: Request): string {
|
|||||||
|
|
||||||
export function deriveWebAppOrigin(signalOrigin: string): string {
|
export function deriveWebAppOrigin(signalOrigin: string): string {
|
||||||
const url = new URL(signalOrigin);
|
const url = new URL(signalOrigin);
|
||||||
const host = url.host;
|
|
||||||
|
|
||||||
if (host === 'signal.toju.app') {
|
if (url.hostname === 'signal.toju.app' && !url.port) {
|
||||||
return 'https://web.toju.app';
|
return 'https://web.toju.app';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (host.startsWith('signal.')) {
|
if (url.hostname.startsWith('signal.')) {
|
||||||
return buildOrigin(url.protocol.replace(':', ''), host.replace(/^signal\./, 'web.'));
|
url.hostname = url.hostname.replace(/^signal\./, 'web.');
|
||||||
|
|
||||||
|
if (url.port === '3001') {
|
||||||
|
url.port = '4200';
|
||||||
|
}
|
||||||
|
|
||||||
|
return originFromUrl(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['localhost:3001', '127.0.0.1:3001'].includes(host)) {
|
if (url.port === '3001') {
|
||||||
return buildOrigin(url.protocol.replace(':', ''), host.replace(/:3001$/, ':4200'));
|
url.port = '4200';
|
||||||
|
return originFromUrl(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'https://web.toju.app';
|
return 'https://web.toju.app';
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
buildSignalingUrl,
|
buildSignalingUrl,
|
||||||
createServerInvite,
|
createServerInvite,
|
||||||
joinServerWithAccess,
|
joinServerWithAccess,
|
||||||
|
leaveServerUser,
|
||||||
passwordHashForInput,
|
passwordHashForInput,
|
||||||
ServerAccessError,
|
ServerAccessError,
|
||||||
kickServerUser,
|
kickServerUser,
|
||||||
@@ -341,6 +342,24 @@ router.post('/:id/moderation/unban', async (req, res) => {
|
|||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post('/:id/leave', async (req, res) => {
|
||||||
|
const { id: serverId } = req.params;
|
||||||
|
const { userId } = req.body;
|
||||||
|
const server = await getServerById(serverId);
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
return res.status(404).json({ error: 'Server not found', errorCode: 'SERVER_NOT_FOUND' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return res.status(400).json({ error: 'Missing userId', errorCode: 'MISSING_USER' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await leaveServerUser(serverId, String(userId));
|
||||||
|
|
||||||
|
res.json({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
router.post('/:id/heartbeat', async (req, res) => {
|
router.post('/:id/heartbeat', async (req, res) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { currentUsers } = req.body;
|
const { currentUsers } = req.body;
|
||||||
|
|||||||
@@ -63,6 +63,10 @@ function normalizePassword(password?: string | null): string | null {
|
|||||||
return normalized.length > 0 ? normalized : null;
|
return normalized.length > 0 ? normalized : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isServerOwner(server: ServerEntity, userId: string): boolean {
|
||||||
|
return server.ownerId === userId;
|
||||||
|
}
|
||||||
|
|
||||||
export function hashServerPassword(password: string): string {
|
export function hashServerPassword(password: string): string {
|
||||||
return crypto.createHash('sha256').update(password)
|
return crypto.createHash('sha256').update(password)
|
||||||
.digest('hex');
|
.digest('hex');
|
||||||
@@ -231,6 +235,18 @@ export async function joinServerWithAccess(options: {
|
|||||||
throw new ServerAccessError(403, 'BANNED', 'Banned users cannot join this server');
|
throw new ServerAccessError(403, 'BANNED', 'Banned users cannot join this server');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isServerOwner(server, options.userId)) {
|
||||||
|
const existingMembership = await findServerMembership(server.id, options.userId);
|
||||||
|
|
||||||
|
await ensureServerMembership(server.id, options.userId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
joinedBefore: !!existingMembership,
|
||||||
|
server: rowToServer(server),
|
||||||
|
via: 'membership'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (options.inviteId) {
|
if (options.inviteId) {
|
||||||
const inviteBundle = await getActiveServerInvite(options.inviteId);
|
const inviteBundle = await getActiveServerInvite(options.inviteId);
|
||||||
|
|
||||||
@@ -305,6 +321,11 @@ export async function authorizeWebSocketJoin(serverId: string, userId: string):
|
|||||||
reason: 'BANNED' };
|
reason: 'BANNED' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isServerOwner(server, userId)) {
|
||||||
|
await ensureServerMembership(serverId, userId);
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
const membership = await findServerMembership(serverId, userId);
|
const membership = await findServerMembership(serverId, userId);
|
||||||
|
|
||||||
if (membership) {
|
if (membership) {
|
||||||
@@ -327,6 +348,10 @@ export async function kickServerUser(serverId: string, userId: string): Promise<
|
|||||||
await removeServerMembership(serverId, userId);
|
await removeServerMembership(serverId, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function leaveServerUser(serverId: string, userId: string): Promise<void> {
|
||||||
|
await removeServerMembership(serverId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
export async function banServerUser(options: BanServerUserOptions): Promise<ServerBanEntity> {
|
export async function banServerUser(options: BanServerUserOptions): Promise<ServerBanEntity> {
|
||||||
await removeServerMembership(options.serverId, options.userId);
|
await removeServerMembership(options.serverId, options.userId);
|
||||||
|
|
||||||
|
|||||||
@@ -563,10 +563,10 @@ export class ServerDirectoryService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Notify the directory that a user has left a server. */
|
/** Remove a user's remembered membership after leaving a server. */
|
||||||
notifyLeave(serverId: string, userId: string): Observable<void> {
|
notifyLeave(serverId: string, userId: string, selector?: ServerSourceSelector): Observable<void> {
|
||||||
return this.http
|
return this.http
|
||||||
.post<void>(`${this.buildApiBaseUrl()}/servers/${serverId}/leave`, { userId })
|
.post<void>(`${this.buildApiBaseUrl(selector)}/servers/${serverId}/leave`, { userId })
|
||||||
.pipe(
|
.pipe(
|
||||||
catchError((error) => {
|
catchError((error) => {
|
||||||
console.error('Failed to notify leave:', error);
|
console.error('Failed to notify leave:', error);
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
<div class="mx-auto flex min-h-[calc(100vh-8rem)] max-w-4xl items-center justify-center">
|
<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="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="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
|
Invite link
|
||||||
</div>
|
</div>
|
||||||
<h1 class="mt-4 text-3xl font-semibold tracking-tight text-foreground sm:text-4xl">
|
<h1 class="mt-4 text-3xl font-semibold tracking-tight text-foreground sm:text-4xl">
|
||||||
@@ -51,9 +53,7 @@
|
|||||||
@if (invite()!.server.hasPassword) {
|
@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-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">
|
<span class="rounded-full bg-primary/10 px-2.5 py-1 text-primary"> Expires {{ invite()!.expiresAt | date: 'medium' }} </span>
|
||||||
Expires {{ invite()!.expiresAt | date:'medium' }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,9 +165,7 @@
|
|||||||
Owner: <span class="text-foreground/80">{{ server.ownerName || server.ownerId || 'Unknown' }}</span>
|
Owner: <span class="text-foreground/80">{{ server.ownerName || server.ownerId || 'Unknown' }}</span>
|
||||||
</div>
|
</div>
|
||||||
@if (server.hasPassword && !server.isPrivate && !isServerMarkedBanned(server)) {
|
@if (server.hasPassword && !server.isPrivate && !isServerMarkedBanned(server)) {
|
||||||
<div class="text-muted-foreground">
|
<div class="text-muted-foreground">Access: <span class="text-foreground/80">Password required</span></div>
|
||||||
Access: <span class="text-foreground/80">Password required</span>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -327,9 +325,7 @@
|
|||||||
id="create-server-password"
|
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"
|
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">
|
<p class="mt-1 text-xs text-muted-foreground">Users who already joined keep access even if you change the password later.</p>
|
||||||
Users who already joined keep access even if you change the password later.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ export class ServersRailComponent {
|
|||||||
this.prepareVoiceContext(room);
|
this.prepareVoiceContext(room);
|
||||||
|
|
||||||
if (this.webrtc.hasJoinedServer(room.id) && roomWsUrl === currentWsUrl) {
|
if (this.webrtc.hasJoinedServer(room.id) && roomWsUrl === currentWsUrl) {
|
||||||
|
this.store.dispatch(RoomsActions.setSignalServerReconnecting({ isReconnecting: false }));
|
||||||
this.store.dispatch(RoomsActions.viewServer({ room }));
|
this.store.dispatch(RoomsActions.viewServer({ room }));
|
||||||
} else {
|
} else {
|
||||||
await this.attemptJoinRoom(room);
|
await this.attemptJoinRoom(room);
|
||||||
@@ -301,10 +302,31 @@ export class ServersRailComponent {
|
|||||||
if (errorCode === 'BANNED') {
|
if (errorCode === 'BANNED') {
|
||||||
this.bannedServerName.set(room.name);
|
this.bannedServerName.set(room.name);
|
||||||
this.showBannedDialog.set(true);
|
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) {
|
private toServerInfo(room: Room) {
|
||||||
return {
|
return {
|
||||||
id: room.id,
|
id: room.id,
|
||||||
|
|||||||
@@ -13,6 +13,16 @@
|
|||||||
/>
|
/>
|
||||||
<span class="text-sm font-semibold text-foreground truncate">{{ roomName() }}</span>
|
<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()) {
|
@if (roomDescription()) {
|
||||||
<span class="hidden md:inline text-sm text-muted-foreground border-l border-border pl-2 truncate">
|
<span class="hidden md:inline text-sm text-muted-foreground border-l border-border pl-2 truncate">
|
||||||
{{ roomDescription() }}
|
{{ roomDescription() }}
|
||||||
|
|||||||
@@ -15,10 +15,14 @@ import {
|
|||||||
lucideX,
|
lucideX,
|
||||||
lucideChevronLeft,
|
lucideChevronLeft,
|
||||||
lucideHash,
|
lucideHash,
|
||||||
lucideMenu
|
lucideMenu,
|
||||||
|
lucideRefreshCw
|
||||||
} from '@ng-icons/lucide';
|
} from '@ng-icons/lucide';
|
||||||
import { Router } from '@angular/router';
|
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 { RoomsActions } from '../../store/rooms/rooms.actions';
|
||||||
import { selectCurrentUser } from '../../store/users/users.selectors';
|
import { selectCurrentUser } from '../../store/users/users.selectors';
|
||||||
import { ServerDirectoryService } from '../../core/services/server-directory.service';
|
import { ServerDirectoryService } from '../../core/services/server-directory.service';
|
||||||
@@ -52,7 +56,8 @@ type ElectronWindow = Window & {
|
|||||||
lucideX,
|
lucideX,
|
||||||
lucideChevronLeft,
|
lucideChevronLeft,
|
||||||
lucideHash,
|
lucideHash,
|
||||||
lucideMenu })
|
lucideMenu,
|
||||||
|
lucideRefreshCw })
|
||||||
],
|
],
|
||||||
templateUrl: './title-bar.component.html'
|
templateUrl: './title-bar.component.html'
|
||||||
})
|
})
|
||||||
@@ -80,9 +85,17 @@ export class TitleBarComponent {
|
|||||||
isReconnecting = computed(() => !this.webrtc.isConnected() && this.webrtc.hasEverConnected());
|
isReconnecting = computed(() => !this.webrtc.isConnected() && this.webrtc.hasEverConnected());
|
||||||
isAuthed = computed(() => !!this.currentUser());
|
isAuthed = computed(() => !!this.currentUser());
|
||||||
currentRoom = this.store.selectSignal(selectCurrentRoom);
|
currentRoom = this.store.selectSignal(selectCurrentRoom);
|
||||||
|
isSignalServerReconnecting = this.store.selectSignal(selectIsSignalServerReconnecting);
|
||||||
inRoom = computed(() => !!this.currentRoom());
|
inRoom = computed(() => !!this.currentRoom());
|
||||||
roomName = computed(() => this.currentRoom()?.name || '');
|
roomName = computed(() => this.currentRoom()?.name || '');
|
||||||
roomDescription = computed(() => this.currentRoom()?.description || '');
|
roomDescription = computed(() => this.currentRoom()?.description || '');
|
||||||
|
showRoomReconnectNotice = computed(() =>
|
||||||
|
this.inRoom() && (
|
||||||
|
this.isSignalServerReconnecting()
|
||||||
|
|| this.webrtc.shouldShowConnectionError()
|
||||||
|
|| this.isReconnecting()
|
||||||
|
)
|
||||||
|
);
|
||||||
private _showMenu = signal(false);
|
private _showMenu = signal(false);
|
||||||
showMenu = computed(() => this._showMenu());
|
showMenu = computed(() => this._showMenu());
|
||||||
showLeaveConfirm = signal(false);
|
showLeaveConfirm = signal(false);
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export const RoomsActions = createActionGroup({
|
|||||||
'Rename Channel': props<{ channelId: string; name: string }>(),
|
'Rename Channel': props<{ channelId: string; name: string }>(),
|
||||||
|
|
||||||
'Clear Search Results': emptyProps(),
|
'Clear Search Results': emptyProps(),
|
||||||
'Set Connecting': props<{ isConnecting: boolean }>()
|
'Set Connecting': props<{ isConnecting: boolean }>(),
|
||||||
|
'Set Signal Server Reconnecting': props<{ isReconnecting: boolean }>()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ function isWrongServer(
|
|||||||
|
|
||||||
interface RoomPresenceSignalingMessage {
|
interface RoomPresenceSignalingMessage {
|
||||||
type: string;
|
type: string;
|
||||||
|
reason?: string;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
users?: { oderId: string; displayName: string }[];
|
users?: { oderId: string; displayName: string }[];
|
||||||
oderId?: 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
|
// Delete from local DB
|
||||||
this.db.deleteRoom(roomId);
|
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': {
|
case 'user_joined': {
|
||||||
@@ -780,6 +794,7 @@ export class RoomsEffects {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
RoomsActions.setSignalServerReconnecting({ isReconnecting: false }),
|
||||||
UsersActions.userJoined({
|
UsersActions.userJoined({
|
||||||
user: buildSignalingUser(joinedUser, buildKnownUserExtras(currentRoom, joinedUser.oderId))
|
user: buildSignalingUser(joinedUser, buildKnownUserExtras(currentRoom, joinedUser.oderId))
|
||||||
})
|
})
|
||||||
@@ -794,7 +809,20 @@ export class RoomsEffects {
|
|||||||
return EMPTY;
|
return EMPTY;
|
||||||
|
|
||||||
this.knownVoiceUsers.delete(signalingMessage.oderId);
|
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:
|
default:
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ export interface RoomsState {
|
|||||||
isConnecting: boolean;
|
isConnecting: boolean;
|
||||||
/** Whether the user is connected to a room. */
|
/** Whether the user is connected to a room. */
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
|
/** Whether the current room is using locally cached data while reconnecting. */
|
||||||
|
isSignalServerReconnecting: boolean;
|
||||||
/** Whether rooms are being loaded from local storage. */
|
/** Whether rooms are being loaded from local storage. */
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
/** Most recent error message, if any. */
|
/** Most recent error message, if any. */
|
||||||
@@ -98,6 +100,7 @@ export const initialState: RoomsState = {
|
|||||||
isSearching: false,
|
isSearching: false,
|
||||||
isConnecting: false,
|
isConnecting: false,
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
|
isSignalServerReconnecting: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
activeChannelId: 'general'
|
activeChannelId: 'general'
|
||||||
@@ -159,6 +162,7 @@ export const roomsReducer = createReducer(
|
|||||||
currentRoom: enriched,
|
currentRoom: enriched,
|
||||||
savedRooms: upsertRoom(state.savedRooms, enriched),
|
savedRooms: upsertRoom(state.savedRooms, enriched),
|
||||||
isConnecting: false,
|
isConnecting: false,
|
||||||
|
isSignalServerReconnecting: false,
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
activeChannelId: 'general'
|
activeChannelId: 'general'
|
||||||
};
|
};
|
||||||
@@ -185,6 +189,7 @@ export const roomsReducer = createReducer(
|
|||||||
currentRoom: enriched,
|
currentRoom: enriched,
|
||||||
savedRooms: upsertRoom(state.savedRooms, enriched),
|
savedRooms: upsertRoom(state.savedRooms, enriched),
|
||||||
isConnecting: false,
|
isConnecting: false,
|
||||||
|
isSignalServerReconnecting: false,
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
activeChannelId: 'general'
|
activeChannelId: 'general'
|
||||||
};
|
};
|
||||||
@@ -206,6 +211,7 @@ export const roomsReducer = createReducer(
|
|||||||
...state,
|
...state,
|
||||||
currentRoom: null,
|
currentRoom: null,
|
||||||
roomSettings: null,
|
roomSettings: null,
|
||||||
|
isSignalServerReconnecting: false,
|
||||||
isConnecting: false,
|
isConnecting: false,
|
||||||
isConnected: false
|
isConnected: false
|
||||||
})),
|
})),
|
||||||
@@ -279,6 +285,7 @@ export const roomsReducer = createReducer(
|
|||||||
// Delete room
|
// Delete room
|
||||||
on(RoomsActions.deleteRoomSuccess, (state, { roomId }) => ({
|
on(RoomsActions.deleteRoomSuccess, (state, { roomId }) => ({
|
||||||
...state,
|
...state,
|
||||||
|
isSignalServerReconnecting: state.currentRoom?.id === roomId ? false : state.isSignalServerReconnecting,
|
||||||
savedRooms: state.savedRooms.filter((room) => room.id !== roomId),
|
savedRooms: state.savedRooms.filter((room) => room.id !== roomId),
|
||||||
currentRoom: state.currentRoom?.id === roomId ? null : state.currentRoom
|
currentRoom: state.currentRoom?.id === roomId ? null : state.currentRoom
|
||||||
})),
|
})),
|
||||||
@@ -286,6 +293,7 @@ export const roomsReducer = createReducer(
|
|||||||
// Forget room (local only)
|
// Forget room (local only)
|
||||||
on(RoomsActions.forgetRoomSuccess, (state, { roomId }) => ({
|
on(RoomsActions.forgetRoomSuccess, (state, { roomId }) => ({
|
||||||
...state,
|
...state,
|
||||||
|
isSignalServerReconnecting: state.currentRoom?.id === roomId ? false : state.isSignalServerReconnecting,
|
||||||
savedRooms: state.savedRooms.filter((room) => room.id !== roomId),
|
savedRooms: state.savedRooms.filter((room) => room.id !== roomId),
|
||||||
currentRoom: state.currentRoom?.id === roomId ? null : state.currentRoom
|
currentRoom: state.currentRoom?.id === roomId ? null : state.currentRoom
|
||||||
})),
|
})),
|
||||||
@@ -295,6 +303,7 @@ export const roomsReducer = createReducer(
|
|||||||
...state,
|
...state,
|
||||||
currentRoom: enrichRoom(room),
|
currentRoom: enrichRoom(room),
|
||||||
savedRooms: upsertRoom(state.savedRooms, room),
|
savedRooms: upsertRoom(state.savedRooms, room),
|
||||||
|
isSignalServerReconnecting: false,
|
||||||
isConnected: true
|
isConnected: true
|
||||||
})),
|
})),
|
||||||
|
|
||||||
@@ -303,6 +312,7 @@ export const roomsReducer = createReducer(
|
|||||||
...state,
|
...state,
|
||||||
currentRoom: null,
|
currentRoom: null,
|
||||||
roomSettings: null,
|
roomSettings: null,
|
||||||
|
isSignalServerReconnecting: false,
|
||||||
isConnected: false
|
isConnected: false
|
||||||
})),
|
})),
|
||||||
|
|
||||||
@@ -367,6 +377,11 @@ export const roomsReducer = createReducer(
|
|||||||
isConnecting
|
isConnecting
|
||||||
})),
|
})),
|
||||||
|
|
||||||
|
on(RoomsActions.setSignalServerReconnecting, (state, { isReconnecting }) => ({
|
||||||
|
...state,
|
||||||
|
isSignalServerReconnecting: isReconnecting
|
||||||
|
})),
|
||||||
|
|
||||||
// Channel management
|
// Channel management
|
||||||
on(RoomsActions.selectChannel, (state, { channelId }) => ({
|
on(RoomsActions.selectChannel, (state, { channelId }) => ({
|
||||||
...state,
|
...state,
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ export const selectIsConnected = createSelector(
|
|||||||
selectRoomsState,
|
selectRoomsState,
|
||||||
(state) => state.isConnected
|
(state) => state.isConnected
|
||||||
);
|
);
|
||||||
|
export const selectIsSignalServerReconnecting = createSelector(
|
||||||
|
selectRoomsState,
|
||||||
|
(state) => state.isSignalServerReconnecting
|
||||||
|
);
|
||||||
export const selectRoomsError = createSelector(
|
export const selectRoomsError = createSelector(
|
||||||
selectRoomsState,
|
selectRoomsState,
|
||||||
(state) => state.error
|
(state) => state.error
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
const { spawn } = require('child_process');
|
const { spawn } = require('child_process');
|
||||||
|
|
||||||
|
const DEV_SINGLE_INSTANCE_EXIT_CODE = 23;
|
||||||
|
const DEV_SINGLE_INSTANCE_EXIT_CODE_ENV = 'METOYOU_SINGLE_INSTANCE_EXIT_CODE';
|
||||||
|
|
||||||
function isWaylandSession(env) {
|
function isWaylandSession(env) {
|
||||||
const sessionType = String(env.XDG_SESSION_TYPE || '').trim().toLowerCase();
|
const sessionType = String(env.XDG_SESSION_TYPE || '').trim().toLowerCase();
|
||||||
|
|
||||||
@@ -44,11 +47,40 @@ function buildElectronArgs(argv) {
|
|||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isDevelopmentLaunch(env) {
|
||||||
|
return String(env.NODE_ENV || '').trim().toLowerCase() === 'development';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildChildEnv(env) {
|
||||||
|
const nextEnv = { ...env };
|
||||||
|
|
||||||
|
if (isDevelopmentLaunch(env)) {
|
||||||
|
nextEnv[DEV_SINGLE_INSTANCE_EXIT_CODE_ENV] = String(DEV_SINGLE_INSTANCE_EXIT_CODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextEnv;
|
||||||
|
}
|
||||||
|
|
||||||
|
function keepProcessAliveForExistingInstance() {
|
||||||
|
console.log(
|
||||||
|
'Electron is already running; keeping the dev services alive and routing links to the open window.'
|
||||||
|
);
|
||||||
|
|
||||||
|
const intervalId = setInterval(() => {}, 60_000);
|
||||||
|
const shutdown = () => {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
process.exit(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.once('SIGINT', shutdown);
|
||||||
|
process.once('SIGTERM', shutdown);
|
||||||
|
}
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
const electronBinary = resolveElectronBinary();
|
const electronBinary = resolveElectronBinary();
|
||||||
const args = buildElectronArgs(process.argv.slice(2));
|
const args = buildElectronArgs(process.argv.slice(2));
|
||||||
const child = spawn(electronBinary, args, {
|
const child = spawn(electronBinary, args, {
|
||||||
env: process.env,
|
env: buildChildEnv(process.env),
|
||||||
stdio: 'inherit'
|
stdio: 'inherit'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,6 +90,11 @@ function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
child.on('exit', (code, signal) => {
|
child.on('exit', (code, signal) => {
|
||||||
|
if (code === DEV_SINGLE_INSTANCE_EXIT_CODE) {
|
||||||
|
keepProcessAliveForExistingInstance();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (signal) {
|
if (signal) {
|
||||||
process.kill(process.pid, signal);
|
process.kill(process.pid, signal);
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user