From 1f38b43d64ae8bdb7b5deab96d5cb5613ef86618 Mon Sep 17 00:00:00 2001 From: Adolfo Reyna Date: Tue, 24 Feb 2026 00:28:54 -0500 Subject: [PATCH] Fix YouTube embeds and improve global chat auto-scroll --- Views/GlobalChat.js | 94 +++++++++++++++++++++++++++++++++++++++++++++ components/Media.js | 15 +++++--- 2 files changed, 104 insertions(+), 5 deletions(-) diff --git a/Views/GlobalChat.js b/Views/GlobalChat.js index 16cc12b..de3bd6c 100644 --- a/Views/GlobalChat.js +++ b/Views/GlobalChat.js @@ -3,6 +3,7 @@ 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"; @@ -10,6 +11,32 @@ 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 }) => ( @@ -62,6 +90,30 @@ const ChatMessage = ({ item, myProfileId, showOriginal, onPressIn, onPressOut }) {createdAt && !Number.isNaN(createdAt.getTime()) ? createdAt.toLocaleTimeString() : ""} + {youtubeVideoId ? ( + + + + ) : <>} ); }; @@ -76,8 +128,24 @@ let GlobalChat = () => { 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); @@ -193,6 +261,27 @@ let GlobalChat = () => { }, }), [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 ( @@ -212,6 +301,7 @@ let GlobalChat = () => { {i18n.t("message.loadingChat")} ) : ( item?._id?.toString?.() || `${index}-${item?.createdAt || "msg"}`} renderItem={({ item }) => { @@ -226,6 +316,10 @@ let GlobalChat = () => { /> ); }} + onScroll={handleChatScroll} + onLayout={runInitialScrollIfNeeded} + onContentSizeChange={runInitialScrollIfNeeded} + scrollEventThrottle={16} contentContainerStyle={{ paddingBottom: 10 }} style={{ flex: 1 }} /> diff --git a/components/Media.js b/components/Media.js index eb41b7c..b31a06c 100644 --- a/components/Media.js +++ b/components/Media.js @@ -26,11 +26,10 @@ const videoIdF = (content) => { // Extract YouTube video ID from content string const youtubeIdF = (content) => { - let youtubeTag = content.match(/@youtube:[0-z]+/); + // Accept classic 11-char IDs and common ID-safe chars. + let youtubeTag = content.match(/@youtube:([A-Za-z0-9_-]+)/); if (!youtubeTag) return ''; - let tag = youtubeTag; - tag = tag[0].substring(1); - return tag.split(':')[1]; + return youtubeTag[1]; }; // Extract HLS URL from content string @@ -174,7 +173,13 @@ let Media = (props) => { return ( ); }