import { chat } from '@chat-app/shared'; import type { AttachmentHandle } from '@chat-app/shared/chat'; import { Buffer } from 'buffer'; import { useEffect, useState } from 'react'; import { ActivityIndicator, Image, StyleSheet, Text, View } from 'react-native'; import { getCachedAttachment, setCachedAttachment } from '../lib/attachmentCache'; import { supabase } from '../lib/supabase'; import { colors } from '../theme/colors'; interface Props { handle: AttachmentHandle; ownDeviceId: string; ownPrivateKey: Uint8Array; } // Decrypts an encrypted image attachment on first mount and renders it // inline. Subsequent mounts hit the in-memory cache. Failure (key not // shared yet for this device, network error, etc.) shows a small error // placeholder rather than crashing the parent message bubble. export function AttachmentImage({ handle }: Props) { const [dataUrl, setDataUrl] = useState(() => getCachedAttachment(handle.id) ?? null); const [error, setError] = useState(null); useEffect(() => { if (dataUrl) return; let cancelled = false; void (async () => { try { const blob = await chat.downloadAndDecryptAttachment({ client: supabase, handle, }); const bytes = new Uint8Array(await blob.arrayBuffer()); const b64 = Buffer.from(bytes as unknown as ArrayLike).toString('base64'); const url = 'data:' + handle.mimeType + ';base64,' + b64; if (cancelled) return; setCachedAttachment(handle.id, url); setDataUrl(url); } catch (err: unknown) { if (cancelled) return; setError(err instanceof Error ? err.message : 'decrypt failed'); } })(); return () => { cancelled = true; }; }, [dataUrl, handle]); // Aspect ratio honoured if available; fall back to a 4:3 placeholder. const aspect = handle.width && handle.height && handle.height > 0 ? handle.width / handle.height : 4 / 3; if (error) { return ( 🔒 {error} ); } if (!dataUrl) { return ( ); } return ( ); } const styles = StyleSheet.create({ image: { width: '100%', borderRadius: 8, marginTop: 4, }, placeholder: { width: '100%', alignItems: 'center', justifyContent: 'center', backgroundColor: colors.surface, borderRadius: 8, borderColor: colors.border, borderWidth: 1, marginTop: 4, }, errorText: { color: colors.danger, fontSize: 12 }, });