u
This commit is contained in:
1 parent
513a991b57
commit
8f8e8d03c8
22 files changed
+2212
-213
No files matched your search
@@ -0,0 +1,194 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { toast } from "sonner-native";
|
||||
import { useUserStore, useFamilyStore } from "@/store";
|
||||
|
||||
const RECONNECT_BASE = 1000;
|
||||
const RECONNECT_MAX = 30000;
|
||||
|
||||
export interface ChatMessage {
|
||||
id: number;
|
||||
family_id: number;
|
||||
user_id: number;
|
||||
nickname: string;
|
||||
avatar: string;
|
||||
content: string;
|
||||
msg_type: number;
|
||||
source_table: string | null;
|
||||
source_id: number | null;
|
||||
source_data: string | null;
|
||||
attachments: Attachment[] | null;
|
||||
recalled: number;
|
||||
deleted: number;
|
||||
create_time: string;
|
||||
}
|
||||
|
||||
export interface Attachment {
|
||||
file_key: string;
|
||||
file_name: string;
|
||||
file_type: number;
|
||||
mime_type: string;
|
||||
file_size: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export interface OnlineUser {
|
||||
user_id: number;
|
||||
nickname: string;
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
interface SendPayload {
|
||||
type: "send";
|
||||
content: string;
|
||||
msg_type?: number;
|
||||
source_table?: string;
|
||||
source_id?: number;
|
||||
attachments?: Omit<Attachment, "id">[];
|
||||
}
|
||||
|
||||
export function useChatWebSocket() {
|
||||
const token = useUserStore((s) => s.userInfo?.token);
|
||||
const familyId = useFamilyStore((s) => s.currentFamily?.family_id);
|
||||
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
const [onlineUsers, setOnlineUsers] = useState<OnlineUser[]>([]);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const lastReceivedId = useRef<number>(0);
|
||||
const reconnectTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const reconnectDelay = useRef<number>(RECONNECT_BASE);
|
||||
const mounted = useRef(true);
|
||||
const wasConnected = useRef(false);
|
||||
|
||||
const buildWsUrl = useCallback(() => {
|
||||
const apiUrl = process.env.EXPO_PUBLIC_API_URL || "";
|
||||
const wsProtocol = apiUrl.startsWith("https") ? "wss" : "ws";
|
||||
const host = apiUrl.replace(/^https?:\/\//, "");
|
||||
return `${wsProtocol}://${host}/app/chat/ws?token=${token}&family_id=${familyId}`;
|
||||
}, [token, familyId]);
|
||||
|
||||
const connect = useCallback(() => {
|
||||
if (!token || !familyId) return;
|
||||
if (wsRef.current) return;
|
||||
|
||||
const url = buildWsUrl();
|
||||
const ws = new WebSocket(url);
|
||||
wsRef.current = ws;
|
||||
|
||||
ws.onopen = () => {
|
||||
if (!mounted.current) return;
|
||||
setIsConnected(true);
|
||||
reconnectDelay.current = RECONNECT_BASE;
|
||||
if (wasConnected.current) {
|
||||
toast.success("聊天已重连");
|
||||
}
|
||||
wasConnected.current = true;
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
if (!mounted.current) return;
|
||||
try {
|
||||
const msg = JSON.parse(event.data);
|
||||
handleMessage(msg);
|
||||
} catch {
|
||||
// ignore parse errors
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
if (!mounted.current) return;
|
||||
setIsConnected(false);
|
||||
wsRef.current = null;
|
||||
if (wasConnected.current) {
|
||||
toast.error("聊天已断开");
|
||||
}
|
||||
scheduleReconnect();
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
ws.close();
|
||||
};
|
||||
}, [token, familyId, buildWsUrl]);
|
||||
|
||||
const scheduleReconnect = useCallback(() => {
|
||||
if (!mounted.current) return;
|
||||
reconnectTimer.current = setTimeout(() => {
|
||||
reconnectDelay.current = Math.min(reconnectDelay.current * 2, RECONNECT_MAX);
|
||||
connect();
|
||||
}, reconnectDelay.current);
|
||||
}, [connect]);
|
||||
|
||||
const handleMessage = useCallback((msg: any) => {
|
||||
switch (msg.type) {
|
||||
case "message": {
|
||||
const data = msg.data as ChatMessage;
|
||||
if (data.id <= lastReceivedId.current) return;
|
||||
lastReceivedId.current = data.id;
|
||||
setMessages((prev) => [...prev, data]);
|
||||
break;
|
||||
}
|
||||
case "message_recalled": {
|
||||
setMessages((prev) =>
|
||||
prev.map((m) => (m.id === msg.message_id ? { ...m, recalled: 1 } : m))
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "message_deleted": {
|
||||
setMessages((prev) => prev.filter((m) => m.id !== msg.message_id));
|
||||
break;
|
||||
}
|
||||
case "online": {
|
||||
setOnlineUsers(msg.users || []);
|
||||
break;
|
||||
}
|
||||
case "offline": {
|
||||
setOnlineUsers((prev) =>
|
||||
prev.filter((u) => u.user_id !== msg.user_id)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const send = useCallback((payload: SendPayload) => {
|
||||
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return;
|
||||
wsRef.current.send(JSON.stringify(payload));
|
||||
}, []);
|
||||
|
||||
const recall = useCallback((messageId: number) => {
|
||||
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return;
|
||||
wsRef.current.send(JSON.stringify({ type: "recall", message_id: messageId }));
|
||||
}, []);
|
||||
|
||||
const deleteMessage = useCallback((messageId: number) => {
|
||||
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return;
|
||||
wsRef.current.send(JSON.stringify({ type: "delete", message_id: messageId }));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
mounted.current = true;
|
||||
connect();
|
||||
|
||||
return () => {
|
||||
mounted.current = false;
|
||||
if (reconnectTimer.current) clearTimeout(reconnectTimer.current);
|
||||
if (wsRef.current) {
|
||||
wsRef.current.onclose = null;
|
||||
wsRef.current.close();
|
||||
wsRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [connect]);
|
||||
|
||||
return {
|
||||
messages,
|
||||
onlineUsers,
|
||||
isConnected,
|
||||
send,
|
||||
recall,
|
||||
deleteMessage,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user