Fix YouTube embeds and improve global chat auto-scroll

This commit is contained in:
Adolfo Reyna
2026-02-24 00:28:54 -05:00
parent d5850bb91e
commit 1f38b43d64
2 changed files with 104 additions and 5 deletions

View File

@@ -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 }) => (
<Pressable
onPress={onPress}
@@ -33,6 +60,7 @@ 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 (
<View style={{ marginBottom: 12, alignItems: isMine ? "flex-end" : "flex-start" }}>
@@ -62,6 +90,30 @@ const ChatMessage = ({ item, myProfileId, showOriginal, onPressIn, onPressOut })
{createdAt && !Number.isNaN(createdAt.getTime()) ? createdAt.toLocaleTimeString() : ""}
</Text>
</Pressable>
{youtubeVideoId ? (
<View
style={{
marginTop: 6,
width: "85%",
minHeight: 210,
borderRadius: 12,
overflow: "hidden",
borderWidth: 1,
borderColor: "#e5e7eb",
}}
>
<WebView
style={{ flex: 1, minHeight: 210 }}
source={{
uri: `https://www.youtube.com/embed/${youtubeVideoId}?fs=0`,
headers: {
Referer: "https://social.emmint.com",
"Referrer-Policy": "strict-origin-when-cross-origin",
},
}}
/>
</View>
) : <></>}
</View>
);
};
@@ -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 (
<View style={{ flex: 1 }} {...panResponder.panHandlers}>
<View style={{ flex: 1, padding: 14 }}>
@@ -212,6 +301,7 @@ let GlobalChat = () => {
<Text>{i18n.t("message.loadingChat")}</Text>
) : (
<FlatList
ref={listRef}
data={messages}
keyExtractor={(item, index) => 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 }}
/>

View File

@@ -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 (
<WebView
style={styles.iframe}
source={{ uri: "https://www.youtube.com/embed/" + youtubeId + "?fs=0" }}
source={{
uri: "https://www.youtube.com/embed/" + youtubeId + "?fs=0",
headers: {
Referer: "https://social.emmint.com",
"Referrer-Policy": "strict-origin-when-cross-origin",
},
}}
/>
);
}