| .. | .. |
|---|
| 30 | 30 | const [isTextMode, setIsTextMode] = useState(false); |
|---|
| 31 | 31 | const [showSessions, setShowSessions] = useState(false); |
|---|
| 32 | 32 | const [audioPlaying, setAudioPlaying] = useState(false); |
|---|
| 33 | | - const [stagedImage, setStagedImage] = useState<StagedImage | null>(null); |
|---|
| 33 | + const [stagedImages, setStagedImages] = useState<StagedImage[]>([]); |
|---|
| 34 | 34 | |
|---|
| 35 | 35 | useEffect(() => { |
|---|
| 36 | 36 | return onPlayingChange((uri) => setAudioPlaying(uri !== null)); |
|---|
| .. | .. |
|---|
| 59 | 59 | clearMessages(); |
|---|
| 60 | 60 | }, [clearMessages]); |
|---|
| 61 | 61 | |
|---|
| 62 | | - // Resolve a picked asset into a StagedImage |
|---|
| 63 | | - const stageAsset = useCallback(async (asset: { base64?: string | null; uri: string; mimeType?: string | null }) => { |
|---|
| 64 | | - const mimeType = asset.mimeType ?? (asset.uri.endsWith(".png") ? "image/png" : "image/jpeg"); |
|---|
| 65 | | - let base64 = asset.base64 ?? ""; |
|---|
| 66 | | - if (!base64 && asset.uri) { |
|---|
| 67 | | - const { readAsStringAsync } = await import("expo-file-system/legacy"); |
|---|
| 68 | | - base64 = await readAsStringAsync(asset.uri, { encoding: "base64" }); |
|---|
| 62 | + // Resolve picked assets into StagedImage array |
|---|
| 63 | + const stageAssets = useCallback(async (assets: Array<{ base64?: string | null; uri: string; mimeType?: string | null }>) => { |
|---|
| 64 | + const staged: StagedImage[] = []; |
|---|
| 65 | + for (const asset of assets) { |
|---|
| 66 | + const mimeType = asset.mimeType ?? (asset.uri.endsWith(".png") ? "image/png" : "image/jpeg"); |
|---|
| 67 | + let base64 = asset.base64 ?? ""; |
|---|
| 68 | + if (!base64 && asset.uri) { |
|---|
| 69 | + const { readAsStringAsync } = await import("expo-file-system/legacy"); |
|---|
| 70 | + base64 = await readAsStringAsync(asset.uri, { encoding: "base64" }); |
|---|
| 71 | + } |
|---|
| 72 | + if (base64) staged.push({ base64, uri: asset.uri, mimeType }); |
|---|
| 69 | 73 | } |
|---|
| 70 | | - if (base64) { |
|---|
| 71 | | - setStagedImage({ base64, uri: asset.uri, mimeType }); |
|---|
| 72 | | - } |
|---|
| 74 | + if (staged.length > 0) setStagedImages(staged); |
|---|
| 73 | 75 | }, []); |
|---|
| 74 | 76 | |
|---|
| 75 | 77 | const pickFromLibrary = useCallback(async () => { |
|---|
| .. | .. |
|---|
| 82 | 84 | } |
|---|
| 83 | 85 | const result = await ImagePicker.launchImageLibraryAsync({ |
|---|
| 84 | 86 | mediaTypes: ["images"], |
|---|
| 87 | + allowsMultipleSelection: true, |
|---|
| 88 | + selectionLimit: 10, |
|---|
| 85 | 89 | quality: 0.7, |
|---|
| 86 | 90 | base64: true, |
|---|
| 87 | 91 | }); |
|---|
| 88 | | - if (result.canceled || !result.assets?.[0]) return; |
|---|
| 89 | | - await stageAsset(result.assets[0]); |
|---|
| 92 | + if (result.canceled || !result.assets?.length) return; |
|---|
| 93 | + await stageAssets(result.assets); |
|---|
| 90 | 94 | } catch (err: any) { |
|---|
| 91 | 95 | Alert.alert("Image Error", err?.message ?? String(err)); |
|---|
| 92 | 96 | } |
|---|
| 93 | | - }, [stageAsset]); |
|---|
| 97 | + }, [stageAssets]); |
|---|
| 94 | 98 | |
|---|
| 95 | 99 | const pickFromCamera = useCallback(async () => { |
|---|
| 96 | 100 | try { |
|---|
| .. | .. |
|---|
| 105 | 109 | base64: true, |
|---|
| 106 | 110 | }); |
|---|
| 107 | 111 | if (result.canceled || !result.assets?.[0]) return; |
|---|
| 108 | | - await stageAsset(result.assets[0]); |
|---|
| 112 | + await stageAssets([result.assets[0]]); |
|---|
| 109 | 113 | } catch (err: any) { |
|---|
| 110 | 114 | Alert.alert("Camera Error", err?.message ?? String(err)); |
|---|
| 111 | 115 | } |
|---|
| 112 | | - }, [stageAsset]); |
|---|
| 116 | + }, [stageAssets]); |
|---|
| 113 | 117 | |
|---|
| 114 | 118 | const handlePickImage = useCallback(() => { |
|---|
| 115 | 119 | if (Platform.OS === "ios") { |
|---|
| .. | .. |
|---|
| 131 | 135 | |
|---|
| 132 | 136 | const handleImageSend = useCallback( |
|---|
| 133 | 137 | (caption: string) => { |
|---|
| 134 | | - if (!stagedImage) return; |
|---|
| 135 | | - sendImageMessage(stagedImage.base64, caption, stagedImage.mimeType); |
|---|
| 136 | | - setStagedImage(null); |
|---|
| 138 | + if (stagedImages.length === 0) return; |
|---|
| 139 | + // Send each image as a separate message; caption on the first only |
|---|
| 140 | + stagedImages.forEach((img, i) => { |
|---|
| 141 | + sendImageMessage(img.base64, i === 0 ? caption : "", img.mimeType); |
|---|
| 142 | + }); |
|---|
| 143 | + setStagedImages([]); |
|---|
| 137 | 144 | }, |
|---|
| 138 | | - [stagedImage, sendImageMessage], |
|---|
| 145 | + [stagedImages, sendImageMessage], |
|---|
| 139 | 146 | ); |
|---|
| 140 | 147 | |
|---|
| 141 | 148 | const handleReplay = useCallback(async () => { |
|---|
| .. | .. |
|---|
| 340 | 347 | |
|---|
| 341 | 348 | {/* Image caption modal — WhatsApp-style full-screen preview */} |
|---|
| 342 | 349 | <ImageCaptionModal |
|---|
| 343 | | - visible={!!stagedImage} |
|---|
| 344 | | - imageUri={stagedImage ? `data:${stagedImage.mimeType};base64,${stagedImage.base64}` : ""} |
|---|
| 350 | + visible={stagedImages.length > 0} |
|---|
| 351 | + images={stagedImages.map((img) => ({ uri: `data:${img.mimeType};base64,${img.base64}` }))} |
|---|
| 345 | 352 | onSend={handleImageSend} |
|---|
| 346 | | - onCancel={() => setStagedImage(null)} |
|---|
| 353 | + onCancel={() => setStagedImages([])} |
|---|
| 347 | 354 | /> |
|---|
| 348 | 355 | |
|---|
| 349 | 356 | {/* Session drawer — absolute overlay outside KAV */} |
|---|