refactor: Clean lint errors and organise files
This commit is contained in:
@@ -0,0 +1,562 @@
|
||||
/* 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, NgOptimizedImage } 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 { 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 { type ServerInfo, ServerDirectoryFacade } from '../../../domains/server-directory';
|
||||
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,
|
||||
LeaveServerDialogComponent,
|
||||
NgOptimizedImage,
|
||||
UserBarComponent
|
||||
],
|
||||
viewProviders: [provideIcons({ 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);
|
||||
private serverDirectory = inject(ServerDirectoryFacade);
|
||||
private destroyRef = inject(DestroyRef);
|
||||
private banLookupRequestVersion = 0;
|
||||
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);
|
||||
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') }
|
||||
);
|
||||
bannedServerName = signal('');
|
||||
showBannedDialog = signal(false);
|
||||
showPasswordDialog = signal(false);
|
||||
passwordPromptRoom = signal<Room | null>(null);
|
||||
joinPassword = signal('');
|
||||
joinPasswordError = signal<string | null>(null);
|
||||
visibleSavedRooms = computed(() => 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);
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
if (voiceServerId) {
|
||||
this.voiceSession.setViewingVoiceServer(false);
|
||||
}
|
||||
|
||||
this.router.navigate(['/search']);
|
||||
}
|
||||
|
||||
joinSavedRoom(room: Room): void {
|
||||
const currentUserId = localStorage.getItem('metoyou_currentUserId');
|
||||
|
||||
if (!currentUserId) {
|
||||
this.router.navigate(['/login']);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isRoomMarkedBanned(room)) {
|
||||
this.bannedServerName.set(room.name);
|
||||
this.showBannedDialog.set(true);
|
||||
return;
|
||||
}
|
||||
|
||||
this.activateSavedRoom(room);
|
||||
this.savedRoomJoinRequests.next({ room });
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
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.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 {
|
||||
return this.currentRoom()?.id === room.id;
|
||||
}
|
||||
|
||||
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.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
|
||||
) ?? await firstValueFrom(this.serverDirectory.findServerAcrossActiveEndpoints(room.id, {
|
||||
sourceId: resolvedRoom.sourceId,
|
||||
sourceName: resolvedRoom.sourceName,
|
||||
sourceUrl: resolvedRoom.sourceUrl,
|
||||
fallbackName: resolvedRoom.sourceName ?? resolvedRoom.name
|
||||
}));
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user