199 lines
5.7 KiB
TypeScript
199 lines
5.7 KiB
TypeScript
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<ServerInviteInfo | null>(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<void> {
|
|
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<User | null> {
|
|
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<void> {
|
|
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<void> {
|
|
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
|
|
};
|
|
}
|
|
}
|