added list funcitons and keep scroll height before expiry

This commit is contained in:
PhilReact 2025-03-15 15:16:32 +02:00
parent e8866f1585
commit 52f1d570a6
13 changed files with 480 additions and 144 deletions

13
package-lock.json generated
View File

@ -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"
}
},

View File

@ -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"
},

View File

@ -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,

View 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
};

View 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]);
};

View File

@ -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",

View 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>
);
};

View File

@ -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
View File

@ -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>

View 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;
}

View File

@ -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
};
};

View File

@ -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,
};
}),
}));
);

View File

@ -1,5 +1,6 @@
import {create} from "zustand";
import { QortalMetadata } from "../types/interfaces/resources";
import { persist } from "zustand/middleware";
interface ListsState {