feat(app): add drag-and-drop file upload and file picker to composer (#1474)

- Desktop web: drag-and-drop non-image files onto the chat pane to upload
- Desktop web: file picker option in the composer attachment sheet
- Uploaded files are sent to the LLM as filename references in a tmp directory
- New useFilePicker hook for cross-platform file picking
- Extended useFileDropZone to support generic file drops
- Added UploadedFileAttachment type to protocol and composer attachment system
- Fixed adaptive modal sheet closing on backdrop press in selection mode
This commit is contained in:
Mohamed Boudra
2026-06-12 21:56:51 +08:00
committed by GitHub
parent 9c43e0222a
commit 736b2da1a2
18 changed files with 563 additions and 16 deletions
@@ -453,6 +453,7 @@ export interface AdaptiveModalSheetProps {
desktopMaxWidth?: number;
/** When provided, wraps the card content in a FileDropZone. */
onFilesDropped?: (files: ImageAttachment[]) => void;
onGenericFilesDropped?: (items: import("@/hooks/use-file-drop-zone").DroppedItem[]) => void;
scrollable?: boolean;
presentation?: "push" | "replace";
}
@@ -467,6 +468,7 @@ export function AdaptiveModalSheet({
testID,
desktopMaxWidth,
onFilesDropped,
onGenericFilesDropped,
scrollable = true,
presentation,
}: AdaptiveModalSheetProps) {
@@ -602,7 +604,12 @@ export function AdaptiveModalSheet({
/>
<View style={desktopCardStyle}>
{onFilesDropped ? (
<FileDropZone onFilesDropped={onFilesDropped}>{cardInner}</FileDropZone>
<FileDropZone
onFilesDropped={onFilesDropped}
onGenericFilesDropped={onGenericFilesDropped}
>
{cardInner}
</FileDropZone>
) : (
cardInner
)}
+10 -3
View File
@@ -4,23 +4,30 @@ import Animated, { useAnimatedStyle, withTiming, useSharedValue } from "react-na
import { useEffect, useMemo } from "react";
import { Upload } from "lucide-react-native";
import { useTranslation } from "react-i18next";
import { useFileDropZone } from "@/hooks/use-file-drop-zone";
import { useFileDropZone, type DroppedItem } from "@/hooks/use-file-drop-zone";
import type { ImageAttachment } from "@/composer/types";
import { isWeb } from "@/constants/platform";
interface FileDropZoneProps {
children: React.ReactNode;
onFilesDropped: (files: ImageAttachment[]) => void;
onGenericFilesDropped?: (items: DroppedItem[]) => void;
disabled?: boolean;
}
const IS_WEB = isWeb;
export function FileDropZone({ children, onFilesDropped, disabled = false }: FileDropZoneProps) {
export function FileDropZone({
children,
onFilesDropped,
onGenericFilesDropped,
disabled = false,
}: FileDropZoneProps) {
const { t } = useTranslation();
const { theme } = useUnistyles();
const { isDragging, containerRef } = useFileDropZone({
onFilesDropped,
onGenericFilesDropped,
disabled,
});
@@ -60,7 +67,7 @@ export function FileDropZone({ children, onFilesDropped, disabled = false }: Fil
{/* Content */}
<View style={styles.overlayContent}>
<Upload size={32} color={theme.colors.primary} />
<Text style={styles.overlayText}>{t("composer.attachments.dropImagesHere")}</Text>
<Text style={styles.overlayText}>{t("composer.attachments.dropFilesHere")}</Text>
</View>
</Animated.View>
</View>
+2
View File
@@ -443,6 +443,8 @@ function getUserMessageAttachmentLabel(
return `Issue #${attachment.number}`;
case "text":
return attachment.title ?? t("message.attachments.textAttachment");
case "uploaded_file":
return attachment.fileName;
default:
return "";
}