145 lines
5.0 KiB
TypeScript
145 lines
5.0 KiB
TypeScript
/**
|
|
* useChatWindow — gerencia o estado e handlers da área de chat ativa.
|
|
*
|
|
* Extrai de inbox/index.tsx:
|
|
* - scroll + carregamento de histórico
|
|
* - indicador de digitação
|
|
* - reply / reaction
|
|
* - envio de mídia e áudio
|
|
* - sync de avatar
|
|
* - abertura de perfil / menu de chat
|
|
*/
|
|
import { useState, useRef, useCallback } from 'react'
|
|
import { mediaApi } from '../../services/chatApiService'
|
|
import type { NewWhatsChat } from '../../types/inboxTypes'
|
|
import type { Message } from '../../types/inboxTypes'
|
|
|
|
interface UseChatWindowOptions {
|
|
selectedChat: NewWhatsChat | null
|
|
activeInstanceId: string | null
|
|
onLoadMoreHistory: () => Promise<{ hasMore: boolean }>
|
|
}
|
|
|
|
export function useChatWindow({
|
|
selectedChat,
|
|
activeInstanceId,
|
|
onLoadMoreHistory,
|
|
}: UseChatWindowOptions) {
|
|
// ── Refs ────────────────────────────────────────────────────────────────────
|
|
const scrollRef = useRef<HTMLDivElement>(null)
|
|
const isTypingRef = useRef(false)
|
|
const typingTimeout = useRef<NodeJS.Timeout | null>(null)
|
|
|
|
// ── Estado ──────────────────────────────────────────────────────────────────
|
|
const [isTyping, setIsTyping] = useState(false)
|
|
const [replyingTo, setReplyingTo] = useState<Message | null>(null)
|
|
const [reactionPickerFor, setReactionPickerFor] = useState<string | null>(null)
|
|
const [hasMoreHistory, setHasMoreHistory] = useState(true)
|
|
const [loadingMore, setLoadingMore] = useState(false)
|
|
const [isProfileOpen, setIsProfileOpen] = useState(false)
|
|
const [isSyncingAvatar, setIsSyncingAvatar] = useState(false)
|
|
const [isChatMenuOpen, setIsChatMenuOpen] = useState(false)
|
|
|
|
// ── Handlers ────────────────────────────────────────────────────────────────
|
|
|
|
const handleTyping = useCallback(() => {
|
|
if (!selectedChat) return
|
|
if (!isTypingRef.current) {
|
|
isTypingRef.current = true
|
|
setIsTyping(true)
|
|
}
|
|
if (typingTimeout.current) clearTimeout(typingTimeout.current)
|
|
typingTimeout.current = setTimeout(() => {
|
|
isTypingRef.current = false
|
|
setIsTyping(false)
|
|
}, 3000)
|
|
}, [selectedChat])
|
|
|
|
const handleScroll = useCallback(async () => {
|
|
const el = scrollRef.current
|
|
if (!el || !hasMoreHistory || loadingMore) return
|
|
if (el.scrollTop < 100) {
|
|
setLoadingMore(true)
|
|
const { hasMore } = await onLoadMoreHistory()
|
|
setHasMoreHistory(hasMore)
|
|
setLoadingMore(false)
|
|
}
|
|
}, [hasMoreHistory, loadingMore, onLoadMoreHistory])
|
|
|
|
const handleSelectChat = useCallback(() => {
|
|
setHasMoreHistory(true)
|
|
}, [])
|
|
|
|
const handleReactToMessage = useCallback((_msg: Message, _emoji: string) => {
|
|
setReactionPickerFor(null)
|
|
// Será implementado via endpoint /api/instances/:id/react
|
|
}, [])
|
|
|
|
const handleSendMedia = useCallback(async (file: File, caption?: string) => {
|
|
if (!selectedChat || !activeInstanceId) return
|
|
try {
|
|
await mediaApi.sendMedia(activeInstanceId, selectedChat.remote_jid, file, caption || undefined, undefined)
|
|
} catch (err) {
|
|
console.error('Erro ao enviar mídia:', err)
|
|
}
|
|
}, [selectedChat, activeInstanceId])
|
|
|
|
const handleSendAudio = useCallback(async (blob: Blob) => {
|
|
if (!selectedChat || !activeInstanceId) return
|
|
try {
|
|
await mediaApi.sendAudio(activeInstanceId, selectedChat.remote_jid, blob)
|
|
} catch (err) {
|
|
console.error('Erro ao enviar áudio:', err)
|
|
}
|
|
}, [selectedChat, activeInstanceId])
|
|
|
|
const handleSyncAvatar = useCallback(async () => {
|
|
if (!selectedChat || !activeInstanceId) return
|
|
setIsSyncingAvatar(true)
|
|
try {
|
|
const API = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3003'
|
|
const token = localStorage.getItem('token')
|
|
await fetch(
|
|
`${API}/api/inbox/avatar/${encodeURIComponent(selectedChat.remote_jid)}?instance=${activeInstanceId}`,
|
|
{ headers: { Authorization: `Bearer ${token}` } }
|
|
)
|
|
} finally {
|
|
setIsSyncingAvatar(false)
|
|
}
|
|
}, [selectedChat, activeInstanceId])
|
|
|
|
const toggleReactionPicker = useCallback((id: string) => {
|
|
setReactionPickerFor(prev => prev === id ? null : id)
|
|
}, [])
|
|
|
|
return {
|
|
// Refs
|
|
scrollRef,
|
|
|
|
// Estado
|
|
isTyping,
|
|
replyingTo,
|
|
reactionPickerFor,
|
|
hasMoreHistory,
|
|
loadingMore,
|
|
isProfileOpen,
|
|
isSyncingAvatar,
|
|
isChatMenuOpen,
|
|
|
|
// Setters simples
|
|
setReplyingTo,
|
|
setIsProfileOpen,
|
|
setIsChatMenuOpen,
|
|
|
|
// Handlers
|
|
handleTyping,
|
|
handleScroll,
|
|
handleSelectChat,
|
|
handleReactToMessage,
|
|
handleSendMedia,
|
|
handleSendAudio,
|
|
handleSyncAvatar,
|
|
toggleReactionPicker,
|
|
}
|
|
}
|