232 lines
6.4 KiB
TypeScript
232 lines
6.4 KiB
TypeScript
import {ChangeDetectorRef, Component, HostListener, inject, input} from '@angular/core';
|
|
import {
|
|
TUI_BREAKPOINT,
|
|
TuiAppearance,
|
|
TuiButton,
|
|
TuiDialog,
|
|
TuiGroup,
|
|
TuiIcon,
|
|
TuiScrollbarDirective,
|
|
TuiTextfield
|
|
} from '@taiga-ui/core';
|
|
import {TranslatePipe} from '@ngx-translate/core';
|
|
import {ServiceManager} from '../../../service-manager';
|
|
import {MessageBoxViewModel} from './message-box-viewmodel';
|
|
import {FormsModule} from '@angular/forms';
|
|
import {v4 as uuidv4} from 'uuid';
|
|
import {Masonry} from '../masonry/masonry';
|
|
import {Oimg} from '../oimg/oimg';
|
|
import {FileData} from '@chatenium/chatenium-sdk/domain/fileUploadService.schema';
|
|
import {TuiTextarea, TuiTextareaComponent} from '@taiga-ui/kit';
|
|
|
|
@Component({
|
|
selector: 'message-box',
|
|
imports: [
|
|
TuiAppearance,
|
|
TranslatePipe,
|
|
TuiScrollbarDirective,
|
|
TuiGroup,
|
|
TuiIcon,
|
|
TuiButton,
|
|
FormsModule,
|
|
TuiDialog,
|
|
Oimg,
|
|
Masonry,
|
|
TuiTextfield,
|
|
TuiTextarea
|
|
],
|
|
templateUrl: './message-box.html',
|
|
styleUrl: './message-box.scss',
|
|
})
|
|
export class MessageBox {
|
|
viewModel = input.required<MessageBoxViewModel>()
|
|
|
|
breakpoint = inject(TUI_BREAKPOINT)
|
|
cdr = inject(ChangeDetectorRef)
|
|
|
|
textareaHeight = 25
|
|
messageBoxRadius = 200
|
|
|
|
private dragCounter = 0;
|
|
isDraggingOverWindow = false;
|
|
|
|
@HostListener('window:dragenter', ['$event'])
|
|
onDragEnter(event: DragEvent) {
|
|
event.preventDefault();
|
|
this.dragCounter++;
|
|
|
|
if (this.dragCounter === 1) {
|
|
this.isDraggingOverWindow = true;
|
|
console.log("Drag entered the window area");
|
|
}
|
|
}
|
|
|
|
@HostListener('window:dragleave', ['$event'])
|
|
onDragLeave(event: DragEvent) {
|
|
event.preventDefault();
|
|
this.dragCounter--;
|
|
|
|
if (this.dragCounter === 0) {
|
|
this.isDraggingOverWindow = false;
|
|
console.log("Drag left the window completely");
|
|
}
|
|
}
|
|
|
|
onDragOver(event: DragEvent) {
|
|
// This is the "magic" line that enables the drop event to fire
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
}
|
|
|
|
onDrop(event: DragEvent) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
const files = event.dataTransfer?.files;
|
|
if (files) this.processFiles(files)
|
|
this.isDraggingOverWindow = false
|
|
this.cdr.detectChanges();
|
|
}
|
|
|
|
onTextAreaInput(e: HTMLTextAreaElement) {
|
|
const calculatedHeight = e.value.split(/\r?\n/).length * 25
|
|
const calculatedRadius = (200 - this.textareaHeight)
|
|
this.textareaHeight = calculatedHeight > 180 ? 180 : calculatedHeight
|
|
this.messageBoxRadius = calculatedRadius < 30 ? 30 : calculatedRadius
|
|
}
|
|
|
|
handleFileInput(event: any) {
|
|
this.processFiles(event.target.files)
|
|
}
|
|
|
|
sendMessageWithCaption() {
|
|
this.viewModel().onMessageSend(this.viewModel().message(), this.viewModel().files())
|
|
this.viewModel().message.set("")
|
|
this.viewModel().files.set([])
|
|
this.viewModel().dialogOpen.set(false)
|
|
}
|
|
|
|
handleEnterKeydown(e: any) {
|
|
e.preventDefault()
|
|
return false
|
|
}
|
|
|
|
filterPictureVideo(files: FileDataWithPreview[]) {
|
|
return files.filter(f => f.type == "image")
|
|
}
|
|
|
|
async processFiles(fileList: FileList) {
|
|
for (let i = 0; i < fileList.length; i++) {
|
|
const file = fileList[i];
|
|
const type = file.type.split("/").shift() ?? ""
|
|
let blob = ""
|
|
let thumbnailBlob = undefined
|
|
let height = 0
|
|
let width = 0
|
|
|
|
console.log(type)
|
|
|
|
blob = URL.createObjectURL(file)
|
|
if (type == "image") {
|
|
const imgData = await fetchImageDimensions(file)
|
|
console.log(imgData)
|
|
height = imgData.height
|
|
width = imgData.width
|
|
} else if (type == "video") {
|
|
const videoData = await processVideo(file)
|
|
thumbnailBlob = videoData.thumbnailBlob
|
|
height = videoData.height
|
|
width = videoData.width
|
|
}
|
|
|
|
this.viewModel().files().push({
|
|
fileId: uuidv4(),
|
|
data: file,
|
|
name: file.name,
|
|
type: type,
|
|
extension: file.name.split(".").pop() ?? "",
|
|
blob: blob,
|
|
height: height,
|
|
width: width,
|
|
videoThumbnail: thumbnailBlob,
|
|
})
|
|
}
|
|
this.viewModel().dialogOpen.set(true)
|
|
|
|
function fetchImageDimensions(file: File): Promise<{ height: number, width: number }> {
|
|
return new Promise((resolve, reject) => {
|
|
const img = new Image();
|
|
const objectUrl = URL.createObjectURL(file);
|
|
|
|
img.onload = () => {
|
|
console.log("Loaded image")
|
|
resolve({
|
|
width: img.naturalWidth,
|
|
height: img.naturalHeight,
|
|
});
|
|
};
|
|
|
|
img.onerror = (err) => {
|
|
URL.revokeObjectURL(objectUrl);
|
|
console.error("Error loading image:", err);
|
|
reject(err);
|
|
};
|
|
|
|
img.src = objectUrl;
|
|
});
|
|
}
|
|
|
|
async function processVideo(file: File): Promise<{ thumbnailBlob: string, height: number, width: number }> {
|
|
const objectUrl = URL.createObjectURL(file);
|
|
const video = document.createElement('video');
|
|
video.src = objectUrl;
|
|
video.preload = 'metadata'; // Ensure metadata is loaded
|
|
video.muted = true;
|
|
video.playsInline = true;
|
|
|
|
return new Promise((resolve) => {
|
|
video.addEventListener('loadeddata', () => {
|
|
// Step 1: Seek to a tiny bit past 0 to ensure a frame is available
|
|
video.currentTime = 0.1;
|
|
});
|
|
|
|
video.addEventListener('seeked', async () => {
|
|
// Step 2: Now that the seek is done, the frame is ready
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = video.videoWidth;
|
|
canvas.height = video.videoHeight;
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
ctx!.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
|
|
// Clean up: Convert directly to blob to avoid double-processing
|
|
canvas.toBlob((blob) => {
|
|
const thumbnailBlobUrl = URL.createObjectURL(blob!);
|
|
|
|
// Revoke the original video URL to save memory
|
|
URL.revokeObjectURL(objectUrl);
|
|
|
|
resolve({
|
|
thumbnailBlob: thumbnailBlobUrl,
|
|
height: video.videoHeight,
|
|
width: video.videoWidth,
|
|
});
|
|
}, 'image/jpeg', 0.8);
|
|
}, { once: true }); // Only trigger once
|
|
});
|
|
}
|
|
}
|
|
|
|
protected readonly console = console;
|
|
}
|
|
|
|
/**
|
|
* All the extra data for client-side rendering. (With finished messages these are generated on the server)
|
|
*/
|
|
export interface FileDataWithPreview extends FileData {
|
|
blob: string;
|
|
videoThumbnail?: string;
|
|
height: number;
|
|
width: number;
|
|
}
|