Refactor 4 with bugfixes

This commit is contained in:
2026-03-04 03:56:23 +01:00
parent be91b6dfe8
commit 0ed9ca93d3
51 changed files with 1552 additions and 996 deletions

View File

@@ -516,6 +516,10 @@
class="chat-input-wrapper relative"
(mouseenter)="inputHovered.set(true)"
(mouseleave)="inputHovered.set(false)"
(dragenter)="onDragEnter($event)"
(dragover)="onDragOver($event)"
(dragleave)="onDragLeave($event)"
(drop)="onDrop($event)"
>
<textarea
#messageInputRef

View File

@@ -187,6 +187,7 @@ export class ChatMessagesComponent implements AfterViewChecked, OnInit, OnDestro
private toolbarHovering = false;
inlineCodeToken = '`';
dragActive = signal(false);
private dragDepth = 0;
inputHovered = signal(false);
ctrlHeld = signal(false);
private boundCtrlDown: ((e: KeyboardEvent) => void) | null = null;
@@ -766,49 +767,140 @@ export class ChatMessagesComponent implements AfterViewChecked, OnInit, OnDestro
/** Handle drag-enter to activate the drop zone overlay. */
// Attachments: drag/drop and rendering
onDragEnter(evt: DragEvent): void {
if (!this.hasPotentialFilePayload(evt))
return;
evt.preventDefault();
evt.stopPropagation();
this.dragDepth++;
this.dragActive.set(true);
}
/** Keep the drop zone active while dragging over. */
onDragOver(evt: DragEvent): void {
if (!this.hasPotentialFilePayload(evt))
return;
evt.preventDefault();
evt.stopPropagation();
if (evt.dataTransfer) {
evt.dataTransfer.dropEffect = 'copy';
}
this.dragActive.set(true);
}
/** Deactivate the drop zone when dragging leaves. */
onDragLeave(evt: DragEvent): void {
if (!this.dragActive())
return;
evt.preventDefault();
this.dragActive.set(false);
evt.stopPropagation();
this.dragDepth = Math.max(0, this.dragDepth - 1);
if (this.dragDepth === 0) {
this.dragActive.set(false);
}
}
/** Handle dropped files, adding them to the pending upload queue. */
onDrop(evt: DragEvent): void {
evt.preventDefault();
const files: File[] = [];
const items = evt.dataTransfer?.items;
evt.stopPropagation();
this.dragDepth = 0;
const droppedFiles = this.extractDroppedFiles(evt);
if (droppedFiles.length === 0) {
this.dragActive.set(false);
return;
}
this.pendingFiles.push(...droppedFiles);
// Keep toolbar visible so user sees options
this.toolbarVisible.set(true);
this.dragActive.set(false);
}
private hasPotentialFilePayload(evt: DragEvent): boolean {
const dataTransfer = evt.dataTransfer;
if (!dataTransfer)
return false;
if (dataTransfer.files?.length)
return true;
const items = dataTransfer.items;
if (items?.length) {
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
if (items[itemIndex].kind === 'file') {
return true;
}
}
}
const types = evt.dataTransfer?.types;
if (!types || types.length === 0)
// Some desktop-to-browser drags expose no types until drop.
return true;
for (let typeIndex = 0; typeIndex < types.length; typeIndex++) {
const type = types[typeIndex];
if (
type === 'Files' ||
type === 'application/x-moz-file' ||
type === 'public.file-url' ||
type === 'text/uri-list'
) {
return true;
}
}
return false;
}
private extractDroppedFiles(evt: DragEvent): File[] {
const droppedFiles: File[] = [];
const items = evt.dataTransfer?.items ?? null;
if (items && items.length) {
for (let i = 0; i < items.length; i++) {
const item = items[i];
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
const item = items[itemIndex];
if (item.kind === 'file') {
const file = item.getAsFile();
if (file)
files.push(file);
droppedFiles.push(file);
}
}
} else if (evt.dataTransfer?.files?.length) {
for (let i = 0; i < evt.dataTransfer.files.length; i++) {
files.push(evt.dataTransfer.files[i]);
}
}
files.forEach((file) => this.pendingFiles.push(file));
// Keep toolbar visible so user sees options
this.toolbarVisible.set(true);
this.dragActive.set(false);
const files = evt.dataTransfer?.files;
if (!files?.length)
return droppedFiles;
for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
const file = files[fileIndex];
const exists = droppedFiles.some((existing) =>
existing.name === file.name &&
existing.size === file.size &&
existing.type === file.type &&
existing.lastModified === file.lastModified
);
if (!exists) {
droppedFiles.push(file);
}
}
return droppedFiles;
}
/** Return all file attachments associated with a message. */