import { createContext, useCallback, useContext, useEffect, useState } from "react"; import { useDatabase } from "@/app/providers/DatabaseProvider/useDatabase"; import { useConsoleLogger } from "@/app/hooks/useConsoleLogger"; import { usePublicKey } from "../AccountProvider/usePublicKey"; import { DIALOG_DROP_TO_REQUESTS_IF_NO_MESSAGES_FROM_ME_COUNT } from "@/app/constants"; import { useQueue } from "@/app/hooks/useQueue"; import { useSystemAccounts } from "../SystemAccountsProvider/useSystemAccounts"; import { DialogStateContext } from "../DialogStateProvider.tsx/DialogStateProvider"; interface DialogListContextValue { dialogs: DialogRow[]; setDialogs: (dialogs: DialogRow[]) => void; updateDialog: (dialog_id: string) => void; loadingDialogs: number; //if > 0 then loading } export const DialogListContext = createContext(null); interface DialogListProviderProps { children: React.ReactNode; } export interface DialogRow { dialog_id: string; last_message_id: string; last_timestamp: number; //timestamp update last_message_timestamp: number; //message timestamp is_request: boolean; /** * Закреплен ли диалог */ pinned?: boolean; } export function DialogListProvider(props : DialogListProviderProps) { const [dialogs, setDialogs] = useState([]); const {info} = useConsoleLogger('DialogListProvider'); const { allQuery, getQuery, runQuery } = useDatabase(); const publicKey = usePublicKey(); const { inProcess, addToQueue, removeFromQueue, queue } = useQueue(); const systemAccounts = useSystemAccounts(); const { pinned } = useContext(DialogStateContext)!; const [loadingDialogs, setLoadingDialogs] = useState(0); useEffect(() => { initialLoadingFromDatabase(); }, [publicKey]); useEffect(() => { //console.info(pinned); setDialogs((prevDialogs) => { const newDialogs = prevDialogs.map(d => { return { ...d, pinned: pinned.includes(d.dialog_id) } }); return newDialogs.sort((a, b) => a.pinned === b.pinned ? b.last_message_timestamp - a.last_message_timestamp : (b.pinned ? 1 : 0) - (a.pinned ? 1 : 0)); }); }, [pinned]); const initialLoadingFromDatabase = async () => { if(publicKey == ''){ return; } const rows = await allQuery(`SELECT * FROM dialogs WHERE account = ? ORDER BY last_timestamp DESC`, [publicKey]); const loadedDialogs: DialogRow[] = rows.map((row: any) => { /** * Если диалог системный (бот, канал и т.д.), то он не может быть запросом */ const isRequest = row.is_request && !systemAccounts.some(acc => acc.publicKey === row.dialog_id); return { dialog_id: row.dialog_id, last_message_id: row.last_message_id, last_timestamp: Date.now(), is_request: isRequest, last_message_timestamp: row.last_timestamp, pinned: pinned.includes(row.dialog_id), } }); const prepartedDialogsForBatching = loadedDialogs.sort((a, b) => a.pinned === b.pinned ? b.last_message_timestamp - a.last_message_timestamp : (b.pinned ? 1 : 0) - (a.pinned ? 1 : 0)); if(prepartedDialogsForBatching.length > 0){ /** * Показываем индикатор загрузки, потому что * есть баг с отрисовкой большого количества диалогов сразу * TODO: найти причину бага и исправить * UI блокируется на время отрисовки большого количества диалогов * не надо так))) */ setLoadingDialogs(prepartedDialogsForBatching.length); setTimeout(() => { setLoadingDialogs(0); setDialogs(prepartedDialogsForBatching); }, 100); } info(`Loaded ${loadedDialogs.length} dialogs from database.`); } /** * Обновляет информацию о диалоге в общем списке диалогов * @param dialog_id id диалога в формате public_key или #group:group_id * @returns */ const updateDialog = useCallback(async (dialog_id: string) => { if(inProcess(dialog_id)){ info(`Dialog ${dialog_id} is already being processed, skipping update.`); setTimeout(() => { /** * Попытка обновить диалог еще раз, чтобы избежать гонки */ updateDialog(dialog_id); }, 100); return; } addToQueue(dialog_id); let last_message : any = null; if(dialog_id.startsWith('#group:')){ last_message = await getQuery(`SELECT * FROM messages WHERE to_public_key = ? AND account = ? ORDER BY timestamp DESC LIMIT 1`, [dialog_id, publicKey]); }else{ last_message = await getQuery(`SELECT * FROM messages WHERE ((from_public_key = ? AND to_public_key = ?) OR (from_public_key = ? AND to_public_key = ?)) AND account = ? ORDER BY timestamp DESC LIMIT 1`, [dialog_id, publicKey, publicKey, dialog_id, publicKey]); } let dialogsWithId = await getQuery(`SELECT COUNT(*) as count FROM dialogs WHERE dialog_id = ? AND account = ?`, [dialog_id, publicKey]); if(!last_message && dialogsWithId.count > 0){ setDialogs((prevDialogs) => { const filteredDialogs = prevDialogs.filter(d => d.dialog_id !== dialog_id); return filteredDialogs.sort((a, b) => a.pinned === b.pinned ? b.last_message_timestamp - a.last_message_timestamp : (b.pinned ? 1 : 0) - (a.pinned ? 1 : 0));; }); info(`Delete dialog ${dialog_id} as it has no messages.`); await runQuery(`DELETE FROM dialogs WHERE dialog_id = ? AND account = ?`, [dialog_id, publicKey]); removeFromQueue(dialog_id); return; } if(!last_message){ info(`No messages found for dialog ${dialog_id}, skipping update.`); removeFromQueue(dialog_id); return; } let messagesFromMeCount = await getQuery(`SELECT COUNT(*) as count FROM messages WHERE ( (from_public_key = ? AND to_public_key = ?) OR (from_public_key = ? AND to_public_key = ?) ) AND from_public_key = ? AND account = ? ORDER BY timestamp DESC LIMIT ?`, [dialog_id, publicKey, publicKey, dialog_id, publicKey, publicKey, DIALOG_DROP_TO_REQUESTS_IF_NO_MESSAGES_FROM_ME_COUNT]); const updatedDialog: DialogRow = { dialog_id: dialog_id, last_message_id: last_message.message_id, last_timestamp: Date.now(), is_request: messagesFromMeCount.count <= 0 && !systemAccounts.some(acc => acc.publicKey === dialog_id), last_message_timestamp: last_message.timestamp, pinned: pinned.includes(dialog_id) }; // Обновляем состояние диалога в базе if(dialogsWithId.count > 0){ await runQuery(`UPDATE dialogs SET last_message_id = ?, last_timestamp = ?, is_request = ? WHERE dialog_id = ? AND account = ?`, [updatedDialog.last_message_id, updatedDialog.last_message_timestamp, updatedDialog.is_request, dialog_id, publicKey]); }else{ await runQuery(`INSERT INTO dialogs (dialog_id, last_message_id, last_timestamp, is_request, account) VALUES (?, ?, ?, ?, ?)`, [dialog_id, updatedDialog.last_message_id, updatedDialog.last_message_timestamp, updatedDialog.is_request, publicKey]); } setDialogs((prevDialogs) => { const filteredDialogs = prevDialogs.filter(d => d.dialog_id !== dialog_id); const newDialogs = [updatedDialog, ...filteredDialogs]; return newDialogs.sort((a, b) => a.pinned === b.pinned ? b.last_message_timestamp - a.last_message_timestamp : (b.pinned ? 1 : 0) - (a.pinned ? 1 : 0)); }); removeFromQueue(dialog_id); info(`Dialog ${dialog_id} updated.`); }, [publicKey, queue, pinned, systemAccounts]); return ( {props.children} ) }