Revamp global chat header, online panel, and composer UI
This commit is contained in:
@@ -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,17 +135,77 @@ 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 }} {...panResponder.panHandlers}>
|
||||
<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 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 ? (
|
||||
@@ -145,25 +231,94 @@ let GlobalChat = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
<View style={{ marginTop: 10 }}>
|
||||
<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,
|
||||
}}
|
||||
>
|
||||
<CircleIconAction icon="add" onPress={() => { }} />
|
||||
<View style={{ flex: 1, paddingRight: 2 }}>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
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 }}
|
||||
/>
|
||||
<Button
|
||||
mode="contained"
|
||||
</View>
|
||||
<CircleIconAction icon="photo-camera" onPress={() => { }} />
|
||||
<CircleIconAction
|
||||
icon="send"
|
||||
onPress={sendMessage}
|
||||
disabled={sending || !(text || "").trim()}
|
||||
style={{ marginTop: 10 }}
|
||||
>
|
||||
{i18n.t("message.send")}
|
||||
</Button>
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user