Revamp global chat header, online panel, and composer UI

This commit is contained in:
Adolfo Reyna
2026-02-24 00:07:36 -05:00
parent 24515caf70
commit d5850bb91e

View File

@@ -1,11 +1,34 @@
import React from "react";
import { View, FlatList, Pressable } from "react-native";
import { Button, Chip, Text, TextInput } from "react-native-paper";
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 { useSnapshot } from "valtio";
import API from "../API.js";
import GlobalState from "../contexts/GlobalState.js";
import i18n from "../i18nMessages.js";
const PANEL_WIDTH = 260;
const CircleIconAction = ({ icon, onPress, color = "#6b7280", disabled = false }) => (
<Pressable
onPress={onPress}
disabled={disabled}
style={({ pressed }) => ({
width: 36,
height: 36,
borderRadius: 18,
alignItems: "center",
justifyContent: "center",
backgroundColor: disabled ? "#f3f4f6" : (pressed ? "#e5e7eb" : "#f9fafb"),
marginHorizontal: 2,
transform: [{ scale: pressed ? 0.96 : 1 }],
})}
>
<MaterialIcons name={icon} size={20} color={disabled ? "#9ca3af" : color} />
</Pressable>
);
const ChatMessage = ({ item, myProfileId, showOriginal, onPressIn, onPressOut }) => {
const isMine = item?.senderProfileId === myProfileId;
const isTranslated = !!(item?.textOriginal && item?.text && item.textOriginal !== item.text);
@@ -52,6 +75,9 @@ let GlobalChat = () => {
const [loading, setLoading] = React.useState(true);
const [sending, setSending] = React.useState(false);
const [pressedMessageId, setPressedMessageId] = React.useState("");
const [isOnlinePanelOpen, setIsOnlinePanelOpen] = React.useState(false);
const panelX = React.useRef(new Animated.Value(-PANEL_WIDTH)).current;
const panelOpacity = React.useRef(new Animated.Value(0)).current;
const loadMessages = React.useCallback(async () => {
const data = await API.getChatMessages(100);
@@ -109,60 +135,189 @@ let GlobalChat = () => {
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]);
return (
<View style={{ flex: 1, padding: 14 }}>
<Text style={{ fontSize: 22, fontWeight: "700", marginBottom: 10 }}>{i18n.t("message.globalChat")}</Text>
<Text style={{ color: "#9a3412", marginBottom: 8 }}>* {i18n.t("message.chatRoomBeta")}</Text>
<Text style={{ color: "#6b7280", marginBottom: 8 }}>{i18n.t("message.onlineNow")} ({activeUsers.length})</Text>
<View style={{ flexDirection: "row", flexWrap: "wrap", marginBottom: 12 }}>
{activeUsers.slice(0, 20).map((user) => (
<Chip key={user.profileId} style={{ marginRight: 8, marginBottom: 8 }}>
{user?.displayName || "Anonymous"}
</Chip>
))}
</View>
<View style={{ flex: 1 }} {...panResponder.panHandlers}>
<View style={{ flex: 1, padding: 14 }}>
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginBottom: 12 }}>
<View style={{ flexDirection: "row", alignItems: "center" }}>
<Text style={{ color: "#dc2626", fontSize: 12, fontWeight: "700", marginRight: 6 }}>Beta</Text>
<Text style={{ fontSize: 22, fontWeight: "700" }}>{i18n.t("message.globalChat")}</Text>
</View>
<Pressable onPress={openOnlinePanel} hitSlop={8}>
<Text style={{ color: "#6b7280", fontSize: 14 }}>
{i18n.t("message.onlineNow")} ({activeUsers.length})
</Text>
</Pressable>
</View>
{loading ? (
<Text>{i18n.t("message.loadingChat")}</Text>
) : (
<FlatList
data={messages}
keyExtractor={(item, index) => item?._id?.toString?.() || `${index}-${item?.createdAt || "msg"}`}
renderItem={({ item }) => {
const messageId = item?._id?.toString?.() || "";
return (
<ChatMessage
item={item}
myProfileId={myProfileId}
showOriginal={pressedMessageId === messageId}
onPressIn={() => setPressedMessageId(messageId)}
onPressOut={() => setPressedMessageId("")}
/>
);
{loading ? (
<Text>{i18n.t("message.loadingChat")}</Text>
) : (
<FlatList
data={messages}
keyExtractor={(item, index) => item?._id?.toString?.() || `${index}-${item?.createdAt || "msg"}`}
renderItem={({ item }) => {
const messageId = item?._id?.toString?.() || "";
return (
<ChatMessage
item={item}
myProfileId={myProfileId}
showOriginal={pressedMessageId === messageId}
onPressIn={() => setPressedMessageId(messageId)}
onPressOut={() => setPressedMessageId("")}
/>
);
}}
contentContainerStyle={{ paddingBottom: 10 }}
style={{ flex: 1 }}
/>
)}
<View
style={{
marginTop: 10,
borderWidth: 1,
borderColor: "#e5e7eb",
borderRadius: 28,
backgroundColor: "#ffffff",
paddingHorizontal: 4,
paddingVertical: 2,
flexDirection: "row",
alignItems: "flex-end",
shadowColor: "#111827",
shadowOpacity: 0.08,
shadowRadius: 10,
shadowOffset: { width: 0, height: 3 },
elevation: 2,
}}
contentContainerStyle={{ paddingBottom: 10 }}
style={{ flex: 1 }}
/>
)}
<View style={{ marginTop: 10 }}>
<TextInput
mode="outlined"
placeholder={i18n.t("message.writeMessage")}
value={text}
onChangeText={setText}
multiline
maxLength={500}
/>
<Button
mode="contained"
onPress={sendMessage}
disabled={sending || !(text || "").trim()}
style={{ marginTop: 10 }}
>
{i18n.t("message.send")}
</Button>
<CircleIconAction icon="add" onPress={() => { }} />
<View style={{ flex: 1, paddingRight: 2 }}>
<TextInput
mode="flat"
placeholder={i18n.t("message.writeMessage")}
value={text}
onChangeText={setText}
multiline
maxLength={500}
dense
underlineColor="transparent"
activeUnderlineColor="transparent"
style={{ backgroundColor: "transparent", maxHeight: 120 }}
contentStyle={{ paddingVertical: 10 }}
/>
</View>
<CircleIconAction icon="photo-camera" onPress={() => { }} />
<CircleIconAction
icon="send"
onPress={sendMessage}
disabled={sending || !(text || "").trim()}
color="#0d6efd"
/>
</View>
</View>
{isOnlinePanelOpen ? (
<Pressable
onPress={closeOnlinePanel}
style={{
position: "absolute",
top: 0,
right: 0,
bottom: 0,
left: 0,
backgroundColor: "rgba(17, 24, 39, 0.2)",
}}
/>
) : <></>}
<Animated.View
style={{
position: "absolute",
top: 0,
left: 0,
bottom: 0,
width: PANEL_WIDTH,
backgroundColor: "#ffffff",
borderRightWidth: 1,
borderRightColor: "#e5e7eb",
paddingHorizontal: 12,
paddingTop: 18,
opacity: panelOpacity,
transform: [{ translateX: panelX }],
}}
>
<Text style={{ fontSize: 16, fontWeight: "700", marginBottom: 12 }}>
{i18n.t("message.onlineNow")} ({activeUsers.length})
</Text>
<FlatList
data={activeUsers}
keyExtractor={(item, index) => item?.profileId || item?.displayName || `${index}`}
renderItem={({ item }) => (
<View style={{ paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: "#f3f4f6" }}>
<Text style={{ color: "#111827" }}>{item?.displayName || "Anonymous"}</Text>
</View>
)}
ListEmptyComponent={<Text style={{ color: "#6b7280" }}>No users online</Text>}
/>
</Animated.View>
</View>
);
};