Files
desktop/app/providers/AvatarProvider/AvatarProvider.tsx
rosetta 83f38dc63f 'init'
2026-01-30 05:01:05 +02:00

223 lines
8.4 KiB
TypeScript

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<string[]>([]);
const {readFile, writeFile} = useFileStorage();
const loadCacheRunningRef = useRef<string[]>([]);
const { error } = useConsoleLogger("AvatarProvider");
const systemAccounts = useSystemAccounts();
/**
* Дополнительный кэширующий слой для декодированных аватарок,
* чтобы не декодировать их каждый раз из базы данных.
*/
const [decodedAvatarsCache, setDecodedAvatarsCache] = useState<AvatarCacheEntry[]>([]);
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 (
<AvatarContext.Provider value={{
deliveredAvatars,
saveAvatar,
loadAvatarsFromCacheByPublicKey,
changeAvatar,
decodedAvatarsCache
}}>
{props.children}
</AvatarContext.Provider>
)
}