This commit is contained in:
PhilReact 2025-03-24 09:03:56 +02:00
parent fb70b91622
commit 9ca28b0645
10 changed files with 62 additions and 66 deletions

6
package-lock.json generated
View File

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

View File

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

View File

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

View File

@ -89,7 +89,7 @@ export const VerticalPaginatedList = ({
</React.Fragment>
);
})}
{!disablePagination && displayedItems?.length <= (displayedLimit * 3) && (
{!disablePagination && displayedItems?.length >= (displayedLimit * 3) && (
<LazyLoad
onLoadMore={async () => {
await onLoadMore(displayedLimit);

View File

@ -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<GlobalContextType | null>(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]);

View File

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

View File

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

View File

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

View File

@ -39,36 +39,15 @@ interface EntityConfig {
children?: Record<string, EntityConfig>;
}
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<string> {
// 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<string> {
// Hash app name (11 chars)
const appHash: string = await hashWord(
@ -297,12 +274,14 @@ export interface SecretKeyValue {
messageKey: string;
}
export type SymmetricKeys = Record<number, SecretKeyValue>
export const decryptWithSymmetricKeys = async ({
base64,
secretKeyObject,
}: {
base64: string;
secretKeyObject: Record<number, SecretKeyValue>;
secretKeyObject: SymmetricKeys;
}) => {
// First, decode the base64-encoded input (if skipDecodeBase64 is not set)
const decodedData = base64;

28
src/utils/time.ts Normal file
View File

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