mirror of
https://github.com/Qortal/qapp-core.git
synced 2025-06-18 19:01:21 +00:00
added list funcitons and keep scroll height before expiry
This commit is contained in:
parent
e8866f1585
commit
52f1d570a6
13
package-lock.json
generated
13
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "qapp-core",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "qapp-core",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
@ -14,12 +14,12 @@
|
||||
"@mui/icons-material": "^6.4.7",
|
||||
"@mui/material": "^6.4.7",
|
||||
"@tanstack/react-virtual": "^3.13.2",
|
||||
"@types/react": "^19.0.10",
|
||||
"react": "^19.0.0",
|
||||
"react-intersection-observer": "^9.16.0",
|
||||
"zustand": "^4.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.27",
|
||||
"tsup": "^7.0.0",
|
||||
"typescript": "^5.2.0"
|
||||
}
|
||||
@ -1012,11 +1012,10 @@
|
||||
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
|
||||
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
|
||||
"version": "19.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz",
|
||||
"integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "qapp-core",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.3",
|
||||
"description": "Qortal's core React library with global state, UI components, and utilities",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.mjs",
|
||||
@ -25,12 +25,12 @@
|
||||
"@mui/icons-material": "^6.4.7",
|
||||
"@mui/material": "^6.4.7",
|
||||
"@tanstack/react-virtual": "^3.13.2",
|
||||
"@types/react": "^19.0.10",
|
||||
"react": "^19.0.0",
|
||||
"react-intersection-observer": "^9.16.0",
|
||||
"zustand": "^4.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.27",
|
||||
"tsup": "^7.0.0",
|
||||
"typescript": "^5.2.0"
|
||||
},
|
||||
|
@ -8,14 +8,18 @@ import React, {
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
import { QortalMetadata } from "../types/interfaces/resources";
|
||||
import { useScrollTrackerRef } from "./useScrollTrackerRef";
|
||||
import { useMergeRefs } from "../hooks/useMergeRefs";
|
||||
|
||||
interface PropsVirtualizedList {
|
||||
list: any[];
|
||||
children: (item: any, index: number) => React.ReactNode;
|
||||
onSeenLastItem?: (item: QortalMetadata)=> void;
|
||||
listName: string
|
||||
}
|
||||
export const VirtualizedList = ({ list, children, onSeenLastItem }: PropsVirtualizedList) => {
|
||||
export const VirtualizedList = ({ list, children, onSeenLastItem, listName }: PropsVirtualizedList) => {
|
||||
const parentRef = useRef(null);
|
||||
useScrollTrackerRef(listName, parentRef)
|
||||
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: list.length,
|
||||
|
26
src/common/useScrollTracker.tsx
Normal file
26
src/common/useScrollTracker.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export const useScrollTracker = (listName: string) => {
|
||||
useEffect(() => {
|
||||
if (!listName) return;
|
||||
|
||||
const SCROLL_KEY = `scroll-position-${listName}`;
|
||||
|
||||
// 🔹 Restore saved scroll position for the given list
|
||||
const savedPosition = sessionStorage.getItem(SCROLL_KEY);
|
||||
if (savedPosition) {
|
||||
window.scrollTo(0, parseInt(savedPosition, 10));
|
||||
}
|
||||
|
||||
const handleScroll = () => {
|
||||
sessionStorage.setItem(SCROLL_KEY, window.scrollY.toString());
|
||||
};
|
||||
|
||||
// 🔹 Save scroll position on scroll
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, [listName]); // ✅ Only runs when listName changes
|
||||
};
|
30
src/common/useScrollTrackerRef.tsx
Normal file
30
src/common/useScrollTrackerRef.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const useScrollTrackerRef = (listName: string, ref: React.RefObject<HTMLElement | null>) => {
|
||||
useEffect(() => {
|
||||
if (!listName || !ref.current) return;
|
||||
|
||||
const SCROLL_KEY = `scroll-position-${listName}`;
|
||||
const savedPosition = sessionStorage.getItem(SCROLL_KEY);
|
||||
|
||||
|
||||
if (savedPosition && ref.current) {
|
||||
ref.current.scrollTop = parseInt(savedPosition, 10);
|
||||
}
|
||||
|
||||
|
||||
const handleScroll = () => {
|
||||
if (ref.current) {
|
||||
sessionStorage.setItem(SCROLL_KEY, ref.current.scrollTop.toString());
|
||||
}
|
||||
};
|
||||
|
||||
ref.current.addEventListener("scroll", handleScroll);
|
||||
|
||||
return () => {
|
||||
if (ref.current) {
|
||||
ref.current.removeEventListener("scroll", handleScroll);
|
||||
}
|
||||
};
|
||||
}, [listName, ref]);
|
||||
};
|
@ -13,10 +13,11 @@ const DynamicGrid: React.FC<DynamicGridProps> = ({
|
||||
minItemWidth = 200, // Minimum width per item
|
||||
gap = 10, // Space between items
|
||||
children,
|
||||
}) => {
|
||||
|
||||
|
||||
}) => {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", width: "100%" }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", width: "100%" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
|
113
src/components/ResourceList/HorizontalPaginationList.tsx
Normal file
113
src/components/ResourceList/HorizontalPaginationList.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
import React, { useRef, useEffect, useState, useCallback } from "react";
|
||||
import DynamicGrid from "./DynamicGrid";
|
||||
import LazyLoad from "../../common/LazyLoad";
|
||||
import { ListItem } from "../../state/cache";
|
||||
import { QortalMetadata } from "../../types/interfaces/resources";
|
||||
import { ListItemWrapper } from "./ResourceListDisplay";
|
||||
|
||||
interface HorizontalPaginatedListProps {
|
||||
items: QortalMetadata[];
|
||||
listItem: (item: ListItem, index: number) => React.ReactNode;
|
||||
loaderItem?: (status: "LOADING" | "ERROR") => React.ReactNode;
|
||||
onLoadMore: () => void;
|
||||
maxItems?: number;
|
||||
minItemWidth?: number;
|
||||
gap?: number;
|
||||
isLoading?: boolean;
|
||||
onSeenLastItem?: (listItem: ListItem) => void;
|
||||
}
|
||||
|
||||
export const HorizontalPaginatedList = ({
|
||||
items,
|
||||
listItem,
|
||||
loaderItem,
|
||||
onLoadMore,
|
||||
maxItems = 60,
|
||||
minItemWidth,
|
||||
gap,
|
||||
isLoading,
|
||||
onSeenLastItem,
|
||||
}: HorizontalPaginatedListProps) => {
|
||||
const listRef = useRef<HTMLDivElement | null>(null);
|
||||
const [displayedItems, setDisplayedItems] = useState(items);
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayedItems(items);
|
||||
}, [items]);
|
||||
|
||||
const preserveScroll = useCallback((updateFunction: () => void) => {
|
||||
const container = listRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const previousScrollLeft = container.scrollLeft;
|
||||
const previousScrollWidth = container.scrollWidth;
|
||||
|
||||
updateFunction(); // Perform the update (fetch new data, remove old)
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const newScrollWidth = container.scrollWidth;
|
||||
container.scrollLeft = previousScrollLeft - (previousScrollWidth - newScrollWidth);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (displayedItems.length > maxItems) {
|
||||
preserveScroll(() => {
|
||||
const excess = displayedItems.length - maxItems;
|
||||
setDisplayedItems((prev) => prev.slice(excess)); // Trim from the start
|
||||
});
|
||||
}
|
||||
}, [displayedItems, maxItems, preserveScroll]);
|
||||
|
||||
useEffect(() => {
|
||||
const container = listRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const handleScroll = () => {
|
||||
if (
|
||||
container.scrollLeft + container.clientWidth >= container.scrollWidth - 10 &&
|
||||
!isLoading
|
||||
) {
|
||||
onLoadMore();
|
||||
}
|
||||
};
|
||||
|
||||
container.addEventListener("scroll", handleScroll);
|
||||
return () => container.removeEventListener("scroll", handleScroll);
|
||||
}, [onLoadMore, isLoading]);
|
||||
|
||||
return (
|
||||
<div ref={listRef} style={{
|
||||
overflow: 'auto', width: '100%', display: 'flex', flexGrow: 1
|
||||
}}>
|
||||
<DynamicGrid
|
||||
|
||||
minItemWidth={minItemWidth}
|
||||
gap={gap}
|
||||
items={displayedItems.map((item, index) => (
|
||||
<React.Fragment key={`${item?.name}-${item?.service}-${item?.identifier}`}>
|
||||
<ListItemWrapper
|
||||
item={item}
|
||||
index={index}
|
||||
render={listItem}
|
||||
renderListItemLoader={loaderItem}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
>
|
||||
{!isLoading && displayedItems.length > 0 && (
|
||||
<LazyLoad
|
||||
onLoadMore={() => {
|
||||
onLoadMore();
|
||||
if (onSeenLastItem) {
|
||||
// onSeenLastItem(displayedItems[displayedItems.length - 1]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</DynamicGrid>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
@ -2,8 +2,11 @@ import React, {
|
||||
CSSProperties,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
useTransition,
|
||||
} from "react";
|
||||
import {
|
||||
QortalMetadata,
|
||||
@ -19,6 +22,7 @@ import { Spacer } from "../../common/Spacer";
|
||||
import DynamicGrid from "./DynamicGrid";
|
||||
import LazyLoad from "../../common/LazyLoad";
|
||||
import { useListStore } from "../../state/lists";
|
||||
import { useScrollTracker } from "../../common/useScrollTracker";
|
||||
type Direction = "VERTICAL" | "HORIZONTAL";
|
||||
|
||||
interface ResourceListStyles {
|
||||
@ -39,7 +43,7 @@ interface DefaultLoaderParams {
|
||||
listItemErrorText?: string;
|
||||
}
|
||||
|
||||
interface BaseProps {
|
||||
interface BaseProps {
|
||||
params: QortalSearchParams;
|
||||
listItem: (item: ListItem, index: number) => React.ReactNode;
|
||||
styles?: ResourceListStyles;
|
||||
@ -48,7 +52,8 @@ interface BaseProps {
|
||||
loaderList?: (status: "LOADING" | "NO_RESULTS") => React.ReactNode;
|
||||
disableVirtualization?: boolean;
|
||||
onSeenLastItem?: (listItem: QortalMetadata) => void;
|
||||
listName: string
|
||||
listName: string,
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
// ✅ Restrict `direction` only when `disableVirtualization = false`
|
||||
@ -64,7 +69,7 @@ interface NonVirtualizedProps extends BaseProps {
|
||||
|
||||
type PropsResourceListDisplay = VirtualizedProps | NonVirtualizedProps;
|
||||
|
||||
export const ResourceListDisplay = ({
|
||||
export const MemorizedComponent = ({
|
||||
params,
|
||||
listItem,
|
||||
styles = {
|
||||
@ -76,25 +81,40 @@ export const ResourceListDisplay = ({
|
||||
disableVirtualization,
|
||||
direction = "VERTICAL",
|
||||
onSeenLastItem,
|
||||
listName
|
||||
}: PropsResourceListDisplay) => {
|
||||
listName,
|
||||
}: PropsResourceListDisplay) => {
|
||||
const { fetchResources } = useResources();
|
||||
const { getTemporaryResources } = useCacheStore();
|
||||
const { getTemporaryResources, filterOutDeletedResources } = useCacheStore();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const memoizedParams = useMemo(() => JSON.stringify(params), [params]);
|
||||
const addList = useListStore().addList
|
||||
const addItems = useListStore().addItems
|
||||
const getListByName = useListStore().getListByName
|
||||
const list = useListStore().getListByName(listName)
|
||||
const isListExpired = useCacheStore().isListExpired(listName)
|
||||
const initialized = useRef(false)
|
||||
useScrollTracker(listName);
|
||||
|
||||
const listToDisplay = useMemo(()=> {
|
||||
return [...getTemporaryResources(listName), ...list]
|
||||
}, [list, listName])
|
||||
return filterOutDeletedResources([...getTemporaryResources(listName), ...list])
|
||||
}, [list, listName, filterOutDeletedResources, getTemporaryResources])
|
||||
|
||||
|
||||
const getResourceList = useCallback(async () => {
|
||||
try {
|
||||
await new Promise((res)=> {
|
||||
setTimeout(() => {
|
||||
res(null)
|
||||
}, 500);
|
||||
})
|
||||
setIsLoading(true);
|
||||
const parsedParams = JSON.parse(memoizedParams);
|
||||
const res = await fetchResources(parsedParams, listName, true); // Awaiting the async function
|
||||
addList(listName, res || [])
|
||||
const responseData = await fetchResources(parsedParams, listName, true); // Awaiting the async function
|
||||
|
||||
|
||||
|
||||
addList(listName, responseData || []);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch resources:", error);
|
||||
} finally {
|
||||
@ -108,8 +128,8 @@ export const ResourceListDisplay = ({
|
||||
const parsedParams = {...(JSON.parse(memoizedParams))};
|
||||
parsedParams.before = list.length === 0 ? null : list[list.length - 1]?.created
|
||||
parsedParams.offset = null
|
||||
const res = await fetchResources(parsedParams, listName); // Awaiting the async function
|
||||
addItems(listName, res || [])
|
||||
const responseData = await fetchResources(parsedParams, listName); // Awaiting the async function
|
||||
addItems(listName, responseData || [])
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch resources:", error);
|
||||
} finally {
|
||||
@ -118,8 +138,12 @@ export const ResourceListDisplay = ({
|
||||
}, [memoizedParams, listName, list]);
|
||||
|
||||
useEffect(() => {
|
||||
if(initialized.current) return
|
||||
initialized.current = true
|
||||
if(!isListExpired) return
|
||||
sessionStorage.removeItem(`scroll-position-${listName}`);
|
||||
getResourceList();
|
||||
}, [getResourceList]); // Runs when dependencies change
|
||||
}, [getResourceList, isListExpired]); // Runs when dependencies change
|
||||
|
||||
const disabledVirutalizationStyles: CSSProperties = useMemo(() => {
|
||||
if (styles?.disabledVirutalizationStyles?.parentContainer)
|
||||
@ -133,6 +157,15 @@ export const ResourceListDisplay = ({
|
||||
};
|
||||
}, [styles?.disabledVirutalizationStyles, styles?.gap, direction]);
|
||||
|
||||
useEffect(() => {
|
||||
const clearOnReload = () => {
|
||||
sessionStorage.removeItem(`scroll-position-${listName}`);
|
||||
};
|
||||
|
||||
window.addEventListener("beforeunload", clearOnReload);
|
||||
return () => window.removeEventListener("beforeunload", clearOnReload);
|
||||
}, [listName]);
|
||||
|
||||
return (
|
||||
<ListLoader
|
||||
noResultsMessage={
|
||||
@ -155,7 +188,7 @@ export const ResourceListDisplay = ({
|
||||
>
|
||||
<div style={{ display: "flex", flexGrow: 1 }}>
|
||||
{!disableVirtualization && (
|
||||
<VirtualizedList list={listToDisplay} onSeenLastItem={(item)=> {
|
||||
<VirtualizedList listName={listName} list={listToDisplay} onSeenLastItem={(item)=> {
|
||||
getResourceMoreList()
|
||||
if(onSeenLastItem){
|
||||
onSeenLastItem(item)
|
||||
@ -179,6 +212,7 @@ export const ResourceListDisplay = ({
|
||||
)}
|
||||
{disableVirtualization && direction === "HORIZONTAL" && (
|
||||
<>
|
||||
|
||||
<DynamicGrid
|
||||
minItemWidth={styles?.horizontalStyles?.minItemWidth}
|
||||
gap={styles?.gap}
|
||||
@ -246,7 +280,25 @@ export const ResourceListDisplay = ({
|
||||
</div>
|
||||
</ListLoader>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function arePropsEqual(
|
||||
prevProps: PropsResourceListDisplay,
|
||||
nextProps: PropsResourceListDisplay
|
||||
): boolean {
|
||||
return (
|
||||
prevProps.listName === nextProps.listName &&
|
||||
prevProps.disableVirtualization === nextProps.disableVirtualization &&
|
||||
prevProps.direction === nextProps.direction &&
|
||||
prevProps.onSeenLastItem === nextProps.onSeenLastItem &&
|
||||
JSON.stringify(prevProps.params) === JSON.stringify(nextProps.params) &&
|
||||
JSON.stringify(prevProps.styles) === JSON.stringify(nextProps.styles)
|
||||
);
|
||||
}
|
||||
|
||||
export const ResourceListDisplay = React.memo(MemorizedComponent, arePropsEqual);
|
||||
|
||||
|
||||
interface ListItemWrapperProps {
|
||||
item: QortalMetadata;
|
||||
@ -256,7 +308,7 @@ interface ListItemWrapperProps {
|
||||
renderListItemLoader?: (status: "LOADING" | "ERROR") => React.ReactNode;
|
||||
}
|
||||
|
||||
const ListItemWrapper: React.FC<ListItemWrapperProps> = ({
|
||||
export const ListItemWrapper: React.FC<ListItemWrapperProps> = ({
|
||||
item,
|
||||
index,
|
||||
render,
|
||||
|
3
src/global.d.ts
vendored
3
src/global.d.ts
vendored
@ -41,7 +41,8 @@ interface QortalRequestOptions {
|
||||
payments?: any[]
|
||||
assetId?: number,
|
||||
publicKeys?: string[],
|
||||
recipient?: string
|
||||
recipient?: string,
|
||||
before?: number | null
|
||||
}
|
||||
|
||||
declare function qortalRequest(options: QortalRequestOptions): Promise<any>
|
||||
|
18
src/hooks/useMergeRefs.tsx
Normal file
18
src/hooks/useMergeRefs.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export function useMergeRefs<T extends HTMLElement>(...refs: (React.Ref<T> | undefined)[]) {
|
||||
const mergedRef = useRef<T>(null as unknown as T); // ✅ Ensures correct type
|
||||
|
||||
useEffect(() => {
|
||||
refs.forEach((ref) => {
|
||||
if (!ref) return;
|
||||
if (typeof ref === "function") {
|
||||
ref(mergedRef.current);
|
||||
} else if (ref && "current" in ref) {
|
||||
(ref as React.MutableRefObject<T>).current = mergedRef.current;
|
||||
}
|
||||
});
|
||||
}, [refs]);
|
||||
|
||||
return mergedRef;
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import React, { useCallback } from "react";
|
||||
import {
|
||||
|
||||
QortalMetadata,
|
||||
QortalSearchParams,
|
||||
} from "../types/interfaces/resources";
|
||||
@ -23,7 +24,8 @@ export const useResources = () => {
|
||||
getSearchCache,
|
||||
getResourceCache,
|
||||
setResourceCache,
|
||||
addTemporaryResource
|
||||
addTemporaryResource,
|
||||
markResourceAsDeleted
|
||||
} = useCacheStore();
|
||||
const requestControllers = new Map<string, AbortController>();
|
||||
|
||||
@ -151,35 +153,54 @@ export const useResources = () => {
|
||||
): Promise<QortalMetadata[]> => {
|
||||
if (cancelRequests) {
|
||||
cancelAllRequests();
|
||||
await new Promise((res) => {
|
||||
setTimeout(() => {
|
||||
res(null);
|
||||
}, 250);
|
||||
});
|
||||
}
|
||||
|
||||
const cacheKey = generateCacheKey(params);
|
||||
const searchCache = getSearchCache(listName, cacheKey);
|
||||
let responseData = [];
|
||||
|
||||
if (searchCache) {
|
||||
responseData = searchCache;
|
||||
} else {
|
||||
return searchCache;
|
||||
}
|
||||
|
||||
let responseData: QortalMetadata[] = [];
|
||||
let filteredResults: QortalMetadata[] = [];
|
||||
let lastCreated = params.before || null;
|
||||
const targetLimit = params.limit ?? 20; // Use `params.limit` if provided, else default to 20
|
||||
|
||||
while (filteredResults.length < targetLimit) {
|
||||
const response = await qortalRequest({
|
||||
action: "SEARCH_QDN_RESOURCES",
|
||||
mode: "ALL",
|
||||
limit: 20,
|
||||
...params,
|
||||
limit: targetLimit - filteredResults.length, // Adjust limit dynamically
|
||||
before: lastCreated,
|
||||
});
|
||||
if (!response) throw new Error("Unable to fetch resources");
|
||||
|
||||
if (!response || response.length === 0) {
|
||||
break; // No more data available
|
||||
}
|
||||
|
||||
responseData = response;
|
||||
const validResults = responseData.filter(item => item.size !== 32);
|
||||
filteredResults = [...filteredResults, ...validResults];
|
||||
|
||||
if (filteredResults.length >= targetLimit) {
|
||||
filteredResults = filteredResults.slice(0, targetLimit);
|
||||
break;
|
||||
}
|
||||
|
||||
lastCreated = responseData[responseData.length - 1]?.created;
|
||||
if (!lastCreated) break;
|
||||
}
|
||||
setSearchCache(listName, cacheKey, responseData);
|
||||
fetchDataFromResults(responseData);
|
||||
|
||||
return responseData;
|
||||
|
||||
setSearchCache(listName, cacheKey, filteredResults);
|
||||
fetchDataFromResults(filteredResults);
|
||||
|
||||
return filteredResults;
|
||||
},
|
||||
[getSearchCache, setSearchCache, fetchDataFromResults]
|
||||
);
|
||||
|
||||
|
||||
|
||||
const addNewResources = useCallback(
|
||||
(listName: string, resources: TemporaryResource[]) => {
|
||||
@ -209,11 +230,26 @@ export const useResources = () => {
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const deleteProduct = useCallback(async (qortalMetadata: QortalMetadata)=> {
|
||||
if(!qortalMetadata?.service || !qortalMetadata?.identifier) throw new Error('Missing fields')
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
service: qortalMetadata.service,
|
||||
identifier: qortalMetadata.identifier,
|
||||
base64: 'RA==',
|
||||
});
|
||||
markResourceAsDeleted(qortalMetadata)
|
||||
return true
|
||||
}, [])
|
||||
|
||||
|
||||
return {
|
||||
fetchResources,
|
||||
fetchIndividualPublish,
|
||||
addNewResources,
|
||||
updateNewResources
|
||||
updateNewResources,
|
||||
deleteProduct
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { create } from "zustand";
|
||||
import { QortalMetadata } from "../types/interfaces/resources";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
|
||||
interface SearchCache {
|
||||
@ -43,6 +44,10 @@ interface resourceCache {
|
||||
};
|
||||
}
|
||||
|
||||
interface DeletedResources {
|
||||
[key: string]: { deleted: true; expiry: number }; // ✅ Added expiry field
|
||||
}
|
||||
|
||||
interface CacheState {
|
||||
resourceCache: resourceCache;
|
||||
|
||||
@ -56,107 +61,157 @@ interface CacheState {
|
||||
getResourceCache: (id: string, ignoreExpire?: boolean) => ListItem | false | null;
|
||||
addTemporaryResource: (listName: string, newResources: QortalMetadata[], customExpiry?: number)=> void;
|
||||
getTemporaryResources:(listName: string)=> QortalMetadata[]
|
||||
deletedResources: DeletedResources;
|
||||
markResourceAsDeleted: (item: QortalMetadata) => void;
|
||||
filterOutDeletedResources: (items: QortalMetadata[]) => QortalMetadata[];
|
||||
isListExpired: (listName: string)=> boolean
|
||||
}
|
||||
|
||||
export const useCacheStore = create<CacheState>((set, get) => ({
|
||||
resourceCache: {},
|
||||
searchCache: {},
|
||||
orderCache: {},
|
||||
messageCache: {},
|
||||
export const useCacheStore = create<CacheState>
|
||||
((set, get) => ({
|
||||
resourceCache: {},
|
||||
searchCache: {},
|
||||
deletedResources: {},
|
||||
|
||||
getResourceCache: (id, ignoreExpire) => {
|
||||
const cache = get().resourceCache[id];
|
||||
if (cache && (cache.expiry > Date.now() || ignoreExpire)) {
|
||||
return cache.data; // Return cached product if not expired
|
||||
}
|
||||
return null; // Cache expired or doesn't exist
|
||||
},
|
||||
setResourceCache: (id, data, customExpiry) =>
|
||||
set((state) => {
|
||||
const expiry = Date.now() + (customExpiry || (30 * 60 * 1000)); // 30mins from now
|
||||
return {
|
||||
resourceCache: {
|
||||
...state.resourceCache,
|
||||
[id]: { data, expiry },
|
||||
},
|
||||
};
|
||||
}),
|
||||
// Add search results to cache
|
||||
setSearchCache: (listName, searchTerm, data, customExpiry) =>
|
||||
set((state) => {
|
||||
const expiry = Date.now() + (customExpiry || 5 * 60 * 1000); // 5mins from now
|
||||
|
||||
return {
|
||||
searchCache: {
|
||||
...state.searchCache,
|
||||
[listName]: {
|
||||
searches: {
|
||||
...(state.searchCache[listName]?.searches || {}), // Preserve existing searches
|
||||
[searchTerm]: data, // Store new search term results
|
||||
},
|
||||
temporaryNewResources: state.searchCache[listName]?.temporaryNewResources || [], // Preserve existing temp resources
|
||||
expiry, // Expiry for the entire list
|
||||
},
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
|
||||
// Retrieve cached search results
|
||||
getSearchCache: (listName, searchTerm) => {
|
||||
const cache = get().searchCache[listName];
|
||||
if (cache && cache.expiry > Date.now()) {
|
||||
return cache.searches[searchTerm] || null; // Return specific search term results
|
||||
}
|
||||
return null; // Cache expired or doesn't exist
|
||||
},
|
||||
addTemporaryResource: (listName, newResources, customExpiry) =>
|
||||
set((state) => {
|
||||
const expiry = Date.now() + (customExpiry || 5 * 60 * 1000); // Reset expiry
|
||||
|
||||
const existingResources = state.searchCache[listName]?.temporaryNewResources || [];
|
||||
|
||||
// Merge and remove duplicates, keeping the latest by `created` timestamp
|
||||
const uniqueResourcesMap = new Map<string, QortalMetadata>();
|
||||
|
||||
[...existingResources, ...newResources].forEach((item) => {
|
||||
const key = `${item.service}-${item.name}-${item.identifier}`;
|
||||
const existingItem = uniqueResourcesMap.get(key);
|
||||
|
||||
if (!existingItem || item.created > existingItem.created) {
|
||||
uniqueResourcesMap.set(key, item);
|
||||
getResourceCache: (id, ignoreExpire) => {
|
||||
const cache = get().resourceCache[id];
|
||||
if (cache) {
|
||||
if (cache.expiry > Date.now() || ignoreExpire) {
|
||||
return cache.data; // ✅ Return data if not expired
|
||||
} else {
|
||||
set((state) => {
|
||||
const updatedCache = { ...state.resourceCache };
|
||||
delete updatedCache[id]; // ✅ Remove expired entry
|
||||
return { resourceCache: updatedCache };
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
searchCache: {
|
||||
...state.searchCache,
|
||||
[listName]: {
|
||||
...state.searchCache[listName],
|
||||
temporaryNewResources: Array.from(uniqueResourcesMap.values()), // Store unique items
|
||||
expiry, // Reset expiry
|
||||
},
|
||||
return null;
|
||||
},
|
||||
|
||||
setResourceCache: (id, data, customExpiry) =>
|
||||
set((state) => {
|
||||
const expiry = Date.now() + (customExpiry || 30 * 60 * 1000); // 30 mins
|
||||
return {
|
||||
resourceCache: {
|
||||
...state.resourceCache,
|
||||
[id]: { data, expiry },
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
setSearchCache: (listName, searchTerm, data, customExpiry) =>
|
||||
set((state) => {
|
||||
const expiry = Date.now() + (customExpiry || 5 * 60 * 1000); // 5 mins
|
||||
|
||||
return {
|
||||
searchCache: {
|
||||
...state.searchCache,
|
||||
[listName]: {
|
||||
searches: {
|
||||
...(state.searchCache[listName]?.searches || {}),
|
||||
[searchTerm]: data,
|
||||
},
|
||||
temporaryNewResources: state.searchCache[listName]?.temporaryNewResources || [],
|
||||
expiry,
|
||||
},
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
getSearchCache: (listName, searchTerm) => {
|
||||
const cache = get().searchCache[listName];
|
||||
if (cache) {
|
||||
if (cache.expiry > Date.now()) {
|
||||
return cache.searches[searchTerm] || null; // ✅ Return if valid
|
||||
} else {
|
||||
set((state) => {
|
||||
const updatedCache = { ...state.searchCache };
|
||||
delete updatedCache[listName]; // ✅ Remove expired list
|
||||
return { searchCache: updatedCache };
|
||||
});
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
addTemporaryResource: (listName, newResources, customExpiry) =>
|
||||
set((state) => {
|
||||
const expiry = Date.now() + (customExpiry || 5 * 60 * 1000);
|
||||
|
||||
const existingResources = state.searchCache[listName]?.temporaryNewResources || [];
|
||||
|
||||
// Merge & remove duplicates, keeping the latest by `created` timestamp
|
||||
const uniqueResourcesMap = new Map<string, QortalMetadata>();
|
||||
|
||||
[...existingResources, ...newResources].forEach((item) => {
|
||||
const key = `${item.service}-${item.name}-${item.identifier}`;
|
||||
const existingItem = uniqueResourcesMap.get(key);
|
||||
|
||||
if (!existingItem || item.created > existingItem.created) {
|
||||
uniqueResourcesMap.set(key, item);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
searchCache: {
|
||||
...state.searchCache,
|
||||
[listName]: {
|
||||
...state.searchCache[listName],
|
||||
temporaryNewResources: Array.from(uniqueResourcesMap.values()),
|
||||
expiry,
|
||||
},
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
getTemporaryResources: (listName: string) => {
|
||||
const cache = get().searchCache[listName];
|
||||
if (cache && cache.expiry > Date.now()) {
|
||||
return cache.temporaryNewResources || [];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
|
||||
markResourceAsDeleted: (item) =>
|
||||
set((state) => {
|
||||
const now = Date.now();
|
||||
const expiry = now + 5 * 60 * 1000; // ✅ Expires in 5 minutes
|
||||
|
||||
// ✅ Remove expired deletions before adding a new one
|
||||
const validDeletedResources = Object.fromEntries(
|
||||
Object.entries(state.deletedResources).filter(([_, value]) => value.expiry > now)
|
||||
);
|
||||
|
||||
const key = `${item.service}-${item.name}-${item.identifier}`;
|
||||
return {
|
||||
deletedResources: {
|
||||
...validDeletedResources, // ✅ Keep only non-expired ones
|
||||
[key]: { deleted: true, expiry },
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
filterOutDeletedResources: (items) => {
|
||||
const deletedResources = get().deletedResources; // ✅ Read without modifying store
|
||||
return items.filter(
|
||||
(item) => !deletedResources[`${item.service}-${item.name}-${item.identifier}`]
|
||||
);
|
||||
},
|
||||
isListExpired: (listName: string): boolean => {
|
||||
const cache = get().searchCache[listName];
|
||||
return cache ? cache.expiry <= Date.now() : true; // ✅ Expired if expiry timestamp is in the past
|
||||
},
|
||||
|
||||
|
||||
clearExpiredCache: () =>
|
||||
set((state) => {
|
||||
const now = Date.now();
|
||||
const validSearchCache = Object.fromEntries(
|
||||
Object.entries(state.searchCache).filter(([, value]) => value.expiry > now)
|
||||
);
|
||||
return { searchCache: validSearchCache };
|
||||
}),
|
||||
}),
|
||||
getTemporaryResources: (listName: string) => {
|
||||
const cache = get().searchCache[listName];
|
||||
if (cache && cache.expiry > Date.now()) {
|
||||
return cache.temporaryNewResources || [];
|
||||
}
|
||||
return []; // Return empty array if expired or doesn't exist
|
||||
},
|
||||
// Clear expired caches
|
||||
clearExpiredCache: () =>
|
||||
set((state) => {
|
||||
const now = Date.now();
|
||||
const validSearchCache = Object.fromEntries(
|
||||
Object.entries(state.searchCache).filter(
|
||||
([, value]) => value.expiry > now // Only keep unexpired lists
|
||||
)
|
||||
);
|
||||
return {
|
||||
searchCache: validSearchCache,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
);
|
@ -1,5 +1,6 @@
|
||||
import {create} from "zustand";
|
||||
import { QortalMetadata } from "../types/interfaces/resources";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
|
||||
interface ListsState {
|
||||
|
Loading…
x
Reference in New Issue
Block a user