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";
import BibleEmbeddedView from "../components/BibleEmbeddedView.js";
import { stripBibleTokens } from "../utils/bibleReferences.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 messageTextRaw = isTranslated && showOriginal ? item?.textOriginal : item?.text;
const messageText = stripBibleTokens(messageTextRaw || "");
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 = ({ navigation }) => {
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 biblePickerSelectionTs = gState?.biblePickerSelection?.ts;
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]);
React.useEffect(() => {
const selection = gState?.biblePickerSelection;
if (!selection || selection.target !== "chat" || !selection.token) return;
setText((prev) => {
if ((prev || "").includes(selection.token)) return prev;
return `${(prev || "").trim()} ${selection.token}`.trim();
});
GlobalState.biblePickerSelection = null;
}, [biblePickerSelectionTs, gState?.biblePickerSelection]);
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 }}
/>
)}
navigation.navigate("BiblePicker", { target: "chat" })} />
{ }} />
{isOnlinePanelOpen ? (
) : <>>}
{i18n.t("message.onlineNow")} ({activeUsers.length})
item?.profileId || item?.displayName || `${index}`}
renderItem={({ item }) => (
{item?.displayName || "Anonymous"}
)}
ListEmptyComponent={No users online}
/>
);
};
export default GlobalChat;