187 lines
6.5 KiB
TypeScript
187 lines
6.5 KiB
TypeScript
/**
|
|
* instanceStore.ts — Gerencia instâncias WhatsApp e QR Codes.
|
|
* Separado do chatStore para responsabilidade única.
|
|
*/
|
|
import { create } from 'zustand'
|
|
import { immer } from 'zustand/middleware/immer'
|
|
import { instanceApi } from '../services/chatApiService'
|
|
import { socketService } from '../services/socketService'
|
|
import { notify } from './notificationStore'
|
|
|
|
export type InstanceStatus = 'DISCONNECTED' | 'CONNECTING' | 'QR_PENDING' | 'CONNECTED' | 'BANNED'
|
|
|
|
export interface Instance {
|
|
id: string
|
|
name: string
|
|
status: InstanceStatus
|
|
phone: string | null
|
|
avatar: string | null
|
|
}
|
|
|
|
interface InstanceState {
|
|
instances: Instance[]
|
|
activeInstanceId: string | null
|
|
qrByInstance: Record<string, string>
|
|
loading: boolean
|
|
|
|
// Sync actions
|
|
setInstances: (instances: Instance[]) => void
|
|
setActiveInstance: (id: string | null) => void
|
|
patchInstance: (id: string, patch: Partial<Instance>) => void
|
|
setQr: (instanceId: string, qrBase64: string) => void
|
|
clearQr: (instanceId: string) => void
|
|
removeInstance: (id: string) => void
|
|
|
|
// Async actions
|
|
loadInstances: () => Promise<void>
|
|
connectInstance: (id: string) => Promise<void>
|
|
disconnectInstance: (id: string) => Promise<void>
|
|
createInstance: (name: string) => Promise<Instance>
|
|
deleteInstance: (id: string) => Promise<void>
|
|
|
|
// Socket
|
|
initSocketListeners: () => void
|
|
}
|
|
|
|
export const useInstanceStore = create<InstanceState>()(
|
|
immer((set, get) => ({
|
|
instances: [],
|
|
// Persiste a seleção ativa no localStorage para sobreviver a reloads
|
|
activeInstanceId: typeof window !== 'undefined' ? localStorage.getItem('activeInstanceId') : null,
|
|
qrByInstance: {},
|
|
loading: false,
|
|
|
|
// ── Sync ─────────────────────────────────────────────────────────────────
|
|
|
|
setInstances: (instances) =>
|
|
set((s) => { s.instances = instances }),
|
|
|
|
setActiveInstance: (id) => {
|
|
if (typeof window !== 'undefined') {
|
|
if (id) localStorage.setItem('activeInstanceId', id)
|
|
else localStorage.removeItem('activeInstanceId')
|
|
}
|
|
set((s) => { s.activeInstanceId = id })
|
|
},
|
|
|
|
patchInstance: (id, patch) =>
|
|
set((s) => {
|
|
const idx = s.instances.findIndex((i) => i.id === id)
|
|
if (idx !== -1) Object.assign(s.instances[idx], patch)
|
|
else s.instances.push({ id, name: '', phone: null, status: 'CONNECTING', ...patch } as Instance)
|
|
}),
|
|
|
|
setQr: (instanceId, qrBase64) =>
|
|
set((s) => { s.qrByInstance[instanceId] = qrBase64 }),
|
|
|
|
clearQr: (instanceId) =>
|
|
set((s) => { delete s.qrByInstance[instanceId] }),
|
|
|
|
removeInstance: (id) =>
|
|
set((s) => { s.instances = s.instances.filter((i) => i.id !== id) }),
|
|
|
|
// ── Async ─────────────────────────────────────────────────────────────────
|
|
|
|
loadInstances: async () => {
|
|
set((s) => { s.loading = true })
|
|
try {
|
|
const data = await instanceApi.list()
|
|
const list: Instance[] = (Array.isArray(data) ? data : (data.instances ?? [])).map((i: any) => ({
|
|
...i,
|
|
avatar: i.avatar ?? null,
|
|
}))
|
|
set((s) => {
|
|
s.loading = false
|
|
// Mescla em vez de substituir: preserva status CONNECTED que veio via socket
|
|
// (socket events são mais recentes que leituras REST — o banco pode estar
|
|
// desatualizado se a resposta da API chegou antes do DB ser gravado pelo backend)
|
|
const memMap = new Map(s.instances.map((i) => [i.id, i]))
|
|
s.instances = list.map((fromDb) => {
|
|
const inMem = memMap.get(fromDb.id)
|
|
if (inMem?.status === 'CONNECTED' && fromDb.status !== 'CONNECTED') {
|
|
// Socket já confirmou CONNECTED — mantém, não deixa REST stale sobrescrever
|
|
return { ...fromDb, status: 'CONNECTED' as InstanceStatus }
|
|
}
|
|
return fromDb
|
|
})
|
|
})
|
|
} catch {
|
|
set((s) => { s.loading = false })
|
|
}
|
|
},
|
|
|
|
connectInstance: async (id) => {
|
|
get().patchInstance(id, { status: 'CONNECTING' })
|
|
socketService.joinInstance(id)
|
|
await instanceApi.connect(id)
|
|
},
|
|
|
|
disconnectInstance: async (id) => {
|
|
await instanceApi.disconnect(id)
|
|
get().patchInstance(id, { status: 'DISCONNECTED' })
|
|
get().clearQr(id)
|
|
},
|
|
|
|
createInstance: async (name) => {
|
|
const created: Instance = await instanceApi.create(name)
|
|
set((s) => { s.instances.push(created) })
|
|
return created
|
|
},
|
|
|
|
deleteInstance: async (id) => {
|
|
await instanceApi.delete(id)
|
|
get().removeInstance(id)
|
|
if (get().activeInstanceId === id) {
|
|
set((s) => { s.activeInstanceId = null })
|
|
}
|
|
},
|
|
|
|
// ── Socket ────────────────────────────────────────────────────────────────
|
|
|
|
initSocketListeners: () => {
|
|
socketService.on('instance:status', ({
|
|
instanceId,
|
|
status,
|
|
qrBase64,
|
|
}: {
|
|
instanceId: string
|
|
status: InstanceStatus
|
|
qrBase64?: string
|
|
}) => {
|
|
get().patchInstance(instanceId, { status })
|
|
if (qrBase64) get().setQr(instanceId, qrBase64)
|
|
if (status === 'CONNECTED') get().clearQr(instanceId)
|
|
})
|
|
|
|
socketService.on('qr', ({ instanceId, qrBase64 }: { instanceId: string; qrBase64: string }) => {
|
|
get().setQr(instanceId, qrBase64)
|
|
get().patchInstance(instanceId, { status: 'QR_PENDING' })
|
|
})
|
|
|
|
socketService.on('instance:avatar', ({ instanceId, avatar }: { instanceId: string; avatar: string }) => {
|
|
get().patchInstance(instanceId, { avatar })
|
|
})
|
|
|
|
socketService.on('instance:syncing', ({ instanceId, gapMinutes }: { instanceId: string; gapMinutes: number }) => {
|
|
const inst = get().instances.find((i) => i.id === instanceId)
|
|
const label = inst?.name ?? instanceId.slice(0, 8)
|
|
if (gapMinutes >= 60) {
|
|
notify(
|
|
`📥 ${label}: sincronizando mensagens dos últimos ${Math.round(gapMinutes / 60)}h`,
|
|
'warning',
|
|
8000,
|
|
'syncing',
|
|
)
|
|
} else if (gapMinutes > 0) {
|
|
notify(
|
|
`📥 ${label}: buscando mensagens perdidas (${gapMinutes} min offline)`,
|
|
'info',
|
|
6000,
|
|
'syncing',
|
|
)
|
|
}
|
|
})
|
|
},
|
|
}))
|
|
)
|