feat(matrix): add drag & drop file upload to chat

- Add DropZoneOverlay component with visual feedback
- Implement drag events on desktop and mobile chat pages
- Support multi-file upload (files uploaded sequentially)
- Show overlay only when a chat room is selected

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-02-14 10:55:13 +01:00
parent 40a3c89852
commit 7c0bc735b9
4 changed files with 131 additions and 3 deletions

View file

@ -0,0 +1,27 @@
<script lang="ts">
import { UploadSimple } from '@manacore/shared-icons';
interface Props {
visible: boolean;
}
let { visible }: Props = $props();
</script>
{#if visible}
<div
class="fixed inset-0 z-50 bg-primary/10 backdrop-blur-sm flex items-center justify-center pointer-events-none"
>
<div
class="bg-white dark:bg-zinc-800 rounded-2xl p-8 shadow-2xl border-2 border-dashed border-primary flex flex-col items-center gap-4"
>
<div class="p-4 rounded-full bg-primary/10">
<UploadSimple class="h-12 w-12 text-primary" />
</div>
<div class="text-center">
<p class="text-lg font-semibold text-foreground">Datei hier ablegen</p>
<p class="text-sm text-muted-foreground mt-1">Bilder, Videos oder Dateien</p>
</div>
</div>
</div>
{/if}

View file

@ -7,3 +7,4 @@ export { default as MessageInput } from './MessageInput.svelte';
export { default as TypingIndicator } from './TypingIndicator.svelte';
export { default as CreateRoomDialog } from './CreateRoomDialog.svelte';
export { default as RoomSettingsPanel } from './RoomSettingsPanel.svelte';
export { default as DropZoneOverlay } from './DropZoneOverlay.svelte';

View file

@ -1,6 +1,12 @@
<script lang="ts">
import { matrixStore, type SimpleMessage } from '$lib/matrix';
import { RoomList, RoomHeader, Timeline, MessageInput } from '$lib/components/chat';
import {
RoomList,
RoomHeader,
Timeline,
MessageInput,
DropZoneOverlay,
} from '$lib/components/chat';
import CreateRoomDialog from '$lib/components/chat/CreateRoomDialog.svelte';
import RoomSettingsPanel from '$lib/components/chat/RoomSettingsPanel.svelte';
import SearchDialog from '$lib/components/chat/SearchDialog.svelte';
@ -20,6 +26,10 @@
let showSearch = $state(false);
let showForward = $state(false);
// Drag & Drop state
let isDragging = $state(false);
let dragCounter = $state(0);
// Reply/Edit/Forward state
let replyTo = $state<SimpleMessage | null>(null);
let editMessage = $state<SimpleMessage | null>(null);
@ -132,6 +142,41 @@
function handleCallReject() {
// Call rejected - UI will update automatically
}
// Drag & Drop handlers
function handleDragEnter(e: DragEvent) {
e.preventDefault();
dragCounter++;
if (e.dataTransfer?.types.includes('Files')) {
isDragging = true;
}
}
function handleDragLeave(e: DragEvent) {
e.preventDefault();
dragCounter--;
if (dragCounter === 0) {
isDragging = false;
}
}
function handleDragOver(e: DragEvent) {
e.preventDefault();
}
async function handleDrop(e: DragEvent) {
e.preventDefault();
isDragging = false;
dragCounter = 0;
const files = e.dataTransfer?.files;
if (!files?.length || !matrixStore.currentRoom) return;
// Upload all dropped files sequentially
for (const file of files) {
await matrixStore.sendFile(file);
}
}
</script>
{#if isMobile}
@ -228,7 +273,16 @@
</aside>
<!-- Main Chat Area -->
<main class="flex flex-1 min-h-0 flex-col overflow-hidden bg-background">
<main
class="flex flex-1 min-h-0 flex-col overflow-hidden bg-background relative"
ondragenter={handleDragEnter}
ondragleave={handleDragLeave}
ondragover={handleDragOver}
ondrop={handleDrop}
>
<!-- Drop Zone Overlay -->
<DropZoneOverlay visible={isDragging && !!matrixStore.currentRoom} />
{#if matrixStore.currentRoom}
<!-- Room Header -->
<RoomHeader

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { page } from '$app/stores';
import { matrixStore, type SimpleMessage } from '$lib/matrix';
import { RoomHeader, Timeline, MessageInput } from '$lib/components/chat';
import { RoomHeader, Timeline, MessageInput, DropZoneOverlay } from '$lib/components/chat';
import RoomSettingsPanel from '$lib/components/chat/RoomSettingsPanel.svelte';
import SearchDialog from '$lib/components/chat/SearchDialog.svelte';
import ForwardMessageDialog from '$lib/components/chat/ForwardMessageDialog.svelte';
@ -26,6 +26,10 @@
let showSearch = $state(false);
let showForward = $state(false);
// Drag & Drop state
let isDragging = $state(false);
let dragCounter = $state(0);
// Reply/Edit/Forward state
let replyTo = $state<SimpleMessage | null>(null);
let editMessage = $state<SimpleMessage | null>(null);
@ -178,6 +182,41 @@
function handleCallReject() {
// Call rejected - UI will update automatically
}
// Drag & Drop handlers
function handleDragEnter(e: DragEvent) {
e.preventDefault();
dragCounter++;
if (e.dataTransfer?.types.includes('Files')) {
isDragging = true;
}
}
function handleDragLeave(e: DragEvent) {
e.preventDefault();
dragCounter--;
if (dragCounter === 0) {
isDragging = false;
}
}
function handleDragOver(e: DragEvent) {
e.preventDefault();
}
async function handleDrop(e: DragEvent) {
e.preventDefault();
isDragging = false;
dragCounter = 0;
const files = e.dataTransfer?.files;
if (!files?.length || !matrixStore.currentRoom) return;
// Upload all dropped files sequentially
for (const file of files) {
await matrixStore.sendFile(file);
}
}
</script>
<!-- Full-screen chat view for mobile -->
@ -188,7 +227,14 @@
ontouchmove={handleTouchMove}
ontouchend={handleTouchEnd}
ontouchcancel={handleTouchCancel}
ondragenter={handleDragEnter}
ondragleave={handleDragLeave}
ondragover={handleDragOver}
ondrop={handleDrop}
>
<!-- Drop Zone Overlay -->
<DropZoneOverlay visible={isDragging && !!matrixStore.currentRoom} />
<!-- Swipe-back visual indicator -->
{#if isSwiping && swipeProgress > 0}
<div