'init'
This commit is contained in:
223
app/providers/AvatarProvider/AvatarProvider.tsx
Normal file
223
app/providers/AvatarProvider/AvatarProvider.tsx
Normal file
@@ -0,0 +1,223 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
10
app/providers/AvatarProvider/useAvatarDelivery.ts
Normal file
10
app/providers/AvatarProvider/useAvatarDelivery.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useContext } from "react";
|
||||
import { AvatarContext } from "./AvatarProvider";
|
||||
|
||||
export function useAvatarDelivery(publicKey: string) {
|
||||
const context : any = useContext(AvatarContext);
|
||||
if (!context) {
|
||||
throw new Error("useAvatarDelivery must be used within an AvatarProvider");
|
||||
}
|
||||
return [context.deliveredAvatars.includes(publicKey), context.laterAvatarDelivery.bind(context), context.sendMyAvatarTo.bind(context)];
|
||||
}
|
||||
15
app/providers/AvatarProvider/useAvatars.ts
Normal file
15
app/providers/AvatarProvider/useAvatars.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useContext } from "react";
|
||||
import { AvatarContext, AvatarInformation } from "./AvatarProvider";
|
||||
|
||||
export function useAvatars(publicKey: string, allDecode: boolean = false) : AvatarInformation[] {
|
||||
const context : any = useContext(AvatarContext);
|
||||
if(!context){
|
||||
throw new Error("useAvatars must be used within an AvatarProvider");
|
||||
}
|
||||
/**
|
||||
* Load avatar to cache
|
||||
*/
|
||||
context.loadAvatarsFromCacheByPublicKey(publicKey, allDecode);
|
||||
|
||||
return context.decodedAvatarsCache.find((entry: any) => entry.publicKey === publicKey)?.avatars || [];
|
||||
}
|
||||
10
app/providers/AvatarProvider/useChangeAvatar.ts
Normal file
10
app/providers/AvatarProvider/useChangeAvatar.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useContext } from "react";
|
||||
import { AvatarContext } from "./AvatarProvider";
|
||||
|
||||
export function useAvatarChange() {
|
||||
const context : any = useContext(AvatarContext);
|
||||
if(!context){
|
||||
throw new Error("useAvatarChange must be used within an AvatarProvider");
|
||||
}
|
||||
return context.changeAvatar;
|
||||
}
|
||||
10
app/providers/AvatarProvider/useSaveAvatar.ts
Normal file
10
app/providers/AvatarProvider/useSaveAvatar.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useContext } from "react";
|
||||
import { AvatarContext } from "./AvatarProvider";
|
||||
|
||||
export function useSaveAvatar() {
|
||||
const context : any = useContext(AvatarContext);
|
||||
if(!context){
|
||||
throw new Error("useSaveAvatar must be used within an AvatarProvider");
|
||||
}
|
||||
return context.saveAvatar;
|
||||
}
|
||||
Reference in New Issue
Block a user