diff --git a/package-lock.json b/package-lock.json
index 56660343a..9971b278c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20410,6 +20410,15 @@
"expo": "*"
}
},
+ "node_modules/expo-document-picker": {
+ "version": "14.0.8",
+ "resolved": "https://registry.npmjs.org/expo-document-picker/-/expo-document-picker-14.0.8.tgz",
+ "integrity": "sha512-3tyQKpPqWWFlI8p9RiMX1+T1Zge5mEKeBuXWp1h8PEItFMUDSiOJbQ112sfdC6Hxt8wSxreV9bCRl/NgBdt+fA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-eas-client": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/expo-eas-client/-/expo-eas-client-1.0.8.tgz",
@@ -37111,6 +37120,7 @@
"expo-constants": "~18.0.9",
"expo-crypto": "^15.0.8",
"expo-dev-client": "^6.0.15",
+ "expo-document-picker": "~14.0.8",
"expo-file-system": "~19.0.17",
"expo-haptics": "~15.0.7",
"expo-image": "~3.0.10",
diff --git a/packages/app/package.json b/packages/app/package.json
index 88fc08046..b87959091 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -66,6 +66,7 @@
"expo-constants": "~18.0.9",
"expo-crypto": "^15.0.8",
"expo-dev-client": "^6.0.15",
+ "expo-document-picker": "~14.0.8",
"expo-file-system": "~19.0.17",
"expo-haptics": "~15.0.7",
"expo-image": "~3.0.10",
diff --git a/packages/app/src/attachments/file-types.ts b/packages/app/src/attachments/file-types.ts
index 9144518ce..caa96733e 100644
--- a/packages/app/src/attachments/file-types.ts
+++ b/packages/app/src/attachments/file-types.ts
@@ -69,6 +69,11 @@ export function getFileExtension(path: string): string {
return normalizedPath.slice(extensionIndex).toLowerCase();
}
+export function getFileTypeLabel(path: string): string | null {
+ const extension = getFileExtension(path).slice(1);
+ return extension ? extension.toUpperCase() : null;
+}
+
export function getMimeTypeFromPath(path: string): string {
return MIME_TYPE_BY_EXTENSION[getFileExtension(path)] ?? "application/octet-stream";
}
diff --git a/packages/app/src/components/attachment-pill.tsx b/packages/app/src/components/attachment-pill.tsx
index dccd8ad54..86639d387 100644
--- a/packages/app/src/components/attachment-pill.tsx
+++ b/packages/app/src/components/attachment-pill.tsx
@@ -1,9 +1,16 @@
import { type ReactNode, useCallback, useMemo, useState } from "react";
-import { Pressable, View } from "react-native";
-import { StyleSheet, useUnistyles } from "react-native-unistyles";
+import { Image, Pressable, Text, View } from "react-native";
+import { StyleSheet, withUnistyles } from "react-native-unistyles";
import { X } from "lucide-react-native";
import { isNative } from "@/constants/platform";
import { useIsCompactFormFactor } from "@/constants/layout";
+import type { AttachmentMetadata } from "@/attachments/types";
+import { useAttachmentPreviewUrl } from "@/attachments/use-attachment-preview-url";
+import type { Theme } from "@/styles/theme";
+
+// Every attachment pill body — image thumbnail or labelled — renders at this
+// height so mixed attachment trays line up.
+const ATTACHMENT_CONTENT_HEIGHT = 48;
interface AttachmentPillProps {
onOpen: () => void;
@@ -24,7 +31,6 @@ export function AttachmentPill({
testID,
children,
}: AttachmentPillProps) {
- const { theme } = useUnistyles();
const isCompact = useIsCompactFormFactor();
const [isBodyHovered, setIsBodyHovered] = useState(false);
const [isCloseHovered, setIsCloseHovered] = useState(false);
@@ -48,7 +54,7 @@ export function AttachmentPill({
onHoverOut={handleBodyHoverOut}
accessibilityRole="button"
accessibilityLabel={openAccessibilityLabel}
- style={styles.body}
+ style={styles.frame}
>
{children}
@@ -62,22 +68,127 @@ export function AttachmentPill({
accessibilityLabel={removeAccessibilityLabel}
style={closeButtonStyle}
>
-
+
);
}
+interface AttachmentFrameProps {
+ onPress?: () => void;
+ accessibilityLabel?: string;
+ testID?: string;
+ children: ReactNode;
+}
+
+/** Bare attachment frame for read-only surfaces (sent messages) — no remove button. */
+export function AttachmentFrame({
+ onPress,
+ accessibilityLabel,
+ testID,
+ children,
+}: AttachmentFrameProps) {
+ if (!onPress) {
+ return (
+
+ {children}
+
+ );
+ }
+ return (
+
+ {children}
+
+ );
+}
+
+interface AttachmentLabelProps {
+ icon?: ReactNode;
+ title: string;
+ subtitle: string;
+}
+
+/** Two-line labelled pill body: attachment name over its type. */
+export function AttachmentLabel({ icon, title, subtitle }: AttachmentLabelProps) {
+ return (
+
+ {icon ? {icon} : null}
+
+
+ {title}
+
+
+ {subtitle}
+
+
+
+ );
+}
+
+/** Square image preview pill body. */
+export function AttachmentThumbnail({ metadata }: { metadata: AttachmentMetadata }) {
+ const uri = useAttachmentPreviewUrl(metadata);
+ const source = useMemo(() => ({ uri: uri ?? "" }), [uri]);
+ if (!uri) {
+ return ;
+ }
+ return ;
+}
+
+const ThemedX = withUnistyles(X);
+const iconForegroundMutedMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
+
const styles = StyleSheet.create((theme) => ({
wrapper: {
position: "relative",
},
- body: {
+ frame: {
borderRadius: theme.borderRadius.md,
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.borderAccent,
overflow: "hidden",
},
+ labelBody: {
+ height: ATTACHMENT_CONTENT_HEIGHT,
+ maxWidth: 260,
+ flexDirection: "row",
+ alignItems: "center",
+ gap: theme.spacing[2],
+ paddingHorizontal: theme.spacing[3],
+ backgroundColor: theme.colors.surface1,
+ },
+ labelIcon: {
+ width: 18,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ labelTextColumn: {
+ minWidth: 0,
+ flexShrink: 1,
+ },
+ labelTitle: {
+ color: theme.colors.foreground,
+ fontSize: theme.fontSize.sm,
+ },
+ labelSubtitle: {
+ color: theme.colors.foregroundMuted,
+ fontSize: theme.fontSize.xs,
+ },
+ thumbnail: {
+ width: ATTACHMENT_CONTENT_HEIGHT,
+ height: ATTACHMENT_CONTENT_HEIGHT,
+ },
+ thumbnailPlaceholder: {
+ width: ATTACHMENT_CONTENT_HEIGHT,
+ height: ATTACHMENT_CONTENT_HEIGHT,
+ backgroundColor: theme.colors.surface1,
+ },
closeButton: {
position: "absolute",
top: -8,
diff --git a/packages/app/src/components/message.tsx b/packages/app/src/components/message.tsx
index 6befcdbfc..0465ed17c 100644
--- a/packages/app/src/components/message.tsx
+++ b/packages/app/src/components/message.tsx
@@ -32,6 +32,7 @@ import { useQuery } from "@tanstack/react-query";
import MaskedView from "@react-native-masked-view/masked-view";
import {
Circle,
+ CircleDot,
Info,
CheckCircle,
XCircle,
@@ -41,13 +42,15 @@ import {
Check,
CheckSquare,
Copy,
+ GitPullRequest,
+ MessageSquareCode,
TriangleAlertIcon,
Scissors,
MicVocal,
FileSymlink,
} from "lucide-react-native";
import { StyleSheet, withUnistyles } from "react-native-unistyles";
-import type { Theme } from "@/styles/theme";
+import { ICON_SIZE, type Theme } from "@/styles/theme";
import { useIsCompactFormFactor } from "@/constants/layout";
import Animated, {
Easing,
@@ -101,6 +104,13 @@ import {
import { getCompactionMarkerLabel } from "./message-compaction-label";
import { useAttachmentPreviewUrl } from "@/attachments/use-attachment-preview-url";
import { persistAttachmentFromBytes, persistAttachmentFromDataUrl } from "@/attachments/service";
+import { getFileTypeLabel } from "@/attachments/file-types";
+import {
+ AttachmentFrame,
+ AttachmentLabel,
+ AttachmentThumbnail,
+} from "@/components/attachment-pill";
+import { AttachmentLightbox } from "@/components/attachment-lightbox";
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
import { isWeb, isNative } from "@/constants/platform";
import type { AgentCapabilityFlags } from "@getpaseo/protocol/agent-types";
@@ -160,6 +170,10 @@ const ThemedTodoCheckIcon = withUnistyles(Check);
const ThemedFileSymlinkIcon = withUnistyles(FileSymlink);
const ThemedTriangleAlertIcon = withUnistyles(TriangleAlertIcon);
const ThemedChevronRightIcon = withUnistyles(ChevronRight);
+const ThemedAttachmentFileText = withUnistyles(FileText);
+const ThemedGitPullRequest = withUnistyles(GitPullRequest);
+const ThemedCircleDot = withUnistyles(CircleDot);
+const ThemedMessageSquareCode = withUnistyles(MessageSquareCode);
const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
const foregroundMutedColorMapping = (theme: Theme) => ({
@@ -363,34 +377,6 @@ const userMessageStylesheet = StyleSheet.create((theme) => ({
imagePreviewSpacing: {
marginBottom: theme.spacing[2],
},
- imagePill: {
- borderRadius: theme.borderRadius.md,
- borderWidth: 1,
- borderColor: theme.colors.borderAccent,
- overflow: "hidden",
- },
- imageThumbnail: {
- width: 48,
- height: 48,
- },
- imageThumbnailPlaceholder: {
- width: 48,
- height: 48,
- backgroundColor: theme.colors.surface1,
- },
- structuredAttachmentPill: {
- maxWidth: 220,
- borderRadius: theme.borderRadius.md,
- borderWidth: 1,
- borderColor: theme.colors.borderAccent,
- backgroundColor: theme.colors.surface1,
- paddingHorizontal: theme.spacing[3],
- paddingVertical: theme.spacing[2],
- },
- structuredAttachmentText: {
- color: theme.colors.foreground,
- fontSize: theme.fontSize.sm,
- },
copyButton: {
alignSelf: "center",
padding: theme.spacing[1],
@@ -417,36 +403,82 @@ const userMessageStylesheet = StyleSheet.create((theme) => ({
},
}));
-function UserMessageAttachmentThumbnail({ image }: { image: UserMessageImageAttachment }) {
- const uri = useAttachmentPreviewUrl(image);
- const imageSource = useMemo(() => ({ uri: uri ?? "" }), [uri]);
- if (!uri) {
- return ;
- }
- return ;
+const attachmentReviewIcon = (
+
+);
+const attachmentGithubPrIcon = (
+
+);
+const attachmentGithubIssueIcon = (
+
+);
+const attachmentFileIcon = (
+
+);
+
+interface UserMessageImagePillProps {
+ image: UserMessageImageAttachment;
+ onOpen: (image: UserMessageImageAttachment) => void;
+ accessibilityLabel: string;
}
-function getUserMessageAttachmentLabel(
+function UserMessageImagePill({ image, onOpen, accessibilityLabel }: UserMessageImagePillProps) {
+ const handlePress = useCallback(() => {
+ onOpen(image);
+ }, [onOpen, image]);
+ return (
+
+
+
+ );
+}
+
+interface UserMessageAttachmentContent {
+ icon: ReactNode;
+ title: string;
+ subtitle: string;
+}
+
+function getUserMessageAttachmentContent(
attachment: AgentAttachment,
t: ReturnType["t"],
-): string {
+): UserMessageAttachmentContent {
switch (attachment.type) {
case "review": {
const count = attachment.comments.length;
- return count === 1
- ? t("message.attachments.reviewOne")
- : t("message.attachments.reviewMany", { count });
+ return {
+ icon: attachmentReviewIcon,
+ title: t("message.attachments.review"),
+ subtitle:
+ count === 1
+ ? t("message.attachments.commentsOne")
+ : t("message.attachments.commentsMany", { count }),
+ };
}
case "github_pr":
- return `PR #${attachment.number}`;
+ return {
+ icon: attachmentGithubPrIcon,
+ title: attachment.title,
+ subtitle: `PR #${attachment.number}`,
+ };
case "github_issue":
- return `Issue #${attachment.number}`;
+ return {
+ icon: attachmentGithubIssueIcon,
+ title: attachment.title,
+ subtitle: `Issue #${attachment.number}`,
+ };
case "text":
- return attachment.title ?? t("message.attachments.textAttachment");
+ return {
+ icon: attachmentFileIcon,
+ title: attachment.title ?? t("message.attachments.textAttachment"),
+ subtitle: t("message.attachments.text"),
+ };
case "uploaded_file":
- return attachment.fileName;
- default:
- return "";
+ return {
+ icon: attachmentFileIcon,
+ title: attachment.fileName,
+ subtitle: getFileTypeLabel(attachment.fileName) ?? t("message.attachments.file"),
+ };
}
}
@@ -467,6 +499,8 @@ export const UserMessage = memo(function UserMessage({
const isCompact = useIsCompactFormFactor();
const { t } = useTranslation();
const [isHovered, setIsHovered] = useState(false);
+ const [lightboxMetadata, setLightboxMetadata] = useState(null);
+ const handleLightboxClose = useCallback(() => setLightboxMetadata(null), []);
const resolvedDisableOuterSpacing = useDisableOuterSpacing(disableOuterSpacing);
const hasText = message.trim().length > 0;
const hasImages = images.length > 0;
@@ -534,24 +568,31 @@ export const UserMessage = memo(function UserMessage({
{hasImages ? (
{images.map((image) => (
-
-
-
+
))}
) : null}
{hasAttachments ? (
- {attachments.map((attachment, index) => (
-
-
- {getUserMessageAttachmentLabel(attachment, t)}
-
-
- ))}
+ {attachments.map((attachment, index) => {
+ const content = getUserMessageAttachmentContent(attachment, t);
+ return (
+
+
+
+ );
+ })}
) : null}
{hasText ? (
@@ -579,6 +620,7 @@ export const UserMessage = memo(function UserMessage({
) : null}
+
);
});
diff --git a/packages/app/src/composer/attachments/workspace.tsx b/packages/app/src/composer/attachments/workspace.tsx
index 613aee53e..e41424793 100644
--- a/packages/app/src/composer/attachments/workspace.tsx
+++ b/packages/app/src/composer/attachments/workspace.tsx
@@ -1,14 +1,13 @@
import { useCallback, useEffect, useMemo, useState, type ReactElement } from "react";
-import { Text, View } from "react-native";
import { useTranslation } from "react-i18next";
-import { StyleSheet, withUnistyles } from "react-native-unistyles";
+import { withUnistyles } from "react-native-unistyles";
import { FileText, MessageSquareCode, MousePointer2 } from "lucide-react-native";
import type {
ComposerAttachment,
UserComposerAttachment,
WorkspaceComposerAttachment,
} from "@/attachments/types";
-import { AttachmentPill } from "@/components/attachment-pill";
+import { AttachmentLabel, AttachmentPill } from "@/components/attachment-pill";
import { useWorkspaceAttachmentsStore } from "@/attachments/workspace-attachments-store";
import {
isWorkspaceAttachment,
@@ -112,16 +111,31 @@ function getContextSourceLabel(attachment: WorkspaceComposerAttachment): string
return "Review";
}
-function getPillLabel(attachment: WorkspaceComposerAttachment, t: TranslationFn): string {
+interface PillContent {
+ title: string;
+ subtitle: string;
+}
+
+function getPillContent(attachment: WorkspaceComposerAttachment, t: TranslationFn): PillContent {
if (attachment.kind === "browser_element") {
- return t("composer.attachments.browserElement", { tag: attachment.attachment.tag });
+ return {
+ title: attachment.attachment.tag,
+ subtitle: t("composer.attachments.element"),
+ };
}
if (isPullRequestContextAttachment(attachment)) {
- return `${getContextSourceLabel(attachment)} · ${attachment.title}`;
+ return {
+ title: attachment.title,
+ subtitle: getContextSourceLabel(attachment),
+ };
}
- return attachment.commentCount === 1
- ? t("message.attachments.reviewOne")
- : t("message.attachments.reviewMany", { count: attachment.commentCount });
+ return {
+ title: t("message.attachments.review"),
+ subtitle:
+ attachment.commentCount === 1
+ ? t("message.attachments.commentsOne")
+ : t("message.attachments.commentsMany", { count: attachment.commentCount }),
+ };
}
function getOpenAccessibilityLabel(
@@ -309,7 +323,7 @@ function WorkspaceAttachmentPill({
onRemove,
}: WorkspaceAttachmentPillProps) {
const { t } = useTranslation();
- const label = getPillLabel(attachment, t);
+ const content = getPillContent(attachment, t);
const handleOpen = useCallback(() => {
onOpen(attachment);
}, [onOpen, attachment]);
@@ -325,12 +339,11 @@ function WorkspaceAttachmentPill({
removeAccessibilityLabel={getRemoveAccessibilityLabel(attachment, t)}
disabled={disabled}
>
-
- {renderPillIcon(attachment)}
-
- {label}
-
-
+
);
}
@@ -343,30 +356,6 @@ export const composerWorkspaceAttachment = {
useBinding: useWorkspaceAttachmentBinding,
};
-const styles = StyleSheet.create((theme: Theme) => ({
- pillBody: {
- minHeight: 48,
- maxWidth: 260,
- flexDirection: "row",
- alignItems: "center",
- gap: theme.spacing[2],
- paddingHorizontal: theme.spacing[3],
- paddingVertical: theme.spacing[2],
- backgroundColor: theme.colors.surface1,
- },
- pillIcon: {
- width: 18,
- alignItems: "center",
- justifyContent: "center",
- },
- pillText: {
- minWidth: 0,
- flexShrink: 1,
- color: theme.colors.foreground,
- fontSize: theme.fontSize.sm,
- },
-})) as unknown as Record;
-
const ThemedMousePointer2 = withUnistyles(MousePointer2);
const ThemedMessageSquareCode = withUnistyles(MessageSquareCode);
const ThemedFileText = withUnistyles(FileText);
diff --git a/packages/app/src/composer/index.tsx b/packages/app/src/composer/index.tsx
index 1ee638f82..43df97310 100644
--- a/packages/app/src/composer/index.tsx
+++ b/packages/app/src/composer/index.tsx
@@ -3,7 +3,6 @@ import {
Pressable,
Text,
ActivityIndicator,
- Image,
type PressableStateCallbackType,
} from "react-native";
import type { TFunction } from "i18next";
@@ -100,9 +99,9 @@ import type {
WorkspaceComposerAttachment,
} from "@/attachments/types";
import { composerWorkspaceAttachment } from "@/composer/attachments/workspace";
-import { useAttachmentPreviewUrl } from "@/attachments/use-attachment-preview-url";
+import { getFileTypeLabel } from "@/attachments/file-types";
import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox";
-import { AttachmentPill } from "@/components/attachment-pill";
+import { AttachmentLabel, AttachmentPill, AttachmentThumbnail } from "@/components/attachment-pill";
import { AttachmentLightbox } from "@/components/attachment-lightbox";
import { openExternalUrl } from "@/utils/open-external-url";
import { useIsDictationReady } from "@/hooks/use-is-dictation-ready";
@@ -560,15 +559,6 @@ function QueuedMessageRow({
);
}
-function ImageAttachmentThumbnail({ image }: { image: ImageAttachment }) {
- const uri = useAttachmentPreviewUrl(image);
- const source = useMemo(() => ({ uri: uri ?? "" }), [uri]);
- if (!uri) {
- return ;
- }
- return ;
-}
-
interface ImageAttachmentPillProps {
attachment: Extract;
index: number;
@@ -603,7 +593,7 @@ function ImageAttachmentPill({
removeAccessibilityLabel={removeLabel}
disabled={disabled}
>
-
+
);
}
@@ -644,18 +634,11 @@ function GithubAttachmentPill({
removeAccessibilityLabel={removeLabel(kindLabel, item.number)}
disabled={disabled}
>
-
-
- {item.kind === "pr" ? (
-
- ) : (
-
- )}
-
-
- #{item.number} {item.title}
-
-
+
);
}
@@ -675,26 +658,25 @@ function FileAttachmentPill({
onRemove,
removeLabel,
}: FileAttachmentPillProps) {
+ const { t } = useTranslation();
const handleRemove = useCallback(() => {
onRemove(index);
}, [onRemove, index]);
+ const fileName = attachment.attachment.fileName;
return (
-
-
-
-
-
- {attachment.attachment.fileName}
-
-
+
);
}
@@ -1705,18 +1687,14 @@ export function Composer({
setIsGithubPickerOpen(true);
},
},
- ...(!isNative
- ? [
- {
- id: "file",
- label: t("composer.attachments.addFile"),
- icon: ,
- onSelect: () => {
- void handlePickFile();
- },
- },
- ]
- : []),
+ {
+ id: "file",
+ label: t("composer.attachments.addFile"),
+ icon: ,
+ onSelect: () => {
+ void handlePickFile();
+ },
+ },
],
[handlePickImage, handlePickFile, t],
);
@@ -2059,50 +2037,6 @@ const styles = StyleSheet.create((theme: Theme) => ({
gap: theme.spacing[2],
flexWrap: "wrap",
},
- imageThumbnail: {
- width: 32,
- height: 32,
- },
- imageThumbnailPlaceholder: {
- width: 32,
- height: 32,
- backgroundColor: theme.colors.surface2,
- },
- githubPillBody: {
- minHeight: 32,
- maxWidth: 260,
- flexDirection: "row",
- alignItems: "center",
- gap: theme.spacing[2],
- paddingHorizontal: theme.spacing[3],
- paddingVertical: theme.spacing[2],
- backgroundColor: theme.colors.surface1,
- },
- githubPillIcon: {
- width: 18,
- alignItems: "center",
- justifyContent: "center",
- },
- filePillBody: {
- flexDirection: "row",
- alignItems: "center",
- gap: theme.spacing[2],
- },
- filePillIcon: {
- justifyContent: "center",
- alignItems: "center",
- },
- filePillText: {
- color: theme.colors.foreground,
- fontSize: theme.fontSize.sm,
- flexShrink: 1,
- },
- githubPillText: {
- minWidth: 0,
- flexShrink: 1,
- color: theme.colors.foreground,
- fontSize: theme.fontSize.sm,
- },
tooltipRow: {
flexDirection: "row",
alignItems: "center",
@@ -2173,3 +2107,11 @@ const ThemedGithub = withUnistyles(Github);
const iconForegroundMapping = (theme: Theme) => ({ color: theme.colors.foreground });
const iconForegroundMutedMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
const iconAccentForegroundMapping = (theme: Theme) => ({ color: theme.colors.accentForeground });
+
+const githubPrPillIcon = (
+
+);
+const githubIssuePillIcon = (
+
+);
+const filePillIcon = ;
diff --git a/packages/app/src/hooks/use-file-picker.ts b/packages/app/src/hooks/use-file-picker.ts
index f0cdcc064..110a273ee 100644
--- a/packages/app/src/hooks/use-file-picker.ts
+++ b/packages/app/src/hooks/use-file-picker.ts
@@ -1,4 +1,6 @@
import { useCallback, useRef } from "react";
+import * as DocumentPicker from "expo-document-picker";
+import { File } from "expo-file-system";
import { getDesktopHost, isElectronRuntime } from "@/desktop/host";
import { copyDesktopAttachmentFile } from "@/desktop/attachments/desktop-file-commands";
import { readDesktopFileBase64 } from "@/desktop/attachments/desktop-preview-url";
@@ -113,6 +115,25 @@ function pickFilesWithWebInput(): Promise {
});
}
+async function pickFilesWithDocumentPicker(): Promise {
+ const result = await DocumentPicker.getDocumentAsync({
+ multiple: true,
+ copyToCacheDirectory: true,
+ });
+
+ if (result.canceled || result.assets.length === 0) {
+ return null;
+ }
+
+ return await Promise.all(
+ result.assets.map(async (asset) => ({
+ fileName: asset.name,
+ mimeType: asset.mimeType ?? getMimeTypeFromPath(asset.name),
+ bytes: await new File(asset.uri).bytes(),
+ })),
+ );
+}
+
export function useFilePicker() {
const isPickingRef = useRef(false);
@@ -131,8 +152,7 @@ export function useFilePicker() {
return await pickFilesWithWebInput();
}
- // Native not supported yet
- return null;
+ return await pickFilesWithDocumentPicker();
} catch (error) {
console.error("[FilePicker] Failed to pick files:", error);
throw error;
diff --git a/packages/app/src/i18n/resources.test.ts b/packages/app/src/i18n/resources.test.ts
index eb22ae67d..570256730 100644
--- a/packages/app/src/i18n/resources.test.ts
+++ b/packages/app/src/i18n/resources.test.ts
@@ -575,10 +575,11 @@ describe("translation resources", () => {
});
it("includes remaining utility chrome keys for the Batch 4T migration", () => {
- expect(en.message.attachments.reviewOne).toBe("Review · 1 comment");
- expect(en.message.attachments.reviewMany).toBe("Review · {{count}} comments");
+ expect(en.message.attachments.review).toBe("Review");
+ expect(en.message.attachments.commentsOne).toBe("1 comment");
+ expect(en.message.attachments.commentsMany).toBe("{{count}} comments");
expect(en.message.attachments.textAttachment).toBe("Text attachment");
- expect(en.composer.attachments.browserElement).toBe("Element · {{tag}}");
+ expect(en.composer.attachments.element).toBe("Element");
expect(en.workspace.hoverCard.scriptsAccessibility).toBe("Workspace scripts");
expect(en.branchSwitcher.restoreStashTitle).toBe("Restore stashed changes?");
expect(en.branchSwitcher.stashRestored).toBe("Stashed changes restored");
diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts
index f15f5408d..f32c150f8 100644
--- a/packages/app/src/i18n/resources/ar.ts
+++ b/packages/app/src/i18n/resources/ar.ts
@@ -110,7 +110,7 @@ export const ar: TranslationResources = {
removeFile: "Remove file attachment",
openGithub: "افتح{{kind}}#{{number}}",
removeGithub: "إزالة{{kind}}#{{number}}",
- browserElement: "العنصر ·{{tag}}",
+ element: "عنصر",
openBrowserElement: "افتح مرفق عنصر المتصفح",
removeBrowserElement: "إزالة مرفق عنصر المتصفح",
openReview: "فتح مرفق المراجعة",
@@ -255,9 +255,12 @@ export const ar: TranslationResources = {
imageUnavailable: "الصورة غير متاحة",
imagePreviewUnavailable: "معاينة الصورة غير متاحة.",
imagePreviewLoadFailed: "غير قادر على تحميل معاينة الصورة.",
- reviewOne: "مراجعة · تعليق واحد",
- reviewMany: "مراجعة · تعليقات{{count}}",
+ review: "مراجعة",
+ commentsOne: "تعليق واحد",
+ commentsMany: "{{count}} تعليقات",
textAttachment: "مرفق النص",
+ text: "نص",
+ file: "ملف",
},
speak: {
header: "تكلم",
diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts
index 96e192047..864f28b44 100644
--- a/packages/app/src/i18n/resources/en.ts
+++ b/packages/app/src/i18n/resources/en.ts
@@ -108,7 +108,7 @@ export const en = {
removeFile: "Remove file attachment",
openGithub: "Open {{kind}} #{{number}}",
removeGithub: "Remove {{kind}} #{{number}}",
- browserElement: "Element · {{tag}}",
+ element: "Element",
openBrowserElement: "Open browser element attachment",
removeBrowserElement: "Remove browser element attachment",
openReview: "Open review attachment",
@@ -254,9 +254,12 @@ export const en = {
imageUnavailable: "Image unavailable",
imagePreviewUnavailable: "Image preview unavailable.",
imagePreviewLoadFailed: "Unable to load image preview.",
- reviewOne: "Review · 1 comment",
- reviewMany: "Review · {{count}} comments",
+ review: "Review",
+ commentsOne: "1 comment",
+ commentsMany: "{{count}} comments",
textAttachment: "Text attachment",
+ text: "Text",
+ file: "File",
},
speak: {
header: "Spoke",
diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts
index 110a7ff24..3c38068e1 100644
--- a/packages/app/src/i18n/resources/es.ts
+++ b/packages/app/src/i18n/resources/es.ts
@@ -110,7 +110,7 @@ export const es: TranslationResources = {
removeFile: "Remove file attachment",
openGithub: "Abrir{{kind}}#{{number}}",
removeGithub: "Quitar{{kind}}#{{number}}",
- browserElement: "Elemento ·{{tag}}",
+ element: "Elemento",
openBrowserElement: "Abrir archivo adjunto de elemento del navegador",
removeBrowserElement: "Eliminar el archivo adjunto del elemento del navegador",
openReview: "Abrir archivo adjunto de reseña",
@@ -258,9 +258,12 @@ export const es: TranslationResources = {
imageUnavailable: "Imagen no disponible",
imagePreviewUnavailable: "Vista previa de la imagen no disponible.",
imagePreviewLoadFailed: "No se puede cargar la vista previa de la imagen.",
- reviewOne: "Reseña · 1 comentario",
- reviewMany: "Revisión · Comentarios{{count}}",
+ review: "Revisión",
+ commentsOne: "1 comentario",
+ commentsMany: "{{count}} comentarios",
textAttachment: "Adjunto de texto",
+ text: "Texto",
+ file: "Archivo",
},
speak: {
header: "Habló",
diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts
index 4cf40b011..fce2175ea 100644
--- a/packages/app/src/i18n/resources/fr.ts
+++ b/packages/app/src/i18n/resources/fr.ts
@@ -111,7 +111,7 @@ export const fr: TranslationResources = {
removeFile: "Remove file attachment",
openGithub: "Ouvrir{{kind}}#{{number}}",
removeGithub: "Supprimer{{kind}}#{{number}}",
- browserElement: "Élément ·{{tag}}",
+ element: "Élément",
openBrowserElement: "Ouvrir la pièce jointe de l'élément de navigateur",
removeBrowserElement: "Supprimer la pièce jointe d'un élément de navigateur",
openReview: "Ouvrir la pièce jointe de l'avis",
@@ -258,9 +258,12 @@ export const fr: TranslationResources = {
imageUnavailable: "Image indisponible",
imagePreviewUnavailable: "Aperçu de l'image indisponible.",
imagePreviewLoadFailed: "Impossible de charger l'aperçu de l'image.",
- reviewOne: "Avis · 1 commentaire",
- reviewMany: "Révision · Commentaires{{count}}",
+ review: "Revue",
+ commentsOne: "1 commentaire",
+ commentsMany: "{{count}} commentaires",
textAttachment: "Texte en pièce jointe",
+ text: "Texte",
+ file: "Fichier",
},
speak: {
header: "Rayon",
diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts
index 368b2fb92..8422dd4bb 100644
--- a/packages/app/src/i18n/resources/ru.ts
+++ b/packages/app/src/i18n/resources/ru.ts
@@ -110,7 +110,7 @@ export const ru: TranslationResources = {
removeFile: "Remove file attachment",
openGithub: "Открыть{{kind}}#{{number}}",
removeGithub: "Удалить{{kind}}#{{number}}",
- browserElement: "Элемент ·{{tag}}",
+ element: "Элемент",
openBrowserElement: "Открыть вложение элемента браузера",
removeBrowserElement: "Удалить вложение элемента браузера",
openReview: "Открыть прикрепленный файл с отзывом",
@@ -257,9 +257,12 @@ export const ru: TranslationResources = {
imageUnavailable: "Изображение недоступно",
imagePreviewUnavailable: "Предварительный просмотр изображения недоступен.",
imagePreviewLoadFailed: "Невозможно загрузить предварительный просмотр изображения.",
- reviewOne: "Отзыв · 1 комментарий",
- reviewMany: "Обзор · Комментарии{{count}}",
+ review: "Ревью",
+ commentsOne: "1 комментарий",
+ commentsMany: "Комментарии: {{count}}",
textAttachment: "Текстовое вложение",
+ text: "Текст",
+ file: "Файл",
},
speak: {
header: "Говорил",
diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts
index 8d140bc3a..6316f25bf 100644
--- a/packages/app/src/i18n/resources/zh-CN.ts
+++ b/packages/app/src/i18n/resources/zh-CN.ts
@@ -110,7 +110,7 @@ export const zhCN: TranslationResources = {
removeFile: "Remove file attachment",
openGithub: "打开 {{kind}} #{{number}}",
removeGithub: "移除 {{kind}} #{{number}}",
- browserElement: "元素 · {{tag}}",
+ element: "元素",
openBrowserElement: "打开浏览器元素附件",
removeBrowserElement: "移除浏览器元素附件",
openReview: "打开 review 附件",
@@ -255,9 +255,12 @@ export const zhCN: TranslationResources = {
imageUnavailable: "图片不可用",
imagePreviewUnavailable: "图片预览不可用。",
imagePreviewLoadFailed: "无法加载图片预览。",
- reviewOne: "Review · 1 条评论",
- reviewMany: "Review · {{count}} 条评论",
+ review: "审查",
+ commentsOne: "1 条评论",
+ commentsMany: "{{count}} 条评论",
textAttachment: "文本附件",
+ text: "文本",
+ file: "文件",
},
speak: {
header: "已朗读",