This commit is contained in:
oneao committed 2026-06-25 16:47:54 +08:00
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,
};
}