Files
PrivateChat/client/src/app/chat-page.component.ts
2026-03-09 20:40:21 +01:00

172 lines
4.4 KiB
TypeScript

import { CommonModule } from '@angular/common';
import { Component, computed, effect, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { ChatSessionService } from './chat-session.service';
import { JsonFileViewerComponent } from './json-file-viewer.component';
import type { ChatEntry, ConnectionState } from './models';
@Component({
selector: 'app-chat-page',
imports: [CommonModule, FormsModule, RouterLink, JsonFileViewerComponent],
templateUrl: './chat-page.component.html',
styleUrl: './chat-page.component.scss',
})
export class ChatPageComponent {
private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
private readonly routeParamMap = toSignal(this.route.paramMap, {
initialValue: this.route.snapshot.paramMap,
});
messageText = '';
readonly peerId = computed(() => this.routeParamMap().get('peerId') ?? '');
readonly peer = computed(() => this.session.peers().find((item) => item.id === this.peerId()) ?? null);
readonly currentUser = computed(() => this.session.currentUser());
readonly conversation = computed(() =>
this.session
.messages()
.filter((entry) => entry.peerId === this.peerId()),
);
readonly webRtcState = computed<ConnectionState>(() => {
const selectedPeer = this.peer();
if (!selectedPeer) {
return 'disconnected';
}
if (selectedPeer.channelState === 'open' || selectedPeer.connectionState === 'connected') {
return 'connected';
}
if (selectedPeer.channelState === 'connecting' || selectedPeer.connectionState === 'connecting') {
return 'connecting';
}
return 'disconnected';
});
constructor(readonly session: ChatSessionService) {
if (!this.session.currentUser()) {
void this.router.navigateByUrl('/');
}
effect(() => {
const peerId = this.peerId();
if (!peerId) {
return;
}
this.session.selectPeer(peerId);
void this.session.connectToPeer(peerId);
});
}
async ensureConnection(): Promise<void> {
const peerId = this.peerId();
if (!peerId) {
return;
}
this.session.selectPeer(peerId);
await this.session.connectToPeer(peerId);
}
async sendMessage(): Promise<void> {
const peerId = this.peerId();
if (!peerId) {
return;
}
await this.session.sendText(peerId, this.messageText);
this.messageText = '';
}
handleComposerEnter(event: Event): void {
if (!(event instanceof KeyboardEvent) || event.shiftKey) {
return;
}
event.preventDefault();
void this.sendMessage();
}
handleMessageTextChange(text: string): void {
const peerId = this.peerId();
if (!peerId) {
return;
}
this.session.notifyTypingActivity(peerId, text);
}
async sendFile(peerId: string, input: HTMLInputElement): Promise<void> {
const file = input.files?.item(0);
if (!file) {
return;
}
await this.session.sendFile(peerId, file);
input.value = '';
}
async deleteMessage(entry: ChatEntry): Promise<void> {
await this.session.deleteMessage(entry);
}
async deleteConversation(peerId: string, event?: Event): Promise<void> {
event?.stopPropagation();
await this.session.deleteConversation(peerId);
}
isImageEntry(entry: ChatEntry): boolean {
return entry.kind === 'file' && !!entry.downloadUrl && (entry.fileMimeType?.startsWith('image/') ?? false);
}
isIncomingJsonFileEntry(entry: ChatEntry): boolean {
return (
entry.kind === 'file' &&
entry.direction === 'incoming' &&
!!entry.downloadUrl &&
!!entry.fileName &&
entry.fileName.toLowerCase().endsWith('.json')
);
}
isPeerTyping(peerId: string): boolean {
return this.session.typingPeerIds().includes(peerId);
}
indicatorTone(state: ConnectionState): 'ok' | 'connecting' | 'offline' {
if (state === 'connected') {
return 'ok';
}
if (state === 'connecting') {
return 'connecting';
}
return 'offline';
}
canReconnectWebRtc(): boolean {
return this.indicatorTone(this.webRtcState()) === 'offline';
}
async switchPeer(peerId: string): Promise<void> {
if (!peerId || peerId === this.peerId()) {
return;
}
this.session.selectPeer(peerId);
await this.router.navigate(['/chat', peerId]);
}
}