feat: add global chat UI with i18n and translation indicators
This commit is contained in:
170
Views/GlobalChat.js
Normal file
170
Views/GlobalChat.js
Normal file
@@ -0,0 +1,170 @@
|
||||
import React from "react";
|
||||
import { View, FlatList, Pressable } from "react-native";
|
||||
import { Button, Chip, Text, TextInput } from "react-native-paper";
|
||||
import { useSnapshot } from "valtio";
|
||||
import API from "../API.js";
|
||||
import GlobalState from "../contexts/GlobalState.js";
|
||||
import i18n from "../i18nMessages.js";
|
||||
|
||||
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 createdAt = item?.createdAt ? new Date(item.createdAt) : null;
|
||||
return (
|
||||
<View style={{ marginBottom: 12, alignItems: isMine ? "flex-end" : "flex-start" }}>
|
||||
<Pressable
|
||||
onPressIn={isTranslated ? onPressIn : undefined}
|
||||
onPressOut={isTranslated ? onPressOut : undefined}
|
||||
style={{
|
||||
maxWidth: "85%",
|
||||
backgroundColor: isMine ? "#0d6efd" : "#f1f3f5",
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: isMine ? "#fff" : "#3f3f46", fontSize: 12, marginBottom: 4 }}>
|
||||
{item?.senderName || "Anonymous"}
|
||||
</Text>
|
||||
<Text style={{ color: isMine ? "#fff" : "#111827", fontSize: 15 }}>
|
||||
{messageText || ""}
|
||||
</Text>
|
||||
{isTranslated ? (
|
||||
<Text style={{ color: isMine ? "#dbeafe" : "#6b7280", fontSize: 11, marginTop: 4 }}>
|
||||
* {i18n.t("message.aiTranslated")}
|
||||
</Text>
|
||||
) : <></>}
|
||||
<Text style={{ color: isMine ? "#dbeafe" : "#6b7280", fontSize: 11, marginTop: 4 }}>
|
||||
{createdAt && !Number.isNaN(createdAt.getTime()) ? createdAt.toLocaleTimeString() : ""}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
let GlobalChat = () => {
|
||||
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 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]);
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
{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 }}>
|
||||
<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>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalChat;
|
||||
@@ -79,6 +79,7 @@ let MenuView = ({ navigation }) => {
|
||||
</List.Accordion>
|
||||
<List.Section title={i18n.t("message.userActions")}>
|
||||
<List.Item key='ProfileEditor' title={i18n.t('message.profile')} onPress={() => { navigation.navigate("ProfileSettings") }} left={props => <List.Icon {...props} icon="person" />} />
|
||||
<List.Item key='GlobalChat' title='Global Chat' onPress={() => { navigation.navigate("GlobalChat") }} left={props => <List.Icon {...props} icon="chat" />} />
|
||||
<List.Item key='Settings' title={i18n.t('message.settings')} left={props => <List.Icon {...props} icon="settings" />} />
|
||||
<List.Item key="Logout" title={i18n.t('message.logout')} onPress={() => { navigation.navigate("Logout") }} left={props => <List.Icon {...props} icon="logout" />} />
|
||||
</List.Section>
|
||||
|
||||
Reference in New Issue
Block a user