Sheets via neuer KeyboardAwareSheet-Composable (in Modal pattern, auto-grow mit Tastatur, paddingBottom-Lift): EditMail, AddDomain, CreateRoom, ConnectMail. GameOverScreen behält Spring-Slide-In, nutzt RN Keyboard.addListener für Lift. - KeyboardAwareSheet.tsx — universal modal with sheet-grow + keyboard-padding - react-native-keyboard-controller installiert + KeyboardProvider in Root - Snake: time + ScoreProgressBar + useSnakeSounds (haptic, audio TODO) - Tetris: title weg, Buttons zentriert, kein Pressable mit style-fn - DPad-Buttons 60→48, more bg, no scale - useMe: pub-sub listener pattern für app-weite avatar/nickname-Updates - dm.tsx: resolveAvatar wrap (iron.png-Warning) - Mail-error-humanizer + locales Recovery-Doc-Update in docs/internal/RECOVERY_LOG_2026-05-10.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
273 lines
7.1 KiB
TypeScript
273 lines
7.1 KiB
TypeScript
import { useState } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
TextInput,
|
|
Pressable,
|
|
StyleSheet,
|
|
ActivityIndicator,
|
|
} from 'react-native';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { apiFetch } from '../../lib/api';
|
|
import { useColors } from '../../lib/theme';
|
|
import { KeyboardAwareSheet } from '../KeyboardAwareSheet';
|
|
|
|
const COLLAPSED_HEIGHT = 480;
|
|
|
|
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);
|
|
|
|
function reset() {
|
|
setName('');
|
|
setDescription('');
|
|
setIsPublic(true);
|
|
setJoinMode('approval');
|
|
}
|
|
|
|
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 (err: any) {
|
|
console.error('Room erstellen fehlgeschlagen:', err.message);
|
|
} finally {
|
|
setCreating(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<KeyboardAwareSheet
|
|
visible={visible}
|
|
onClose={handleClose}
|
|
collapsedHeight={COLLAPSED_HEIGHT}
|
|
pushChildrenToBottom={false}
|
|
topRadius={22}
|
|
>
|
|
<View style={{ flex: 1, paddingHorizontal: 18, paddingTop: 6 }}>
|
|
<Text style={styles.title}>{t('chat.create_group')}</Text>
|
|
|
|
<TextInput
|
|
value={name}
|
|
onChangeText={setName}
|
|
placeholder={t('chat.room_name')}
|
|
placeholderTextColor="#a3a3a3"
|
|
style={styles.input}
|
|
maxLength={60}
|
|
/>
|
|
<TextInput
|
|
value={description}
|
|
onChangeText={setDescription}
|
|
placeholder={t('chat.room_description')}
|
|
placeholderTextColor="#a3a3a3"
|
|
multiline
|
|
style={[styles.input, { height: 70, textAlignVertical: 'top' }]}
|
|
maxLength={250}
|
|
/>
|
|
|
|
{/* Public toggle */}
|
|
<Pressable 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>
|
|
</Pressable>
|
|
|
|
{/* Join mode (private only) */}
|
|
{!isPublic && (
|
|
<View style={{ marginTop: 8 }}>
|
|
<Text style={styles.subLabel}>{t('chat.join_mode')}</Text>
|
|
<View style={styles.modeRow}>
|
|
{(['approval', 'invite_only'] as const).map((mode) => (
|
|
<Pressable
|
|
key={mode}
|
|
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>
|
|
</Pressable>
|
|
))}
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
<View style={{ flex: 1 }} />
|
|
|
|
{/* Actions */}
|
|
<View style={styles.actions}>
|
|
<Pressable onPress={handleClose} style={styles.cancelBtn}>
|
|
<Text style={styles.cancelText}>{t('common.cancel')}</Text>
|
|
</Pressable>
|
|
<Pressable
|
|
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>
|
|
)}
|
|
</Pressable>
|
|
</View>
|
|
</View>
|
|
</KeyboardAwareSheet>
|
|
);
|
|
}
|
|
|
|
function makeStyles(colors: ReturnType<typeof useColors>) {
|
|
return StyleSheet.create({
|
|
title: {
|
|
fontSize: 17,
|
|
fontFamily: 'Nunito_700Bold',
|
|
color: colors.text,
|
|
marginBottom: 14,
|
|
},
|
|
input: {
|
|
backgroundColor: colors.surfaceElevated,
|
|
borderRadius: 12,
|
|
paddingHorizontal: 14,
|
|
paddingVertical: 12,
|
|
fontSize: 14,
|
|
fontFamily: 'Nunito_400Regular',
|
|
color: colors.text,
|
|
marginBottom: 10,
|
|
},
|
|
toggleRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
paddingVertical: 6,
|
|
marginTop: 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: 4,
|
|
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',
|
|
},
|
|
});
|
|
}
|