PostCommentsSheet: - Fix Resize-Bug: PanResponder nur auf Grabber+Header, kein onStartShouldSetPanResponderCapture (das stahl Touch-Events von der FlatList und brach Drag-Resize) - Height-Limits (MAX/MIN/INITIAL) als Refs in PanResponder-Closure, damit sie nicht auf den ersten-Render-Stand eingefroren werden - Keyboard-Show/-Hide animiert currentHeight korrekt ohne den Resize-Referenzpunkt zu verlieren - Avatar in CommentRow: resolveAvatar() wenn authorAvatar vorhanden, Initialen-Fallback sonst. Bereit sobald Backend authorAvatar in Comments-Response mitliefert. - Alle Pressable durch TouchableOpacity ersetzt SheetFieldStack (neu): - Progressives Multi-Input-Pattern als FormSheet-Inhalt - Ausgefüllte Felder werden als antippbare Chips (mit Stift-Icon) nach oben verschoben - Aktives Feld: TextInput + →/✓-Button (letztes Feld = Checkmark) - Validate + Normalize pro Feld, Fehleranzeige unter dem Input - suffix-Slot für Eye-Toggle etc. - Nach letztem Feld: Keyboard.dismiss() + children (Rest des Formulars) erscheint Migriert auf FormSheet + SheetFieldStack: - ConnectMailSheet: Grid-View unveraendert; Form-View (email+password) via SheetFieldStack; Zurück/Abbrechen-Header-Buttons entfernt (Schliessen = Swipe/Backdrop) - EditMailAccountSheet: single-password-field via SheetFieldStack; Cancel-Header-Button weg - AddDomainSheet: domain-field via SheetFieldStack; Favicon-Preview+Warning+Checkbox+Button als children; Cancel-Header-Button weg - CreateRoomSheet: name+description via SheetFieldStack; Public-Toggle+JoinMode+Buttons als children; Abbrechen-Button bleibt (kein Header-Button, design-OK) useSheetKeyboardLift: geloescht (keine Aufrufer mehr nach Migration) KeyboardAwareSheet bleibt (AddMacSheet + AddWindowsSheet nutzen es noch) tsc --noEmit: gruen Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
265 lines
7.1 KiB
TypeScript
265 lines
7.1 KiB
TypeScript
import { useState } from 'react';
|
|
import {
|
|
ActivityIndicator,
|
|
StyleSheet,
|
|
Text,
|
|
TouchableOpacity,
|
|
View,
|
|
} from 'react-native';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { apiFetch } from '../../lib/api';
|
|
import { useColors } from '../../lib/theme';
|
|
import { FormSheet } from '../FormSheet';
|
|
import { SheetFieldStack } from '../SheetFieldStack';
|
|
|
|
type Props = {
|
|
visible: boolean;
|
|
onClose: () => void;
|
|
onCreated: (room: any) => void;
|
|
};
|
|
|
|
export function CreateRoomSheet({ visible, onClose, onCreated }: Props) {
|
|
const { t } = useTranslation();
|
|
const colors = useColors();
|
|
const styles = makeStyles(colors);
|
|
const [name, setName] = useState('');
|
|
const [description, setDescription] = useState('');
|
|
const [isPublic, setIsPublic] = useState(true);
|
|
const [joinMode, setJoinMode] = useState<'approval' | 'invite_only'>('approval');
|
|
const [creating, setCreating] = useState(false);
|
|
const [fieldsDone, setFieldsDone] = useState(false);
|
|
|
|
function reset() {
|
|
setName('');
|
|
setDescription('');
|
|
setIsPublic(true);
|
|
setJoinMode('approval');
|
|
setFieldsDone(false);
|
|
}
|
|
|
|
function handleClose() {
|
|
reset();
|
|
onClose();
|
|
}
|
|
|
|
async function create() {
|
|
const trimmed = name.trim();
|
|
if (!trimmed || creating) return;
|
|
setCreating(true);
|
|
try {
|
|
const room = await apiFetch<any>('/api/chat/rooms', {
|
|
method: 'POST',
|
|
body: {
|
|
name: trimmed,
|
|
description: description.trim() || undefined,
|
|
isPublic,
|
|
joinMode: isPublic ? 'open' : joinMode,
|
|
},
|
|
});
|
|
onCreated(room);
|
|
reset();
|
|
onClose();
|
|
} catch {
|
|
// ignore — Server-Error wird in einem späteren Pass mit Feedback versehen
|
|
} finally {
|
|
setCreating(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<FormSheet
|
|
visible={visible}
|
|
onClose={handleClose}
|
|
title={t('chat.create_group')}
|
|
initialHeightPct={0.75}
|
|
topRadius={22}
|
|
growWithKeyboard
|
|
>
|
|
<SheetFieldStack
|
|
fields={[
|
|
{
|
|
key: 'name',
|
|
label: t('chat.room_name'),
|
|
placeholder: t('chat.room_name'),
|
|
value: name,
|
|
onChangeText: setName,
|
|
autoCapitalize: 'sentences',
|
|
autoCorrect: false,
|
|
validate: (v) => (v.trim().length === 0 ? ' ' : undefined),
|
|
},
|
|
{
|
|
key: 'description',
|
|
label: t('chat.room_description'),
|
|
placeholder: t('chat.room_description'),
|
|
value: description,
|
|
onChangeText: setDescription,
|
|
autoCapitalize: 'sentences',
|
|
},
|
|
]}
|
|
onComplete={() => setFieldsDone(true)}
|
|
>
|
|
{/* Public-Toggle */}
|
|
<TouchableOpacity
|
|
activeOpacity={0.7}
|
|
style={styles.toggleRow}
|
|
onPress={() => setIsPublic((v) => !v)}
|
|
>
|
|
<Text style={styles.toggleLabel}>{t('chat.public_room')}</Text>
|
|
<View style={[styles.toggle, isPublic && styles.toggleOn]}>
|
|
<View style={[styles.toggleKnob, isPublic && styles.toggleKnobOn]} />
|
|
</View>
|
|
</TouchableOpacity>
|
|
|
|
{/* Join-Mode (nur bei privaten Gruppen) */}
|
|
{!isPublic && (
|
|
<View style={{ marginTop: 8, marginBottom: 4 }}>
|
|
<Text style={styles.subLabel}>{t('chat.join_mode')}</Text>
|
|
<View style={styles.modeRow}>
|
|
{(['approval', 'invite_only'] as const).map((mode) => (
|
|
<TouchableOpacity
|
|
key={mode}
|
|
activeOpacity={0.7}
|
|
style={[styles.modeBtn, joinMode === mode && styles.modeBtnActive]}
|
|
onPress={() => setJoinMode(mode)}
|
|
>
|
|
<Text
|
|
style={[
|
|
styles.modeBtnText,
|
|
joinMode === mode && styles.modeBtnTextActive,
|
|
]}
|
|
>
|
|
{t(`chat.join_mode_${mode === 'approval' ? 'approval' : 'invite'}`)}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{/* Action-Buttons — Abbrechen bleibt (kein Header-Button) */}
|
|
<View style={styles.actions}>
|
|
<TouchableOpacity activeOpacity={0.7} onPress={handleClose} style={styles.cancelBtn}>
|
|
<Text style={styles.cancelText}>{t('common.cancel')}</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
activeOpacity={0.7}
|
|
onPress={create}
|
|
disabled={!name.trim() || creating}
|
|
style={[styles.createBtn, { opacity: !name.trim() || creating ? 0.5 : 1 }]}
|
|
>
|
|
{creating ? (
|
|
<ActivityIndicator size="small" color="#fff" />
|
|
) : (
|
|
<Text style={styles.createText}>{t('chat.create')}</Text>
|
|
)}
|
|
</TouchableOpacity>
|
|
</View>
|
|
</SheetFieldStack>
|
|
</FormSheet>
|
|
);
|
|
}
|
|
|
|
function makeStyles(colors: ReturnType<typeof useColors>) {
|
|
return StyleSheet.create({
|
|
toggleRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
paddingVertical: 8,
|
|
marginTop: 4,
|
|
marginBottom: 4,
|
|
},
|
|
toggleLabel: {
|
|
fontSize: 14,
|
|
fontFamily: 'Nunito_600SemiBold',
|
|
color: colors.text,
|
|
},
|
|
toggle: {
|
|
width: 46,
|
|
height: 28,
|
|
borderRadius: 14,
|
|
backgroundColor: colors.surfaceElevated,
|
|
padding: 2,
|
|
justifyContent: 'center',
|
|
},
|
|
toggleOn: {
|
|
backgroundColor: '#007AFF',
|
|
},
|
|
toggleKnob: {
|
|
width: 24,
|
|
height: 24,
|
|
borderRadius: 12,
|
|
backgroundColor: colors.bg,
|
|
shadowColor: '#000',
|
|
shadowOpacity: 0.15,
|
|
shadowRadius: 2,
|
|
shadowOffset: { width: 0, height: 1 },
|
|
elevation: 2,
|
|
},
|
|
toggleKnobOn: {
|
|
transform: [{ translateX: 18 }],
|
|
},
|
|
subLabel: {
|
|
fontSize: 12,
|
|
fontFamily: 'Nunito_600SemiBold',
|
|
color: colors.textMuted,
|
|
marginBottom: 6,
|
|
},
|
|
modeRow: {
|
|
flexDirection: 'row',
|
|
},
|
|
modeBtn: {
|
|
flex: 1,
|
|
paddingVertical: 8,
|
|
borderRadius: 10,
|
|
borderWidth: 1,
|
|
borderColor: colors.border,
|
|
alignItems: 'center',
|
|
marginRight: 6,
|
|
},
|
|
modeBtnActive: {
|
|
backgroundColor: colors.surface,
|
|
borderColor: '#007AFF',
|
|
},
|
|
modeBtnText: {
|
|
fontSize: 12,
|
|
fontFamily: 'Nunito_600SemiBold',
|
|
color: colors.textMuted,
|
|
},
|
|
modeBtnTextActive: {
|
|
color: '#007AFF',
|
|
},
|
|
actions: {
|
|
flexDirection: 'row',
|
|
marginTop: 12,
|
|
marginBottom: 10,
|
|
},
|
|
cancelBtn: {
|
|
flex: 1,
|
|
backgroundColor: colors.surfaceElevated,
|
|
paddingVertical: 12,
|
|
borderRadius: 12,
|
|
alignItems: 'center',
|
|
marginRight: 6,
|
|
},
|
|
cancelText: {
|
|
fontSize: 14,
|
|
fontFamily: 'Nunito_600SemiBold',
|
|
color: colors.text,
|
|
},
|
|
createBtn: {
|
|
flex: 1,
|
|
backgroundColor: '#007AFF',
|
|
paddingVertical: 12,
|
|
borderRadius: 12,
|
|
alignItems: 'center',
|
|
marginLeft: 6,
|
|
},
|
|
createText: {
|
|
fontSize: 14,
|
|
fontFamily: 'Nunito_700Bold',
|
|
color: '#fff',
|
|
},
|
|
});
|
|
}
|