'use client'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useAuth } from '@/components/id/auth-provider'; import { ChatMessage, ChatRoom, fetchUserE2EPublicKey, setUserE2EPublicKey } from '@/lib/api'; import { ensureE2EKeyPair } from '@/lib/e2e-crypto'; import { decryptE2EPayload, encryptE2EPayload, readableE2EPayload, type E2EMessagePayload } from '@/lib/e2e-chat'; export function resolveChatPeerUserId(room: ChatRoom | null | undefined, userId: string | undefined) { if (!room || !userId) return null; const fromMembers = room.members.find((member) => member.userId !== userId)?.userId; if (fromMembers) return fromMembers; if (room.peerUserId && room.peerUserId !== userId) return room.peerUserId; return null; } export function getE2EMessageText(options: { message: ChatMessage; isE2E: boolean; peerE2eKey: string | null; peerKeyLoading: boolean; e2ePayload?: E2EMessagePayload | null; e2eError?: string; }) { const { message, isE2E, peerE2eKey, peerKeyLoading, e2ePayload, e2eError } = options; if (!message.isEncrypted) { return message.content ?? ''; } if (e2ePayload) { return readableE2EPayload(e2ePayload); } if (e2eError) { return e2eError; } if (isE2E && peerKeyLoading) { return 'πŸ”’ Π Π°ΡΡˆΠΈΡ„Ρ€ΠΎΠ²ΠΊΠ°...'; } if (isE2E && !peerE2eKey) { return 'πŸ”’ БобСсСдник Π½Π΅ ΠΎΠΏΡƒΠ±Π»ΠΈΠΊΠΎΠ²Π°Π» ΠΊΠ»ΡŽΡ‡ ΡˆΠΈΡ„Ρ€ΠΎΠ²Π°Π½ΠΈΡ'; } return 'πŸ”’ Π Π°ΡΡˆΠΈΡ„Ρ€ΠΎΠ²ΠΊΠ°...'; } export function useE2EChat(options: { room: ChatRoom | null | undefined; messages: ChatMessage[]; userId?: string; token?: string | null; }) { const { isLoading, isApiReady } = useAuth(); const isE2E = options.room?.type === 'E2E'; const peerUserId = useMemo( () => resolveChatPeerUserId(options.room, options.userId), [options.room, options.userId] ); const [peerE2eKey, setPeerE2eKey] = useState(null); const [peerKeyLoading, setPeerKeyLoading] = useState(false); const [e2ePayloads, setE2ePayloads] = useState>({}); const [e2eErrors, setE2eErrors] = useState>({}); useEffect(() => { if (!options.userId || !options.token || isLoading || !isApiReady) return; void (async () => { try { const { publicKey } = await ensureE2EKeyPair(options.userId!); await setUserE2EPublicKey(options.userId!, publicKey, options.token); } catch { // фоновая инициализация E2E } })(); }, [isApiReady, isLoading, options.token, options.userId]); useEffect(() => { if (!isE2E || !peerUserId || !options.token) { setPeerE2eKey(null); setPeerKeyLoading(false); return; } setPeerKeyLoading(true); void fetchUserE2EPublicKey(peerUserId, options.token) .then((response) => setPeerE2eKey(response.e2ePublicKey ?? null)) .catch(() => setPeerE2eKey(null)) .finally(() => setPeerKeyLoading(false)); }, [isE2E, options.token, peerUserId]); useEffect(() => { if (!isE2E || !peerE2eKey || !options.userId) { setE2ePayloads({}); setE2eErrors({}); return; } let cancelled = false; void (async () => { const nextPayloads: Record = {}; const nextErrors: Record = {}; for (const message of options.messages) { if (!message.isEncrypted || !message.content) continue; const payload = await decryptE2EPayload(options.userId!, peerE2eKey, message.content); if (payload) { nextPayloads[message.id] = payload; } else { nextErrors[message.id] = 'πŸ”’ НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ Ρ€Π°ΡΡˆΠΈΡ„Ρ€ΠΎΠ²Π°Ρ‚ΡŒ сообщСниС'; } } if (!cancelled) { setE2ePayloads(nextPayloads); setE2eErrors(nextErrors); } })(); return () => { cancelled = true; }; }, [isE2E, options.messages, options.userId, peerE2eKey]); const encryptOutgoing = useCallback( async (payload: Parameters[2]) => { if (!isE2E || !options.userId) { throw new Error('E2E нСдоступСн'); } if (!peerE2eKey) { throw new Error('БобСсСдник Π΅Ρ‰Ρ‘ Π½Π΅ ΠΎΠΏΡƒΠ±Π»ΠΈΠΊΠΎΠ²Π°Π» ΠΊΠ»ΡŽΡ‡ ΡˆΠΈΡ„Ρ€ΠΎΠ²Π°Π½ΠΈΡ'); } return encryptE2EPayload(options.userId, peerE2eKey, payload); }, [isE2E, options.userId, peerE2eKey] ); return { isE2E, peerUserId, peerE2eKey, peerKeyLoading, e2ePayloads, e2eErrors, encryptOutgoing }; }