import React from "react"; import { View, FlatList, Pressable, Animated, PanResponder } from "react-native"; import { Text, TextInput } from "react-native-paper"; import { useFocusEffect } from "@react-navigation/native"; import MaterialIcons from "react-native-vector-icons/MaterialIcons"; import { WebView } from "react-native-webview"; import { useSnapshot } from "valtio"; import API from "../API.js"; import GlobalState from "../contexts/GlobalState.js"; import i18n from "../i18nMessages.js"; const PANEL_WIDTH = 260; const getYouTubeVideoIdFromText = (text = "") => { const matches = text.match(/https?:\/\/[^\s]+/gi) || []; for (const match of matches) { const rawUrl = match.replace(/[),.;!?]+$/, ""); try { const parsed = new URL(rawUrl); const host = parsed.hostname.replace(/^www\./, ""); if (host === "youtu.be") { const shortId = (parsed.pathname || "").replace("/", "").split("/")[0] || ""; if (shortId) return shortId; } if (host === "youtube.com" || host === "m.youtube.com") { const watchId = parsed.searchParams.get("v"); if (watchId) return watchId; const pathParts = (parsed.pathname || "").split("/").filter(Boolean); if (pathParts[0] === "shorts" || pathParts[0] === "embed") { if (pathParts[1]) return pathParts[1]; } } } catch (error) { continue; } } return ""; }; const CircleIconAction = ({ icon, onPress, color = "#6b7280", disabled = false }) => ( ({ width: 36, height: 36, borderRadius: 18, alignItems: "center", justifyContent: "center", backgroundColor: disabled ? "#f3f4f6" : (pressed ? "#e5e7eb" : "#f9fafb"), marginHorizontal: 2, transform: [{ scale: pressed ? 0.96 : 1 }], })} > ); const ChatMessage = ({ item, myProfileId, showOriginal, onPressIn, onPressOut }) => { const isMine = item?.senderProfileId === myProfileId; const isTranslated = !!(item?.textOriginal && item?.text && item.textOriginal !== item.text); const messageText = isTranslated && showOriginal ? item?.textOriginal : item?.text; const youtubeVideoId = getYouTubeVideoIdFromText(item?.textOriginal || "") || getYouTubeVideoIdFromText(item?.text || ""); const createdAt = item?.createdAt ? new Date(item.createdAt) : null; return ( {item?.senderName || "Anonymous"} {messageText || ""} {isTranslated ? ( * {i18n.t("message.aiTranslated")} ) : <>} {createdAt && !Number.isNaN(createdAt.getTime()) ? createdAt.toLocaleTimeString() : ""} {youtubeVideoId ? ( ) : <>} ); }; let GlobalChat = () => { const gState = useSnapshot(GlobalState); const myProfileId = gState?.me?._id || ""; const [messages, setMessages] = React.useState([]); const [activeUsers, setActiveUsers] = React.useState([]); const [text, setText] = React.useState(""); const [loading, setLoading] = React.useState(true); const [sending, setSending] = React.useState(false); const [pressedMessageId, setPressedMessageId] = React.useState(""); const [isOnlinePanelOpen, setIsOnlinePanelOpen] = React.useState(false); const [isAtBottom, setIsAtBottom] = React.useState(true); const panelX = React.useRef(new Animated.Value(-PANEL_WIDTH)).current; const panelOpacity = React.useRef(new Animated.Value(0)).current; const listRef = React.useRef(null); const hasDoneInitialScrollRef = React.useRef(false); const scrollToBottom = React.useCallback((animated = true) => { if (!listRef.current) return; listRef.current.scrollToEnd({ animated }); }, []); const runInitialScrollIfNeeded = React.useCallback(() => { if (!messages.length || hasDoneInitialScrollRef.current) return; hasDoneInitialScrollRef.current = true; requestAnimationFrame(() => scrollToBottom(false)); setTimeout(() => scrollToBottom(false), 180); setIsAtBottom(true); }, [messages.length, scrollToBottom]); const loadMessages = React.useCallback(async () => { const data = await API.getChatMessages(100); setMessages(Array.isArray(data) ? data : []); }, []); const refreshPresence = React.useCallback(async () => { const users = await API.pingChatPresence(); setActiveUsers(Array.isArray(users) ? users : []); }, []); React.useEffect(() => { let mounted = true; const bootstrap = async () => { setLoading(true); const [initialMessages, users] = await Promise.all([ API.getChatMessages(100), API.pingChatPresence(), ]); if (!mounted) return; setMessages(Array.isArray(initialMessages) ? initialMessages : []); setActiveUsers(Array.isArray(users) ? users : []); setLoading(false); }; bootstrap(); const refreshInterval = setInterval(() => { loadMessages(); refreshPresence(); }, 8000); const pingInterval = setInterval(() => { refreshPresence(); }, 25000); return () => { mounted = false; clearInterval(refreshInterval); clearInterval(pingInterval); }; }, [loadMessages, refreshPresence]); const sendMessage = async () => { const trimmedText = (text || "").trim(); if (!trimmedText || sending) return; setSending(true); const response = await API.sendChatMessage(trimmedText); if (response?.status === "ok" && response?.message) { setMessages((prev) => [...prev, response.message]); if (Array.isArray(response.activeUsers)) { setActiveUsers(response.activeUsers); } setText(""); } setSending(false); }; const openOnlinePanel = React.useCallback(() => { setIsOnlinePanelOpen(true); Animated.parallel([ Animated.timing(panelX, { toValue: 0, duration: 220, useNativeDriver: true, }), Animated.timing(panelOpacity, { toValue: 1, duration: 220, useNativeDriver: true, }), ]).start(); }, [panelOpacity, panelX]); const closeOnlinePanel = React.useCallback(() => { Animated.parallel([ Animated.timing(panelX, { toValue: -PANEL_WIDTH, duration: 220, useNativeDriver: true, }), Animated.timing(panelOpacity, { toValue: 0, duration: 220, useNativeDriver: true, }), ]).start(() => setIsOnlinePanelOpen(false)); }, [panelOpacity, panelX]); useFocusEffect( React.useCallback(() => () => { setIsOnlinePanelOpen(false); panelX.setValue(-PANEL_WIDTH); panelOpacity.setValue(0); }, [panelOpacity, panelX]) ); const panResponder = React.useMemo(() => PanResponder.create({ onMoveShouldSetPanResponder: (_, gesture) => { const isHorizontal = Math.abs(gesture.dx) > 14 && Math.abs(gesture.dy) < 16; if (!isHorizontal) return false; if (!isOnlinePanelOpen && gesture.dx > 0) return true; if (isOnlinePanelOpen && gesture.dx < 0) return true; return false; }, onPanResponderRelease: (_, gesture) => { if (!isOnlinePanelOpen && gesture.dx > 45) { openOnlinePanel(); return; } if (isOnlinePanelOpen && gesture.dx < -45) { closeOnlinePanel(); } }, }), [closeOnlinePanel, isOnlinePanelOpen, openOnlinePanel]); React.useEffect(() => { if (!messages.length) return; if (!hasDoneInitialScrollRef.current) { runInitialScrollIfNeeded(); return; } if (isAtBottom) { requestAnimationFrame(() => scrollToBottom(true)); setTimeout(() => scrollToBottom(true), 120); } }, [isAtBottom, messages.length, runInitialScrollIfNeeded, scrollToBottom]); const handleChatScroll = React.useCallback((event) => { const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent || {}; const yOffset = contentOffset?.y || 0; const contentHeight = contentSize?.height || 0; const viewportHeight = layoutMeasurement?.height || 0; const distanceFromBottom = contentHeight - (yOffset + viewportHeight); setIsAtBottom(distanceFromBottom < 50); }, []); return ( Beta {i18n.t("message.globalChat")} {i18n.t("message.onlineNow")} ({activeUsers.length}) {loading ? ( {i18n.t("message.loadingChat")} ) : ( item?._id?.toString?.() || `${index}-${item?.createdAt || "msg"}`} renderItem={({ item }) => { const messageId = item?._id?.toString?.() || ""; return ( setPressedMessageId(messageId)} onPressOut={() => setPressedMessageId("")} /> ); }} onScroll={handleChatScroll} onLayout={runInitialScrollIfNeeded} onContentSizeChange={runInitialScrollIfNeeded} scrollEventThrottle={16} contentContainerStyle={{ paddingBottom: 10 }} style={{ flex: 1 }} /> )} { }} /> { }} /> {isOnlinePanelOpen ? ( ) : <>} {i18n.t("message.onlineNow")} ({activeUsers.length}) item?.profileId || item?.displayName || `${index}`} renderItem={({ item }) => ( {item?.displayName || "Anonymous"} )} ListEmptyComponent={No users online} /> ); }; export default GlobalChat;