mirror of
https://github.com/Qortal/qapp-core.git
synced 2025-06-15 01:41:21 +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",
|
"buffer": "^6.0.3",
|
||||||
"compressorjs": "^1.2.1",
|
"compressorjs": "^1.2.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
|
"dompurify": "^3.2.4",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dropzone": "^14.3.8",
|
"react-dropzone": "^14.3.8",
|
||||||
|
"react-hot-toast": "^2.5.2",
|
||||||
"react-intersection-observer": "^9.16.0",
|
"react-intersection-observer": "^9.16.0",
|
||||||
"short-unique-id": "^5.2.0",
|
"short-unique-id": "^5.2.0",
|
||||||
"zustand": "^4.3.2"
|
"zustand": "^4.3.2"
|
||||||
@ -1304,6 +1306,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-3.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-3.0.8.tgz",
|
||||||
"integrity": "sha512-TY1eezMU2zH2ozQoAFAQFOPpvP15g+ZgSfTZt31AUUH/Rxtnz3H+A/Sv1Snw2/amp//omibc+AEkTaA8KUeOLQ=="
|
"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": {
|
"node_modules/ansi-regex": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||||
@ -1626,6 +1634,14 @@
|
|||||||
"csstype": "^3.0.2"
|
"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": {
|
"node_modules/eastasianwidth": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||||
@ -1793,6 +1809,14 @@
|
|||||||
"node": ">=4"
|
"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": {
|
"node_modules/hasown": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
@ -2278,6 +2302,22 @@
|
|||||||
"react": ">= 16.8 || 18.0.0"
|
"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": {
|
"node_modules/react-intersection-observer": {
|
||||||
"version": "9.16.0",
|
"version": "9.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz",
|
||||||
|
@ -30,8 +30,10 @@
|
|||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"compressorjs": "^1.2.1",
|
"compressorjs": "^1.2.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
|
"dompurify": "^3.2.4",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dropzone": "^14.3.8",
|
"react-dropzone": "^14.3.8",
|
||||||
|
"react-hot-toast": "^2.5.2",
|
||||||
"react-intersection-observer": "^9.16.0",
|
"react-intersection-observer": "^9.16.0",
|
||||||
"short-unique-id": "^5.2.0",
|
"short-unique-id": "^5.2.0",
|
||||||
"zustand": "^4.3.2"
|
"zustand": "^4.3.2"
|
||||||
|
@ -52,6 +52,10 @@ export interface DefaultLoaderParams {
|
|||||||
|
|
||||||
export type ReturnType = 'JSON' | 'BASE64'
|
export type ReturnType = 'JSON' | 'BASE64'
|
||||||
|
|
||||||
|
export interface Results {
|
||||||
|
resourceItems: QortalMetadata[]
|
||||||
|
isLoadingList: boolean
|
||||||
|
}
|
||||||
interface BaseProps {
|
interface BaseProps {
|
||||||
search: QortalSearchParams;
|
search: QortalSearchParams;
|
||||||
entityParams?: EntityParams;
|
entityParams?: EntityParams;
|
||||||
@ -68,8 +72,9 @@ interface BaseProps {
|
|||||||
resourceCacheDuration?: number
|
resourceCacheDuration?: number
|
||||||
disablePagination?: boolean
|
disablePagination?: boolean
|
||||||
disableScrollTracker?: boolean
|
disableScrollTracker?: boolean
|
||||||
retryAttempts: number,
|
retryAttempts: number
|
||||||
returnType: 'JSON' | 'BASE64'
|
returnType: 'JSON' | 'BASE64'
|
||||||
|
onResults?: (results: Results)=> void
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Restrict `direction` only when `disableVirtualization = false`
|
// ✅ Restrict `direction` only when `disableVirtualization = false`
|
||||||
@ -105,7 +110,8 @@ export const MemorizedComponent = ({
|
|||||||
disableScrollTracker,
|
disableScrollTracker,
|
||||||
entityParams,
|
entityParams,
|
||||||
returnType = 'JSON',
|
returnType = 'JSON',
|
||||||
retryAttempts = 2
|
retryAttempts = 2,
|
||||||
|
onResults
|
||||||
}: PropsResourceListDisplay) => {
|
}: PropsResourceListDisplay) => {
|
||||||
const { filterOutDeletedResources } = useCacheStore();
|
const { filterOutDeletedResources } = useCacheStore();
|
||||||
const {identifierOperations, lists} = useGlobal()
|
const {identifierOperations, lists} = useGlobal()
|
||||||
@ -210,7 +216,14 @@ export const MemorizedComponent = ({
|
|||||||
return filterOutDeletedResources([...temporaryResources, ...list])
|
return filterOutDeletedResources([...temporaryResources, ...list])
|
||||||
}, [list, listName, deletedResources, temporaryResources])
|
}, [list, listName, deletedResources, temporaryResources])
|
||||||
|
|
||||||
|
useEffect(()=> {
|
||||||
|
if(onResults){
|
||||||
|
onResults({
|
||||||
|
resourceItems: listToDisplay,
|
||||||
|
isLoadingList: isLoading
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [listToDisplay, onResults, isLoading])
|
||||||
|
|
||||||
|
|
||||||
const getResourceMoreList = useCallback(async (displayLimit?: number) => {
|
const getResourceMoreList = useCallback(async (displayLimit?: number) => {
|
||||||
@ -267,7 +280,7 @@ export const MemorizedComponent = ({
|
|||||||
return (
|
return (
|
||||||
<div ref={elementRef} style={{
|
<div ref={elementRef} style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%'
|
height: disableVirtualization ? 'auto' : '100%'
|
||||||
}}>
|
}}>
|
||||||
<ListLoader
|
<ListLoader
|
||||||
noResultsMessage={
|
noResultsMessage={
|
||||||
@ -284,7 +297,7 @@ export const MemorizedComponent = ({
|
|||||||
<div
|
<div
|
||||||
|
|
||||||
style={{
|
style={{
|
||||||
height: "100%",
|
height: disableVirtualization ? 'auto' : "100%",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
width: "100%",
|
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 { useAuth, UseAuthProps } from "../hooks/useAuth";
|
||||||
import { useResources } from "../hooks/useResources";
|
import { useResources } from "../hooks/useResources";
|
||||||
import { useAppInfo } from "../hooks/useAppInfo";
|
import { useAppInfo } from "../hooks/useAppInfo";
|
||||||
@ -8,6 +8,7 @@ import { objectToBase64 } from "../utils/base64";
|
|||||||
import { base64ToObject } from "../utils/publish";
|
import { base64ToObject } from "../utils/publish";
|
||||||
import { generateBloomFilterBase64, isInsideBloom } from "../utils/bloomFilter";
|
import { generateBloomFilterBase64, isInsideBloom } from "../utils/bloomFilter";
|
||||||
import { formatTimestamp } from "../utils/time";
|
import { formatTimestamp } from "../utils/time";
|
||||||
|
import { Toaster } from "react-hot-toast";
|
||||||
|
|
||||||
|
|
||||||
const utils = {
|
const utils = {
|
||||||
@ -41,6 +42,7 @@ interface GlobalProviderProps {
|
|||||||
appName: string;
|
appName: string;
|
||||||
publicSalt: string
|
publicSalt: string
|
||||||
};
|
};
|
||||||
|
toastStyle: CSSProperties
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Create Context with Proper Type
|
// ✅ Create Context with Proper Type
|
||||||
@ -49,7 +51,7 @@ const GlobalContext = createContext<GlobalContextType | null>(null);
|
|||||||
|
|
||||||
|
|
||||||
// 🔹 Global Provider (Handles Multiple Hooks)
|
// 🔹 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
|
// ✅ Call hooks and pass in options dynamically
|
||||||
const auth = useAuth(config?.auth || {});
|
const auth = useAuth(config?.auth || {});
|
||||||
const appInfo = useAppInfo(config.appName, config?.publicSalt)
|
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]);
|
const contextValue = useMemo(() => ({ auth, lists, appInfo, identifierOperations, utils }), [auth, lists, appInfo, identifierOperations]);
|
||||||
return (
|
return (
|
||||||
<GlobalContext.Provider value={contextValue}>
|
<GlobalContext.Provider value={contextValue}>
|
||||||
|
<Toaster
|
||||||
|
position="top-center"
|
||||||
|
toastOptions={{
|
||||||
|
duration: 4000,
|
||||||
|
style: toastStyle
|
||||||
|
}}
|
||||||
|
containerStyle={{zIndex: 999999}}
|
||||||
|
/>
|
||||||
{children}
|
{children}
|
||||||
</GlobalContext.Provider>
|
</GlobalContext.Provider>
|
||||||
);
|
);
|
||||||
|
5
src/global.d.ts
vendored
5
src/global.d.ts
vendored
@ -36,13 +36,14 @@ interface QortalRequestOptions {
|
|||||||
prefix?: boolean
|
prefix?: boolean
|
||||||
exactMatchNames?: boolean
|
exactMatchNames?: boolean
|
||||||
base64?: string
|
base64?: string
|
||||||
groupId?: number
|
groupId?: number | string
|
||||||
isAdmins?: boolean
|
isAdmins?: boolean
|
||||||
payments?: any[]
|
payments?: any[]
|
||||||
assetId?: number,
|
assetId?: number,
|
||||||
publicKeys?: string[],
|
publicKeys?: string[]
|
||||||
recipient?: string,
|
recipient?: string,
|
||||||
before?: number | null
|
before?: number | null
|
||||||
|
qortalLink?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
declare function qortalRequest(options: QortalRequestOptions): Promise<any>
|
declare function qortalRequest(options: QortalRequestOptions): Promise<any>
|
||||||
|
@ -15,7 +15,7 @@ export const useNameSearch = (value: string, limit = 20) => {
|
|||||||
setNameList([])
|
setNameList([])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setIsLoading(true);
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`/names/search?query=${name}&prefix=true&limit=${listLimit}`
|
`/names/search?query=${name}&prefix=true&limit=${listLimit}`
|
||||||
);
|
);
|
||||||
@ -36,6 +36,7 @@ export const useNameSearch = (value: string, limit = 20) => {
|
|||||||
);
|
);
|
||||||
// Debounce logic
|
// Debounce logic
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setIsLoading(true);
|
||||||
const handler = setTimeout(() => {
|
const handler = setTimeout(() => {
|
||||||
checkIfNameExisits(value, limit);
|
checkIfNameExisits(value, limit);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
@ -8,6 +8,7 @@ import { RequestQueueWithPromise } from "../utils/queue";
|
|||||||
import { base64ToUint8Array, uint8ArrayToObject } from "../utils/base64";
|
import { base64ToUint8Array, uint8ArrayToObject } from "../utils/base64";
|
||||||
import { retryTransaction } from "../utils/publish";
|
import { retryTransaction } from "../utils/publish";
|
||||||
import { ReturnType } from "../components/ResourceList/ResourceListDisplay";
|
import { ReturnType } from "../components/ResourceList/ResourceListDisplay";
|
||||||
|
import { useListStore } from "../state/lists";
|
||||||
|
|
||||||
export const requestQueueProductPublishes = new RequestQueueWithPromise(20);
|
export const requestQueueProductPublishes = new RequestQueueWithPromise(20);
|
||||||
export const requestQueueProductPublishesBackup = new RequestQueueWithPromise(
|
export const requestQueueProductPublishesBackup = new RequestQueueWithPromise(
|
||||||
@ -25,8 +26,9 @@ export const useResources = (retryAttempts: number = 2) => {
|
|||||||
getResourceCache,
|
getResourceCache,
|
||||||
setResourceCache,
|
setResourceCache,
|
||||||
addTemporaryResource,
|
addTemporaryResource,
|
||||||
markResourceAsDeleted,
|
markResourceAsDeleted
|
||||||
} = useCacheStore();
|
} = useCacheStore();
|
||||||
|
const deleteList = useListStore(state => state.deleteList)
|
||||||
const requestControllers = new Map<string, AbortController>();
|
const requestControllers = new Map<string, AbortController>();
|
||||||
|
|
||||||
const getArbitraryResource = async (
|
const getArbitraryResource = async (
|
||||||
@ -305,6 +307,7 @@ export const useResources = (retryAttempts: number = 2) => {
|
|||||||
addNewResources,
|
addNewResources,
|
||||||
updateNewResources,
|
updateNewResources,
|
||||||
deleteResource,
|
deleteResource,
|
||||||
|
deleteList
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import './index.css'
|
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 { RequestQueueWithPromise } from './utils/queue';
|
||||||
export { GlobalProvider, useGlobal } from "./context/GlobalProvider";
|
export { GlobalProvider, useGlobal } from "./context/GlobalProvider";
|
||||||
export {usePublish} from "./hooks/usePublish"
|
export {usePublish} from "./hooks/usePublish"
|
||||||
@ -9,4 +11,4 @@ export {useNameSearch} from './hooks/useNameSearch'
|
|||||||
export {Resource} from './hooks/useResources'
|
export {Resource} from './hooks/useResources'
|
||||||
export {Service} from './types/interfaces/resources'
|
export {Service} from './types/interfaces/resources'
|
||||||
export {ListItem} from './state/cache'
|
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 ({
|
export const addAndEncryptSymmetricKeys = async ({
|
||||||
previousData,
|
previousData,
|
||||||
names,
|
names,
|
||||||
|
disableAddNewKey
|
||||||
}: {
|
}: {
|
||||||
previousData: Object;
|
previousData: Object;
|
||||||
names: string[];
|
names: string[];
|
||||||
|
disableAddNewKey?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
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;
|
let highestKey = 0;
|
||||||
if (previousData && Object.keys(previousData)?.length > 0) {
|
if (previousData && Object.keys(previousData)?.length > 0) {
|
||||||
highestKey = Math.max(
|
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