mirror of
https://github.com/Qortal/qapp-core.git
synced 2025-06-14 17:41:20 +00:00
fixes
This commit is contained in:
parent
9ca28b0645
commit
2898eeabf0
40
package-lock.json
generated
40
package-lock.json
generated
@ -19,8 +19,10 @@
|
||||
"buffer": "^6.0.3",
|
||||
"compressorjs": "^1.2.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"dompurify": "^3.2.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-hot-toast": "^2.5.2",
|
||||
"react-intersection-observer": "^9.16.0",
|
||||
"short-unique-id": "^5.2.0",
|
||||
"zustand": "^4.3.2"
|
||||
@ -1304,6 +1306,12 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-3.0.8.tgz",
|
||||
"integrity": "sha512-TY1eezMU2zH2ozQoAFAQFOPpvP15g+ZgSfTZt31AUUH/Rxtnz3H+A/Sv1Snw2/amp//omibc+AEkTaA8KUeOLQ=="
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||
@ -1626,6 +1634,14 @@
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz",
|
||||
"integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
@ -1793,6 +1809,14 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/goober": {
|
||||
"version": "2.1.16",
|
||||
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz",
|
||||
"integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==",
|
||||
"peerDependencies": {
|
||||
"csstype": "^3.0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
@ -2278,6 +2302,22 @@
|
||||
"react": ">= 16.8 || 18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hot-toast": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.2.tgz",
|
||||
"integrity": "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw==",
|
||||
"dependencies": {
|
||||
"csstype": "^3.1.3",
|
||||
"goober": "^2.1.16"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16",
|
||||
"react-dom": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/react-intersection-observer": {
|
||||
"version": "9.16.0",
|
||||
"resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz",
|
||||
|
@ -30,8 +30,10 @@
|
||||
"buffer": "^6.0.3",
|
||||
"compressorjs": "^1.2.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"dompurify": "^3.2.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-hot-toast": "^2.5.2",
|
||||
"react-intersection-observer": "^9.16.0",
|
||||
"short-unique-id": "^5.2.0",
|
||||
"zustand": "^4.3.2"
|
||||
|
@ -52,6 +52,10 @@ export interface DefaultLoaderParams {
|
||||
|
||||
export type ReturnType = 'JSON' | 'BASE64'
|
||||
|
||||
export interface Results {
|
||||
resourceItems: QortalMetadata[]
|
||||
isLoadingList: boolean
|
||||
}
|
||||
interface BaseProps {
|
||||
search: QortalSearchParams;
|
||||
entityParams?: EntityParams;
|
||||
@ -68,8 +72,9 @@ interface BaseProps {
|
||||
resourceCacheDuration?: number
|
||||
disablePagination?: boolean
|
||||
disableScrollTracker?: boolean
|
||||
retryAttempts: number,
|
||||
retryAttempts: number
|
||||
returnType: 'JSON' | 'BASE64'
|
||||
onResults?: (results: Results)=> void
|
||||
}
|
||||
|
||||
// ✅ Restrict `direction` only when `disableVirtualization = false`
|
||||
@ -105,7 +110,8 @@ export const MemorizedComponent = ({
|
||||
disableScrollTracker,
|
||||
entityParams,
|
||||
returnType = 'JSON',
|
||||
retryAttempts = 2
|
||||
retryAttempts = 2,
|
||||
onResults
|
||||
}: PropsResourceListDisplay) => {
|
||||
const { filterOutDeletedResources } = useCacheStore();
|
||||
const {identifierOperations, lists} = useGlobal()
|
||||
@ -210,7 +216,14 @@ export const MemorizedComponent = ({
|
||||
return filterOutDeletedResources([...temporaryResources, ...list])
|
||||
}, [list, listName, deletedResources, temporaryResources])
|
||||
|
||||
|
||||
useEffect(()=> {
|
||||
if(onResults){
|
||||
onResults({
|
||||
resourceItems: listToDisplay,
|
||||
isLoadingList: isLoading
|
||||
})
|
||||
}
|
||||
}, [listToDisplay, onResults, isLoading])
|
||||
|
||||
|
||||
const getResourceMoreList = useCallback(async (displayLimit?: number) => {
|
||||
@ -267,7 +280,7 @@ export const MemorizedComponent = ({
|
||||
return (
|
||||
<div ref={elementRef} style={{
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
height: disableVirtualization ? 'auto' : '100%'
|
||||
}}>
|
||||
<ListLoader
|
||||
noResultsMessage={
|
||||
@ -284,7 +297,7 @@ export const MemorizedComponent = ({
|
||||
<div
|
||||
|
||||
style={{
|
||||
height: "100%",
|
||||
height: disableVirtualization ? 'auto' : "100%",
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
}}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { createContext, useContext, useMemo } from "react";
|
||||
import React, { createContext, CSSProperties, useContext, useMemo } from "react";
|
||||
import { useAuth, UseAuthProps } from "../hooks/useAuth";
|
||||
import { useResources } from "../hooks/useResources";
|
||||
import { useAppInfo } from "../hooks/useAppInfo";
|
||||
@ -8,6 +8,7 @@ import { objectToBase64 } from "../utils/base64";
|
||||
import { base64ToObject } from "../utils/publish";
|
||||
import { generateBloomFilterBase64, isInsideBloom } from "../utils/bloomFilter";
|
||||
import { formatTimestamp } from "../utils/time";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
|
||||
const utils = {
|
||||
@ -41,6 +42,7 @@ interface GlobalProviderProps {
|
||||
appName: string;
|
||||
publicSalt: string
|
||||
};
|
||||
toastStyle: CSSProperties
|
||||
}
|
||||
|
||||
// ✅ Create Context with Proper Type
|
||||
@ -49,7 +51,7 @@ const GlobalContext = createContext<GlobalContextType | null>(null);
|
||||
|
||||
|
||||
// 🔹 Global Provider (Handles Multiple Hooks)
|
||||
export const GlobalProvider = ({ children, config }: GlobalProviderProps) => {
|
||||
export const GlobalProvider = ({ children, config, toastStyle = {} }: GlobalProviderProps) => {
|
||||
// ✅ Call hooks and pass in options dynamically
|
||||
const auth = useAuth(config?.auth || {});
|
||||
const appInfo = useAppInfo(config.appName, config?.publicSalt)
|
||||
@ -60,6 +62,14 @@ export const GlobalProvider = ({ children, config }: GlobalProviderProps) => {
|
||||
const contextValue = useMemo(() => ({ auth, lists, appInfo, identifierOperations, utils }), [auth, lists, appInfo, identifierOperations]);
|
||||
return (
|
||||
<GlobalContext.Provider value={contextValue}>
|
||||
<Toaster
|
||||
position="top-center"
|
||||
toastOptions={{
|
||||
duration: 4000,
|
||||
style: toastStyle
|
||||
}}
|
||||
containerStyle={{zIndex: 999999}}
|
||||
/>
|
||||
{children}
|
||||
</GlobalContext.Provider>
|
||||
);
|
||||
|
5
src/global.d.ts
vendored
5
src/global.d.ts
vendored
@ -36,13 +36,14 @@ interface QortalRequestOptions {
|
||||
prefix?: boolean
|
||||
exactMatchNames?: boolean
|
||||
base64?: string
|
||||
groupId?: number
|
||||
groupId?: number | string
|
||||
isAdmins?: boolean
|
||||
payments?: any[]
|
||||
assetId?: number,
|
||||
publicKeys?: string[],
|
||||
publicKeys?: string[]
|
||||
recipient?: string,
|
||||
before?: number | null
|
||||
qortalLink?: string
|
||||
}
|
||||
|
||||
declare function qortalRequest(options: QortalRequestOptions): Promise<any>
|
||||
|
@ -15,7 +15,7 @@ export const useNameSearch = (value: string, limit = 20) => {
|
||||
setNameList([])
|
||||
return
|
||||
}
|
||||
setIsLoading(true);
|
||||
|
||||
const res = await fetch(
|
||||
`/names/search?query=${name}&prefix=true&limit=${listLimit}`
|
||||
);
|
||||
@ -36,6 +36,7 @@ export const useNameSearch = (value: string, limit = 20) => {
|
||||
);
|
||||
// Debounce logic
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
const handler = setTimeout(() => {
|
||||
checkIfNameExisits(value, limit);
|
||||
}, 500);
|
||||
|
@ -8,6 +8,7 @@ import { RequestQueueWithPromise } from "../utils/queue";
|
||||
import { base64ToUint8Array, uint8ArrayToObject } from "../utils/base64";
|
||||
import { retryTransaction } from "../utils/publish";
|
||||
import { ReturnType } from "../components/ResourceList/ResourceListDisplay";
|
||||
import { useListStore } from "../state/lists";
|
||||
|
||||
export const requestQueueProductPublishes = new RequestQueueWithPromise(20);
|
||||
export const requestQueueProductPublishesBackup = new RequestQueueWithPromise(
|
||||
@ -25,8 +26,9 @@ export const useResources = (retryAttempts: number = 2) => {
|
||||
getResourceCache,
|
||||
setResourceCache,
|
||||
addTemporaryResource,
|
||||
markResourceAsDeleted,
|
||||
markResourceAsDeleted
|
||||
} = useCacheStore();
|
||||
const deleteList = useListStore(state => state.deleteList)
|
||||
const requestControllers = new Map<string, AbortController>();
|
||||
|
||||
const getArbitraryResource = async (
|
||||
@ -305,6 +307,7 @@ export const useResources = (retryAttempts: number = 2) => {
|
||||
addNewResources,
|
||||
updateNewResources,
|
||||
deleteResource,
|
||||
deleteList
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
import './index.css'
|
||||
export { showLoading, dismissToast, showError, showSuccess } from './utils/toast';
|
||||
export { processText, sanitizedContent, extractComponents, handleClickText} from './utils/text';
|
||||
export { RequestQueueWithPromise } from './utils/queue';
|
||||
export { GlobalProvider, useGlobal } from "./context/GlobalProvider";
|
||||
export {usePublish} from "./hooks/usePublish"
|
||||
@ -9,4 +11,4 @@ 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'
|
||||
export {SymmetricKeys} from './utils/encryption'
|
||||
|
@ -150,11 +150,29 @@ const getPublicKeysByNames = async (names: string[]) => {
|
||||
export const addAndEncryptSymmetricKeys = async ({
|
||||
previousData,
|
||||
names,
|
||||
disableAddNewKey
|
||||
}: {
|
||||
previousData: Object;
|
||||
names: string[];
|
||||
disableAddNewKey?: boolean
|
||||
}) => {
|
||||
try {
|
||||
if(disableAddNewKey){
|
||||
|
||||
const groupmemberPublicKeys = await getPublicKeysByNames(names);
|
||||
const symmetricKeyAndNonceBase64 = await objectToBase64(previousData);
|
||||
const encryptedData = await qortalRequest({
|
||||
action: "ENCRYPT_DATA",
|
||||
base64: symmetricKeyAndNonceBase64,
|
||||
publicKeys: groupmemberPublicKeys,
|
||||
});
|
||||
|
||||
if (encryptedData) {
|
||||
return {encryptedData, publicKeys: groupmemberPublicKeys, symmetricKeys: previousData};
|
||||
} else {
|
||||
throw new Error("Cannot encrypt content");
|
||||
}
|
||||
}
|
||||
let highestKey = 0;
|
||||
if (previousData && Object.keys(previousData)?.length > 0) {
|
||||
highestKey = Math.max(
|
||||
|
123
src/utils/text.ts
Normal file
123
src/utils/text.ts
Normal file
@ -0,0 +1,123 @@
|
||||
import DOMPurify from 'dompurify';
|
||||
import React from 'react'
|
||||
export function processText(input: string): string {
|
||||
const linkRegex = /(qortal:\/\/\S+)/g;
|
||||
|
||||
function processNode(node: Node): void {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
const textContent = node.textContent ?? '';
|
||||
const parts = textContent.split(linkRegex);
|
||||
|
||||
if (parts.length > 0) {
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
parts.forEach((part) => {
|
||||
if (part.startsWith('qortal://')) {
|
||||
const link = document.createElement('span');
|
||||
link.setAttribute('data-url', part);
|
||||
link.textContent = part;
|
||||
link.style.color = 'var(--code-block-text-color)';
|
||||
link.style.textDecoration = 'underline';
|
||||
link.style.cursor = 'pointer';
|
||||
fragment.appendChild(link);
|
||||
} else {
|
||||
fragment.appendChild(document.createTextNode(part));
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ Ensure node is a ChildNode before calling replaceWith
|
||||
const parent = node.parentNode;
|
||||
if (parent) {
|
||||
parent.replaceChild(fragment, node);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Array.from(node.childNodes).forEach(processNode);
|
||||
}
|
||||
}
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.innerHTML = input;
|
||||
processNode(wrapper);
|
||||
|
||||
return wrapper.innerHTML;
|
||||
}
|
||||
|
||||
|
||||
export const sanitizedContent = (htmlContent: string)=> {
|
||||
return DOMPurify.sanitize(htmlContent, {
|
||||
ALLOWED_TAGS: [
|
||||
'a', 'b', 'i', 'em', 'strong', 'p', 'br', 'div', 'span', 'img',
|
||||
'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'code', 'pre', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 's', 'hr'
|
||||
],
|
||||
ALLOWED_ATTR: [
|
||||
'href', 'target', 'rel', 'class', 'src', 'alt', 'title',
|
||||
'width', 'height', 'style', 'align', 'valign', 'colspan', 'rowspan', 'border', 'cellpadding', 'cellspacing', 'data-url'
|
||||
],
|
||||
}).replace(/<span[^>]*data-url="qortal:\/\/use-embed\/[^"]*"[^>]*>.*?<\/span>/g, '');
|
||||
};
|
||||
|
||||
export const extractComponents = (url: string) => {
|
||||
if (!url || !url.startsWith("qortal://")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Skip links starting with "qortal://use-"
|
||||
if (url.startsWith("qortal://use-")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
url = url.replace(/^(qortal\:\/\/)/, "");
|
||||
if (url.includes("/")) {
|
||||
let parts = url.split("/");
|
||||
const service = parts[0].toUpperCase();
|
||||
parts.shift();
|
||||
const name = parts[0];
|
||||
parts.shift();
|
||||
let identifier;
|
||||
const path = parts.join("/");
|
||||
return { service, name, identifier, path };
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const handleClickText = async (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.preventDefault();
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.getAttribute('data-url')) {
|
||||
const url = target.getAttribute('data-url');
|
||||
if(!url) return
|
||||
let copyUrl: string = url
|
||||
|
||||
try {
|
||||
copyUrl = copyUrl?.replace(/^(qortal:\/\/)/, '')
|
||||
if (copyUrl && copyUrl?.startsWith('use-')) {
|
||||
// Handle the new 'use' format
|
||||
const parts = copyUrl.split('/')
|
||||
parts.shift()
|
||||
const action = parts.length > 0 ? parts[0].split('-')[1] : null // e.g., 'invite' from 'action-invite'
|
||||
parts.shift()
|
||||
const id = parts.length > 0 ? parts[0].split('-')[1] : null // e.g., '321' from 'groupid-321'
|
||||
if(action === 'join' && id){
|
||||
e.stopPropagation()
|
||||
qortalRequest({
|
||||
action: 'JOIN_GROUP',
|
||||
groupId: id
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
//error
|
||||
}
|
||||
const res = extractComponents(url);
|
||||
if (res) {
|
||||
e.stopPropagation()
|
||||
qortalRequest({
|
||||
action: 'OPEN_NEW_TAB',
|
||||
qortalLink: url
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
31
src/utils/toast.tsx
Normal file
31
src/utils/toast.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { toast } from "react-hot-toast";
|
||||
import React from "react";
|
||||
export const showSuccess = (message: string, duration = 4000) => {
|
||||
toast.success(message, {
|
||||
duration,
|
||||
});
|
||||
};
|
||||
|
||||
export const showError = (message: string, duration = 4000) => {
|
||||
toast.error(message, {
|
||||
duration,
|
||||
});
|
||||
};
|
||||
|
||||
export const showLoading = (message: string, duration = Infinity): string => {
|
||||
return toast.loading(
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<span role="img" aria-label="loading-icon">
|
||||
⏳
|
||||
</span>
|
||||
<span style={{ marginLeft: 8 }}>{message}</span>
|
||||
</div>,
|
||||
{
|
||||
duration,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const dismissToast = (toastId?: string) => {
|
||||
toast.dismiss(toastId);
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user