'Teilen -> ARIA' aus WhatsApp, Galerie, Browser & Co. Ablauf wie gewuenscht: sehen WAS geteilt wurde -> WOHIN (Projekt oder Hauptchat) -> optional Text dazu. Ohne Text geht genauso. - Manifest: intent-filter fuer ACTION_SEND (text/image/video/audio/application) und ACTION_SEND_MULTIPLE. launchMode=singleTask war bereits gesetzt. - ShareIntentModule (Kotlin): liest den Intent und kopiert die Inhalte SOFORT in den App-Cache — die Leseerlaubnis fuer content://-URIs gilt nur fuer diesen Intent und waere nach der Projektauswahl womoeglich erloschen. Liefert echte Dateipfade, dadurch laeuft Teilen durch exakt dieselbe Upload-Strecke wie die Bueroklammer (RNFS -> Base64 -> Anhang im Chat). - MainActivity.onNewIntent: setIntent + Event an JS (best effort). Zweitweg in JS ueber AppState 'active' -> getSharedData(), falls das Event je nach RN-Architektur nicht durchkommt; getSharedData markiert den Intent als verbraucht, also kein doppeltes Aufpoppen. - ShareModal: Vorschau der geteilten Inhalte, Projektliste (inkl. Hauptchat), optionales Textfeld. Senden erst nach Zielwahl moeglich. - sendPendingAttachments nimmt die Anhaenge jetzt optional als Parameter — beim Teilen waere der State zum Aufrufzeitpunkt noch leer. tsc --noEmit: 0 Fehler. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
184 lines
6.7 KiB
TypeScript
184 lines
6.7 KiB
TypeScript
/**
|
|
* Teilen-Dialog — was aus anderen Apps per "Teilen → ARIA" hereinkommt.
|
|
*
|
|
* Ablauf wie gewuenscht: erst sehen WAS geteilt wurde, dann WOHIN (Projekt
|
|
* oder Hauptchat), dann optional ein Text dazu. Ohne Text geht genauso.
|
|
*/
|
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
import {
|
|
ActivityIndicator, FlatList, Image, Modal, StyleSheet, Text,
|
|
TextInput, TouchableOpacity, View,
|
|
} from 'react-native';
|
|
import brainApi, { Project } from '../services/brainApi';
|
|
|
|
export interface SharedItem {
|
|
path: string;
|
|
name: string;
|
|
type: string;
|
|
size: number;
|
|
}
|
|
|
|
interface Props {
|
|
visible: boolean;
|
|
items: SharedItem[];
|
|
/** Text, den die sendende App mitgegeben hat (z.B. geteilter Link). */
|
|
initialText?: string;
|
|
onCancel: () => void;
|
|
/** projectId: '' = Hauptchat. text: optional, darf leer sein. */
|
|
onSend: (projectId: string, text: string) => void;
|
|
}
|
|
|
|
const fmtSize = (n: number) =>
|
|
n > 1024 * 1024 ? `${(n / 1024 / 1024).toFixed(1)} MB`
|
|
: n > 1024 ? `${Math.round(n / 1024)} KB` : `${n} B`;
|
|
|
|
const ShareModal: React.FC<Props> = ({ visible, items, initialText, onCancel, onSend }) => {
|
|
const [projects, setProjects] = useState<Project[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [pid, setPid] = useState<string | null>(null); // null = noch nichts gewaehlt
|
|
const [text, setText] = useState('');
|
|
const [sending, setSending] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!visible) return;
|
|
setPid(null);
|
|
setText(initialText || '');
|
|
setSending(false);
|
|
setLoading(true);
|
|
brainApi.listProjects(false)
|
|
.then(ps => setProjects(ps || []))
|
|
.catch(() => setProjects([]))
|
|
.finally(() => setLoading(false));
|
|
}, [visible, initialText]);
|
|
|
|
const send = useCallback(() => {
|
|
if (pid === null || sending) return;
|
|
setSending(true);
|
|
onSend(pid, text.trim());
|
|
}, [pid, text, sending, onSend]);
|
|
|
|
const preview = items.find(i => i.type?.startsWith('image/'));
|
|
|
|
return (
|
|
<Modal visible={visible} animationType="slide" transparent onRequestClose={onCancel}>
|
|
<View style={s.backdrop}>
|
|
<View style={s.box}>
|
|
<Text style={s.title}>Mit ARIA teilen</Text>
|
|
|
|
{/* Was wird geteilt */}
|
|
<View style={s.itemsBox}>
|
|
{preview ? (
|
|
<Image source={{ uri: 'file://' + preview.path }} style={s.thumb} resizeMode="cover" />
|
|
) : null}
|
|
<View style={{ flex: 1 }}>
|
|
{items.length === 0 ? (
|
|
<Text style={s.dim}>Nur Text</Text>
|
|
) : items.slice(0, 3).map((it, i) => (
|
|
<Text key={i} style={s.itemLine} numberOfLines={1}>
|
|
{it.name} <Text style={s.dim}>({fmtSize(it.size)})</Text>
|
|
</Text>
|
|
))}
|
|
{items.length > 3 ? (
|
|
<Text style={s.dim}>+ {items.length - 3} weitere</Text>
|
|
) : null}
|
|
</View>
|
|
</View>
|
|
|
|
{/* Wohin */}
|
|
<Text style={s.label}>Wohin soll das?</Text>
|
|
{loading ? (
|
|
<ActivityIndicator color="#FFD60A" style={{ marginVertical: 12 }} />
|
|
) : (
|
|
<FlatList
|
|
style={s.list}
|
|
data={[{ id: '', name: 'Hauptchat', description: '' } as Project, ...projects]}
|
|
keyExtractor={p => p.id || '__main__'}
|
|
keyboardShouldPersistTaps="handled"
|
|
renderItem={({ item }) => {
|
|
const sel = pid === item.id;
|
|
return (
|
|
<TouchableOpacity
|
|
style={[s.row, sel && s.rowSel]}
|
|
onPress={() => setPid(item.id)}>
|
|
<Text style={[s.rowText, sel && s.rowTextSel]} numberOfLines={1}>
|
|
{item.id ? '📁 ' : '💬 '}{item.name}
|
|
</Text>
|
|
{sel ? <Text style={s.check}>✓</Text> : null}
|
|
</TouchableOpacity>
|
|
);
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* Optionaler Text */}
|
|
<Text style={s.label}>Text dazu <Text style={s.dim}>(optional)</Text></Text>
|
|
<TextInput
|
|
style={s.input}
|
|
value={text}
|
|
onChangeText={setText}
|
|
placeholder="z.B. „Schau dir das mal an“ — kann auch leer bleiben"
|
|
placeholderTextColor="#666680"
|
|
multiline
|
|
/>
|
|
|
|
<View style={s.actions}>
|
|
<TouchableOpacity style={s.btnGhost} onPress={onCancel} disabled={sending}>
|
|
<Text style={s.btnGhostText}>Abbrechen</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
style={[s.btn, (pid === null || sending) && s.btnDisabled]}
|
|
onPress={send}
|
|
disabled={pid === null || sending}>
|
|
<Text style={s.btnText}>
|
|
{sending ? 'Senden …' : pid === null ? 'Ziel wählen' : 'Senden'}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
const s = StyleSheet.create({
|
|
backdrop: { flex: 1, backgroundColor: 'rgba(0,0,0,0.75)', justifyContent: 'flex-end' },
|
|
box: {
|
|
backgroundColor: '#0D0D1A', borderTopLeftRadius: 16, borderTopRightRadius: 16,
|
|
padding: 18, maxHeight: '88%',
|
|
},
|
|
title: { color: '#E0E0F0', fontSize: 19, fontWeight: '700', marginBottom: 12 },
|
|
itemsBox: {
|
|
flexDirection: 'row', gap: 10, alignItems: 'center',
|
|
backgroundColor: '#12121E', borderRadius: 10, padding: 10, marginBottom: 14,
|
|
},
|
|
thumb: { width: 56, height: 56, borderRadius: 8, backgroundColor: '#1E1E2E' },
|
|
itemLine: { color: '#E0E0F0', fontSize: 13 },
|
|
dim: { color: '#8888AA', fontSize: 12 },
|
|
label: { color: '#8888AA', fontSize: 12, marginBottom: 6 },
|
|
list: { maxHeight: 190, marginBottom: 14 },
|
|
row: {
|
|
flexDirection: 'row', alignItems: 'center', paddingVertical: 10,
|
|
paddingHorizontal: 12, borderRadius: 8, marginBottom: 4, backgroundColor: '#12121E',
|
|
},
|
|
rowSel: { backgroundColor: '#1E2A1E', borderWidth: 1, borderColor: '#3FFF9F' },
|
|
rowText: { color: '#E0E0F0', fontSize: 14, flex: 1 },
|
|
rowTextSel: { color: '#3FFF9F', fontWeight: '600' },
|
|
check: { color: '#3FFF9F', fontSize: 16 },
|
|
input: {
|
|
backgroundColor: '#12121E', borderRadius: 8, color: '#E0E0F0',
|
|
paddingHorizontal: 12, paddingVertical: 10, minHeight: 56,
|
|
textAlignVertical: 'top', marginBottom: 16, fontSize: 14,
|
|
},
|
|
actions: { flexDirection: 'row', gap: 10, justifyContent: 'flex-end' },
|
|
btnGhost: { paddingVertical: 11, paddingHorizontal: 18 },
|
|
btnGhostText: { color: '#8888AA', fontSize: 15 },
|
|
btn: {
|
|
backgroundColor: '#FFD60A', borderRadius: 8,
|
|
paddingVertical: 11, paddingHorizontal: 24,
|
|
},
|
|
btnDisabled: { backgroundColor: '#3A3A2A' },
|
|
btnText: { color: '#0D0D1A', fontSize: 15, fontWeight: '700' },
|
|
});
|
|
|
|
export default ShareModal;
|