import { useDatabase } from "@/app/providers/DatabaseProvider/useDatabase"; import { createContext, useEffect, useRef, useState } from "react"; import { usePublicKey } from "../AccountProvider/usePublicKey"; import { useFileStorage } from "@/app/hooks/useFileStorage"; import { decodeWithPassword, encodeWithPassword, generateMd5 } from "@/app/crypto/crypto"; import { useConsoleLogger } from "@/app/hooks/useConsoleLogger"; import { useSystemAccounts } from "../SystemAccountsProvider/useSystemAccounts"; import { AVATAR_PASSWORD_TO_ENCODE } from "@/app/constants"; export const AvatarContext = createContext({}); interface AvatarProviderProps { children: React.ReactNode; } export interface AvatarInformation { avatar: string; timestamp: number; } interface AvatarCacheEntry { publicKey: string; avatars: AvatarInformation[]; } export function AvatarProvider(props : AvatarProviderProps) { const {runQuery, allQuery} = useDatabase(); const publicKey = usePublicKey(); const [deliveredAvatars, setDeliveredAvatars] = useState([]); const {readFile, writeFile} = useFileStorage(); const loadCacheRunningRef = useRef([]); const { error } = useConsoleLogger("AvatarProvider"); const systemAccounts = useSystemAccounts(); /** * Дополнительный кэширующий слой для декодированных аватарок, * чтобы не декодировать их каждый раз из базы данных. */ const [decodedAvatarsCache, setDecodedAvatarsCache] = useState([]); useEffect(() => { loadSystemAvatars(); }, []); useEffect(() => { syncAvatarDeliveryWithLocalDb(); }, [publicKey]); const saveAvatar = async (fromPublicKey: string, path : string, decryptedContent : string) => { const timestamp = Date.now(); await runQuery("INSERT INTO `avatar_cache` (public_key, avatar, timestamp) VALUES (?, ?, ?)", [fromPublicKey, path, timestamp]); setDecodedAvatarsCache((prev) => { const existingEntry = prev.find(e => e.publicKey === fromPublicKey); if(existingEntry){ return prev.map(e => { if(e.publicKey === fromPublicKey){ return { publicKey: fromPublicKey, avatars: [{ avatar: decryptedContent, timestamp, }, ...e.avatars] } } return e; }); } else { return [...prev, { publicKey: fromPublicKey, avatars: [{ avatar: decryptedContent, timestamp, }] }]; } }); } const loadSystemAvatars = async () => { let avatarCacheEntrys : AvatarCacheEntry[] = []; for(let i = 0; i < systemAccounts.length; i++){ let account = systemAccounts[i]; avatarCacheEntrys.push({ publicKey: account.publicKey, avatars: [{ avatar: account.avatar, timestamp: Date.now(), }] }); } setDecodedAvatarsCache((prev) => { return [...prev, ...avatarCacheEntrys]; }); } const syncAvatarDeliveryWithLocalDb = async () => { const result = await allQuery("SELECT * FROM `avatar_delivery` WHERE account = ?", [publicKey]); for(let i = 0; i < result.length; i++){ let publicKey = result[i].public_key; setDeliveredAvatars((prev) => [...prev, publicKey]); } } const loadAvatarsFromCacheByPublicKey = async (publicKey : string, allDecode : boolean = true) => { if(loadCacheRunningRef.current.indexOf(publicKey) !== -1){ return; } loadCacheRunningRef.current.push(publicKey); const result = await allQuery("SELECT * FROM `avatar_cache` WHERE public_key = ? ORDER BY timestamp DESC", [publicKey]); if(result.length == 0){ loadCacheRunningRef.current = loadCacheRunningRef.current.filter(pk => pk !== publicKey); return; } if( decodedAvatarsCache.find(e => e.publicKey === publicKey) && (decodedAvatarsCache.find(e => e.publicKey === publicKey)?.avatars.length == result.length || !allDecode) ){ loadCacheRunningRef.current = loadCacheRunningRef.current.filter(pk => pk !== publicKey); return; } let avatars : AvatarInformation[] = []; for(let i = 0; i < result.length; i++){ let file = await readFile(result[i].avatar); if(!file){ error("Avatar file not found: " + result[i].avatar); await runQuery("DELETE FROM `avatar_cache` WHERE avatar = ?", [result[i].avatar]); continue; } let decodedAvatar = ""; try{ decodedAvatar = await decodeWithPassword(AVATAR_PASSWORD_TO_ENCODE, Buffer.from(file, 'binary').toString() ); }catch(e){ error("Failed to decode avatar from file: " + result[i].avatar); await runQuery("DELETE FROM `avatar_cache` WHERE avatar = ?", [result[i].avatar]); continue; } avatars.push({ avatar: decodedAvatar, timestamp: result[i].timestamp, }); if(!allDecode){ break; } } setDecodedAvatarsCache((prev) => { const existingEntry = prev.find(e => e.publicKey === publicKey); if(existingEntry){ let nextState = prev.map(e => { if(e.publicKey === publicKey){ return { publicKey: publicKey, avatars: avatars } } return e; }); return [...nextState]; } else { return [...prev, { publicKey: publicKey, avatars: avatars }]; } }); loadCacheRunningRef.current = loadCacheRunningRef.current.filter(pk => pk !== publicKey); } /** * Change the avatar for a specific entity * @param base64Image The base64 encoded image * @param entity The entity to change the avatar for (groupId or publicKey) */ const changeAvatar = async (base64Image : string, entity : string) => { const timestamp = Date.now(); const avatarPath = `a/${await generateMd5(base64Image + entity)}`; const encodedForStorage = await encodeWithPassword(AVATAR_PASSWORD_TO_ENCODE, base64Image); await writeFile(avatarPath, Buffer.from(encodedForStorage, 'binary')); await runQuery("INSERT INTO `avatar_cache` (public_key, avatar, timestamp) VALUES (?, ?, ?)", [entity, avatarPath, timestamp]); setDecodedAvatarsCache((prev) => { const existingEntry = prev.find(e => e.publicKey === entity); if(existingEntry){ let nextState = prev.map(e => { if(e.publicKey === entity){ return { publicKey: entity, avatars: [{ avatar: base64Image, timestamp, }, ...e.avatars] } } return e; }); return [...nextState]; } else { return [...prev, { publicKey: entity, avatars: [{ avatar: base64Image, timestamp, }] }]; } }); } return ( {props.children} ) }