This commit is contained in:
PhilReact 2025-03-23 20:07:18 +02:00
parent d1580a3162
commit fb70b91622
12 changed files with 228 additions and 115 deletions

View File

@ -77,7 +77,7 @@ const displayedItems = disablePagination ? items : items?.length < (displayedLim
</React.Fragment>
))}
>
{!disablePagination && displayedItems?.length <= (displayedLimit * 3) && (
<LazyLoad
onLoadMore={async () => {
await onLoadMore(displayedLimit);
@ -88,7 +88,7 @@ const displayedItems = disablePagination ? items : items?.length < (displayedLim
}}
/>
)}
</DynamicGrid>

View File

@ -50,6 +50,8 @@ export interface DefaultLoaderParams {
listItemErrorText?: string;
}
export type ReturnType = 'JSON' | 'BASE64'
interface BaseProps {
search: QortalSearchParams;
entityParams?: EntityParams;
@ -66,7 +68,8 @@ interface BaseProps {
resourceCacheDuration?: number
disablePagination?: boolean
disableScrollTracker?: boolean
retryAttempts: number
retryAttempts: number,
returnType: 'JSON' | 'BASE64'
}
// ✅ Restrict `direction` only when `disableVirtualization = false`
@ -101,6 +104,7 @@ export const MemorizedComponent = ({
disablePagination,
disableScrollTracker,
entityParams,
returnType = 'JSON',
retryAttempts = 2
}: PropsResourceListDisplay) => {
const { filterOutDeletedResources } = useCacheStore();
@ -152,7 +156,6 @@ export const MemorizedComponent = ({
const getResourceList = useCallback(async () => {
try {
if(!generatedIdentifier) return
await new Promise((res)=> {
@ -163,7 +166,7 @@ export const MemorizedComponent = ({
setIsLoading(true);
const parsedParams = {...(JSON.parse(memoizedParams))};
parsedParams.identifier = generatedIdentifier
const responseData = await lists.fetchResources(parsedParams, listName, true); // Awaiting the async function
const responseData = await lists.fetchResources(parsedParams, listName, returnType, true); // Awaiting the async function
@ -221,7 +224,7 @@ export const MemorizedComponent = ({
if(displayLimit){
parsedParams.limit = displayLimit
}
const responseData = await lists.fetchResources(parsedParams, listName); // Awaiting the async function
const responseData = await lists.fetchResources(parsedParams, listName, returnType); // Awaiting the async function
addItems(listName, responseData || [])
} catch (error) {
console.error("Failed to fetch resources:", error);

View File

@ -1,4 +1,10 @@
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import React, {
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react";
import DynamicGrid from "./DynamicGrid";
import LazyLoad from "../../common/LazyLoad";
import { ListItem } from "../../state/cache";
@ -11,9 +17,9 @@ interface VerticalPaginatedListProps {
listItem: (item: ListItem, index: number) => React.ReactNode;
loaderItem?: (status: "LOADING" | "ERROR") => React.ReactNode;
onLoadMore: (limit: number) => void;
onLoadLess: (limit: number)=> void;
limit: number,
disablePagination?: boolean
onLoadLess: (limit: number) => void;
limit: number;
disablePagination?: boolean;
defaultLoaderParams?: DefaultLoaderParams;
}
@ -25,63 +31,78 @@ export const VerticalPaginatedList = ({
onLoadLess,
limit,
disablePagination,
defaultLoaderParams
defaultLoaderParams,
}: VerticalPaginatedListProps) => {
const lastItemRef = useRef<any>(null);
const lastItemRef2 = useRef<any>(null);
const lastItemRef= useRef<any>(null)
const lastItemRef2= useRef<any>(null)
const displayedLimit = limit || 20;
const displayedLimit = limit || 20
const displayedItems = disablePagination ? items : items.slice(- (displayedLimit * 3))
const displayedItems = disablePagination
? items
: items.slice(-(displayedLimit * 3));
return (
<>
{!disablePagination && items?.length > (displayedLimit * 3) && (
<LazyLoad
onLoadMore={async () => {
{!disablePagination && items?.length > displayedLimit * 3 && (
<LazyLoad
onLoadMore={async () => {
await onLoadLess(displayedLimit);
lastItemRef2.current.scrollIntoView({
behavior: "auto",
block: "start",
});
setTimeout(() => {
window.scrollBy({ top: -50, behavior: "instant" }); // 'smooth' if needed
}, 0);
}}
/>
)}
await onLoadLess(displayedLimit);
lastItemRef2.current.scrollIntoView({ behavior: "auto", block: "start" });
setTimeout(() => {
window.scrollBy({ top: -50, behavior: "instant" }); // 'smooth' if needed
}, 0);
}}
/>
)}
{displayedItems?.map((item, index, list) => {
return (
<React.Fragment
key={`${item?.name}-${item?.service}-${item?.identifier}`}
>
<div style={{
width: '100%',
display: 'flex',
justifyContent: 'center'
}} ref={index === displayedLimit ? lastItemRef2 : index === list.length -displayedLimit - 1 ? lastItemRef : null}>
<ListItemWrapper
defaultLoaderParams={defaultLoaderParams}
item={item}
index={index}
render={listItem}
renderListItemLoader={loaderItem}
/>
</div>
</React.Fragment>
);
})}
<LazyLoad
onLoadMore={async () => {
await onLoadMore(displayedLimit);
lastItemRef.current.scrollIntoView({ behavior: "auto", block: "end" });
setTimeout(() => {
window.scrollBy({ top: 50, behavior: "instant" }); // 'smooth' if needed
}, 0);
}}
/>
{displayedItems?.map((item, index, list) => {
return (
<React.Fragment
key={`${item?.name}-${item?.service}-${item?.identifier}`}
>
<div
style={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
ref={
index === displayedLimit
? lastItemRef2
: index === list.length - displayedLimit - 1
? lastItemRef
: null
}
>
<ListItemWrapper
defaultLoaderParams={defaultLoaderParams}
item={item}
index={index}
render={listItem}
renderListItemLoader={loaderItem}
/>
</div>
</React.Fragment>
);
})}
{!disablePagination && displayedItems?.length <= (displayedLimit * 3) && (
<LazyLoad
onLoadMore={async () => {
await onLoadMore(displayedLimit);
lastItemRef.current.scrollIntoView({
behavior: "auto",
block: "end",
});
setTimeout(() => {
window.scrollBy({ top: 50, behavior: "instant" }); // 'smooth' if needed
}, 0);
}}
/>
)}
</>
);
};

View File

@ -6,6 +6,7 @@ import { addAndEncryptSymmetricKeys, decryptWithSymmetricKeys, encryptWithSymmet
import { useIdentifiers } from "../hooks/useIdentifiers";
import { objectToBase64 } from "../utils/base64";
import { base64ToObject } from "../utils/publish";
import { generateBloomFilterBase64, isInsideBloom } from "../utils/bloomFilter";
const utils = {
@ -13,7 +14,9 @@ const utils = {
base64ToObject,
addAndEncryptSymmetricKeys,
encryptWithSymmetricKeys,
decryptWithSymmetricKeys
decryptWithSymmetricKeys,
generateBloomFilterBase64,
isInsideBloom
}

View File

@ -30,6 +30,12 @@ export const useIdentifiers = (builder?: IdentifierBuilder, publicSalt?: string)
const appNameHashed = await hashWord(appName, EnumCollisionStrength.HIGH, publicSalt)
return appNameHashed + '_' + partialIdentifier
}, [appName, publicSalt])
const hashQortalName = useCallback(async ( qortalName: string)=> {
if(!qortalName || !publicSalt) return null
const hashedQortalName = await hashWord(qortalName, EnumCollisionStrength.HIGH, publicSalt)
return hashedQortalName
}, [publicSalt])
useEffect(()=> {
@ -40,6 +46,7 @@ export const useIdentifiers = (builder?: IdentifierBuilder, publicSalt?: string)
return {
buildIdentifier: buildIdentifierFunc,
buildSearchPrefix: buildSearchPrefixFunc,
createSingleIdentifier
createSingleIdentifier,
hashQortalName
};
};

View File

@ -0,0 +1,52 @@
import { useCallback, useEffect, useState } from "react";
interface NameListItem {
name: string
address: string
}
export const useNameSearch = (value: string, limit = 20) => {
const [nameList, setNameList] = useState<NameListItem[]>([]);
const [isLoading, setIsLoading] = useState(false);
const checkIfNameExisits = useCallback(
async (name: string, listLimit: number) => {
try {
if(!name){
setNameList([])
return
}
setIsLoading(true);
const res = await fetch(
`/names/search?query=${name}&prefix=true&limit=${listLimit}`
);
const data = await res.json();
setNameList(data?.map((item: any)=> {
return {
name: item.name,
address: item.owner
}
}));
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
},
[]
);
// Debounce logic
useEffect(() => {
const handler = setTimeout(() => {
checkIfNameExisits(value, limit);
}, 500);
// Cleanup timeout if searchValue changes before the timeout completes
return () => {
clearTimeout(handler);
};
}, [value, limit]);
return {
isLoading,
results: nameList,
};
};

View File

@ -3,6 +3,7 @@ import { usePublishStore } from "../state/publishes";
import { QortalGetMetadata, QortalMetadata } from "../types/interfaces/resources";
import { base64ToObject, retryTransaction } from "../utils/publish";
import { useGlobal } from "../context/GlobalProvider";
import { ReturnType } from "../components/ResourceList/ResourceListDisplay";
const STORAGE_EXPIRY_DURATION = 5 * 60 * 1000;
interface StoredPublish {
@ -12,7 +13,7 @@ interface StoredPublish {
}
export const usePublish = (
maxFetchTries: number = 3,
returnType: "PUBLIC_JSON" = "PUBLIC_JSON",
returnType: ReturnType = "JSON",
metadata?: QortalGetMetadata
) => {
const {auth, appInfo} = useGlobal()
@ -30,8 +31,11 @@ export const usePublish = (
const url = `/arbitrary/${item?.service}/${item?.name}/${item?.identifier}?encoding=base64`;
const res = await fetch(url);
const data = await res.text();
if(returnType === 'BASE64'){
return data
}
return base64ToObject(data);
}, []);
}, [returnType]);
const getStorageKey = useCallback(() => {
if (!username || !appNameHashed) return null;
@ -66,7 +70,7 @@ export const usePublish = (
const fetchPublish = useCallback(
async (
metadataProp: QortalGetMetadata,
returnTypeProp: "PUBLIC_JSON" = "PUBLIC_JSON"
returnTypeProp: ReturnType = "JSON"
) => {
let resourceExists = null;
let resource = null;

View File

@ -7,13 +7,14 @@ import { ListItem, useCacheStore } from "../state/cache";
import { RequestQueueWithPromise } from "../utils/queue";
import { base64ToUint8Array, uint8ArrayToObject } from "../utils/base64";
import { retryTransaction } from "../utils/publish";
import { ReturnType } from "../components/ResourceList/ResourceListDisplay";
export const requestQueueProductPublishes = new RequestQueueWithPromise(20);
export const requestQueueProductPublishesBackup = new RequestQueueWithPromise(
10
);
export interface TemporaryResource {
export interface Resource {
qortalMetadata: QortalMetadata;
data: any;
}
@ -66,6 +67,7 @@ export const useResources = (retryAttempts: number = 2) => {
const fetchIndividualPublishJson = useCallback(
async (
item: QortalMetadata,
returnType: ReturnType,
includeMetadata?: boolean
): Promise<false | ListItem | null | undefined> => {
try {
@ -150,6 +152,17 @@ export const useResources = (retryAttempts: number = 2) => {
}
}
if (res) {
if(returnType === 'BASE64'){
const fullDataObject = {
data: res,
qortalMetadata: includeMetadata ? metadata : item,
};
setResourceCache(
`${item?.service}-${item?.name}-${item?.identifier}`,
fullDataObject
);
return fullDataObject;
}
const toUint = base64ToUint8Array(res);
const toObject = uint8ArrayToObject(toUint);
const fullDataObject = {
@ -170,9 +183,9 @@ export const useResources = (retryAttempts: number = 2) => {
);
const fetchDataFromResults = useCallback(
(responseData: QortalMetadata[]): void => {
(responseData: QortalMetadata[], returnType: ReturnType): void => {
for (const item of responseData) {
fetchIndividualPublishJson(item, false);
fetchIndividualPublishJson(item, returnType, false);
}
},
[fetchIndividualPublishJson]
@ -182,7 +195,8 @@ export const useResources = (retryAttempts: number = 2) => {
async (
params: QortalSearchParams,
listName: string,
cancelRequests?: boolean
returnType: ReturnType = 'JSON',
cancelRequests?: boolean,
): Promise<QortalMetadata[]> => {
if (cancelRequests) {
cancelAllRequests();
@ -226,7 +240,7 @@ export const useResources = (retryAttempts: number = 2) => {
}
setSearchCache(listName, cacheKey, filteredResults);
fetchDataFromResults(filteredResults);
fetchDataFromResults(filteredResults, returnType);
return filteredResults;
},
@ -234,7 +248,7 @@ export const useResources = (retryAttempts: number = 2) => {
);
const addNewResources = useCallback(
(listName: string, resources: TemporaryResource[]) => {
(listName: string, resources: Resource[]) => {
addTemporaryResource(
listName,
@ -250,7 +264,7 @@ export const useResources = (retryAttempts: number = 2) => {
[]
);
const updateNewResources = useCallback((resources: TemporaryResource[]) => {
const updateNewResources = useCallback((resources: Resource[]) => {
resources.forEach((temporaryResource) => {
setResourceCache(
`${temporaryResource?.qortalMetadata?.service}-${temporaryResource?.qortalMetadata?.name}-${temporaryResource?.qortalMetadata?.identifier}`,

View File

@ -1,6 +1,10 @@
import './index.css'
export { RequestQueueWithPromise } from './utils/queue';
export { GlobalProvider, useGlobal } from "./context/GlobalProvider";
export {usePublish} from "./hooks/usePublish"
export {ResourceListDisplay} from "./components/ResourceList/ResourceListDisplay"
export {QortalSearchParams} from './types/interfaces/resources'
export {ImagePicker} from './common/ImagePicker'
export {useNameSearch} from './hooks/useNameSearch'
export {Resource} from './hooks/useResources'

View File

@ -1,17 +1,17 @@
import { create } from "zustand";
import { QortalGetMetadata } from "../types/interfaces/resources";
import { TemporaryResource } from "../hooks/useResources";
import { Resource } from "../hooks/useResources";
interface PublishCache {
data: TemporaryResource | null;
data: Resource | null;
expiry: number;
}
interface PublishState {
publishes: Record<string, PublishCache>;
getPublish: (qortalGetMetadata: QortalGetMetadata | null, ignoreExpire?: boolean) => TemporaryResource | null;
setPublish: (qortalGetMetadata: QortalGetMetadata, data: TemporaryResource | null, customExpiry?: number) => void;
getPublish: (qortalGetMetadata: QortalGetMetadata | null, ignoreExpire?: boolean) => Resource | null;
setPublish: (qortalGetMetadata: QortalGetMetadata, data: Resource | null, customExpiry?: number) => void;
clearExpiredPublishes: () => void;
publishExpiryDuration: number; // Default expiry duration
}

View File

@ -1,48 +1,54 @@
import { BloomFilter } from 'bloom-filters';
import { base64ToObject } from './base64';
import { Buffer } from 'buffer'
// Polyfill Buffer first
if (!(globalThis as any).Buffer) {
;(globalThis as any).Buffer = Buffer
}
export async function hashPublicKey(publicKeyString: string) {
const encoder = new TextEncoder();
const data = encoder.encode(publicKeyString);
const digest = await crypto.subtle.digest("SHA-256", data);
const hashBytes = new Uint8Array(digest);
return Array.from(hashBytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
export async function generateBloomFilterBase64(publicKeys: string[]) {
if (publicKeys.length > 100) throw new Error("Max 100 users allowed");
const bloom = BloomFilter.create(100, 0.0004); // ~0.04% FPR
for (const pk of publicKeys) {
const hash = await hashPublicKey(pk);
bloom.add(hash);
}
// Serialize to compact form
const byteArray = new Uint8Array(bloom.saveAsJSON()._data);
const base64 = btoa(String.fromCharCode(...byteArray));
if (byteArray.length > 230) {
throw new Error(`Bloom filter exceeds 230 bytes: ${byteArray.length}`);
}
return base64;
async function getBloomFilter() {
const { BloomFilter } = await import('bloom-filters')
return BloomFilter
}
export async function userCanProbablyDecrypt(base64Bloom: string, userPublicKey: string) {
const base64ToJson = base64ToObject(base64Bloom)
export async function generateBloomFilterBase64(values: string[]) {
const maxItems = 100
if (values.length > maxItems) {
throw new Error(`Max ${maxItems} items allowed`)
}
// Create filter for the expected number of items and desired false positive rate
const BloomFilter = await getBloomFilter()
const bloom = BloomFilter.create(values.length, 0.025) // ~0.04% FPR
for (const value of values) {
bloom.add(value)
}
// Convert filter to JSON, then to base64
const json = bloom.saveAsJSON()
const jsonString = JSON.stringify(json)
const base64 = Buffer.from(jsonString).toString('base64')
const size = Buffer.byteLength(jsonString)
if (size > 238) {
throw new Error(`Bloom filter exceeds 230 bytes: ${size}`)
}
return base64
}
export async function isInsideBloom(base64Bloom: string, userPublicKey: string) {
const base64ToJson = base64ToObject(base64Bloom)
const BloomFilter = await getBloomFilter()
const bloom = BloomFilter.fromJSON(base64ToJson)
const hash = await hashPublicKey(userPublicKey);
return bloom.has(hash);
return bloom.has(userPublicKey);
}

View File

@ -157,7 +157,7 @@ const getPublicKeysByNames = async (names: string[]) => {
try {
const response = await fetch(`/names/${name}`);
const nameInfo = await response.json();
const resAddress = await fetch(`/addresses/${nameInfo}`);
const resAddress = await fetch(`/addresses/${nameInfo.owner}`);
const resData = await resAddress.json();
return resData.publicKey;
} catch (error) {
@ -186,7 +186,6 @@ export const addAndEncryptSymmetricKeys = async ({
.map(Number)
);
}
const groupmemberPublicKeys = await getPublicKeysByNames(names);
const symmetricKey = createSymmetricKeyAndNonce();
const nextNumber = highestKey + 1;