import { useState, useRef } from 'react'; import { View, Text, TextInput, TouchableOpacity, Image, StyleSheet, ActivityIndicator, Platform, Alert, } from 'react-native'; import * as ImagePicker from 'expo-image-picker'; // TODO(sdk54): migrate to new expo-file-system class-based API (File/Directory/Paths) — see Task #14 import * as FileSystem from 'expo-file-system/legacy'; import { Ionicons } from '@expo/vector-icons'; import { useTranslation } from 'react-i18next'; import { supabase } from '../../lib/supabase'; import { useColors } from '../../lib/theme'; type ReplyTo = { id: string; nickname: string; content: string }; export type SendPayload = { content: string; replyToId?: string; attachmentUrl?: string; attachmentType?: string; attachmentName?: string; }; type Props = { replyTo: ReplyTo | null; sending?: boolean; placeholder?: string; disabled?: boolean; onSend: (data: SendPayload) => void; onCancelReply: () => void; }; export function ChatInput({ replyTo, sending, placeholder, disabled, onSend, onCancelReply, }: Props) { const { t } = useTranslation(); const colors = useColors(); const [text, setText] = useState(''); const [attachment, setAttachment] = useState<{ uri: string; name: string; isImage: boolean; } | null>(null); const [uploading, setUploading] = useState(false); const inputRef = useRef(null); const hasContent = text.trim().length > 0 || attachment !== null; async function pickImage() { const perm = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (!perm.granted) { Alert.alert('Foto-Zugriff', 'Bitte Foto-Zugriff in den Einstellungen erlauben.'); return; } const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, quality: 0.8, }); if (!result.canceled && result.assets[0]?.uri) { const a = result.assets[0]; setAttachment({ uri: a.uri, name: a.fileName ?? `image-${Date.now()}.jpg`, isImage: true, }); } } function clearAttachment() { setAttachment(null); } async function uploadAttachment(): Promise<{ url: string; type: string; name: string; } | null> { if (!attachment) return null; try { setUploading(true); const ext = attachment.name.split('.').pop() || 'jpg'; const path = `chat/${Date.now()}_${Math.random().toString(36).slice(2, 8)}.${ext}`; const base64 = await FileSystem.readAsStringAsync(attachment.uri, { encoding: FileSystem.EncodingType.Base64, }); const arrayBuffer = decodeBase64(base64); const { error } = await supabase.storage .from('chat-attachments') .upload(path, arrayBuffer, { cacheControl: '3600', upsert: false, contentType: attachment.isImage ? 'image/jpeg' : 'application/octet-stream', }); if (error) throw error; const { data } = supabase.storage.from('chat-attachments').getPublicUrl(path); return { url: data.publicUrl, type: attachment.isImage ? 'image' : 'file', name: attachment.name, }; } catch (err: any) { Alert.alert(t('chat.upload_failed'), err?.message ?? ''); return null; } finally { setUploading(false); } } async function handleSend() { const content = text.trim(); if (!content && !attachment) return; let attachmentMeta: { url: string; type: string; name: string } | null = null; if (attachment) { attachmentMeta = await uploadAttachment(); if (!attachmentMeta) return; } onSend({ content, replyToId: replyTo?.id, attachmentUrl: attachmentMeta?.url, attachmentType: attachmentMeta?.type, attachmentName: attachmentMeta?.name, }); setText(''); setAttachment(null); } const styles = makeStyles(colors); return ( {/* Reply preview */} {replyTo && ( {t('chat.reply_to')} {replyTo.nickname} {replyTo.content || '…'} )} {/* Attachment preview */} {attachment && ( {attachment.isImage ? ( ) : ( )} {attachment.name} )} {/* Input row */} {sending || uploading ? ( ) : ( )} ); } // Base64 → Uint8Array (für Supabase Storage Upload) function decodeBase64(base64: string): Uint8Array { const binary = typeof atob === 'function' ? atob(base64) : Buffer.from(base64, 'base64').toString('binary'); const len = binary.length; const bytes = new Uint8Array(len); for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i); return bytes; } function makeStyles(colors: ReturnType) { return StyleSheet.create({ container: { backgroundColor: colors.surface, borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: colors.border, }, replyBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 12, paddingVertical: 8, backgroundColor: colors.surface, borderLeftWidth: 3, borderLeftColor: '#007AFF', marginHorizontal: 8, marginTop: 6, borderRadius: 8, }, replyName: { fontSize: 11, fontFamily: 'Nunito_700Bold', color: '#007AFF', }, replyContent: { fontSize: 11, fontFamily: 'Nunito_400Regular', color: colors.textMuted, marginTop: 1, }, attachBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 12, paddingVertical: 6, backgroundColor: colors.surface, marginHorizontal: 8, marginTop: 6, borderRadius: 8, }, attachImg: { width: 36, height: 36, borderRadius: 6, marginRight: 8, }, attachFileIcon: { width: 36, height: 36, borderRadius: 6, backgroundColor: colors.surfaceElevated, alignItems: 'center', justifyContent: 'center', marginRight: 8, }, attachName: { flex: 1, fontSize: 12, fontFamily: 'Nunito_600SemiBold', color: colors.text, }, row: { flexDirection: 'row', alignItems: 'flex-end', paddingHorizontal: 8, paddingTop: 8, paddingBottom: 8, }, iconBtn: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center', marginRight: 4, }, inputWrap: { flex: 1, backgroundColor: colors.bg, borderRadius: 22, borderWidth: StyleSheet.hairlineWidth, borderColor: colors.border, paddingHorizontal: 14, minHeight: 38, maxHeight: 120, justifyContent: 'center', }, input: { fontSize: 15, lineHeight: 20, fontFamily: 'Nunito_400Regular', color: colors.text, paddingVertical: Platform.OS === 'ios' ? 9 : 5, }, sendBtn: { width: 38, height: 38, borderRadius: 19, alignItems: 'center', justifyContent: 'center', marginLeft: 6, }, }); }