'init'
This commit is contained in:
210
app/providers/DialogListProvider/DialogListProvider.tsx
Normal file
210
app/providers/DialogListProvider/DialogListProvider.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
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<DialogListContextValue|null>(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<DialogRow[]>([]);
|
||||
const {info} = useConsoleLogger('DialogListProvider');
|
||||
const { allQuery, getQuery, runQuery } = useDatabase();
|
||||
const publicKey = usePublicKey();
|
||||
const { inProcess, addToQueue, removeFromQueue, queue } = useQueue<string>();
|
||||
const systemAccounts = useSystemAccounts();
|
||||
const { pinned } = useContext(DialogStateContext)!;
|
||||
const [loadingDialogs, setLoadingDialogs] = useState<number>(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 (
|
||||
<DialogListContext.Provider value={{
|
||||
dialogs,
|
||||
setDialogs,
|
||||
updateDialog,
|
||||
loadingDialogs
|
||||
}}>
|
||||
{props.children}
|
||||
</DialogListContext.Provider>
|
||||
)
|
||||
}
|
||||
88
app/providers/DialogListProvider/useDialogInfo.ts
Normal file
88
app/providers/DialogListProvider/useDialogInfo.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { DialogRow } from "./DialogListProvider";
|
||||
import { useDatabase } from "@/app/providers/DatabaseProvider/useDatabase";
|
||||
import { usePublicKey } from "../AccountProvider/usePublicKey";
|
||||
import { decodeWithPassword } from "@/app/crypto/crypto";
|
||||
import { constructLastMessageTextByAttachments } from "@/app/utils/constructLastMessageTextByAttachments";
|
||||
import { usePrivatePlain } from "../AccountProvider/usePrivatePlain";
|
||||
import { DeliveredMessageState, Message } from "../DialogProvider/DialogProvider";
|
||||
|
||||
/**
|
||||
* Получает информацию о последнем сообщении в диалоге и количестве непрочитанных сообщений,
|
||||
* работает как с групповыми диалогами, так и с личными.
|
||||
* Последнее сообщение содержит расшифрованный текст в поле plain_message.
|
||||
* @param row Диалог из списка диалогов
|
||||
* @returns информация о последнем сообщении и количестве непрочитанных сообщений
|
||||
*/
|
||||
export function useDialogInfo(row : DialogRow) : {
|
||||
lastMessage: Message;
|
||||
unreaded: number;
|
||||
loading: boolean;
|
||||
} {
|
||||
const {getQuery} = useDatabase();
|
||||
const publicKey = usePublicKey();
|
||||
const privatePlain = usePrivatePlain();
|
||||
const [lastMessage, setLastMessage] = useState<Message>({
|
||||
from_public_key: '',
|
||||
to_public_key: '',
|
||||
content: '',
|
||||
timestamp: 0,
|
||||
chacha_key: '',
|
||||
readed: 0,
|
||||
from_me: 0,
|
||||
delivered: DeliveredMessageState.WAITING,
|
||||
message_id: '',
|
||||
plain_message: '',
|
||||
attachments: [],
|
||||
});
|
||||
const [unreaded, setUnreaded] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
loadLastMessageInfo();
|
||||
}, [row.last_timestamp]);
|
||||
|
||||
const loadLastMessageInfo = async () => {
|
||||
let message = await getQuery(`SELECT * FROM messages WHERE message_id = ? AND account = ? LIMIT 1`, [row.last_message_id, publicKey]);
|
||||
if(!message){
|
||||
return;
|
||||
}
|
||||
|
||||
let lastMessage = '';
|
||||
try{
|
||||
lastMessage = await decodeWithPassword(privatePlain, message.plain_message);
|
||||
}catch(e){
|
||||
lastMessage = constructLastMessageTextByAttachments(message.attachments);
|
||||
}
|
||||
|
||||
setLastMessage({
|
||||
...message,
|
||||
plain_message: lastMessage,
|
||||
attachments: JSON.parse(message.attachments),
|
||||
});
|
||||
let unreadedCount = {
|
||||
count: 0
|
||||
};
|
||||
if(row.dialog_id.startsWith('#group:')){
|
||||
unreadedCount = await getQuery(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM messages
|
||||
WHERE read = 0
|
||||
AND to_public_key = ?
|
||||
AND account = ?`, [row.dialog_id, publicKey]);
|
||||
}else{
|
||||
unreadedCount = await getQuery(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM messages
|
||||
WHERE read = 0
|
||||
AND ((from_public_key = ? AND to_public_key = ?) OR (from_public_key = ? AND to_public_key = ?))
|
||||
AND account = ?`, [publicKey, row.dialog_id, row.dialog_id, publicKey, publicKey]);
|
||||
}
|
||||
setUnreaded(unreadedCount.count);
|
||||
}
|
||||
|
||||
return {
|
||||
lastMessage,
|
||||
unreaded,
|
||||
loading: lastMessage.message_id == '',
|
||||
}
|
||||
}
|
||||
17
app/providers/DialogListProvider/useDialogsList.ts
Normal file
17
app/providers/DialogListProvider/useDialogsList.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useContext } from "react";
|
||||
import { DialogListContext } from "./DialogListProvider";
|
||||
|
||||
export function useDialogsList() {
|
||||
const context = useContext(DialogListContext);
|
||||
if(!context){
|
||||
throw new Error("useDialogList must be call in DialogListProvider");
|
||||
}
|
||||
const {dialogs, updateDialog, setDialogs, loadingDialogs} = context;
|
||||
|
||||
return {
|
||||
dialogs,
|
||||
updateDialog,
|
||||
setDialogs,
|
||||
loadingDialogs
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user