documents preview

This commit is contained in:
2026-03-11 09:09:15 +01:00
parent ffdea4fe62
commit 0e4c79b735
13 changed files with 558 additions and 13 deletions

View File

@@ -3,6 +3,7 @@ import { Component, computed, effect, ElementRef, inject, NgZone, OnDestroy, sig
import { toSignal } from '@angular/core/rxjs-interop';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { OfficePdfPreviewComponent } from './office-pdf-preview.component';
import { PeerCallModalComponent } from './peer-call-modal.component';
import { ChatSessionService } from './chat-session.service';
@@ -11,7 +12,14 @@ import type { CallMode, ChatEntry, ConnectionState, PeerSummary } from './models
@Component({
selector: 'app-chat-page',
imports: [CommonModule, FormsModule, RouterLink, JsonFileViewerComponent, PeerCallModalComponent],
imports: [
CommonModule,
FormsModule,
RouterLink,
JsonFileViewerComponent,
OfficePdfPreviewComponent,
PeerCallModalComponent,
],
templateUrl: './chat-page.component.html',
styleUrl: './chat-page.component.scss',
})
@@ -37,12 +45,18 @@ export class ChatPageComponent implements OnDestroy {
private dictationCompletionPromise: Promise<void> | null = null;
private resolveDictationCompletion: (() => void) | null = null;
private dictationApplyToken = 0;
private lastConversationSnapshot: { peerId: string; length: number; lastEntryId: string | null } | null = null;
@ViewChild('callAudioElement')
set callAudioElementRef(value: ElementRef<HTMLAudioElement> | undefined) {
this.callAudioElement = value;
this.syncCallAudioSource();
}
private callAudioElement?: ElementRef<HTMLAudioElement>;
@ViewChild('conversationContainer')
set conversationContainerRef(value: ElementRef<HTMLDivElement> | undefined) {
this.conversationContainer = value;
}
private conversationContainer?: ElementRef<HTMLDivElement>;
messageText = '';
readonly forwardingEntryId = signal<string | null>(null);
@@ -209,6 +223,32 @@ export class ChatPageComponent implements OnDestroy {
this.remoteCallAudioStream();
this.syncCallAudioSource();
});
effect(() => {
const peerId = this.peerId();
const entries = this.conversation();
const snapshot = {
peerId,
length: entries.length,
lastEntryId: entries.at(-1)?.id ?? null,
};
const previousSnapshot = this.lastConversationSnapshot;
this.lastConversationSnapshot = snapshot;
if (!peerId || !previousSnapshot || previousSnapshot.peerId !== peerId) {
return;
}
const hasNewTailEntry = snapshot.length > previousSnapshot.length
|| (snapshot.length > 0 && snapshot.lastEntryId !== previousSnapshot.lastEntryId);
if (!hasNewTailEntry) {
return;
}
this.scrollConversationToBottom();
});
}
ngOnDestroy(): void {
@@ -546,6 +586,16 @@ export class ChatPageComponent implements OnDestroy {
this.forwardingEntryId.set(null);
}
async sendGeneratedImage(entry: ChatEntry): Promise<void> {
const peerId = this.peerId();
if (!peerId) {
return;
}
await this.session.sendGeneratedImageToPeer(entry, peerId);
}
async endVoiceCall(peerId: string): Promise<void> {
await this.session.endVoiceCall(peerId);
}
@@ -570,6 +620,10 @@ export class ChatPageComponent implements OnDestroy {
return entry.kind === 'file' && !!entry.downloadUrl && (entry.fileMimeType?.startsWith('image/') ?? false);
}
isGeneratedImageEntry(entry: ChatEntry): boolean {
return this.isImageEntry(entry) && entry.generatedByAi === true;
}
isVideoEntry(entry: ChatEntry): boolean {
if (entry.kind !== 'file' || !entry.downloadUrl) {
return false;
@@ -592,6 +646,33 @@ export class ChatPageComponent implements OnDestroy {
);
}
hasPdfPreview(entry: ChatEntry): boolean {
return (
entry.kind === 'file' &&
(
(
entry.previewMimeType === 'application/pdf' &&
!!entry.previewDownloadUrl
) ||
(
!!entry.downloadUrl &&
(
entry.fileMimeType === 'application/pdf' ||
entry.fileName?.toLowerCase().endsWith('.pdf') === true
)
)
)
);
}
pdfPreviewUrl(entry: ChatEntry): string | null {
if (!this.hasPdfPreview(entry)) {
return null;
}
return entry.previewDownloadUrl ?? entry.downloadUrl ?? null;
}
isPeerTyping(peerId: string): boolean {
return this.session.typingPeerIds().includes(peerId);
}
@@ -804,4 +885,18 @@ export class ChatPageComponent implements OnDestroy {
audio.pause();
audio.srcObject = null;
}
private scrollConversationToBottom(): void {
const container = this.conversationContainer?.nativeElement;
if (!container) {
return;
}
queueMicrotask(() => {
requestAnimationFrame(() => {
container.scrollTop = container.scrollHeight;
});
});
}
}