chahinebrini 7ad523f8ba feat(rebreak-native): phase 2 sheet standardisation — SheetFieldStack + FormSheet migrations
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>
2026-05-12 21:37:46 +02:00

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',
},
});
}