152 lines
3.8 KiB
TypeScript
152 lines
3.8 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 type { ChatEntry, ConnectionState } from './models';
|
||
|
|
|
||
|
|
@Component({
|
||
|
|
selector: 'app-chat-page',
|
||
|
|
imports: [CommonModule, FormsModule, RouterLink],
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
|
||
|
|
isImageEntry(entry: ChatEntry): boolean {
|
||
|
|
return entry.kind === 'file' && !!entry.downloadUrl && (entry.fileMimeType?.startsWith('image/') ?? false);
|
||
|
|
}
|
||
|
|
|
||
|
|
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';
|
||
|
|
}
|
||
|
|
|
||
|
|
async switchPeer(peerId: string): Promise<void> {
|
||
|
|
if (!peerId || peerId === this.peerId()) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
this.session.selectPeer(peerId);
|
||
|
|
await this.router.navigate(['/chat', peerId]);
|
||
|
|
}
|
||
|
|
}
|