diff --git a/package-lock.json b/package-lock.json index a8f926d..2c413c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "bloom-filters": "^3.0.4", "buffer": "^6.0.3", "compressorjs": "^1.2.1", + "dayjs": "^1.11.13", "react": "^19.0.0", "react-dropzone": "^14.3.8", "react-intersection-observer": "^9.16.0", @@ -1595,6 +1596,11 @@ "resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz", "integrity": "sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==" }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", diff --git a/package.json b/package.json index 984fd84..73937ea 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "bloom-filters": "^3.0.4", "buffer": "^6.0.3", "compressorjs": "^1.2.1", + "dayjs": "^1.11.13", "react": "^19.0.0", "react-dropzone": "^14.3.8", "react-intersection-observer": "^9.16.0", diff --git a/src/components/ResourceList/HorizontalPaginationList.tsx b/src/components/ResourceList/HorizontalPaginationList.tsx index fdaa3e3..4dc26cd 100644 --- a/src/components/ResourceList/HorizontalPaginationList.tsx +++ b/src/components/ResourceList/HorizontalPaginationList.tsx @@ -77,7 +77,7 @@ const displayedItems = disablePagination ? items : items?.length < (displayedLim ))} > - {!disablePagination && displayedItems?.length <= (displayedLimit * 3) && ( + {!disablePagination && displayedItems?.length >= (displayedLimit * 3) && ( { await onLoadMore(displayedLimit); diff --git a/src/components/ResourceList/VerticalPaginationList.tsx b/src/components/ResourceList/VerticalPaginationList.tsx index 998ddfa..54b9ff8 100644 --- a/src/components/ResourceList/VerticalPaginationList.tsx +++ b/src/components/ResourceList/VerticalPaginationList.tsx @@ -89,7 +89,7 @@ export const VerticalPaginatedList = ({ ); })} - {!disablePagination && displayedItems?.length <= (displayedLimit * 3) && ( + {!disablePagination && displayedItems?.length >= (displayedLimit * 3) && ( { await onLoadMore(displayedLimit); diff --git a/src/context/GlobalProvider.tsx b/src/context/GlobalProvider.tsx index a361e8c..a27bf3d 100644 --- a/src/context/GlobalProvider.tsx +++ b/src/context/GlobalProvider.tsx @@ -2,11 +2,12 @@ import React, { createContext, useContext, useMemo } from "react"; import { useAuth, UseAuthProps } from "../hooks/useAuth"; import { useResources } from "../hooks/useResources"; import { useAppInfo } from "../hooks/useAppInfo"; -import { addAndEncryptSymmetricKeys, decryptWithSymmetricKeys, encryptWithSymmetricKeys, IdentifierBuilder } from "../utils/encryption"; +import { addAndEncryptSymmetricKeys, decryptWithSymmetricKeys, encryptWithSymmetricKeys } from "../utils/encryption"; import { useIdentifiers } from "../hooks/useIdentifiers"; import { objectToBase64 } from "../utils/base64"; import { base64ToObject } from "../utils/publish"; import { generateBloomFilterBase64, isInsideBloom } from "../utils/bloomFilter"; +import { formatTimestamp } from "../utils/time"; const utils = { @@ -16,7 +17,8 @@ const utils = { encryptWithSymmetricKeys, decryptWithSymmetricKeys, generateBloomFilterBase64, - isInsideBloom + isInsideBloom, + formatTimestamp } @@ -39,7 +41,6 @@ interface GlobalProviderProps { appName: string; publicSalt: string }; - identifierBuilder?: IdentifierBuilder } // ✅ Create Context with Proper Type @@ -48,12 +49,12 @@ const GlobalContext = createContext(null); // 🔹 Global Provider (Handles Multiple Hooks) -export const GlobalProvider = ({ children, config, identifierBuilder }: GlobalProviderProps) => { +export const GlobalProvider = ({ children, config }: GlobalProviderProps) => { // ✅ Call hooks and pass in options dynamically const auth = useAuth(config?.auth || {}); - const appInfo = useAppInfo(config?.appName, config?.publicSalt) + const appInfo = useAppInfo(config.appName, config?.publicSalt) const lists = useResources() - const identifierOperations = useIdentifiers(identifierBuilder, config?.publicSalt) + const identifierOperations = useIdentifiers(config.publicSalt, config.appName) // ✅ Merge all hooks into a single `contextValue` const contextValue = useMemo(() => ({ auth, lists, appInfo, identifierOperations, utils }), [auth, lists, appInfo, identifierOperations]); diff --git a/src/hooks/useIdentifiers.tsx b/src/hooks/useIdentifiers.tsx index 3fc1e1f..b47ae0c 100644 --- a/src/hooks/useIdentifiers.tsx +++ b/src/hooks/useIdentifiers.tsx @@ -1,48 +1,33 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { useAuthStore } from "../state/auth"; +import React, { useCallback, useEffect, useMemo } from "react"; import { useAppStore } from "../state/app"; -import { buildIdentifier, buildSearchPrefix, EnumCollisionStrength, hashWord, IdentifierBuilder } from "../utils/encryption"; +import { buildIdentifier, buildSearchPrefix, EnumCollisionStrength, hashWord } from "../utils/encryption"; -export const useIdentifiers = (builder?: IdentifierBuilder, publicSalt?: string) => { - const setIdentifierBuilder = useAppStore().setIdentifierBuilder - const identifierBuilder = useAppStore().identifierBuilder - const appName = useAppStore().appName +export const useIdentifiers = (publicSalt: string, appName: string) => { - const stringifiedBuilder = useMemo(()=> { - return JSON.stringify(builder) - }, [builder]) const buildIdentifierFunc = useCallback(( entityType: string, parentId: string | null)=> { - if(!appName || !publicSalt || !identifierBuilder) return null - return buildIdentifier(appName, publicSalt, entityType, parentId, identifierBuilder) - }, [appName, publicSalt, identifierBuilder]) + return buildIdentifier(appName, publicSalt, entityType, parentId) + }, [appName, publicSalt]) const buildSearchPrefixFunc = useCallback(( entityType: string, parentId: string | null)=> { - if(!appName || !publicSalt || !identifierBuilder) return null - return buildSearchPrefix(appName, publicSalt, entityType, parentId, identifierBuilder) - }, [appName, publicSalt, identifierBuilder]) + return buildSearchPrefix(appName, publicSalt, entityType, parentId) + }, [appName, publicSalt]) const createSingleIdentifier = useCallback(async ( partialIdentifier: string)=> { - if(!partialIdentifier || !appName || !publicSalt) return null 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(()=> { - if(stringifiedBuilder){ - setIdentifierBuilder(JSON.parse(stringifiedBuilder)) - } - }, [stringifiedBuilder]) + return { buildIdentifier: buildIdentifierFunc, buildSearchPrefix: buildSearchPrefixFunc, diff --git a/src/index.ts b/src/index.ts index 28eca9b..c6921be 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,4 +7,6 @@ export {QortalSearchParams} from './types/interfaces/resources' export {ImagePicker} from './common/ImagePicker' export {useNameSearch} from './hooks/useNameSearch' export {Resource} from './hooks/useResources' - +export {Service} from './types/interfaces/resources' +export {ListItem} from './state/cache' +export {SymmetricKeys} from './utils/encryption' \ No newline at end of file diff --git a/src/state/app.ts b/src/state/app.ts index ee9b263..ccd46e8 100644 --- a/src/state/app.ts +++ b/src/state/app.ts @@ -1,14 +1,11 @@ import { create } from "zustand"; -import { IdentifierBuilder } from "../utils/encryption"; interface AppState { appName: string | null; publicSalt: string | null; appNameHashed: string | null; - identifierBuilder?: IdentifierBuilder | null // Methods setAppState: (appState: { appName: string; publicSalt: string; appNameHashed: string }) => void; - setIdentifierBuilder: (builder: IdentifierBuilder) => void; } // ✅ Typed Zustand Store @@ -16,10 +13,7 @@ export const useAppStore = create((set) => ({ appName: null, publicSalt: null, appNameHashed: null, - identifierBuilder: null, // Methods setAppState: (appState) => set({ appName: appState.appName, publicSalt: appState.publicSalt, appNameHashed: appState.appNameHashed }), - setIdentifierBuilder: (identifierBuilder) => - set({ identifierBuilder }) })); diff --git a/src/utils/encryption.ts b/src/utils/encryption.ts index bab7bf4..29f0ca8 100644 --- a/src/utils/encryption.ts +++ b/src/utils/encryption.ts @@ -39,36 +39,15 @@ interface EntityConfig { children?: Record; } -export type IdentifierBuilder = { - [key: string]: { - children?: IdentifierBuilder; - }; -}; -// Recursive function to traverse identifierBuilder -function findEntityConfig( - identifierBuilder: IdentifierBuilder, - path: string[] -): EntityConfig { - let current: EntityConfig | undefined = { children: identifierBuilder }; // ✅ Wrap it inside `{ children }` so it behaves like other levels - for (const key of path) { - if (!current.children || !current.children[key]) { - throw new Error(`Entity '${key}' is not defined in identifierBuilder`); - } - current = current.children[key]; - } - - return current; -} // Function to generate a prefix for searching export async function buildSearchPrefix( appName: string, publicSalt: string, entityType: string, - parentId: string | null, - identifierBuilder: IdentifierBuilder + parentId: string | null ): Promise { // Hash app name (11 chars) const appHash: string = await hashWord( @@ -84,12 +63,11 @@ export async function buildSearchPrefix( publicSalt ); - // ✅ Detect if this entity is actually a root entity - const isRootEntity = !!identifierBuilder[entityType]; + // Determine parent reference let parentRef = ""; - if (isRootEntity && parentId === null) { + if (parentId === null) { parentRef = "00000000000000"; // ✅ Only for true root entities } else if (parentId) { parentRef = await hashWord( @@ -110,8 +88,7 @@ export async function buildIdentifier( appName: string, publicSalt: string, entityType: string, // ✅ Now takes only the entity type - parentId: string | null, - identifierBuilder: IdentifierBuilder + parentId: string | null ): Promise { // Hash app name (11 chars) const appHash: string = await hashWord( @@ -297,12 +274,14 @@ export interface SecretKeyValue { messageKey: string; } +export type SymmetricKeys = Record + export const decryptWithSymmetricKeys = async ({ base64, secretKeyObject, }: { base64: string; - secretKeyObject: Record; + secretKeyObject: SymmetricKeys; }) => { // First, decode the base64-encoded input (if skipDecodeBase64 is not set) const decodedData = base64; diff --git a/src/utils/time.ts b/src/utils/time.ts new file mode 100644 index 0000000..68c87a4 --- /dev/null +++ b/src/utils/time.ts @@ -0,0 +1,28 @@ +import dayjs from "dayjs"; +import relativeTime from "dayjs/plugin/relativeTime"; +import utc from "dayjs/plugin/utc"; + +dayjs.extend(relativeTime); +dayjs.extend(utc); + +export function formatTimestamp(timestamp: number): string { + const now = dayjs(); + const timestampDayJs = dayjs(timestamp); + const elapsedTime = now.diff(timestampDayJs, "minute"); + + if (elapsedTime < 1) { + return `Just now - ${timestampDayJs.format("h:mm A")}`; + } else if (elapsedTime < 60) { + return `${elapsedTime}m ago - ${timestampDayJs.format("h:mm A")}`; + } else if (elapsedTime < 1440) { + return `${Math.floor(elapsedTime / 60)}h ago - ${timestampDayJs.format("h:mm A")}`; + } else { + return timestampDayJs.format("MMM D, YYYY - h:mm A"); + } +} + + +export function oneMonthAgo(){ + const oneMonthAgoTimestamp = dayjs().subtract(1, "month").valueOf(); + return oneMonthAgoTimestamp +} \ No newline at end of file