Move toju-app into own its folder
This commit is contained in:
@@ -0,0 +1,352 @@
|
||||
/* eslint-disable @typescript-eslint/member-ordering */
|
||||
import {
|
||||
Component,
|
||||
effect,
|
||||
inject,
|
||||
OnInit,
|
||||
signal
|
||||
} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import {
|
||||
debounceTime,
|
||||
distinctUntilChanged,
|
||||
firstValueFrom,
|
||||
Subject
|
||||
} from 'rxjs';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import {
|
||||
lucideSearch,
|
||||
lucideUsers,
|
||||
lucideLock,
|
||||
lucideGlobe,
|
||||
lucidePlus,
|
||||
lucideSettings
|
||||
} from '@ng-icons/lucide';
|
||||
|
||||
import { RoomsActions } from '../../../../store/rooms/rooms.actions';
|
||||
import {
|
||||
selectSearchResults,
|
||||
selectIsSearching,
|
||||
selectRoomsError,
|
||||
selectSavedRooms
|
||||
} from '../../../../store/rooms/rooms.selectors';
|
||||
import { Room, User } from '../../../../shared-kernel';
|
||||
import { SettingsModalService } from '../../../../core/services/settings-modal.service';
|
||||
import { DatabaseService } from '../../../../infrastructure/persistence';
|
||||
import { type ServerInfo } from '../../domain/server-directory.models';
|
||||
import { ServerDirectoryFacade } from '../../application/server-directory.facade';
|
||||
import { selectCurrentUser } from '../../../../store/users/users.selectors';
|
||||
import { ConfirmDialogComponent } from '../../../../shared';
|
||||
import { hasRoomBanForUser } from '../../../../core/helpers/room-ban.helpers';
|
||||
|
||||
@Component({
|
||||
selector: 'app-server-search',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
NgIcon,
|
||||
ConfirmDialogComponent
|
||||
],
|
||||
viewProviders: [
|
||||
provideIcons({
|
||||
lucideSearch,
|
||||
lucideUsers,
|
||||
lucideLock,
|
||||
lucideGlobe,
|
||||
lucidePlus,
|
||||
lucideSettings
|
||||
})
|
||||
],
|
||||
templateUrl: './server-search.component.html'
|
||||
})
|
||||
/**
|
||||
* Server search and discovery view with server creation dialog.
|
||||
* Allows users to search for, join, and create new servers.
|
||||
*/
|
||||
export class ServerSearchComponent implements OnInit {
|
||||
private store = inject(Store);
|
||||
private router = inject(Router);
|
||||
private settingsModal = inject(SettingsModalService);
|
||||
private db = inject(DatabaseService);
|
||||
private serverDirectory = inject(ServerDirectoryFacade);
|
||||
private searchSubject = new Subject<string>();
|
||||
private banLookupRequestVersion = 0;
|
||||
|
||||
searchQuery = '';
|
||||
searchResults = this.store.selectSignal(selectSearchResults);
|
||||
isSearching = this.store.selectSignal(selectIsSearching);
|
||||
error = this.store.selectSignal(selectRoomsError);
|
||||
savedRooms = this.store.selectSignal(selectSavedRooms);
|
||||
currentUser = this.store.selectSignal(selectCurrentUser);
|
||||
activeEndpoints = this.serverDirectory.activeServers;
|
||||
bannedServerLookup = signal<Record<string, boolean>>({});
|
||||
bannedServerName = signal('');
|
||||
showBannedDialog = signal(false);
|
||||
showPasswordDialog = signal(false);
|
||||
passwordPromptServer = signal<ServerInfo | null>(null);
|
||||
joinPassword = signal('');
|
||||
joinPasswordError = signal<string | null>(null);
|
||||
joinErrorMessage = signal<string | null>(null);
|
||||
|
||||
// Create dialog state
|
||||
showCreateDialog = signal(false);
|
||||
newServerName = signal('');
|
||||
newServerDescription = signal('');
|
||||
newServerTopic = signal('');
|
||||
newServerPrivate = signal(false);
|
||||
newServerPassword = signal('');
|
||||
newServerSourceId = '';
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
const servers = this.searchResults();
|
||||
const currentUser = this.currentUser();
|
||||
|
||||
void this.refreshBannedLookup(servers, currentUser ?? null);
|
||||
});
|
||||
}
|
||||
|
||||
/** Initialize server search, load saved rooms, and set up debounced search. */
|
||||
ngOnInit(): void {
|
||||
// Initial load
|
||||
this.store.dispatch(RoomsActions.searchServers({ query: '' }));
|
||||
this.store.dispatch(RoomsActions.loadRooms());
|
||||
|
||||
// Setup debounced search
|
||||
this.searchSubject.pipe(debounceTime(300), distinctUntilChanged()).subscribe((query) => {
|
||||
this.store.dispatch(RoomsActions.searchServers({ query }));
|
||||
});
|
||||
}
|
||||
|
||||
/** Emit a search query to the debounced search subject. */
|
||||
onSearchChange(query: string): void {
|
||||
this.searchSubject.next(query);
|
||||
}
|
||||
|
||||
/** Join a server from the search results. Redirects to login if unauthenticated. */
|
||||
async joinServer(server: ServerInfo): Promise<void> {
|
||||
const currentUserId = localStorage.getItem('metoyou_currentUserId');
|
||||
|
||||
if (!currentUserId) {
|
||||
this.router.navigate(['/login']);
|
||||
return;
|
||||
}
|
||||
|
||||
if (await this.isServerBanned(server)) {
|
||||
this.bannedServerName.set(server.name);
|
||||
this.showBannedDialog.set(true);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.attemptJoinServer(server);
|
||||
}
|
||||
|
||||
/** Open the create-server dialog. */
|
||||
openCreateDialog(): void {
|
||||
this.newServerSourceId = this.activeEndpoints()[0]?.id ?? '';
|
||||
this.showCreateDialog.set(true);
|
||||
}
|
||||
|
||||
/** Close the create-server dialog and reset the form. */
|
||||
closeCreateDialog(): void {
|
||||
this.showCreateDialog.set(false);
|
||||
this.resetCreateForm();
|
||||
}
|
||||
|
||||
/** Submit the new server creation form and dispatch the create action. */
|
||||
createServer(): void {
|
||||
if (!this.newServerName())
|
||||
return;
|
||||
|
||||
const currentUserId = localStorage.getItem('metoyou_currentUserId');
|
||||
|
||||
if (!currentUserId) {
|
||||
this.router.navigate(['/login']);
|
||||
return;
|
||||
}
|
||||
|
||||
this.store.dispatch(
|
||||
RoomsActions.createRoom({
|
||||
name: this.newServerName(),
|
||||
description: this.newServerDescription() || undefined,
|
||||
topic: this.newServerTopic() || undefined,
|
||||
isPrivate: this.newServerPrivate(),
|
||||
password: this.newServerPassword().trim() || undefined,
|
||||
sourceId: this.newServerSourceId || undefined
|
||||
})
|
||||
);
|
||||
|
||||
this.closeCreateDialog();
|
||||
}
|
||||
|
||||
/** Open the unified settings modal to the Network page. */
|
||||
openSettings(): void {
|
||||
this.settingsModal.open('network');
|
||||
}
|
||||
|
||||
/** Join a previously saved room by converting it to a ServerInfo payload. */
|
||||
joinSavedRoom(room: Room): void {
|
||||
void this.joinServer(this.toServerInfo(room));
|
||||
}
|
||||
|
||||
closeBannedDialog(): void {
|
||||
this.showBannedDialog.set(false);
|
||||
this.bannedServerName.set('');
|
||||
}
|
||||
|
||||
closePasswordDialog(): void {
|
||||
this.showPasswordDialog.set(false);
|
||||
this.passwordPromptServer.set(null);
|
||||
this.joinPassword.set('');
|
||||
this.joinPasswordError.set(null);
|
||||
}
|
||||
|
||||
async confirmPasswordJoin(): Promise<void> {
|
||||
const server = this.passwordPromptServer();
|
||||
|
||||
if (!server)
|
||||
return;
|
||||
|
||||
await this.attemptJoinServer(server, this.joinPassword());
|
||||
}
|
||||
|
||||
isServerMarkedBanned(server: ServerInfo): boolean {
|
||||
return !!this.bannedServerLookup()[server.id];
|
||||
}
|
||||
|
||||
getServerUserCount(server: ServerInfo): number {
|
||||
const candidate = server as ServerInfo & { currentUsers?: number };
|
||||
|
||||
if (typeof server.userCount === 'number')
|
||||
return server.userCount;
|
||||
|
||||
return typeof candidate.currentUsers === 'number' ? candidate.currentUsers : 0;
|
||||
}
|
||||
|
||||
getServerCapacityLabel(server: ServerInfo): string {
|
||||
return server.maxUsers > 0 ? String(server.maxUsers) : '∞';
|
||||
}
|
||||
|
||||
private toServerInfo(room: Room): ServerInfo {
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
private async attemptJoinServer(server: ServerInfo, password?: string): Promise<void> {
|
||||
const currentUserId = localStorage.getItem('metoyou_currentUserId');
|
||||
const currentUser = this.currentUser();
|
||||
|
||||
if (!currentUserId) {
|
||||
this.router.navigate(['/login']);
|
||||
return;
|
||||
}
|
||||
|
||||
this.joinErrorMessage.set(null);
|
||||
this.joinPasswordError.set(null);
|
||||
|
||||
try {
|
||||
const response = await firstValueFrom(this.serverDirectory.requestJoin({
|
||||
roomId: server.id,
|
||||
userId: currentUserId,
|
||||
userPublicKey: currentUser?.oderId || currentUserId,
|
||||
displayName: currentUser?.displayName || 'Anonymous',
|
||||
password: password?.trim() || undefined
|
||||
}, {
|
||||
sourceId: server.sourceId,
|
||||
sourceUrl: server.sourceUrl
|
||||
}));
|
||||
const resolvedServer = response.server ?? server;
|
||||
|
||||
this.closePasswordDialog();
|
||||
this.store.dispatch(
|
||||
RoomsActions.joinRoom({
|
||||
roomId: resolvedServer.id,
|
||||
serverInfo: resolvedServer
|
||||
})
|
||||
);
|
||||
} 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.passwordPromptServer.set(server);
|
||||
this.showPasswordDialog.set(true);
|
||||
this.joinPasswordError.set(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (errorCode === 'BANNED') {
|
||||
this.bannedServerName.set(server.name);
|
||||
this.showBannedDialog.set(true);
|
||||
return;
|
||||
}
|
||||
|
||||
this.joinErrorMessage.set(message);
|
||||
}
|
||||
}
|
||||
|
||||
private async refreshBannedLookup(servers: ServerInfo[], currentUser: User | null): Promise<void> {
|
||||
const requestVersion = ++this.banLookupRequestVersion;
|
||||
|
||||
if (!currentUser || servers.length === 0) {
|
||||
this.bannedServerLookup.set({});
|
||||
return;
|
||||
}
|
||||
|
||||
const currentUserId = localStorage.getItem('metoyou_currentUserId');
|
||||
const entries = await Promise.all(
|
||||
servers.map(async (server) => {
|
||||
const bans = await this.db.getBansForRoom(server.id);
|
||||
const isBanned = hasRoomBanForUser(bans, currentUser, currentUserId);
|
||||
|
||||
return [server.id, isBanned] as const;
|
||||
})
|
||||
);
|
||||
|
||||
if (requestVersion !== this.banLookupRequestVersion)
|
||||
return;
|
||||
|
||||
this.bannedServerLookup.set(Object.fromEntries(entries));
|
||||
}
|
||||
|
||||
private async isServerBanned(server: ServerInfo): Promise<boolean> {
|
||||
const currentUser = this.currentUser();
|
||||
const currentUserId = localStorage.getItem('metoyou_currentUserId');
|
||||
|
||||
if (!currentUser && !currentUserId)
|
||||
return false;
|
||||
|
||||
const bans = await this.db.getBansForRoom(server.id);
|
||||
|
||||
return hasRoomBanForUser(bans, currentUser, currentUserId);
|
||||
}
|
||||
|
||||
private resetCreateForm(): void {
|
||||
this.newServerName.set('');
|
||||
this.newServerDescription.set('');
|
||||
this.newServerTopic.set('');
|
||||
this.newServerPrivate.set(false);
|
||||
this.newServerPassword.set('');
|
||||
this.newServerSourceId = this.activeEndpoints()[0]?.id ?? '';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user