import { useState, useCallback, useEffect, useRef } from 'react'; import { useDubs } from '../provider'; import { useChatContext } from './provider'; import type { ChatMessage, OnlineUser, ChatConnectionStatus, Conversation, DirectMessage, FriendUser, FriendRequest, SentFriendRequest, SendMessageParams, } from './types'; import type { WhatsNewPost } from '../types'; // ── Connection ── /** Get the current chat connection status */ export function useChatStatus(): ChatConnectionStatus { return useChatContext().status; } // ── Global Chat ── /** Get global chat messages with refetch */ export function useChatMessages(): { messages: ChatMessage[]; loading: boolean; refetch: () => Promise; } { const { messages, refreshMessages } = useChatContext(); const [loading, setLoading] = useState(false); const refetch = useCallback(async () => { setLoading(true); await refreshMessages(); setLoading(false); }, [refreshMessages]); return { messages, loading, refetch }; } /** Send a message to global chat (via socket for real-time, falls back to REST) */ export function useSendMessage(): { send: (params: SendMessageParams) => void; sendViaREST: (params: { message: string; replyToId?: number }) => Promise; } { const { socket } = useChatContext(); const { client } = useDubs(); const send = useCallback( (params: SendMessageParams) => { socket.sendMessage(params); }, [socket], ); const sendViaREST = useCallback( async (params: { message: string; replyToId?: number }) => { await client.sendChatMessage(params); }, [client], ); return { send, sendViaREST }; } /** Get online users and count */ export function useOnlineUsers(): { users: OnlineUser[]; count: number } { const { onlineUsers, onlineCount } = useChatContext(); return { users: onlineUsers, count: onlineCount }; } /** Get unread notification count */ export function useUnreadCount(): number { return useChatContext().unreadCount; } // ── DMs ── /** Get DM conversations inbox */ export function useConversations(): { conversations: Conversation[]; loading: boolean; refetch: () => Promise; } { const { conversations, refreshConversations } = useChatContext(); const [loading, setLoading] = useState(false); const refetch = useCallback(async () => { setLoading(true); await refreshConversations(); setLoading(false); }, [refreshConversations]); return { conversations, loading, refetch }; } /** Get DM thread with a specific user by wallet address */ export function useDirectMessages(recipientWallet: string): { messages: DirectMessage[]; loading: boolean; otherUser: { id: number; username: string; avatar: string | null; walletAddress: string } | null; send: (message: string) => void; sendViaREST: (message: string) => Promise; markRead: () => void; refetch: () => Promise; } { const { client } = useDubs(); const { socket, refreshConversations } = useChatContext(); const [messages, setMessages] = useState([]); const [otherUser, setOtherUser] = useState<{ id: number; username: string; avatar: string | null; walletAddress: string } | null>(null); const [loading, setLoading] = useState(true); const refetch = useCallback(async () => { try { setLoading(true); const res = await client.getConversation(recipientWallet); setMessages(res.messages); setOtherUser(res.otherUser); } catch (err) { console.error('[Dubs:useDirectMessages] Error loading:', err); } finally { setLoading(false); } }, [client, recipientWallet]); // Load on mount and join DM room useEffect(() => { refetch(); socket.joinDM(recipientWallet); return () => { socket.leaveDM(recipientWallet); }; }, [recipientWallet, refetch, socket]); // Listen for new DMs via socket const socketRef = useRef(socket); socketRef.current = socket; useEffect(() => { const currentSocket = socketRef.current; const prevListeners = (currentSocket as any).listeners; // Augment listeners to catch DM messages for this conversation const originalOnDMNew = prevListeners?.onDMNewMessage; currentSocket.setListeners({ ...prevListeners, onDMNewMessage: (msg: DirectMessage) => { originalOnDMNew?.(msg); // Check if this message belongs to our conversation if (msg.senderWallet === recipientWallet || msg.isOwn) { setMessages((prev) => { if (prev.some((m) => m.id === msg.id)) return prev; return [...prev, msg]; }); } }, onDMMessageSent: (msg: DirectMessage) => { setMessages((prev) => { if (prev.some((m) => m.id === msg.id)) return prev; return [...prev, { ...msg, isOwn: true }]; }); }, }); }, [recipientWallet]); const send = useCallback( (message: string) => { socket.sendDM({ recipientWallet, message }); }, [socket, recipientWallet], ); const sendViaREST = useCallback( async (message: string) => { await client.sendDirectMessage({ recipientWallet, message }); await refetch(); await refreshConversations(); }, [client, recipientWallet, refetch, refreshConversations], ); const markRead = useCallback(() => { socket.markDMRead(recipientWallet); }, [socket, recipientWallet]); return { messages, loading, otherUser, send, sendViaREST, markRead, refetch }; } // ── Social ── /** Get friends list */ export function useFriends(): { friends: FriendUser[]; loading: boolean; refetch: () => Promise; } { const { friends, refreshFriends } = useChatContext(); const [loading, setLoading] = useState(false); const refetch = useCallback(async () => { setLoading(true); await refreshFriends(); setLoading(false); }, [refreshFriends]); return { friends, loading, refetch }; } /** Get pending friend requests */ export function useFriendRequests(): { requests: FriendRequest[]; loading: boolean; refetch: () => Promise; } { const { pendingRequests, refreshPendingRequests } = useChatContext(); const [loading, setLoading] = useState(false); const refetch = useCallback(async () => { setLoading(true); await refreshPendingRequests(); setLoading(false); }, [refreshPendingRequests]); return { requests: pendingRequests, loading, refetch }; } /** Get pending friend requests this user has SENT (still awaiting response). */ export function useSentFriendRequests(): { requests: SentFriendRequest[]; loading: boolean; refetch: () => Promise; } { const { sentFriendRequests, refreshSentFriendRequests } = useChatContext(); const [loading, setLoading] = useState(false); const refetch = useCallback(async () => { setLoading(true); await refreshSentFriendRequests(); setLoading(false); }, [refreshSentFriendRequests]); return { requests: sentFriendRequests, loading, refetch }; } /** Cancel a pending friend request the current user previously sent. */ export function useCancelFriendRequest(): { cancel: (requestId: number) => Promise; loading: boolean; } { const { client } = useDubs(); const { refreshSentFriendRequests } = useChatContext(); const [loading, setLoading] = useState(false); const cancel = useCallback( async (requestId: number) => { setLoading(true); try { await client.cancelFriendRequest(requestId); await refreshSentFriendRequests(); } finally { setLoading(false); } }, [client, refreshSentFriendRequests], ); return { cancel, loading }; } /** Search for users by username */ export function useSearchUsers(): { results: FriendUser[]; loading: boolean; search: (query: string) => Promise; clear: () => void; } { const { client } = useDubs(); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); const search = useCallback( async (query: string) => { setLoading(true); try { const res = await client.searchUsers(query); setResults(res.results); } catch (err) { console.error('[Dubs:useSearchUsers] Error:', err); } finally { setLoading(false); } }, [client], ); const clear = useCallback(() => setResults([]), []); return { results, loading, search, clear }; } /** Send a friend request */ export function useSendFriendRequest(): { send: (targetUserId: number) => Promise; loading: boolean; } { const { client } = useDubs(); const { refreshPendingRequests } = useChatContext(); const [loading, setLoading] = useState(false); const send = useCallback( async (targetUserId: number) => { setLoading(true); try { await client.sendFriendRequest(targetUserId); } finally { setLoading(false); } }, [client, refreshPendingRequests], ); return { send, loading }; } /** Accept or reject a friend request */ export function useRespondToFriendRequest(): { accept: (requestId: number) => Promise; reject: (requestId: number) => Promise; loading: boolean; } { const { client } = useDubs(); const { refreshFriends, refreshPendingRequests } = useChatContext(); const [loading, setLoading] = useState(false); const accept = useCallback( async (requestId: number) => { setLoading(true); try { await client.acceptFriendRequest(requestId); await Promise.all([refreshFriends(), refreshPendingRequests()]); } finally { setLoading(false); } }, [client, refreshFriends, refreshPendingRequests], ); const reject = useCallback( async (requestId: number) => { setLoading(true); try { await client.rejectFriendRequest(requestId); await refreshPendingRequests(); } finally { setLoading(false); } }, [client, refreshPendingRequests], ); return { accept, reject, loading }; } // ── What's New ──────────────────────────────────────────────────────────── /** * Read-only subscription to the calling app's What's New posts. * * Auto-refreshes via the chat socket when a new `whats_new` notification * arrives, so the unread badge / list update without manual refetch. */ export function useWhatsNew(): { posts: WhatsNewPost[]; unreadCount: number; loading: boolean; refetch: () => Promise; markRead: (postIds: number[]) => Promise; markAllRead: () => Promise; } { const { client } = useDubs(); const { whatsNewPosts, whatsNewUnreadCount, refreshWhatsNew } = useChatContext(); const [loading, setLoading] = useState(false); const refetch = useCallback(async () => { setLoading(true); try { await refreshWhatsNew(); } finally { setLoading(false); } }, [refreshWhatsNew]); const markRead = useCallback( async (postIds: number[]) => { if (postIds.length === 0) return; try { await client.markWhatsNewRead(postIds); await refreshWhatsNew(); } catch (err) { console.error('[Dubs:useWhatsNew] markRead error:', err); } }, [client, refreshWhatsNew], ); const markAllRead = useCallback(async () => { try { await client.markAllWhatsNewRead(); await refreshWhatsNew(); } catch (err) { console.error('[Dubs:useWhatsNew] markAllRead error:', err); } }, [client, refreshWhatsNew]); return { posts: whatsNewPosts, unreadCount: whatsNewUnreadCount, loading, refetch, markRead, markAllRead, }; }