feat(app): Teilen-Ziel — Bilder/Videos/Dateien aus anderen Apps an ARIA
'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>
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user