Merge pull request #90 from Qortal/feature/added-txGroupId-groups-publish-status

Feature/added tx group id groups publish status
This commit is contained in:
Phillip 2025-06-21 21:59:03 +03:00 committed by GitHub
commit 302a0600a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 184 additions and 74 deletions

View File

@ -108,6 +108,7 @@ import {
mailsAtom, mailsAtom,
memberGroupsAtom, memberGroupsAtom,
mutedGroupsAtom, mutedGroupsAtom,
myGroupsWhereIAmAdminAtom,
oldPinnedAppsAtom, oldPinnedAppsAtom,
qMailLastEnteredTimestampAtom, qMailLastEnteredTimestampAtom,
settingsLocalLastUpdatedAtom, settingsLocalLastUpdatedAtom,
@ -484,6 +485,9 @@ function App() {
const resetLastPaymentSeenTimestampAtom = useResetAtom( const resetLastPaymentSeenTimestampAtom = useResetAtom(
lastPaymentSeenTimestampAtom lastPaymentSeenTimestampAtom
); );
const resetMyGroupsWhereIAmAdminAtom = useResetAtom(
myGroupsWhereIAmAdminAtom
);
const resetGroupsOwnerNamesAtom = useResetAtom(groupsOwnerNamesAtom); const resetGroupsOwnerNamesAtom = useResetAtom(groupsOwnerNamesAtom);
const resetGroupAnnouncementsAtom = useResetAtom(groupAnnouncementsAtom); const resetGroupAnnouncementsAtom = useResetAtom(groupAnnouncementsAtom);
const resetMutedGroupsAtom = useResetAtom(mutedGroupsAtom); const resetMutedGroupsAtom = useResetAtom(mutedGroupsAtom);
@ -510,6 +514,7 @@ function App() {
resetTimestampEnterAtom(); resetTimestampEnterAtom();
resettxListAtomAtom(); resettxListAtomAtom();
resetmemberGroupsAtomAtom(); resetmemberGroupsAtomAtom();
resetMyGroupsWhereIAmAdminAtom();
}; };
const contextValue = useMemo( const contextValue = useMemo(

View File

@ -165,6 +165,8 @@ export const AppViewer = forwardRef<HTMLIFrameElement, AppViewerProps>(
const publishLocation = e.detail?.publishLocation; const publishLocation = e.detail?.publishLocation;
const chunksSubmitted = e.detail?.chunksSubmitted; const chunksSubmitted = e.detail?.chunksSubmitted;
const totalChunks = e.detail?.totalChunks; const totalChunks = e.detail?.totalChunks;
const retry = e.detail?.retry;
const filename = e.detail?.filename;
try { try {
if (publishLocation === undefined || publishLocation === null) return; if (publishLocation === undefined || publishLocation === null) return;
const dataToBeSent = {}; const dataToBeSent = {};
@ -174,6 +176,12 @@ export const AppViewer = forwardRef<HTMLIFrameElement, AppViewerProps>(
if (totalChunks !== undefined && totalChunks !== null) { if (totalChunks !== undefined && totalChunks !== null) {
dataToBeSent.totalChunks = totalChunks; 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; const targetOrigin = new URL(iframe.src).origin;
iframe.contentWindow?.postMessage( iframe.contentWindow?.postMessage(
{ {

View File

@ -290,8 +290,10 @@ export const AppsPrivate = ({ myName, myAddress }) => {
} }
}, [myAddress]); }, [myAddress]);
useEffect(() => { useEffect(() => {
if (isOpenPrivateModal) {
getNames(); getNames();
}, [getNames]); }
}, [getNames, isOpenPrivateModal]);
return ( return (
<> <>

View File

@ -903,7 +903,7 @@ export const ChatGroup = ({
240000, 240000,
true true
); );
if (res !== true) if (res?.error)
throw new Error( throw new Error(
t('core:message.error.publish_image', { t('core:message.error.publish_image', {
postProcess: 'capitalizeFirstChar', postProcess: 'capitalizeFirstChar',

View File

@ -66,6 +66,7 @@ import {
isRunningPublicNodeAtom, isRunningPublicNodeAtom,
memberGroupsAtom, memberGroupsAtom,
mutedGroupsAtom, mutedGroupsAtom,
myGroupsWhereIAmAdminAtom,
selectedGroupIdAtom, selectedGroupIdAtom,
timestampEnterDataAtom, timestampEnterDataAtom,
} from '../../atoms/global'; } from '../../atoms/global';
@ -75,6 +76,7 @@ import { WalletsAppWrapper } from './WalletsAppWrapper';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { GroupList } from './GroupList'; import { GroupList } from './GroupList';
import { useAtom, useSetAtom } from 'jotai'; import { useAtom, useSetAtom } from 'jotai';
import { requestQueueGroupJoinRequests } from './GroupJoinRequests';
export const getPublishesFromAdmins = async (admins: string[], groupId) => { export const getPublishesFromAdmins = async (admins: string[], groupId) => {
const queryString = admins.map((name) => `name=${name}`).join('&'); const queryString = admins.map((name) => `name=${name}`).join('&');
@ -401,6 +403,7 @@ export const Group = ({
const [timestampEnterData, setTimestampEnterData] = useAtom( const [timestampEnterData, setTimestampEnterData] = useAtom(
timestampEnterDataAtom timestampEnterDataAtom
); );
const groupsPropertiesRef = useRef({});
const [chatMode, setChatMode] = useState('groups'); const [chatMode, setChatMode] = useState('groups');
const [newChat, setNewChat] = useState(false); const [newChat, setNewChat] = useState(false);
const [openSnack, setOpenSnack] = useState(false); const [openSnack, setOpenSnack] = useState(false);
@ -458,7 +461,7 @@ export const Group = ({
const setGroupsOwnerNames = useSetAtom(groupsOwnerNamesAtom); const setGroupsOwnerNames = useSetAtom(groupsOwnerNamesAtom);
const setUserInfoForLevels = useSetAtom(addressInfoControllerAtom); const setUserInfoForLevels = useSetAtom(addressInfoControllerAtom);
const setMyGroupsWhereIAmAdmin = useSetAtom(myGroupsWhereIAmAdminAtom);
const isPrivate = useMemo(() => { const isPrivate = useMemo(() => {
if (selectedGroup?.groupId === '0') return false; if (selectedGroup?.groupId === '0') return false;
if (!selectedGroup?.groupId || !groupsProperties[selectedGroup?.groupId]) if (!selectedGroup?.groupId || !groupsProperties[selectedGroup?.groupId])
@ -898,6 +901,10 @@ export const Group = ({
} }
}; };
useEffect(() => {
groupsPropertiesRef.current = groupsProperties;
}, [groupsProperties]);
const getGroupsProperties = useCallback(async (address) => { const getGroupsProperties = useCallback(async (address) => {
try { try {
const url = `${getBaseApiReact()}/groups/member/${address}`; 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(() => { useEffect(() => {
if (!myAddress) return; if (!myAddress) return;
if ( if (
areKeysEqual( !areKeysEqual(
groups?.map((grp) => grp?.groupId), groups?.map((grp) => grp?.groupId),
Object.keys(groupsProperties) Object.keys(groupsPropertiesRef.current)
) )
) { ) {
// TODO: empty block. Check it!
} else {
getGroupsProperties(myAddress); getGroupsProperties(myAddress);
getGroupsWhereIAmAMember(groups);
} }
}, [groups, myAddress]); }, [groups, myAddress]);

View File

@ -39,40 +39,14 @@ export const GroupJoinRequests = ({
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [txList] = useAtom(txListAtom); const [txList] = useAtom(txListAtom);
const setMyGroupsWhereIAmAdmin = useSetAtom(myGroupsWhereIAmAdminAtom); const [myGroupsWhereIAmAdmin] = useAtom(myGroupsWhereIAmAdminAtom);
const theme = useTheme(); const theme = useTheme();
const getJoinRequests = async () => { const getJoinRequests = async () => {
try { try {
setLoading(true); 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( const res = await Promise.all(
groupsAsAdmin.map(async (group) => { myGroupsWhereIAmAdmin.map(async (group) => {
const joinRequestResponse = const joinRequestResponse =
await requestQueueGroupJoinRequests.enqueue(() => { await requestQueueGroupJoinRequests.enqueue(() => {
return fetch( return fetch(
@ -96,12 +70,12 @@ export const GroupJoinRequests = ({
}; };
useEffect(() => { useEffect(() => {
if (myAddress && groups.length > 0) { if (myAddress && myGroupsWhereIAmAdmin.length > 0) {
getJoinRequests(); getJoinRequests();
} else { } else {
setLoading(false); setLoading(false);
} }
}, [myAddress, groups]); }, [myAddress, myGroupsWhereIAmAdmin]);
const filteredJoinRequests = useMemo(() => { const filteredJoinRequests = useMemo(() => {
return groupsWithJoinRequests.map((group) => { return groupsWithJoinRequests.map((group) => {

View File

@ -566,11 +566,21 @@ export const useQortalMessageListener = (
if (event?.data?.requestedHandler !== 'UI') return; if (event?.data?.requestedHandler !== 'UI') return;
const sendMessageToRuntime = (message, eventPort) => { 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 window
.sendMessage( .sendMessage(
message.action, message.action,
message.payload, message.payload,
300000, timeout,
message.isExtension, message.isExtension,
{ {
name: appName, name: appName,

View File

@ -25,7 +25,7 @@ window.addEventListener('message', (event) => {
export const sendMessageBackground = ( export const sendMessageBackground = (
action, action,
data = {}, data = {},
timeout = 240000, timeout = 600000,
isExtension, isExtension,
appInfo, appInfo,
skipAuth skipAuth

View File

@ -26,6 +26,10 @@ async function reusablePost(endpoint, _body) {
}, },
body: _body, body: _body,
}); });
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText);
}
let data; let data;
try { try {
data = await response.clone().json(); 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`); throw new Error(`Chunk ${index} failed after ${maxRetries} attempts`);
} }
// Wait 10 seconds before next retry // 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) => { 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 () => { const getArbitraryFee = async () => {
@ -163,9 +212,12 @@ export const publishData = async ({
}; };
const processTransactionVersion2 = async (bytes) => { const processTransactionVersion2 = async (bytes) => {
return await reusablePost( return await resuablePostRetry(
'/transactions/process?apiVersion=2', '/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); let transactionBytes = await uploadData(registeredName, data, fee);
if (!transactionBytes || transactionBytes.error) { if (!transactionBytes || transactionBytes.error) {
throw new Error(transactionBytes?.message || 'Error when uploading'); throw new Error(transactionBytes?.message || 'Error when uploading');
} else if (transactionBytes.includes('Error 500 Internal Server Error')) { } else if (transactionBytes.includes('Error 500 Internal Server Error')) {
@ -336,9 +387,14 @@ export const publishData = async ({
chunksSubmitted: 1, chunksSubmitted: 1,
totalChunks: 1, totalChunks: 1,
processed: false, 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; const file = data;
@ -365,6 +421,8 @@ export const publishData = async ({
chunksSubmitted: 0, chunksSubmitted: 0,
totalChunks, totalChunks,
processed: false, processed: false,
filename:
file?.name || filename || title || `${service}-${identifier || ''}`,
}); });
} }
for (let index = 0; index < totalChunks; index++) { for (let index = 0; index < totalChunks; index++) {
@ -399,7 +457,7 @@ export const publishData = async ({
headers: {}, headers: {},
}); });
if (!response.ok) { if (!response?.ok) {
const errorText = await response.text(); const errorText = await response.text();
throw new Error(`Finalize failed: ${errorText}`); throw new Error(`Finalize failed: ${errorText}`);
} }

View File

@ -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; const encrypt = data?.encrypt;
for (const resource of resources) { for (const resource of resources) {
@ -1746,7 +1763,7 @@ export const publishMultipleQDNResources = async (
}; };
const failedPublishesIdentifiers: FailedPublish[] = []; const failedPublishesIdentifiers: FailedPublish[] = [];
const publishedResponses = [];
for (const resource of resources) { for (const resource of resources) {
try { try {
const requiredFields = ['service']; const requiredFields = ['service'];
@ -1861,10 +1878,8 @@ export const publishMultipleQDNResources = async (
resource?.base64 || resource?.data64 || resourceEncrypt resource?.base64 || resource?.data64 || resourceEncrypt
? 'base64' ? 'base64'
: 'file'; : 'file';
await retryTransaction(
publishData, const response = await publishData({
[
{
apiVersion: 2, apiVersion: 2,
category, category,
data: rawData, data: rawData,
@ -1882,10 +1897,10 @@ export const publishMultipleQDNResources = async (
uploadType: dataType, uploadType: dataType,
withFee: true, withFee: true,
appInfo, appInfo,
}, });
], if (response?.signature) {
true publishedResponses.push(response);
); }
await new Promise((res) => { await new Promise((res) => {
setTimeout(() => { setTimeout(() => {
res(); res();
@ -1937,7 +1952,7 @@ export const publishMultipleQDNResources = async (
true true
); );
} }
return true; return publishedResponses;
}; };
export const voteOnPoll = async (data, isFromExtension) => { export const voteOnPoll = async (data, isFromExtension) => {