diff --git a/apps/mobile/components/IncomingCallModal.tsx b/apps/mobile/components/IncomingCallModal.tsx new file mode 100644 index 0000000..a87ec89 --- /dev/null +++ b/apps/mobile/components/IncomingCallModal.tsx @@ -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(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 ( + + + Eingehender Anruf + + {callerName} + {conversation?.type === 'group' && ( + in {conversationTitle} + )} + + { + void rejectIncoming(); + }} + > + Ablehnen + + { + void acceptIncoming(); + }} + > + Annehmen + + + + + ); +} + +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 }, +});