All checks were successful
Queue Release Build / prepare (push) Successful in 17s
Deploy Web Apps / deploy (push) Successful in 9m58s
Queue Release Build / build-linux (push) Successful in 26m26s
Queue Release Build / build-windows (push) Successful in 25m3s
Queue Release Build / finalize (push) Successful in 1m43s
350 lines
9.9 KiB
TypeScript
350 lines
9.9 KiB
TypeScript
/* eslint-disable @typescript-eslint/member-ordering */
|
|
import {
|
|
Component,
|
|
computed,
|
|
effect,
|
|
inject,
|
|
signal
|
|
} from '@angular/core';
|
|
import { CommonModule, NgOptimizedImage } from '@angular/common';
|
|
import { FormsModule } from '@angular/forms';
|
|
import { Store } from '@ngrx/store';
|
|
import { Router } from '@angular/router';
|
|
import { NgIcon, provideIcons } from '@ng-icons/core';
|
|
import { lucidePlus } from '@ng-icons/lucide';
|
|
import { firstValueFrom } from 'rxjs';
|
|
|
|
import { Room, User } from '../../core/models/index';
|
|
import { selectSavedRooms, selectCurrentRoom } from '../../store/rooms/rooms.selectors';
|
|
import { selectCurrentUser } from '../../store/users/users.selectors';
|
|
import { VoiceSessionService } from '../../core/services/voice-session.service';
|
|
import { WebRTCService } from '../../core/services/webrtc.service';
|
|
import { RoomsActions } from '../../store/rooms/rooms.actions';
|
|
import { DatabaseService } from '../../core/services/database.service';
|
|
import { ServerDirectoryService } from '../../core/services/server-directory.service';
|
|
import { hasRoomBanForUser } from '../../core/helpers/room-ban.helpers';
|
|
import {
|
|
ConfirmDialogComponent,
|
|
ContextMenuComponent,
|
|
LeaveServerDialogComponent
|
|
} from '../../shared';
|
|
|
|
@Component({
|
|
selector: 'app-servers-rail',
|
|
standalone: true,
|
|
imports: [
|
|
CommonModule,
|
|
FormsModule,
|
|
NgIcon,
|
|
ConfirmDialogComponent,
|
|
ContextMenuComponent,
|
|
LeaveServerDialogComponent,
|
|
NgOptimizedImage
|
|
],
|
|
viewProviders: [provideIcons({ lucidePlus })],
|
|
templateUrl: './servers-rail.component.html'
|
|
})
|
|
export class ServersRailComponent {
|
|
private store = inject(Store);
|
|
private router = inject(Router);
|
|
private voiceSession = inject(VoiceSessionService);
|
|
private webrtc = inject(WebRTCService);
|
|
private db = inject(DatabaseService);
|
|
private serverDirectory = inject(ServerDirectoryService);
|
|
private banLookupRequestVersion = 0;
|
|
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);
|
|
bannedRoomLookup = signal<Record<string, boolean>>({});
|
|
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)));
|
|
|
|
constructor() {
|
|
effect(() => {
|
|
const rooms = this.savedRooms();
|
|
const currentUser = this.currentUser();
|
|
|
|
void this.refreshBannedLookup(rooms, currentUser ?? null);
|
|
});
|
|
}
|
|
|
|
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']);
|
|
}
|
|
|
|
async joinSavedRoom(room: Room): Promise<void> {
|
|
const currentUserId = localStorage.getItem('metoyou_currentUserId');
|
|
|
|
if (!currentUserId) {
|
|
this.router.navigate(['/login']);
|
|
return;
|
|
}
|
|
|
|
if (await this.isRoomBanned(room)) {
|
|
this.bannedServerName.set(room.name);
|
|
this.showBannedDialog.set(true);
|
|
return;
|
|
}
|
|
|
|
const roomWsUrl = this.serverDirectory.getWebSocketUrl({
|
|
sourceId: room.sourceId,
|
|
sourceUrl: room.sourceUrl
|
|
});
|
|
const currentWsUrl = this.webrtc.getCurrentSignalingUrl();
|
|
|
|
this.prepareVoiceContext(room);
|
|
|
|
if (this.webrtc.hasJoinedServer(room.id) && roomWsUrl === currentWsUrl) {
|
|
this.store.dispatch(RoomsActions.setSignalServerReconnecting({ isReconnecting: false }));
|
|
this.store.dispatch(RoomsActions.viewServer({ room,
|
|
skipBanCheck: true }));
|
|
} else {
|
|
await this.attemptJoinRoom(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);
|
|
}
|
|
|
|
async confirmPasswordJoin(): Promise<void> {
|
|
const room = this.passwordPromptRoom();
|
|
|
|
if (!room)
|
|
return;
|
|
|
|
await this.attemptJoinRoom(room, 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);
|
|
}
|
|
|
|
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 async isRoomBanned(room: Room): Promise<boolean> {
|
|
const currentUser = this.currentUser();
|
|
const persistedUserId = localStorage.getItem('metoyou_currentUserId');
|
|
|
|
if (!currentUser && !persistedUserId) {
|
|
return false;
|
|
}
|
|
|
|
const bans = await this.db.getBansForRoom(room.id);
|
|
|
|
return hasRoomBanForUser(bans, currentUser, persistedUserId);
|
|
}
|
|
|
|
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 async attemptJoinRoom(room: Room, password?: string): Promise<void> {
|
|
const currentUserId = localStorage.getItem('metoyou_currentUserId');
|
|
const currentUser = this.currentUser();
|
|
|
|
if (!currentUserId)
|
|
return;
|
|
|
|
this.joinPasswordError.set(null);
|
|
|
|
try {
|
|
const response = await firstValueFrom(this.serverDirectory.requestJoin({
|
|
roomId: room.id,
|
|
userId: currentUserId,
|
|
userPublicKey: currentUser?.oderId || currentUserId,
|
|
displayName: currentUser?.displayName || 'Anonymous',
|
|
password: password?.trim() || undefined
|
|
}, {
|
|
sourceId: room.sourceId,
|
|
sourceUrl: room.sourceUrl
|
|
}));
|
|
|
|
this.closePasswordDialog();
|
|
this.store.dispatch(
|
|
RoomsActions.joinRoom({
|
|
roomId: room.id,
|
|
serverInfo: {
|
|
...this.toServerInfo(room),
|
|
...response.server
|
|
}
|
|
})
|
|
);
|
|
} catch (error: unknown) {
|
|
const serverError = error as {
|
|
error?: { error?: string; errorCode?: string };
|
|
};
|
|
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.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,
|
|
skipBanCheck: 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 toServerInfo(room: Room) {
|
|
return {
|
|
id: room.id,
|
|
name: room.name,
|
|
description: room.description,
|
|
hostName: room.hostId || 'Unknown',
|
|
userCount: room.userCount ?? 0,
|
|
maxUsers: room.maxUsers ?? 50,
|
|
hasPassword: typeof room.hasPassword === 'boolean' ? room.hasPassword : !!room.password,
|
|
isPrivate: room.isPrivate,
|
|
createdAt: room.createdAt,
|
|
ownerId: room.hostId,
|
|
sourceId: room.sourceId,
|
|
sourceName: room.sourceName,
|
|
sourceUrl: room.sourceUrl
|
|
};
|
|
}
|
|
}
|