documents preview
This commit is contained in:
@@ -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;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user