fix: Arabic STT + DM scroll + info sheet FormSheet

STT (Arabic):
- Deepgram hat nova-2-general ar/tr-Support eingestellt (400 Bad Request)
- Fix: einheitlich nova-3 für alle Sprachen inkl. ar/tr
- Stale Kommentar aus 2026-05-30 entfernt

DM scroll-to-bottom:
- onLayout auf FlatList hinzugefügt → zusätzlicher scrollToEnd nach
  initialem Layout-Pass (Android-specific race condition)
- onOpenImage im FlatList-Renderer auf Lightbox verdrahtet (war () => {})

Info-Sheet:
- Modal(pageSheet) → FormSheet mit initialHeightPct={0.85}
- Nutzt jetzt unsere eigene Sheet-Komponente konsistent

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-06-01 10:25:35 +02:00
parent 0533fcad71
commit 89391a807b
2 changed files with 89 additions and 116 deletions

View File

@ -28,6 +28,7 @@ import * as FileSystem from 'expo-file-system/legacy';
import { apiFetch } from '../lib/api'; import { apiFetch } from '../lib/api';
import { ChatBubble, type ChatMsg, type MessageReaction } from '../components/chat/ChatBubble'; import { ChatBubble, type ChatMsg, type MessageReaction } from '../components/chat/ChatBubble';
import { DmChatBackground } from '../components/chat/DmChatBackground'; import { DmChatBackground } from '../components/chat/DmChatBackground';
import { FormSheet } from '../components/FormSheet';
import { useDmRealtime } from '../hooks/useChatRealtime'; import { useDmRealtime } from '../hooks/useChatRealtime';
import { useColors } from '../lib/theme'; import { useColors } from '../lib/theme';
import { useThemeStore } from '../stores/theme'; import { useThemeStore } from '../stores/theme';
@ -532,7 +533,7 @@ export default function DmScreen() {
onLike={toggleLike} onLike={toggleLike}
onReact={toggleReaction} onReact={toggleReaction}
onDelete={deleteMessage} onDelete={deleteMessage}
onOpenImage={() => {}} onOpenImage={(url) => setLightboxUri(url)}
/> />
)} )}
keyExtractor={(m) => m.id} keyExtractor={(m) => m.id}
@ -545,6 +546,7 @@ export default function DmScreen() {
keyboardDismissMode="interactive" keyboardDismissMode="interactive"
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
onContentSizeChange={() => flatListRef.current?.scrollToEnd({ animated: false })} onContentSizeChange={() => flatListRef.current?.scrollToEnd({ animated: false })}
onLayout={() => flatListRef.current?.scrollToEnd({ animated: false })}
/> />
)} )}
</View> </View>
@ -706,103 +708,90 @@ function DmInfoSheet({
); );
return ( return (
<Modal visible={visible} animationType="slide" presentationStyle="pageSheet" onRequestClose={onClose}> <FormSheet
<View style={{ flex: 1, backgroundColor: colors.bg }}> visible={visible}
{/* Header */} onClose={onClose}
<View style={{ title={t('chat.info')}
flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, initialHeightPct={0.85}
paddingTop: 16, paddingBottom: 12, dismissOnBackdrop
borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: colors.border, >
}}> <ScrollView contentContainerStyle={{ paddingBottom: 40 }}>
<Text style={{ flex: 1, fontSize: 17, fontFamily: 'Nunito_700Bold', color: colors.text }}> {/* Partner-Karte */}
{t('chat.info')} <TouchableOpacity
</Text> activeOpacity={0.7}
<TouchableOpacity onPress={onClose} hitSlop={8} activeOpacity={0.7}> onPress={onViewProfile}
<Ionicons name="close" size={24} color={colors.text} /> style={{ flexDirection: 'row', alignItems: 'center', padding: 16, gap: 14 }}
</TouchableOpacity> >
</View> {partner?.avatar ? (
<Image
<ScrollView contentContainerStyle={{ paddingBottom: 40 }}> source={{ uri: partner.avatar }}
{/* Partner-Karte */} style={{ width: 56, height: 56, borderRadius: 28 }}
<TouchableOpacity contentFit="cover"
activeOpacity={0.7} cachePolicy="memory-disk"
onPress={onViewProfile} />
style={{
flexDirection: 'row', alignItems: 'center',
padding: 16, gap: 14,
}}
>
{partner?.avatar ? (
<Image
source={{ uri: partner.avatar }}
style={{ width: 56, height: 56, borderRadius: 28 }}
contentFit="cover"
cachePolicy="memory-disk"
/>
) : (
<View style={{
width: 56, height: 56, borderRadius: 28,
backgroundColor: colors.brandOrange + '30',
alignItems: 'center', justifyContent: 'center',
}}>
<Text style={{ fontSize: 20, fontFamily: 'Nunito_700Bold', color: colors.brandOrange }}>
{partner?.nickname?.[0]?.toUpperCase() ?? '?'}
</Text>
</View>
)}
<View style={{ flex: 1 }}>
<Text style={{ fontSize: 17, fontFamily: 'Nunito_700Bold', color: colors.text }}>
{partner?.nickname ?? '…'}
</Text>
<Text style={{ fontSize: 13, fontFamily: 'Nunito_400Regular', color: colors.textMuted, marginTop: 2 }}>
{t('dm.view_profile', { defaultValue: 'Profil anzeigen' })}
</Text>
</View>
<Ionicons name="chevron-forward" size={18} color={colors.textMuted} />
</TouchableOpacity>
<View style={{ height: StyleSheet.hairlineWidth, backgroundColor: colors.border, marginHorizontal: 16 }} />
{/* Geteilte Medien */}
<View style={{ paddingHorizontal: 16, paddingTop: 20, paddingBottom: 12 }}>
<Text style={{ fontSize: 15, fontFamily: 'Nunito_700Bold', color: colors.text }}>
{t('dm.shared_media', { defaultValue: 'Geteilte Medien' })}
{sharedMedia.length > 0 && (
<Text style={{ fontSize: 13, fontFamily: 'Nunito_400Regular', color: colors.textMuted }}>
{' '}{sharedMedia.length}
</Text>
)}
</Text>
</View>
{sharedMedia.length === 0 ? (
<View style={{ alignItems: 'center', paddingVertical: 32 }}>
<Ionicons name="images-outline" size={40} color={colors.textMuted} />
<Text style={{ marginTop: 10, fontSize: 13, fontFamily: 'Nunito_400Regular', color: colors.textMuted }}>
{t('dm.no_shared_media', { defaultValue: 'Keine geteilten Medien' })}
</Text>
</View>
) : ( ) : (
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: MEDIA_GAP, paddingHorizontal: MEDIA_GAP }}> <View style={{
{[...sharedMedia].reverse().map((m) => ( width: 56, height: 56, borderRadius: 28,
<TouchableOpacity backgroundColor: colors.brandOrange + '30',
key={m.id} alignItems: 'center', justifyContent: 'center',
activeOpacity={0.8} }}>
onPress={() => onImagePress(m.attachmentUrl!)} <Text style={{ fontSize: 20, fontFamily: 'Nunito_700Bold', color: colors.brandOrange }}>
> {partner?.nickname?.[0]?.toUpperCase() ?? '?'}
<Image </Text>
source={{ uri: m.attachmentUrl! }}
style={{ width: MEDIA_SIZE, height: MEDIA_SIZE }}
contentFit="cover"
cachePolicy="memory-disk"
/>
</TouchableOpacity>
))}
</View> </View>
)} )}
</ScrollView> <View style={{ flex: 1 }}>
</View> <Text style={{ fontSize: 17, fontFamily: 'Nunito_700Bold', color: colors.text }}>
</Modal> {partner?.nickname ?? '…'}
</Text>
<Text style={{ fontSize: 13, fontFamily: 'Nunito_400Regular', color: colors.textMuted, marginTop: 2 }}>
{t('dm.view_profile')}
</Text>
</View>
<Ionicons name="chevron-forward" size={18} color={colors.textMuted} />
</TouchableOpacity>
<View style={{ height: StyleSheet.hairlineWidth, backgroundColor: colors.border, marginHorizontal: 16 }} />
{/* Geteilte Medien */}
<View style={{ paddingHorizontal: 16, paddingTop: 20, paddingBottom: 12, flexDirection: 'row', alignItems: 'baseline', gap: 6 }}>
<Text style={{ fontSize: 15, fontFamily: 'Nunito_700Bold', color: colors.text }}>
{t('dm.shared_media')}
</Text>
{sharedMedia.length > 0 && (
<Text style={{ fontSize: 13, fontFamily: 'Nunito_400Regular', color: colors.textMuted }}>
{sharedMedia.length}
</Text>
)}
</View>
{sharedMedia.length === 0 ? (
<View style={{ alignItems: 'center', paddingVertical: 32 }}>
<Ionicons name="images-outline" size={40} color={colors.textMuted} />
<Text style={{ marginTop: 10, fontSize: 13, fontFamily: 'Nunito_400Regular', color: colors.textMuted }}>
{t('dm.no_shared_media')}
</Text>
</View>
) : (
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: MEDIA_GAP, paddingHorizontal: MEDIA_GAP }}>
{[...sharedMedia].reverse().map((m) => (
<TouchableOpacity
key={m.id}
activeOpacity={0.8}
onPress={() => onImagePress(m.attachmentUrl!)}
>
<Image
source={{ uri: m.attachmentUrl! }}
style={{ width: MEDIA_SIZE, height: MEDIA_SIZE }}
contentFit="cover"
cachePolicy="memory-disk"
/>
</TouchableOpacity>
))}
</View>
)}
</ScrollView>
</FormSheet>
); );
} }

View File

@ -84,34 +84,18 @@ export default defineEventHandler(async (event) => {
); );
// Deepgram language mapping. // Deepgram language mapping.
// Live-Diagnose (2026-05-30): nova-3 lehnt language=ar (und tr) mit // Stand 2026-06-01: nova-3 unterstützt alle Sprachen inkl. ar/tr.
// 400 "No such model/language/tier combination found" ab — entgegen // nova-2-general hat ar/tr-Support eingestellt ("No such model/language/tier
// der vorherigen Annahme. Fallback für ar/tr: nova-2-general // combination found") — daher einheitlich nova-3 für alle Sprachen.
// (multilingual auto-detect). Für alle anderen Sprachen bleibt nova-3
// (bessere Genauigkeit, diskrete language-codes).
const deepgramLang = const deepgramLang =
language && language &&
["de", "en", "tr", "ar", "fr", "es", "pt", "it"].includes(language) ["de", "en", "tr", "ar", "fr", "es", "pt", "it"].includes(language)
? language ? language
: "de"; : "de";
// nova-2-general unterstützt language=ar/tr (im Gegensatz zu nova-3). const deepgramUrl = `https://api.deepgram.com/v1/listen?language=${deepgramLang}&model=nova-3`;
// Ohne expliziten language-Param fällt nova-2 auf Auto-Detect zurück und
// misdetektiert arabisches Audio oft als Englisch (phonetisches Transcript
// wie "salam alaikum" statt "السلام عليكم") — Lyra antwortet dann nicht
// auf Arabisch. Mit language=ar wird der korrekte Acoustic-Model-Pfad
// verwendet und die Schrift bleibt arabisch.
const needsGeneralModel = ["ar", "tr"].includes(deepgramLang);
const deepgramUrl = needsGeneralModel
? `https://api.deepgram.com/v1/listen?language=${deepgramLang}&model=nova-2-general`
: `https://api.deepgram.com/v1/listen?language=${deepgramLang}&model=nova-3`;
console.log( console.log("[transcribe] language:", deepgramLang, "model: nova-3");
"[transcribe] language:",
deepgramLang,
"model:",
needsGeneralModel ? "nova-2-general" : "nova-3",
);
try { try {
const response = await fetch(deepgramUrl, { const response = await fetch(deepgramUrl, {