import { Component, OnInit, inject, signal } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ActivatedRoute, Router } from '@angular/router'; import { firstValueFrom } from 'rxjs'; import { Store } from '@ngrx/store'; import { RoomsActions } from '../../../../store/rooms/rooms.actions'; import { UsersActions } from '../../../../store/users/users.actions'; import { selectCurrentUser } from '../../../../store/users/users.selectors'; import type { ServerInviteInfo } from '../../domain/server-directory.models'; import { STORAGE_KEY_CURRENT_USER_ID } from '../../../../core/constants'; import { DatabaseService } from '../../../../infrastructure/persistence'; import { ServerDirectoryFacade } from '../../application/server-directory.facade'; import { User } from '../../../../shared-kernel'; @Component({ selector: 'app-invite', standalone: true, imports: [CommonModule], templateUrl: './invite.component.html' }) export class InviteComponent implements OnInit { readonly currentUser = inject(Store).selectSignal(selectCurrentUser); readonly invite = signal(null); readonly status = signal<'loading' | 'redirecting' | 'joining' | 'error'>('loading'); readonly message = signal('Loading invite…'); private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); private readonly store = inject(Store); private readonly serverDirectory = inject(ServerDirectoryFacade); private readonly databaseService = inject(DatabaseService); async ngOnInit(): Promise { const inviteContext = this.resolveInviteContext(); if (!inviteContext) { return; } const currentUserId = localStorage.getItem(STORAGE_KEY_CURRENT_USER_ID); if (!currentUserId) { await this.redirectToLogin(); return; } try { await this.joinInvite(inviteContext, currentUserId); } catch (error: unknown) { this.applyInviteError(error); } } goToSearch(): void { this.router.navigate(['/search']).catch(() => {}); } private buildEndpointName(sourceUrl: string): string { try { const url = new URL(sourceUrl); return url.hostname; } catch { return 'Signal Server'; } } private applyInviteError(error: unknown): void { const inviteError = error as { error?: { error?: string; errorCode?: string }; }; const errorCode = inviteError?.error?.errorCode; const fallbackMessage = inviteError?.error?.error || 'Unable to accept this invite.'; this.status.set('error'); if (errorCode === 'BANNED') { this.message.set('You are banned from this server and cannot accept this invite.'); return; } if (errorCode === 'INVITE_EXPIRED') { this.message.set('This invite has expired. Ask for a fresh invite link.'); return; } this.message.set(fallbackMessage); } private async hydrateCurrentUser(): Promise { const currentUser = this.currentUser(); if (currentUser) { return currentUser; } const storedUser = await this.databaseService.getCurrentUser(); if (storedUser) { this.store.dispatch(UsersActions.setCurrentUser({ user: storedUser })); } return storedUser; } private async joinInvite( context: { endpoint: { id: string; name: string }; inviteId: string; sourceUrl: string }, currentUserId: string ): Promise { const invite = await firstValueFrom(this.serverDirectory.getInvite(context.inviteId, { sourceId: context.endpoint.id, sourceUrl: context.sourceUrl })); this.invite.set(invite); this.status.set('joining'); this.message.set(`Joining ${invite.server.name}…`); const currentUser = await this.hydrateCurrentUser(); const joinResponse = await firstValueFrom(this.serverDirectory.requestJoin({ roomId: invite.server.id, userId: currentUserId, userPublicKey: currentUser?.oderId || currentUserId, displayName: currentUser?.displayName || 'Anonymous', inviteId: context.inviteId }, { sourceId: context.endpoint.id, sourceUrl: context.sourceUrl })); this.store.dispatch( RoomsActions.joinRoom({ roomId: joinResponse.server.id, serverInfo: { ...invite.server, ...joinResponse.server, channels: Array.isArray(joinResponse.server.channels) && joinResponse.server.channels.length > 0 ? joinResponse.server.channels : invite.server.channels, sourceId: context.endpoint.id, sourceName: context.endpoint.name, sourceUrl: context.sourceUrl } }) ); } private async redirectToLogin(): Promise { this.status.set('redirecting'); this.message.set('Redirecting to login…'); await this.router.navigate(['/login'], { queryParams: { returnUrl: this.router.url } }); } private resolveInviteContext(): { endpoint: { id: string; name: string }; inviteId: string; sourceUrl: string; } | null { const inviteId = this.route.snapshot.paramMap.get('inviteId')?.trim() || ''; const sourceUrl = this.route.snapshot.queryParamMap.get('server')?.trim() || ''; if (!inviteId || !sourceUrl) { this.status.set('error'); this.message.set('This invite link is missing required server information.'); return null; } const endpoint = this.serverDirectory.ensureServerEndpoint({ name: this.buildEndpointName(sourceUrl), url: sourceUrl }, { setActive: !localStorage.getItem(STORAGE_KEY_CURRENT_USER_ID) }); return { endpoint: { id: endpoint.id, name: endpoint.name }, inviteId, sourceUrl }; } }