import React, { useEffect, useMemo, useRef, useState } from "react"; import { ActivityIndicator, FlatList, KeyboardAvoidingView, Platform, Pressable, RefreshControl, StyleSheet, Text, TextInput, View, } from "react-native"; import { Ionicons } from "@expo/vector-icons"; import { useAuth } from "@clerk/clerk-expo"; import { useChat } from "../../contexts/ChatContext"; import { useTheme } from "../../contexts/ThemeContext"; import { MinimalCard } from "../../components/MinimalCard"; import { SectionHeader } from "../../components/SectionHeader"; export default function ChatScreen() { const { userId } = useAuth(); const { colors, typography } = useTheme(); const { threads, activeThreadId, messagesByThreadId, readByThreadId, loadingThreads, loadingMessages, socketConnected, totalUnreadCount, typingByThreadId, setActiveThread, refreshMessages, refreshThreads, sendMessage, setTyping, loadOlderMessages, hasMoreMessages, isLoadingOlderMessages, } = useChat(); const [draft, setDraft] = useState(""); const typingTimeoutRef = useRef | null>(null); const activeMessages = useMemo( () => (activeThreadId ? (messagesByThreadId[activeThreadId] ?? []) : []), [activeThreadId, messagesByThreadId], ); const typingUsers = useMemo( () => (activeThreadId ? (typingByThreadId[activeThreadId] ?? []) : []), [activeThreadId, typingByThreadId], ); const activeThread = useMemo( () => threads.find((thread) => thread.id === activeThreadId) ?? null, [activeThreadId, threads], ); const orderedThreads = useMemo( () => [...threads].sort((a, b) => { const aTime = a.lastMessageAt ?? a.createdAt; const bTime = b.lastMessageAt ?? b.createdAt; return new Date(bTime).getTime() - new Date(aTime).getTime(); }), [threads], ); const handleSend = async () => { if (!activeThreadId) { return; } const body = draft.trim(); if (!body) { return; } setDraft(""); try { await sendMessage(activeThreadId, body); setTyping(activeThreadId, false); } catch { setDraft(body); } }; const handleDraftChange = (value: string) => { setDraft(value); if (!activeThreadId) { return; } setTyping(activeThreadId, value.trim().length > 0); if (typingTimeoutRef.current) { clearTimeout(typingTimeoutRef.current); } typingTimeoutRef.current = setTimeout(() => { if (activeThreadId) { setTyping(activeThreadId, false); } }, 1200); }; const handleLoadOlder = () => { if (!activeThreadId) { return; } void loadOlderMessages(activeThreadId); }; useEffect(() => { return () => { if (typingTimeoutRef.current) { clearTimeout(typingTimeoutRef.current); } if (activeThreadId) { setTyping(activeThreadId, false); } }; }, [activeThreadId, setTyping]); return ( Conversations {loadingThreads ? ( ) : ( item.id} showsVerticalScrollIndicator={false} contentContainerStyle={styles.threadListContent} renderItem={({ item }) => { const selected = item.id === activeThreadId; return ( setActiveThread(item.id)}> {item.type === "gym" ? "Gym" : "Direct"} {item.unreadCount > 0 && ( {item.unreadCount > 9 ? "9+" : item.unreadCount} )} {item.type === "gym" ? "Gym Room" : "Trainer Chat"} {item.lastMessageBody ?? "No messages yet"} ); }} /> )} {!activeThreadId ? ( Pick a chat Choose a conversation on the left to start messaging. ) : ( <> {activeThread?.type === "gym" ? "Gym Room" : "Trainer Chat"} {socketConnected ? "Live updates enabled" : "Offline sync active"} {loadingMessages ? ( ) : ( item.id} showsVerticalScrollIndicator={false} contentContainerStyle={styles.messagesContent} refreshControl={ activeThreadId ? void refreshMessages(activeThreadId) : void refreshThreads() } tintColor={colors.primary} /> } ListHeaderComponent={ activeThreadId && hasMoreMessages(activeThreadId) ? ( {isLoadingOlderMessages(activeThreadId) ? ( ) : ( Load earlier messages )} ) : null } renderItem={({ item }) => { const own = item.senderUserId === userId; const threadReads = activeThreadId ? (readByThreadId[activeThreadId] ?? {}) : {}; const seen = Object.entries(threadReads).some( ([readerUserId, messageId]) => readerUserId !== userId && messageId === item.id, ); return ( {item.body} {formatTime(item.createdAt)} {own && seen && ( Seen )} ); }} /> )} {typingUsers.length > 0 && ( {typingUsers.length === 1 ? "Someone is typing..." : `${typingUsers.length} people are typing...`} )} )} ); } function formatTime(isoDate: string): string { const date = new Date(isoDate); if (Number.isNaN(date.getTime())) { return ""; } return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", }); } const styles = StyleSheet.create({ container: { flex: 1, }, headerWrap: { paddingHorizontal: 16, paddingTop: 16, paddingBottom: 8, }, content: { flex: 1, flexDirection: "row", paddingHorizontal: 12, paddingBottom: 8, gap: 10, }, threadRail: { width: 148, }, railLabel: { marginBottom: 8, }, loaderWrap: { flex: 1, alignItems: "center", justifyContent: "center", }, threadListContent: { gap: 8, paddingBottom: 20, }, threadCard: { borderRadius: 16, }, threadCardTopRow: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginBottom: 8, }, threadTypeChip: { flexDirection: "row", alignItems: "center", gap: 4, }, threadTypeText: { fontSize: 11, fontWeight: "700", }, threadUnreadBadge: { minWidth: 18, height: 18, borderRadius: 9, alignItems: "center", justifyContent: "center", paddingHorizontal: 4, }, threadUnreadText: { color: "white", fontSize: 10, fontWeight: "700", }, threadTitle: { fontSize: 13, fontWeight: "700", }, threadPreview: { fontSize: 11, marginTop: 4, lineHeight: 15, }, chatArea: { flex: 1, }, emptyCard: { flex: 1, alignItems: "center", justifyContent: "center", marginBottom: 8, }, chatHeadCard: { borderRadius: 14, marginBottom: 8, }, messagesContent: { paddingHorizontal: 6, paddingBottom: 12, gap: 8, }, loadOlderCard: { alignSelf: "center", borderRadius: 12, marginBottom: 10, }, messageBubble: { borderWidth: 1, borderRadius: 16, paddingHorizontal: 12, paddingVertical: 9, maxWidth: "84%", }, metaRow: { marginTop: 4, flexDirection: "row", alignItems: "center", }, typingRow: { paddingHorizontal: 6, paddingBottom: 6, flexDirection: "row", alignItems: "center", gap: 4, }, composer: { borderTopWidth: 1, paddingTop: 8, paddingBottom: 10, flexDirection: "row", alignItems: "flex-end", gap: 8, }, input: { flex: 1, borderWidth: 1, borderRadius: 14, paddingHorizontal: 12, paddingVertical: 9, minHeight: 42, maxHeight: 110, textAlignVertical: "top", fontSize: 14, }, sendButton: { width: 40, height: 40, borderRadius: 20, alignItems: "center", justifyContent: "center", }, });