From e25bdba29f49b1b55a8a8cccdc4583aea3c101ed Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Sun, 15 Mar 2026 13:41:09 +0100
Subject: [PATCH] feat: multi-image upload and catch_up message delivery
---
app/chat.tsx | 53 ++++++++++++++++++++++++++++++-----------------------
1 files changed, 30 insertions(+), 23 deletions(-)
diff --git a/app/chat.tsx b/app/chat.tsx
index 2a177cc..ccfd6d5 100644
--- a/app/chat.tsx
+++ b/app/chat.tsx
@@ -30,7 +30,7 @@
const [isTextMode, setIsTextMode] = useState(false);
const [showSessions, setShowSessions] = useState(false);
const [audioPlaying, setAudioPlaying] = useState(false);
- const [stagedImage, setStagedImage] = useState<StagedImage | null>(null);
+ const [stagedImages, setStagedImages] = useState<StagedImage[]>([]);
useEffect(() => {
return onPlayingChange((uri) => setAudioPlaying(uri !== null));
@@ -59,17 +59,19 @@
clearMessages();
}, [clearMessages]);
- // Resolve a picked asset into a StagedImage
- const stageAsset = useCallback(async (asset: { base64?: string | null; uri: string; mimeType?: string | null }) => {
- const mimeType = asset.mimeType ?? (asset.uri.endsWith(".png") ? "image/png" : "image/jpeg");
- let base64 = asset.base64 ?? "";
- if (!base64 && asset.uri) {
- const { readAsStringAsync } = await import("expo-file-system/legacy");
- base64 = await readAsStringAsync(asset.uri, { encoding: "base64" });
+ // Resolve picked assets into StagedImage array
+ const stageAssets = useCallback(async (assets: Array<{ base64?: string | null; uri: string; mimeType?: string | null }>) => {
+ const staged: StagedImage[] = [];
+ for (const asset of assets) {
+ const mimeType = asset.mimeType ?? (asset.uri.endsWith(".png") ? "image/png" : "image/jpeg");
+ let base64 = asset.base64 ?? "";
+ if (!base64 && asset.uri) {
+ const { readAsStringAsync } = await import("expo-file-system/legacy");
+ base64 = await readAsStringAsync(asset.uri, { encoding: "base64" });
+ }
+ if (base64) staged.push({ base64, uri: asset.uri, mimeType });
}
- if (base64) {
- setStagedImage({ base64, uri: asset.uri, mimeType });
- }
+ if (staged.length > 0) setStagedImages(staged);
}, []);
const pickFromLibrary = useCallback(async () => {
@@ -82,15 +84,17 @@
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ["images"],
+ allowsMultipleSelection: true,
+ selectionLimit: 10,
quality: 0.7,
base64: true,
});
- if (result.canceled || !result.assets?.[0]) return;
- await stageAsset(result.assets[0]);
+ if (result.canceled || !result.assets?.length) return;
+ await stageAssets(result.assets);
} catch (err: any) {
Alert.alert("Image Error", err?.message ?? String(err));
}
- }, [stageAsset]);
+ }, [stageAssets]);
const pickFromCamera = useCallback(async () => {
try {
@@ -105,11 +109,11 @@
base64: true,
});
if (result.canceled || !result.assets?.[0]) return;
- await stageAsset(result.assets[0]);
+ await stageAssets([result.assets[0]]);
} catch (err: any) {
Alert.alert("Camera Error", err?.message ?? String(err));
}
- }, [stageAsset]);
+ }, [stageAssets]);
const handlePickImage = useCallback(() => {
if (Platform.OS === "ios") {
@@ -131,11 +135,14 @@
const handleImageSend = useCallback(
(caption: string) => {
- if (!stagedImage) return;
- sendImageMessage(stagedImage.base64, caption, stagedImage.mimeType);
- setStagedImage(null);
+ if (stagedImages.length === 0) return;
+ // Send each image as a separate message; caption on the first only
+ stagedImages.forEach((img, i) => {
+ sendImageMessage(img.base64, i === 0 ? caption : "", img.mimeType);
+ });
+ setStagedImages([]);
},
- [stagedImage, sendImageMessage],
+ [stagedImages, sendImageMessage],
);
const handleReplay = useCallback(async () => {
@@ -340,10 +347,10 @@
{/* Image caption modal — WhatsApp-style full-screen preview */}
<ImageCaptionModal
- visible={!!stagedImage}
- imageUri={stagedImage ? `data:${stagedImage.mimeType};base64,${stagedImage.base64}` : ""}
+ visible={stagedImages.length > 0}
+ images={stagedImages.map((img) => ({ uri: `data:${img.mimeType};base64,${img.base64}` }))}
onSend={handleImageSend}
- onCancel={() => setStagedImage(null)}
+ onCancel={() => setStagedImages([])}
/>
{/* Session drawer — absolute overlay outside KAV */}
--
Gitblit v1.3.1