699 lines
21 KiB
TypeScript
699 lines
21 KiB
TypeScript
/* eslint-disable @typescript-eslint/member-ordering */
|
|
import {
|
|
Component,
|
|
DestroyRef,
|
|
computed,
|
|
effect,
|
|
inject,
|
|
signal
|
|
} from '@angular/core';
|
|
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
|
import { CommonModule } from '@angular/common';
|
|
import { FormsModule } from '@angular/forms';
|
|
import { Store } from '@ngrx/store';
|
|
import { NavigationEnd, Router } from '@angular/router';
|
|
import { NgIcon, provideIcons } from '@ng-icons/core';
|
|
import { lucidePhone, lucidePlus } from '@ng-icons/lucide';
|
|
import {
|
|
EMPTY,
|
|
Subject,
|
|
catchError,
|
|
filter,
|
|
firstValueFrom,
|
|
from,
|
|
map,
|
|
switchMap,
|
|
tap
|
|
} from 'rxjs';
|
|
|
|
import { Room, User } from '../../../shared-kernel';
|
|
import { UserBarComponent } from '../../../domains/authentication/feature/user-bar/user-bar.component';
|
|
import { VoiceSessionFacade } from '../../../domains/voice-session';
|
|
import { selectSavedRooms, selectCurrentRoom } from '../../../store/rooms/rooms.selectors';
|
|
import { selectCurrentUser, selectOnlineUsers } from '../../../store/users/users.selectors';
|
|
import { RoomsActions } from '../../../store/rooms/rooms.actions';
|
|
import { DatabaseService } from '../../../infrastructure/persistence';
|
|
import { NotificationsFacade } from '../../../domains/notifications';
|
|
import { DirectCallService, DirectCallSession } from '../../../domains/direct-call';
|
|
import { DmRailComponent } from '../../../domains/direct-message/feature/dm-rail/dm-rail.component';
|
|
import { type ServerInfo, ServerDirectoryFacade } from '../../../domains/server-directory';
|
|
import { ThemeNodeDirective } from '../../../domains/theme';
|
|
import { hasRoomBanForUser } from '../../../domains/access-control';
|
|
import {
|
|
ConfirmDialogComponent,
|
|
ContextMenuComponent,
|
|
LeaveServerDialogComponent
|
|
} from '../../../shared';
|
|
|
|
@Component({
|
|
selector: 'app-servers-rail',
|
|
standalone: true,
|
|
imports: [
|
|
CommonModule,
|
|
FormsModule,
|
|
NgIcon,
|
|
ConfirmDialogComponent,
|
|
ContextMenuComponent,
|
|
DmRailComponent,
|
|
LeaveServerDialogComponent,
|
|
ThemeNodeDirective,
|
|
UserBarComponent
|
|
],
|
|
viewProviders: [provideIcons({ lucidePhone, lucidePlus })],
|
|
templateUrl: './servers-rail.component.html'
|
|
})
|
|
export class ServersRailComponent {
|
|
private store = inject(Store);
|
|
private router = inject(Router);
|
|
private voiceSession = inject(VoiceSessionFacade);
|
|
private db = inject(DatabaseService);
|
|
private notifications = inject(NotificationsFacade);
|
|
readonly directCalls = inject(DirectCallService);
|
|
private serverDirectory = inject(ServerDirectoryFacade);
|
|
private destroyRef = inject(DestroyRef);
|
|
private banLookupRequestVersion = 0;
|
|
private visibleSavedRoomCache: Room[] = [];
|
|
private savedRoomJoinRequests = new Subject<{ room: Room; password?: string }>();
|
|
savedRooms = this.store.selectSignal(selectSavedRooms);
|
|
currentRoom = this.store.selectSignal(selectCurrentRoom);
|
|
|
|
showMenu = signal(false);
|
|
menuX = signal(72);
|
|
menuY = signal(100);
|
|
contextRoom = signal<Room | null>(null);
|
|
optimisticSelectedRoomId = signal<string | null>(null);
|
|
showLeaveConfirm = signal(false);
|
|
currentUser = this.store.selectSignal(selectCurrentUser);
|
|
onlineUsers = this.store.selectSignal(selectOnlineUsers);
|
|
bannedRoomLookup = signal<Record<string, boolean>>({});
|
|
isOnSearch = toSignal(
|
|
this.router.events.pipe(
|
|
filter((navigationEvent): navigationEvent is NavigationEnd => navigationEvent instanceof NavigationEnd),
|
|
map((navigationEvent) => navigationEvent.urlAfterRedirects.startsWith('/search'))
|
|
),
|
|
{ initialValue: this.router.url.startsWith('/search') }
|
|
);
|
|
isOnDirectMessage = toSignal(
|
|
this.router.events.pipe(
|
|
filter((navigationEvent): navigationEvent is NavigationEnd => navigationEvent instanceof NavigationEnd),
|
|
map((navigationEvent) => this.isDirectMessageUrl(navigationEvent.urlAfterRedirects))
|
|
),
|
|
{ initialValue: this.isDirectMessageUrl(this.router.url) }
|
|
);
|
|
isOnCall = toSignal(
|
|
this.router.events.pipe(
|
|
filter((navigationEvent): navigationEvent is NavigationEnd => navigationEvent instanceof NavigationEnd),
|
|
map((navigationEvent) => navigationEvent.urlAfterRedirects.startsWith('/call/'))
|
|
),
|
|
{ initialValue: this.router.url.startsWith('/call/') }
|
|
);
|
|
currentCallId = toSignal(
|
|
this.router.events.pipe(
|
|
filter((navigationEvent): navigationEvent is NavigationEnd => navigationEvent instanceof NavigationEnd),
|
|
map((navigationEvent) => this.callIdFromUrl(navigationEvent.urlAfterRedirects))
|
|
),
|
|
{ initialValue: this.callIdFromUrl(this.router.url) }
|
|
);
|
|
selectedCallIndex = computed(() => {
|
|
const routeCallId = this.currentCallId();
|
|
const visibleCalls = this.directCalls.visibleActiveSessions();
|
|
|
|
if (routeCallId) {
|
|
const routeMatchIndex = visibleCalls.findIndex((call) => call.callId === routeCallId || call.conversationId === routeCallId);
|
|
|
|
if (routeMatchIndex >= 0) {
|
|
return routeMatchIndex;
|
|
}
|
|
}
|
|
|
|
const currentSession = this.directCalls.currentSession();
|
|
|
|
if (!currentSession) {
|
|
return -1;
|
|
}
|
|
|
|
return visibleCalls.findIndex((call) => call.callId === currentSession.callId);
|
|
});
|
|
bannedServerName = signal('');
|
|
showBannedDialog = signal(false);
|
|
showPasswordDialog = signal(false);
|
|
passwordPromptRoom = signal<Room | null>(null);
|
|
joinPassword = signal('');
|
|
joinPasswordError = signal<string | null>(null);
|
|
visibleSavedRooms = computed(() => this.stabilizeVisibleSavedRooms(this.savedRooms().filter((room) => !this.isRoomMarkedBanned(room))));
|
|
voicePresenceByRoom = computed(() => {
|
|
const presence: Record<string, number> = {};
|
|
const seenByRoom = new Map<string, Set<string>>();
|
|
const addVoicePresence = (user: User | null | undefined): void => {
|
|
if (!user) {
|
|
return;
|
|
}
|
|
|
|
const voiceState = user?.voiceState;
|
|
const roomId = voiceState?.serverId;
|
|
|
|
if (!voiceState?.isConnected || !roomId) {
|
|
return;
|
|
}
|
|
|
|
const userKey = user.oderId || user.id;
|
|
|
|
let seenUsers = seenByRoom.get(roomId);
|
|
|
|
if (!seenUsers) {
|
|
seenUsers = new Set<string>();
|
|
seenByRoom.set(roomId, seenUsers);
|
|
}
|
|
|
|
if (seenUsers.has(userKey)) {
|
|
return;
|
|
}
|
|
|
|
seenUsers.add(userKey);
|
|
presence[roomId] = (presence[roomId] ?? 0) + 1;
|
|
};
|
|
|
|
for (const user of this.onlineUsers()) {
|
|
addVoicePresence(user);
|
|
}
|
|
|
|
addVoicePresence(this.currentUser());
|
|
|
|
return presence;
|
|
});
|
|
|
|
constructor() {
|
|
effect(() => {
|
|
const rooms = this.savedRooms();
|
|
const currentUser = this.currentUser();
|
|
|
|
void this.refreshBannedLookup(rooms, currentUser ?? null);
|
|
});
|
|
|
|
effect(() => {
|
|
const optimisticRoomId = this.optimisticSelectedRoomId();
|
|
|
|
if (!optimisticRoomId) {
|
|
return;
|
|
}
|
|
|
|
if (this.currentRoom()?.id === optimisticRoomId && !this.isOnDirectMessage() && !this.isOnCall()) {
|
|
this.optimisticSelectedRoomId.set(null);
|
|
}
|
|
});
|
|
|
|
this.savedRoomJoinRequests
|
|
.pipe(
|
|
switchMap(({ room, password }) => this.requestJoinInBackground(room, password)),
|
|
takeUntilDestroyed(this.destroyRef)
|
|
)
|
|
.subscribe();
|
|
}
|
|
|
|
initial(name?: string): string {
|
|
if (!name)
|
|
return '?';
|
|
|
|
const ch = name.trim()[0]?.toUpperCase();
|
|
|
|
return ch || '?';
|
|
}
|
|
|
|
trackRoomId = (index: number, room: Room) => room.id;
|
|
|
|
createServer(): void {
|
|
const voiceServerId = this.voiceSession.getVoiceServerId();
|
|
|
|
this.optimisticSelectedRoomId.set(null);
|
|
|
|
if (voiceServerId) {
|
|
this.voiceSession.setViewingVoiceServer(false);
|
|
}
|
|
|
|
this.router.navigate(['/search']);
|
|
}
|
|
|
|
joinSavedRoom(room: Room): void {
|
|
const targetRoom = this.savedRooms().find((savedRoom) => savedRoom.id === room.id) ?? room;
|
|
const currentUserId = localStorage.getItem('metoyou_currentUserId');
|
|
|
|
if (!currentUserId) {
|
|
this.router.navigate(['/login']);
|
|
return;
|
|
}
|
|
|
|
if (this.isRoomMarkedBanned(targetRoom)) {
|
|
this.bannedServerName.set(targetRoom.name);
|
|
this.showBannedDialog.set(true);
|
|
return;
|
|
}
|
|
|
|
this.optimisticSelectedRoomId.set(targetRoom.id);
|
|
this.activateSavedRoom(targetRoom);
|
|
this.savedRoomJoinRequests.next({ room: targetRoom });
|
|
}
|
|
|
|
openCall(callId: string): void {
|
|
this.optimisticSelectedRoomId.set(null);
|
|
void this.directCalls.openCallView(callId);
|
|
}
|
|
|
|
isSelectedCall(callIndex: number): boolean {
|
|
return this.selectedCallIndex() === callIndex;
|
|
}
|
|
|
|
callAvatarUrls(call: DirectCallSession): string[] {
|
|
if (call.participantIds.length <= 2) {
|
|
return [];
|
|
}
|
|
|
|
return Object.values(call.participants)
|
|
.filter((participant) => participant.joined)
|
|
.map((participant) => this.directCalls.userForParticipant(participant.userId)?.avatarUrl || participant.profile.avatarUrl)
|
|
.filter((avatarUrl): avatarUrl is string => !!avatarUrl)
|
|
.slice(0, 3);
|
|
}
|
|
|
|
closeBannedDialog(): void {
|
|
this.showBannedDialog.set(false);
|
|
this.bannedServerName.set('');
|
|
}
|
|
|
|
closePasswordDialog(): void {
|
|
this.showPasswordDialog.set(false);
|
|
this.passwordPromptRoom.set(null);
|
|
this.joinPassword.set('');
|
|
this.joinPasswordError.set(null);
|
|
}
|
|
|
|
confirmPasswordJoin(): void {
|
|
const room = this.passwordPromptRoom();
|
|
|
|
if (!room)
|
|
return;
|
|
|
|
this.joinPasswordError.set(null);
|
|
this.savedRoomJoinRequests.next({ room, password: this.joinPassword() });
|
|
}
|
|
|
|
isRoomMarkedBanned(room: Room): boolean {
|
|
return !!this.bannedRoomLookup()[room.id];
|
|
}
|
|
|
|
private callIdFromUrl(url: string): string | null {
|
|
const path = url.split(/[?#]/, 1)[0];
|
|
const match = path.match(/^\/call\/([^/]+)/);
|
|
|
|
return match?.[1] ? decodeURIComponent(match[1]) : null;
|
|
}
|
|
|
|
openContextMenu(evt: MouseEvent, room: Room): void {
|
|
evt.preventDefault();
|
|
this.contextRoom.set(room);
|
|
this.menuX.set(Math.max(evt.clientX + 8, 72));
|
|
this.menuY.set(evt.clientY);
|
|
this.showMenu.set(true);
|
|
}
|
|
|
|
closeMenu(): void {
|
|
this.showMenu.set(false);
|
|
}
|
|
|
|
isCurrentContextRoom(): boolean {
|
|
const ctx = this.contextRoom();
|
|
const cur = this.currentRoom();
|
|
|
|
return !!ctx && !!cur && ctx.id === cur.id;
|
|
}
|
|
|
|
openLeaveConfirm(): void {
|
|
this.closeMenu();
|
|
|
|
if (this.contextRoom()) {
|
|
this.showLeaveConfirm.set(true);
|
|
}
|
|
}
|
|
|
|
confirmLeave(result: { nextOwnerKey?: string }): void {
|
|
const ctx = this.contextRoom();
|
|
|
|
if (!ctx)
|
|
return;
|
|
|
|
const isCurrentRoom = this.currentRoom()?.id === ctx.id;
|
|
|
|
this.store.dispatch(
|
|
RoomsActions.forgetRoom({
|
|
roomId: ctx.id,
|
|
nextOwnerKey: result.nextOwnerKey
|
|
})
|
|
);
|
|
|
|
if (isCurrentRoom) {
|
|
this.optimisticSelectedRoomId.set(null);
|
|
this.router.navigate(['/search']);
|
|
}
|
|
|
|
this.showLeaveConfirm.set(false);
|
|
this.contextRoom.set(null);
|
|
}
|
|
|
|
cancelLeave(): void {
|
|
this.showLeaveConfirm.set(false);
|
|
}
|
|
|
|
toggleRoomNotifications(): void {
|
|
const room = this.contextRoom();
|
|
|
|
if (!room) {
|
|
return;
|
|
}
|
|
|
|
this.notifications.setRoomMuted(room.id, !this.notifications.isRoomMuted(room.id));
|
|
this.closeMenu();
|
|
}
|
|
|
|
isRoomNotificationsMuted(roomId: string): boolean {
|
|
return this.notifications.isRoomMuted(roomId);
|
|
}
|
|
|
|
roomUnreadCount(roomId: string): number {
|
|
return this.notifications.roomUnreadCount(roomId);
|
|
}
|
|
|
|
voicePresenceCount(roomId: string): number {
|
|
return this.voicePresenceByRoom()[roomId] ?? 0;
|
|
}
|
|
|
|
formatUnreadCount(count: number): string {
|
|
return count > 99 ? '99+' : String(count);
|
|
}
|
|
|
|
isSelectedRoom(room: Room): boolean {
|
|
if (this.isOnDirectMessage() || this.isOnCall()) {
|
|
return false;
|
|
}
|
|
|
|
const optimisticRoomId = this.optimisticSelectedRoomId();
|
|
|
|
if (optimisticRoomId) {
|
|
return optimisticRoomId === room.id;
|
|
}
|
|
|
|
return this.currentRoom()?.id === room.id;
|
|
}
|
|
|
|
private stabilizeVisibleSavedRooms(nextRooms: Room[]): Room[] {
|
|
const previousById = new Map(this.visibleSavedRoomCache.map((room) => [room.id, room]));
|
|
const stabilizedRooms = nextRooms.map((room) => {
|
|
const previousRoom = previousById.get(room.id);
|
|
|
|
return previousRoom && this.hasSameRailRoomView(previousRoom, room) ? previousRoom : room;
|
|
});
|
|
|
|
if (
|
|
stabilizedRooms.length === this.visibleSavedRoomCache.length
|
|
&& stabilizedRooms.every((room, index) => room === this.visibleSavedRoomCache[index])
|
|
) {
|
|
return this.visibleSavedRoomCache;
|
|
}
|
|
|
|
this.visibleSavedRoomCache = stabilizedRooms;
|
|
return stabilizedRooms;
|
|
}
|
|
|
|
private hasSameRailRoomView(previousRoom: Room, nextRoom: Room): boolean {
|
|
return previousRoom.id === nextRoom.id && previousRoom.name === nextRoom.name && previousRoom.icon === nextRoom.icon;
|
|
}
|
|
|
|
private isDirectMessageUrl(url: string): boolean {
|
|
const path = url.split(/[?#]/, 1)[0];
|
|
|
|
return path === '/dm' || path.startsWith('/dm/') || path === '/pm' || path.startsWith('/pm/');
|
|
}
|
|
|
|
private async refreshBannedLookup(rooms: Room[], currentUser: User | null): Promise<void> {
|
|
const requestVersion = ++this.banLookupRequestVersion;
|
|
|
|
if (!currentUser || rooms.length === 0) {
|
|
this.bannedRoomLookup.set({});
|
|
return;
|
|
}
|
|
|
|
const persistedUserId = localStorage.getItem('metoyou_currentUserId');
|
|
const entries = await Promise.all(
|
|
rooms.map(async (room) => {
|
|
const bans = await this.db.getBansForRoom(room.id);
|
|
|
|
return [room.id, hasRoomBanForUser(bans, currentUser, persistedUserId)] as const;
|
|
})
|
|
);
|
|
|
|
if (requestVersion !== this.banLookupRequestVersion) {
|
|
return;
|
|
}
|
|
|
|
this.bannedRoomLookup.set(Object.fromEntries(entries));
|
|
}
|
|
|
|
private prepareVoiceContext(room: Room): void {
|
|
const voiceServerId = this.voiceSession.getVoiceServerId();
|
|
|
|
if (voiceServerId && voiceServerId !== room.id) {
|
|
this.voiceSession.setViewingVoiceServer(false);
|
|
} else if (voiceServerId === room.id) {
|
|
this.voiceSession.setViewingVoiceServer(true);
|
|
}
|
|
}
|
|
|
|
private activateSavedRoom(room: Room): void {
|
|
this.prepareVoiceContext(room);
|
|
this.closePasswordDialog();
|
|
this.store.dispatch(RoomsActions.setSignalServerReconnecting({ isReconnecting: false }));
|
|
this.store.dispatch(RoomsActions.viewServer({ room, skipBanCheck: true }));
|
|
}
|
|
|
|
private requestJoinInBackground(room: Room, password?: string) {
|
|
const currentUserId = localStorage.getItem('metoyou_currentUserId');
|
|
const currentUser = this.currentUser();
|
|
|
|
if (!currentUserId)
|
|
return EMPTY;
|
|
|
|
this.joinPasswordError.set(null);
|
|
|
|
return from(this.resolveRoomJoinTarget(room)).pipe(
|
|
switchMap((joinTarget) => {
|
|
if (!joinTarget.selector) {
|
|
if (this.currentRoom()?.id === room.id) {
|
|
this.store.dispatch(RoomsActions.setSignalServerReconnecting({ isReconnecting: true }));
|
|
}
|
|
|
|
return EMPTY;
|
|
}
|
|
|
|
return this.serverDirectory
|
|
.requestJoin(
|
|
{
|
|
roomId: room.id,
|
|
userId: currentUserId,
|
|
userPublicKey: currentUser?.oderId || currentUserId,
|
|
displayName: currentUser?.displayName || 'Anonymous',
|
|
password: password?.trim() || undefined
|
|
},
|
|
joinTarget.selector
|
|
)
|
|
.pipe(
|
|
tap((response) => {
|
|
this.closePasswordDialog();
|
|
this.store.dispatch(
|
|
RoomsActions.updateRoom({
|
|
roomId: room.id,
|
|
changes: this.toRoomRefreshChanges(joinTarget.room, response.server, response.signalingUrl)
|
|
})
|
|
);
|
|
|
|
if (this.currentRoom()?.id === room.id) {
|
|
this.store.dispatch(RoomsActions.setSignalServerReconnecting({ isReconnecting: false }));
|
|
}
|
|
})
|
|
);
|
|
}),
|
|
catchError((error: unknown) => {
|
|
this.handleBackgroundJoinError(room, error);
|
|
return EMPTY;
|
|
})
|
|
);
|
|
}
|
|
|
|
private handleBackgroundJoinError(room: Room, error: unknown): void {
|
|
const serverError = error as {
|
|
error?: { error?: string; errorCode?: string };
|
|
status?: number;
|
|
};
|
|
const errorCode = serverError?.error?.errorCode;
|
|
const message = serverError?.error?.error || 'Failed to join server';
|
|
|
|
if (errorCode === 'PASSWORD_REQUIRED') {
|
|
this.passwordPromptRoom.set(room);
|
|
this.showPasswordDialog.set(true);
|
|
this.joinPasswordError.set(message);
|
|
return;
|
|
}
|
|
|
|
if (errorCode === 'BANNED') {
|
|
this.closePasswordDialog();
|
|
this.optimisticSelectedRoomId.set(null);
|
|
this.bannedRoomLookup.update((lookup) => ({
|
|
...lookup,
|
|
[room.id]: true
|
|
}));
|
|
|
|
this.bannedServerName.set(room.name);
|
|
this.showBannedDialog.set(true);
|
|
return;
|
|
}
|
|
|
|
if (this.shouldFallbackToOfflineView(error) && this.currentRoom()?.id === room.id) {
|
|
this.store.dispatch(RoomsActions.setSignalServerReconnecting({ isReconnecting: true }));
|
|
}
|
|
}
|
|
|
|
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 toRoomRefreshChanges(room: Room, server: ServerInfo, signalingUrl?: string): Partial<Room> {
|
|
const resolvedSource = this.serverDirectory.normaliseRoomSignalSource(
|
|
{
|
|
sourceId: server.sourceId ?? room.sourceId,
|
|
sourceName: server.sourceName ?? room.sourceName,
|
|
sourceUrl: server.sourceUrl ?? room.sourceUrl,
|
|
signalingUrl,
|
|
fallbackName: server.sourceName ?? room.sourceName ?? room.name
|
|
},
|
|
{
|
|
ensureEndpoint: true
|
|
}
|
|
);
|
|
|
|
return {
|
|
name: server.name,
|
|
description: server.description,
|
|
topic: server.topic ?? room.topic,
|
|
hostId: server.ownerId || room.hostId,
|
|
userCount: server.userCount,
|
|
maxUsers: server.maxUsers,
|
|
hasPassword:
|
|
typeof server.hasPassword === 'boolean' ? server.hasPassword : typeof room.hasPassword === 'boolean' ? room.hasPassword : !!room.password,
|
|
isPrivate: server.isPrivate,
|
|
createdAt: server.createdAt,
|
|
channels: Array.isArray(server.channels) && server.channels.length > 0 ? server.channels : room.channels,
|
|
...resolvedSource
|
|
};
|
|
}
|
|
|
|
private async resolveRoomJoinTarget(room: Room): Promise<{
|
|
room: Room;
|
|
selector: ReturnType<ServerDirectoryFacade['buildRoomSignalSelector']>;
|
|
}> {
|
|
let resolvedRoom = this.applyResolvedRoomSource(
|
|
room,
|
|
this.serverDirectory.normaliseRoomSignalSource(
|
|
{
|
|
sourceId: room.sourceId,
|
|
sourceName: room.sourceName,
|
|
sourceUrl: room.sourceUrl,
|
|
fallbackName: room.sourceName ?? room.name
|
|
},
|
|
{
|
|
ensureEndpoint: !!room.sourceUrl
|
|
}
|
|
)
|
|
);
|
|
let selector = this.serverDirectory.buildRoomSignalSelector(
|
|
{
|
|
sourceId: resolvedRoom.sourceId,
|
|
sourceName: resolvedRoom.sourceName,
|
|
sourceUrl: resolvedRoom.sourceUrl,
|
|
fallbackName: resolvedRoom.sourceName ?? resolvedRoom.name
|
|
},
|
|
{
|
|
ensureEndpoint: !!resolvedRoom.sourceUrl
|
|
}
|
|
);
|
|
|
|
const authoritativeServer = selector ? await firstValueFrom(this.serverDirectory.getServer(room.id, selector)) : null;
|
|
|
|
if (!authoritativeServer) {
|
|
return {
|
|
room: resolvedRoom,
|
|
selector
|
|
};
|
|
}
|
|
|
|
const authoritativeSource = this.serverDirectory.normaliseRoomSignalSource(
|
|
{
|
|
sourceId: authoritativeServer.sourceId ?? resolvedRoom.sourceId,
|
|
sourceName: authoritativeServer.sourceName ?? resolvedRoom.sourceName,
|
|
sourceUrl: authoritativeServer.sourceUrl ?? resolvedRoom.sourceUrl,
|
|
fallbackName: authoritativeServer.sourceName ?? resolvedRoom.sourceName ?? resolvedRoom.name
|
|
},
|
|
{
|
|
ensureEndpoint: !!(authoritativeServer.sourceUrl ?? resolvedRoom.sourceUrl)
|
|
}
|
|
);
|
|
|
|
resolvedRoom = this.applyResolvedRoomSource(resolvedRoom, authoritativeSource);
|
|
selector = this.serverDirectory.buildRoomSignalSelector(
|
|
{
|
|
sourceId: resolvedRoom.sourceId,
|
|
sourceName: resolvedRoom.sourceName,
|
|
sourceUrl: resolvedRoom.sourceUrl,
|
|
fallbackName: resolvedRoom.sourceName ?? resolvedRoom.name
|
|
},
|
|
{
|
|
ensureEndpoint: !!resolvedRoom.sourceUrl
|
|
}
|
|
);
|
|
|
|
return {
|
|
room: resolvedRoom,
|
|
selector
|
|
};
|
|
}
|
|
|
|
private applyResolvedRoomSource(room: Room, source: Pick<Room, 'sourceId' | 'sourceName' | 'sourceUrl'>): Room {
|
|
const nextRoom: Room = {
|
|
...room,
|
|
sourceId: source.sourceId,
|
|
sourceName: source.sourceName,
|
|
sourceUrl: source.sourceUrl
|
|
};
|
|
|
|
if (room.sourceId === nextRoom.sourceId && room.sourceName === nextRoom.sourceName && room.sourceUrl === nextRoom.sourceUrl) {
|
|
return room;
|
|
}
|
|
|
|
this.store.dispatch(
|
|
RoomsActions.updateRoom({
|
|
roomId: room.id,
|
|
changes: {
|
|
sourceId: nextRoom.sourceId,
|
|
sourceName: nextRoom.sourceName,
|
|
sourceUrl: nextRoom.sourceUrl
|
|
}
|
|
})
|
|
);
|
|
|
|
return nextRoom;
|
|
}
|
|
}
|