223 lines
8.4 KiB
TypeScript
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>
|
|
)
|
|
} |