diff --git a/src/App.tsx b/src/App.tsx index 04cd035..407d8d8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -108,6 +108,7 @@ import { mailsAtom, memberGroupsAtom, mutedGroupsAtom, + myGroupsWhereIAmAdminAtom, oldPinnedAppsAtom, qMailLastEnteredTimestampAtom, settingsLocalLastUpdatedAtom, @@ -484,6 +485,9 @@ function App() { const resetLastPaymentSeenTimestampAtom = useResetAtom( lastPaymentSeenTimestampAtom ); + const resetMyGroupsWhereIAmAdminAtom = useResetAtom( + myGroupsWhereIAmAdminAtom + ); const resetGroupsOwnerNamesAtom = useResetAtom(groupsOwnerNamesAtom); const resetGroupAnnouncementsAtom = useResetAtom(groupAnnouncementsAtom); const resetMutedGroupsAtom = useResetAtom(mutedGroupsAtom); @@ -510,6 +514,7 @@ function App() { resetTimestampEnterAtom(); resettxListAtomAtom(); resetmemberGroupsAtomAtom(); + resetMyGroupsWhereIAmAdminAtom(); }; const contextValue = useMemo( diff --git a/src/components/Apps/AppViewer.tsx b/src/components/Apps/AppViewer.tsx index 24350bf..5ec416d 100644 --- a/src/components/Apps/AppViewer.tsx +++ b/src/components/Apps/AppViewer.tsx @@ -165,6 +165,8 @@ export const AppViewer = forwardRef( const publishLocation = e.detail?.publishLocation; const chunksSubmitted = e.detail?.chunksSubmitted; const totalChunks = e.detail?.totalChunks; + const retry = e.detail?.retry; + const filename = e.detail?.filename; try { if (publishLocation === undefined || publishLocation === null) return; const dataToBeSent = {}; @@ -174,6 +176,12 @@ export const AppViewer = forwardRef( if (totalChunks !== undefined && totalChunks !== null) { dataToBeSent.totalChunks = totalChunks; } + if (retry !== undefined && retry !== null) { + dataToBeSent.retry = retry; + } + if (filename !== undefined && filename !== null) { + dataToBeSent.filename = filename; + } const targetOrigin = new URL(iframe.src).origin; iframe.contentWindow?.postMessage( { diff --git a/src/components/Apps/AppsPrivate.tsx b/src/components/Apps/AppsPrivate.tsx index 5567785..670e8e9 100644 --- a/src/components/Apps/AppsPrivate.tsx +++ b/src/components/Apps/AppsPrivate.tsx @@ -290,8 +290,10 @@ export const AppsPrivate = ({ myName, myAddress }) => { } }, [myAddress]); useEffect(() => { - getNames(); - }, [getNames]); + if (isOpenPrivateModal) { + getNames(); + } + }, [getNames, isOpenPrivateModal]); return ( <> diff --git a/src/components/Chat/ChatGroup.tsx b/src/components/Chat/ChatGroup.tsx index a0efe00..762f281 100644 --- a/src/components/Chat/ChatGroup.tsx +++ b/src/components/Chat/ChatGroup.tsx @@ -903,7 +903,7 @@ export const ChatGroup = ({ 240000, true ); - if (res !== true) + if (res?.error) throw new Error( t('core:message.error.publish_image', { postProcess: 'capitalizeFirstChar', diff --git a/src/components/Group/Group.tsx b/src/components/Group/Group.tsx index d2cc6ef..29c37fc 100644 --- a/src/components/Group/Group.tsx +++ b/src/components/Group/Group.tsx @@ -66,6 +66,7 @@ import { isRunningPublicNodeAtom, memberGroupsAtom, mutedGroupsAtom, + myGroupsWhereIAmAdminAtom, selectedGroupIdAtom, timestampEnterDataAtom, } from '../../atoms/global'; @@ -75,6 +76,7 @@ import { WalletsAppWrapper } from './WalletsAppWrapper'; import { useTranslation } from 'react-i18next'; import { GroupList } from './GroupList'; import { useAtom, useSetAtom } from 'jotai'; +import { requestQueueGroupJoinRequests } from './GroupJoinRequests'; export const getPublishesFromAdmins = async (admins: string[], groupId) => { const queryString = admins.map((name) => `name=${name}`).join('&'); @@ -401,6 +403,7 @@ export const Group = ({ const [timestampEnterData, setTimestampEnterData] = useAtom( timestampEnterDataAtom ); + const groupsPropertiesRef = useRef({}); const [chatMode, setChatMode] = useState('groups'); const [newChat, setNewChat] = useState(false); const [openSnack, setOpenSnack] = useState(false); @@ -458,7 +461,7 @@ export const Group = ({ const setGroupsOwnerNames = useSetAtom(groupsOwnerNamesAtom); const setUserInfoForLevels = useSetAtom(addressInfoControllerAtom); - + const setMyGroupsWhereIAmAdmin = useSetAtom(myGroupsWhereIAmAdminAtom); const isPrivate = useMemo(() => { if (selectedGroup?.groupId === '0') return false; if (!selectedGroup?.groupId || !groupsProperties[selectedGroup?.groupId]) @@ -898,6 +901,10 @@ export const Group = ({ } }; + useEffect(() => { + groupsPropertiesRef.current = groupsProperties; + }, [groupsProperties]); + const getGroupsProperties = useCallback(async (address) => { try { const url = `${getBaseApiReact()}/groups/member/${address}`; @@ -917,17 +924,48 @@ export const Group = ({ } }, []); + const getGroupsWhereIAmAMember = useCallback(async (groups) => { + try { + let groupsAsAdmin = []; + const getAllGroupsAsAdmin = groups + .filter((item) => item.groupId !== '0') + .map(async (group) => { + const isAdminResponse = await requestQueueGroupJoinRequests.enqueue( + () => { + return fetch( + `${getBaseApiReact()}/groups/members/${group.groupId}?limit=0&onlyAdmins=true` + ); + } + ); + const isAdminData = await isAdminResponse.json(); + + const findMyself = isAdminData?.members?.find( + (member) => member.member === myAddress + ); + + if (findMyself) { + groupsAsAdmin.push(group); + } + return true; + }); + + await Promise.all(getAllGroupsAsAdmin); + setMyGroupsWhereIAmAdmin(groupsAsAdmin); + } catch (error) { + console.error(); + } + }, []); + useEffect(() => { if (!myAddress) return; if ( - areKeysEqual( + !areKeysEqual( groups?.map((grp) => grp?.groupId), - Object.keys(groupsProperties) + Object.keys(groupsPropertiesRef.current) ) ) { - // TODO: empty block. Check it! - } else { getGroupsProperties(myAddress); + getGroupsWhereIAmAMember(groups); } }, [groups, myAddress]); diff --git a/src/components/Group/GroupJoinRequests.tsx b/src/components/Group/GroupJoinRequests.tsx index ae1f43d..f113dbf 100644 --- a/src/components/Group/GroupJoinRequests.tsx +++ b/src/components/Group/GroupJoinRequests.tsx @@ -39,40 +39,14 @@ export const GroupJoinRequests = ({ const [loading, setLoading] = useState(true); const [txList] = useAtom(txListAtom); - const setMyGroupsWhereIAmAdmin = useSetAtom(myGroupsWhereIAmAdminAtom); + const [myGroupsWhereIAmAdmin] = useAtom(myGroupsWhereIAmAdminAtom); const theme = useTheme(); const getJoinRequests = async () => { try { setLoading(true); - - let groupsAsAdmin = []; - const getAllGroupsAsAdmin = groups - .filter((item) => item.groupId !== '0') - .map(async (group) => { - const isAdminResponse = await requestQueueGroupJoinRequests.enqueue( - () => { - return fetch( - `${getBaseApiReact()}/groups/members/${group.groupId}?limit=0&onlyAdmins=true` - ); - } - ); - const isAdminData = await isAdminResponse.json(); - - const findMyself = isAdminData?.members?.find( - (member) => member.member === myAddress - ); - - if (findMyself) { - groupsAsAdmin.push(group); - } - return true; - }); - - await Promise.all(getAllGroupsAsAdmin); - setMyGroupsWhereIAmAdmin(groupsAsAdmin); const res = await Promise.all( - groupsAsAdmin.map(async (group) => { + myGroupsWhereIAmAdmin.map(async (group) => { const joinRequestResponse = await requestQueueGroupJoinRequests.enqueue(() => { return fetch( @@ -96,12 +70,12 @@ export const GroupJoinRequests = ({ }; useEffect(() => { - if (myAddress && groups.length > 0) { + if (myAddress && myGroupsWhereIAmAdmin.length > 0) { getJoinRequests(); } else { setLoading(false); } - }, [myAddress, groups]); + }, [myAddress, myGroupsWhereIAmAdmin]); const filteredJoinRequests = useMemo(() => { return groupsWithJoinRequests.map((group) => { diff --git a/src/hooks/useQortalMessageListener.tsx b/src/hooks/useQortalMessageListener.tsx index 9192205..bbdcb66 100644 --- a/src/hooks/useQortalMessageListener.tsx +++ b/src/hooks/useQortalMessageListener.tsx @@ -566,11 +566,21 @@ export const useQortalMessageListener = ( if (event?.data?.requestedHandler !== 'UI') return; const sendMessageToRuntime = (message, eventPort) => { + let timeout: number = 300000; + if ( + message?.action === 'PUBLISH_MULTIPLE_QDN_RESOURCES' && + message?.payload?.resources?.length > 0 + ) { + timeout = message?.payload?.resources?.length * 1200000; + } else if (message?.action === 'PUBLISH_QDN_RESOURCE') { + timeout = 1200000; + } + window .sendMessage( message.action, message.payload, - 300000, + timeout, message.isExtension, { name: appName, diff --git a/src/messaging/MessagesToBackground.tsx b/src/messaging/MessagesToBackground.tsx index a325352..305a826 100644 --- a/src/messaging/MessagesToBackground.tsx +++ b/src/messaging/MessagesToBackground.tsx @@ -25,7 +25,7 @@ window.addEventListener('message', (event) => { export const sendMessageBackground = ( action, data = {}, - timeout = 240000, + timeout = 600000, isExtension, appInfo, skipAuth diff --git a/src/qdn/publish/publish.ts b/src/qdn/publish/publish.ts index 41deab2..82f3dc1 100644 --- a/src/qdn/publish/publish.ts +++ b/src/qdn/publish/publish.ts @@ -26,6 +26,10 @@ async function reusablePost(endpoint, _body) { }, body: _body, }); + if (!response.ok) { + const errorText = await response.text(); + throw new Error(errorText); + } let data; try { data = await response.clone().json(); @@ -68,7 +72,46 @@ async function uploadChunkWithRetry(endpoint, formData, index, maxRetries = 3) { throw new Error(`Chunk ${index} failed after ${maxRetries} attempts`); } // Wait 10 seconds before next retry - await new Promise((res) => setTimeout(res, 10_000)); + await new Promise((res) => setTimeout(res, 25_000)); + } + } +} + +async function resuablePostRetry( + endpoint, + body, + maxRetries = 3, + appInfo, + resourceInfo +) { + let attempt = 0; + while (attempt < maxRetries) { + try { + const response = await reusablePost(endpoint, body); + + return response; + } catch (err) { + attempt++; + if (attempt >= maxRetries) { + throw new Error( + err instanceof Error + ? err?.message || `Failed to make request` + : `Failed to make request` + ); + } + if (appInfo?.tabId && resourceInfo) { + executeEvent('receiveChunks', { + tabId: appInfo.tabId, + publishLocation: { + name: resourceInfo?.name, + identifier: resourceInfo?.identifier, + service: resourceInfo?.service, + }, + retry: true, + }); + } + // Wait 10 seconds before next retry + await new Promise((res) => setTimeout(res, 25_000)); } } } @@ -106,7 +149,13 @@ export const publishData = async ({ }; const convertBytesForSigning = async (transactionBytesBase58: string) => { - return await reusablePost('/transactions/convert', transactionBytesBase58); + return await resuablePostRetry( + '/transactions/convert', + transactionBytesBase58, + 3, + appInfo, + { identifier, name: registeredName, service } + ); }; const getArbitraryFee = async () => { @@ -163,9 +212,12 @@ export const publishData = async ({ }; const processTransactionVersion2 = async (bytes) => { - return await reusablePost( + return await resuablePostRetry( '/transactions/process?apiVersion=2', - Base58.encode(bytes) + Base58.encode(bytes), + 3, + appInfo, + { identifier, name: registeredName, service } ); }; @@ -226,7 +278,6 @@ export const publishData = async ({ } let transactionBytes = await uploadData(registeredName, data, fee); - if (!transactionBytes || transactionBytes.error) { throw new Error(transactionBytes?.message || 'Error when uploading'); } else if (transactionBytes.includes('Error 500 Internal Server Error')) { @@ -336,9 +387,14 @@ export const publishData = async ({ chunksSubmitted: 1, totalChunks: 1, processed: false, + filename: filename || title || `${service}-${identifier || ''}`, }); } - return await reusablePost(uploadDataUrl, postBody); + return await resuablePostRetry(uploadDataUrl, postBody, 3, appInfo, { + identifier, + name: registeredName, + service, + }); } const file = data; @@ -365,6 +421,8 @@ export const publishData = async ({ chunksSubmitted: 0, totalChunks, processed: false, + filename: + file?.name || filename || title || `${service}-${identifier || ''}`, }); } for (let index = 0; index < totalChunks; index++) { @@ -399,7 +457,7 @@ export const publishData = async ({ headers: {}, }); - if (!response.ok) { + if (!response?.ok) { const errorText = await response.text(); throw new Error(`Finalize failed: ${errorText}`); } diff --git a/src/qortal/get.ts b/src/qortal/get.ts index ad85a1d..52e50dc 100644 --- a/src/qortal/get.ts +++ b/src/qortal/get.ts @@ -1600,6 +1600,23 @@ export const publishMultipleQDNResources = async ( ); } + const totalFileSize = resources.reduce((acc, resource) => { + const file = resource?.file; + if (file && file?.size && !isNaN(file?.size)) { + return acc + file.size; + } + return acc; + }, 0); + if (totalFileSize > 0) { + const urlCheck = `/arbitrary/check/tmp?totalSize=${totalFileSize}`; + + const checkEndpoint = await createEndpoint(urlCheck); + const checkRes = await fetch(checkEndpoint); + if (!checkRes.ok) { + throw new Error('Not enough space on your hard drive'); + } + } + const encrypt = data?.encrypt; for (const resource of resources) { @@ -1746,7 +1763,7 @@ export const publishMultipleQDNResources = async ( }; const failedPublishesIdentifiers: FailedPublish[] = []; - + const publishedResponses = []; for (const resource of resources) { try { const requiredFields = ['service']; @@ -1861,31 +1878,29 @@ export const publishMultipleQDNResources = async ( resource?.base64 || resource?.data64 || resourceEncrypt ? 'base64' : 'file'; - await retryTransaction( - publishData, - [ - { - apiVersion: 2, - category, - data: rawData, - description, - filename: filename, - identifier: encodeURIComponent(identifier), - registeredName: encodeURIComponent(resource?.name || name), - service: service, - tag1, - tag2, - tag3, - tag4, - tag5, - title, - uploadType: dataType, - withFee: true, - appInfo, - }, - ], - true - ); + + const response = await publishData({ + apiVersion: 2, + category, + data: rawData, + description, + filename: filename, + identifier: encodeURIComponent(identifier), + registeredName: encodeURIComponent(resource?.name || name), + service: service, + tag1, + tag2, + tag3, + tag4, + tag5, + title, + uploadType: dataType, + withFee: true, + appInfo, + }); + if (response?.signature) { + publishedResponses.push(response); + } await new Promise((res) => { setTimeout(() => { res(); @@ -1937,7 +1952,7 @@ export const publishMultipleQDNResources = async ( true ); } - return true; + return publishedResponses; }; export const voteOnPoll = async (data, isFromExtension) => {