From 2c1781e97a4727ff1d660a70a4b0ec29287067cb Mon Sep 17 00:00:00 2001 From: Moe Haje Date: Thu, 15 Jan 2026 16:40:27 +0100 Subject: [PATCH 1/2] fix: composer tweaks Tweak composer attachments, drag-drop behavior, and action controls styling. --- src-tauri/tauri.conf.json | 1 + .../components/ComposerAttachments.tsx | 11 +++- .../composer/components/ComposerInput.tsx | 53 +++++++++++------ .../composer/hooks/useComposerImageDrop.ts | 59 +++++++++++++++---- .../composer/hooks/useComposerImages.ts | 5 +- src/styles/composer.css | 54 ++++++++++++----- 6 files changed, 132 insertions(+), 51 deletions(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 8b0a92cae..691be928a 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -18,6 +18,7 @@ "height": 700, "minWidth": 360, "minHeight": 600, + "dragDropEnabled": false, "titleBarStyle": "Overlay", "hiddenTitle": true, "transparent": true, diff --git a/src/features/composer/components/ComposerAttachments.tsx b/src/features/composer/components/ComposerAttachments.tsx index 55a70035a..e9496be4e 100644 --- a/src/features/composer/components/ComposerAttachments.tsx +++ b/src/features/composer/components/ComposerAttachments.tsx @@ -1,4 +1,4 @@ -import { X } from "lucide-react"; +import { Image, X } from "lucide-react"; type ComposerAttachmentsProps = { attachments: string[]; @@ -33,7 +33,14 @@ export function ComposerAttachments({ const title = fileTitle(path); const titleAttr = path.startsWith("data:") ? "Pasted image" : path; return ( -
+
+ + + {title} + {/* WIP */}
); diff --git a/src/features/composer/hooks/useComposerImageDrop.ts b/src/features/composer/hooks/useComposerImageDrop.ts index 5e871dcdb..12c1fc371 100644 --- a/src/features/composer/hooks/useComposerImageDrop.ts +++ b/src/features/composer/hooks/useComposerImageDrop.ts @@ -17,6 +17,32 @@ function isImagePath(path: string) { return imageExtensions.some((ext) => lower.endsWith(ext)); } +function isDragFileTransfer(types: readonly string[] | undefined) { + if (!types || types.length === 0) { + return false; + } + return ( + types.includes("Files") || + types.includes("public.file-url") || + types.includes("application/x-moz-file") + ); +} + +function readFilesAsDataUrls(files: File[]) { + return Promise.all( + files.map( + (file) => + new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = () => + resolve(typeof reader.result === "string" ? reader.result : ""); + reader.onerror = () => resolve(""); + reader.readAsDataURL(file); + }), + ), + ).then((items) => items.filter(Boolean)); +} + type UseComposerImageDropArgs = { disabled: boolean; onAttachImages?: (paths: string[]) => void; @@ -86,7 +112,7 @@ export function useComposerImageDrop({ if (disabled) { return; } - if (event.dataTransfer?.types?.includes("Files")) { + if (isDragFileTransfer(event.dataTransfer?.types)) { event.preventDefault(); setIsDragOver(true); } @@ -102,24 +128,35 @@ export function useComposerImageDrop({ } }; - const handleDrop = (event: React.DragEvent) => { + const handleDrop = async (event: React.DragEvent) => { if (disabled) { return; } event.preventDefault(); setIsDragOver(false); const files = Array.from(event.dataTransfer?.files ?? []); - if (!files.length) { - return; - } - const imagePaths = files - .map((file) => { - const candidate = (file as File & { path?: string }).path ?? ""; - return candidate && isImagePath(candidate) ? candidate : null; - }) - .filter((path): path is string => Boolean(path)); + const items = Array.from(event.dataTransfer?.items ?? []); + const itemFiles = items + .filter((item) => item.kind === "file") + .map((item) => item.getAsFile()) + .filter((file): file is File => Boolean(file)); + const filePaths = [...files, ...itemFiles] + .map((file) => (file as File & { path?: string }).path ?? "") + .filter(Boolean); + const imagePaths = filePaths.filter(isImagePath); if (imagePaths.length > 0) { onAttachImages?.(imagePaths); + return; + } + const fileImages = [...files, ...itemFiles].filter((file) => + file.type.startsWith("image/"), + ); + if (fileImages.length === 0) { + return; + } + const dataUrls = await readFilesAsDataUrls(fileImages); + if (dataUrls.length > 0) { + onAttachImages?.(dataUrls); } }; diff --git a/src/features/composer/hooks/useComposerImages.ts b/src/features/composer/hooks/useComposerImages.ts index 71b8844d2..b0c9445e3 100644 --- a/src/features/composer/hooks/useComposerImages.ts +++ b/src/features/composer/hooks/useComposerImages.ts @@ -26,10 +26,7 @@ export function useComposerImages({ } setImagesByThread((prev) => { const existing = prev[draftKey] ?? []; - const merged = [ - ...existing, - ...paths.filter((path) => !existing.includes(path)), - ]; + const merged = Array.from(new Set([...existing, ...paths])); return { ...prev, [draftKey]: merged }; }); }, diff --git a/src/styles/composer.css b/src/styles/composer.css index 82e0e8778..27f53326e 100644 --- a/src/styles/composer.css +++ b/src/styles/composer.css @@ -182,7 +182,7 @@ outline: none; } -.composer-send { +.composer-action { border: 1px solid rgba(255, 255, 255, 0.5); background: transparent; color: #ffffff; @@ -197,19 +197,36 @@ justify-content: center; } -.composer-send:hover { +.composer-action.is-stop { + border-color: rgba(255, 107, 107, 0.6); + background: rgba(255, 107, 107, 0.12); +} + +.composer-action:hover { background: var(--surface-control-hover); color: #ffffff; } -.composer-send svg { +.composer-action.is-stop:hover { + background: rgba(255, 107, 107, 0.2); +} + +.composer-action svg { width: 12px; height: 12px; } -.composer-stop { - border: 1px solid rgba(255, 107, 107, 0.6); - background: rgba(255, 107, 107, 0.12); +.composer-action-stop-square { + width: 6px; + height: 6px; + border-radius: 2px; + background: currentColor; +} + +.composer-mic { + display: none; + border: 1px solid var(--border-strong); + background: transparent; color: #ffffff; padding: 0; border-radius: 999px; @@ -221,15 +238,14 @@ justify-content: center; } -.composer-stop:hover { - background: rgba(255, 107, 107, 0.2); +.composer-mic:hover { + background: var(--surface-control-hover); } -.composer-stop-square { - width: 6px; - height: 6px; - border-radius: 2px; - background: currentColor; +.composer-mic.is-active { + background: rgba(255, 214, 110, 0.2); + border-color: rgba(255, 214, 110, 0.5); + color: #ffd66e; } .composer.is-disabled { @@ -242,7 +258,7 @@ color: var(--text-fainter); } -.composer-send:disabled { +.composer-action:disabled { opacity: 0.5; cursor: not-allowed; background: var(--surface-control-disabled); @@ -250,13 +266,21 @@ border-color: var(--border-subtle); } -.composer-stop:disabled { +.composer-action.is-stop:disabled { opacity: 0.4; cursor: not-allowed; background: rgba(255, 107, 107, 0.08); color: rgba(255, 196, 196, 0.4); } +.composer-mic:disabled { + opacity: 0.5; + cursor: not-allowed; + background: var(--surface-control-disabled); + color: var(--text-fainter); + border-color: var(--border-subtle); +} + .composer-select:disabled { cursor: not-allowed; color: var(--text-fainter); From bf7cf1953ec232e288a3dd86dce85bd55796f996 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Thu, 15 Jan 2026 17:56:29 +0100 Subject: [PATCH 2/2] style: remove composer mic --- .../composer/components/ComposerInput.tsx | 13 +------- src/styles/composer.css | 33 +------------------ 2 files changed, 2 insertions(+), 44 deletions(-) diff --git a/src/features/composer/components/ComposerInput.tsx b/src/features/composer/components/ComposerInput.tsx index c50e74571..81f1b97e0 100644 --- a/src/features/composer/components/ComposerInput.tsx +++ b/src/features/composer/components/ComposerInput.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef } from "react"; import type { KeyboardEvent, RefObject } from "react"; import type { AutocompleteItem } from "../hooks/useComposerAutocomplete"; -import { ImagePlus, Mic } from "lucide-react"; +import { ImagePlus } from "lucide-react"; import { useComposerImageDrop } from "../hooks/useComposerImageDrop"; import { ComposerAttachments } from "./ComposerAttachments"; @@ -71,9 +71,6 @@ export function ComposerInput({ onAttachImages, }); - const isDictationSupported = false; - const isDictationListening = false; - useEffect(() => { if (!suggestionsOpen) { return; @@ -232,14 +229,6 @@ export function ComposerInput({ )} - {/* WIP */} -
); } diff --git a/src/styles/composer.css b/src/styles/composer.css index 27f53326e..f64384a74 100644 --- a/src/styles/composer.css +++ b/src/styles/composer.css @@ -66,7 +66,7 @@ .composer-input { display: grid; - grid-template-columns: 1fr auto auto; + grid-template-columns: 1fr auto; gap: 12px; align-items: center; } @@ -223,30 +223,6 @@ background: currentColor; } -.composer-mic { - display: none; - border: 1px solid var(--border-strong); - background: transparent; - color: #ffffff; - padding: 0; - border-radius: 999px; - cursor: pointer; - width: 24px; - height: 24px; - display: inline-flex; - align-items: center; - justify-content: center; -} - -.composer-mic:hover { - background: var(--surface-control-hover); -} - -.composer-mic.is-active { - background: rgba(255, 214, 110, 0.2); - border-color: rgba(255, 214, 110, 0.5); - color: #ffd66e; -} .composer.is-disabled { opacity: 0.7; @@ -273,13 +249,6 @@ color: rgba(255, 196, 196, 0.4); } -.composer-mic:disabled { - opacity: 0.5; - cursor: not-allowed; - background: var(--surface-control-disabled); - color: var(--text-fainter); - border-color: var(--border-subtle); -} .composer-select:disabled { cursor: not-allowed;