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