feat(mobile): IncomingCallModal with Annehmen/Ablehnen + name resolution

This commit is contained in:
byGalax
2026-05-14 06:11:45 +02:00
parent e83c23d698
commit 098e4f4c1d
@@ -0,0 +1,109 @@
import { chat } from '@chat-app/shared';
import type { ConversationSummary } from '@chat-app/shared/chat';
import { useEffect, useState } from 'react';
import { Modal, Pressable, StyleSheet, Text, View } from 'react-native';
import { Avatar } from './Avatar';
import { useCall } from '../lib/callContext';
import { supabase } from '../lib/supabase';
import { colors } from '../theme/colors';
// Full-screen modal that surfaces over any route when the CallContext
// reports an `incoming` state. Resolves the caller's display name from
// the conversation membership; falls back to "Anrufer" otherwise.
export function IncomingCallModal() {
const { state, acceptIncoming, rejectIncoming } = useCall();
const visible = state.kind === 'incoming';
const [conversation, setConversation] = useState<ConversationSummary | null>(null);
useEffect(() => {
if (state.kind !== 'incoming') {
setConversation(null);
return;
}
let cancelled = false;
void (async () => {
try {
const all = await chat.listConversations(supabase);
if (cancelled) return;
setConversation(all.find((c) => c.id === state.conversationId) ?? null);
} catch {
/* swallow */
}
})();
return () => {
cancelled = true;
};
}, [state]);
if (state.kind !== 'incoming') return null;
const callerName = (() => {
if (!conversation) return 'Anrufer';
const m = conversation.members.find((mm) => mm.userId === state.fromUserId);
return m?.profile?.displayName ?? m?.profile?.username ?? 'Anrufer';
})();
const conversationTitle =
conversation?.type === 'group' ? (conversation.name ?? 'Gruppe') : callerName;
return (
<Modal visible={visible} transparent animationType="fade" onRequestClose={rejectIncoming}>
<View style={styles.container}>
<Text style={styles.subtitle}>Eingehender Anruf</Text>
<Avatar name={callerName} size={96} />
<Text style={styles.title}>{callerName}</Text>
{conversation?.type === 'group' && (
<Text style={styles.subtitle}>in {conversationTitle}</Text>
)}
<View style={styles.actions}>
<Pressable
style={[styles.button, styles.buttonReject]}
onPress={() => {
void rejectIncoming();
}}
>
<Text style={styles.buttonText}>Ablehnen</Text>
</Pressable>
<Pressable
style={[styles.button, styles.buttonAccept]}
onPress={() => {
void acceptIncoming();
}}
>
<Text style={styles.buttonText}>Annehmen</Text>
</Pressable>
</View>
</View>
</Modal>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.bg,
alignItems: 'center',
justifyContent: 'center',
gap: 20,
paddingHorizontal: 32,
},
title: { color: colors.text, fontSize: 28, fontWeight: '700' },
subtitle: { color: colors.textMuted, fontSize: 14 },
actions: {
flexDirection: 'row',
gap: 24,
marginTop: 32,
},
button: {
paddingHorizontal: 28,
paddingVertical: 16,
borderRadius: 14,
minWidth: 140,
alignItems: 'center',
},
buttonAccept: { backgroundColor: colors.success },
buttonReject: { backgroundColor: colors.danger },
buttonText: { color: colors.text, fontWeight: '700', fontSize: 16 },
});