From b946e192360760d60aea665ca07e245bb384833f Mon Sep 17 00:00:00 2001 From: Nicola Benaglia Date: Sat, 10 May 2025 18:49:59 +0200 Subject: [PATCH 01/15] Add translations --- src/Wallets.tsx | 2 +- src/components/Apps/AppsDevModeHome.tsx | 2 +- src/components/Apps/AppsPrivate.tsx | 3 +- src/components/Group/AddGroup.tsx | 2 +- src/components/Group/InviteMember.tsx | 17 +- .../Group/ListOfGroupPromotions.tsx | 302 +++++++++++------- src/components/Snackbar/LoadingSnackbar.tsx | 20 +- src/components/Snackbar/Snackbar.tsx | 1 - src/components/TaskManager/TaskManager.tsx | 2 + src/i18n/locales/en/core.json | 3 +- src/i18n/locales/en/group.json | 14 +- 11 files changed, 224 insertions(+), 144 deletions(-) diff --git a/src/Wallets.tsx b/src/Wallets.tsx index 281e07e..20fb5a6 100644 --- a/src/Wallets.tsx +++ b/src/Wallets.tsx @@ -322,7 +322,7 @@ export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => { sx={{ display: 'flex', flexDirection: 'column', - }} + }} // TODO translate > { const object64 = await objectToBase64(objectToSave); const decryptedData = await window.sendMessage( 'ENCRYPT_QORTAL_GROUP_DATA', - { base64: object64, groupId: selectedGroup, @@ -313,7 +312,7 @@ export const AppsPrivate = ({ myName }) => { display: 'flex', flexDirection: 'column', gap: '5px', - }} + }} // TODO translate > diff --git a/src/components/Group/AddGroup.tsx b/src/components/Group/AddGroup.tsx index e9e9515..f9cbbf8 100644 --- a/src/components/Group/AddGroup.tsx +++ b/src/components/Group/AddGroup.tsx @@ -371,7 +371,6 @@ export const AddGroup = ({ address, open, setOpen }) => { }} > + setValue(e.target.value)} /> + + + + + { const [promotionTimeInterval, setPromotionTimeInterval] = useAtom( promotionTimeIntervalAtom ); - const [isExpanded, setIsExpanded] = React.useState(false); - const [openSnack, setOpenSnack] = useState(false); const [infoSnack, setInfoSnack] = useState(null); const [fee, setFee] = useState(null); @@ -99,7 +96,6 @@ export const ListOfGroupPromotions = () => { const [isLoadingPublish, setIsLoadingPublish] = useState(false); const { show } = useContext(MyContext); const setTxList = useSetAtom(txListAtom); - const theme = useTheme(); const { t } = useTranslation(['core', 'group']); const listRef = useRef(null); @@ -250,8 +246,9 @@ export const ListOfGroupPromotions = () => { }); setInfoSnack({ type: 'success', - message: - 'Successfully published promotion. It may take a couple of minutes for the promotion to appear', + message: t('group:message.success.group_promotion', { + postProcess: 'capitalize', + }), }); setOpenSnack(true); setText(''); @@ -261,7 +258,10 @@ export const ListOfGroupPromotions = () => { setInfoSnack({ type: 'error', message: - error?.message || 'Error publishing the promotion. Please try again', + error?.message || + t('group:message.error.group_promotion', { + postProcess: 'capitalize', + }), }); setOpenSnack(true); } finally { @@ -290,8 +290,9 @@ export const ListOfGroupPromotions = () => { if (!response?.error) { setInfoSnack({ type: 'success', - message: - 'Successfully requested to join group. It may take a couple of minutes for the changes to propagate', + message: t('group:message.success.group_join', { + postProcess: 'capitalize', + }), }); if (isOpen) { @@ -299,8 +300,14 @@ export const ListOfGroupPromotions = () => { { ...response, type: 'joined-group', - label: `Joined Group ${group?.groupName}: awaiting confirmation`, - labelDone: `Joined Group ${group?.groupName}: success!`, + label: t('group:message.success.group_join_label', { + group_name: group?.groupName, + postProcess: 'capitalize', + }), + labelDone: t('group:message.success.group_join_label', { + group_name: group?.groupName, + postProcess: 'capitalize', + }), done: false, groupId, }, @@ -311,15 +318,20 @@ export const ListOfGroupPromotions = () => { { ...response, type: 'joined-group-request', - label: `Requested to join Group ${group?.groupName}: awaiting confirmation`, - labelDone: `Requested to join Group ${group?.groupName}: success!`, + label: t('group:message.success.group_join_request', { + group_name: group?.groupName, + postProcess: 'capitalize', + }), + labelDone: t('group:message.success.group_join_outcome', { + group_name: group?.groupName, + postProcess: 'capitalize', + }), done: false, groupId, }, ...prev, ]); } - setOpenSnack(true); handlePopoverClose(); res(response); @@ -336,7 +348,9 @@ export const ListOfGroupPromotions = () => { .catch((error) => { setInfoSnack({ type: 'error', - message: error.message || 'An error occurred', + message: + error.message || + t('core:message.error.generic', { postProcess: 'capitalize' }), }); setOpenSnack(true); rej(error); @@ -385,7 +399,7 @@ export const ListOfGroupPromotions = () => { fontSize: '1rem', }} > - Group promotions{' '} + {t('group:group.promotions', { postProcess: 'capitalize' })}{' '} {promotions.length > 0 && ` (${promotions.length})`} @@ -444,7 +458,7 @@ export const ListOfGroupPromotions = () => { fontSize: '12px', }} > - Add Promotion + {t('group.action.add_promotion', { postProcess: 'capitalize' })} @@ -490,7 +504,9 @@ export const ListOfGroupPromotions = () => { color: 'rgba(255, 255, 255, 0.2)', }} > - Nothing to display + {t('group.message.generic.no_display', { + postProcess: 'capitalize', + })} )} @@ -537,23 +553,25 @@ export const ListOfGroupPromotions = () => { ref={rowVirtualizer.measureElement} //measure dynamic row height key={promotion?.identifier} style={{ - position: 'absolute', - top: 0, - left: '50%', // Move to the center horizontally - transform: `translateY(${virtualRow.start}px) translateX(-50%)`, // Adjust for centering - width: '100%', // Control width (90% of the parent) - padding: '10px 0', - display: 'flex', alignItems: 'center', - overscrollBehavior: 'none', + display: 'flex', flexDirection: 'column', gap: '5px', + left: '50%', // Move to the center horizontally + overscrollBehavior: 'none', + padding: '10px 0', + position: 'absolute', + top: 0, + transform: `translateY(${virtualRow.start}px) translateX(-50%)`, // Adjust for centering + width: '100%', // Control width (90% of the parent) }} > - Error loading content: Invalid Data + {t('group.message.generic.invalid_data', { + postProcess: 'capitalize', + })} } > @@ -568,7 +586,7 @@ export const ListOfGroupPromotions = () => { { + onClose={(reason) => { if (reason === 'backdropClick') { // Prevent closing on backdrop click return; @@ -603,7 +621,10 @@ export const ListOfGroupPromotions = () => { fontWeight: 600, }} > - Group name: {` ${promotion?.groupName}`} + {t('group:group.name', { + postProcess: 'capitalize', + })} + : {` ${promotion?.groupName}`} { fontWeight: 600, }} > - Number of members:{' '} - {` ${promotion?.memberCount}`} + {t('group:group.member_number', { + postProcess: 'capitalize', + })} + : {` ${promotion?.memberCount}`} {promotion?.description && ( @@ -634,9 +657,9 @@ export const ListOfGroupPromotions = () => { fontWeight: 600, }} > - *This is a closed/private group, so you - will need to wait until an admin accepts - your request + {t('group:message.generic.closed_group', { + postProcess: 'capitalize', + })} )} @@ -657,7 +680,9 @@ export const ListOfGroupPromotions = () => { variant="contained" onClick={handlePopoverClose} > - Close + {t('core:action.close', { + postProcess: 'capitalize', + })} { ) } > - Join + {t('core:action.join', { + postProcess: 'capitalize', + })} @@ -755,8 +782,12 @@ export const ListOfGroupPromotions = () => { }} > {promotion?.isOpen - ? 'Public group' - : 'Private group'} + ? t('group:group.public', { + postProcess: 'capitalize', + }) + : t('group:group.private', { + postProcess: 'capitalize', + })} @@ -790,7 +821,10 @@ export const ListOfGroupPromotions = () => { color: theme.palette.text.primary, }} > - Join Group: {` ${promotion?.groupName}`} + {t('group:action.join_group', { + postProcess: 'capitalize', + })} + : {` ${promotion?.groupName}`} @@ -810,90 +844,114 @@ export const ListOfGroupPromotions = () => { - {isShowModal && ( - - - {'Promote your group to non-members'} - - - - Only the latest promotion from the week will be shown for your - group. - - - Max 200 characters. Publish Fee: {fee && fee} {' QORT'} - - - + + {t('group:action.promote_group', { postProcess: 'capitalize' })} + + + + + {t('group:message.generic.latest_promotion', { + postProcess: 'capitalize', + })} + + + + {t('group:message.generic.max_chars', { + postProcess: 'capitalize', + })} + : {fee && fee} {' QORT'} + + + + + + + + + + setSelectedGroup(e.target.value)} - variant="outlined" - > - {myGroupsWhereIAmAdmin?.map((group) => { - return ( - - {group?.groupName} - - ); - })} - - - - setText(e.target.value)} - inputProps={{ - maxLength: 200, - }} - multiline={true} - sx={{ - '& .MuiFormLabel-root': { - color: theme.palette.text.primary, - }, - '& .MuiFormLabel-root.Mui-focused': { - color: theme.palette.text.primary, - }, - }} - /> - - - - - - - )} + {myGroupsWhereIAmAdmin?.map((group) => { + return ( + + {group?.groupName} + + ); + })} + + + + + + setText(e.target.value)} + inputProps={{ + maxLength: 200, + }} + multiline={true} + sx={{ + '& .MuiFormLabel-root': { + color: theme.palette.text.primary, + }, + '& .MuiFormLabel-root.Mui-focused': { + color: theme.palette.text.primary, + }, + }} + /> + + + + + + + + { - +export const LoadingSnackbar = ({ open, info }) => { return (
- - + + {info?.message}
); -} \ No newline at end of file +}; diff --git a/src/components/Snackbar/Snackbar.tsx b/src/components/Snackbar/Snackbar.tsx index d79677e..d4315ed 100644 --- a/src/components/Snackbar/Snackbar.tsx +++ b/src/components/Snackbar/Snackbar.tsx @@ -1,4 +1,3 @@ -import * as React from 'react'; import Snackbar, { SnackbarCloseReason } from '@mui/material/Snackbar'; import Alert from '@mui/material/Alert'; diff --git a/src/components/TaskManager/TaskManager.tsx b/src/components/TaskManager/TaskManager.tsx index d49077e..d34bb5e 100644 --- a/src/components/TaskManager/TaskManager.tsx +++ b/src/components/TaskManager/TaskManager.tsx @@ -198,9 +198,11 @@ export const TaskManager = ({ getUserInfo }) => { /> )} + {open ? : } + {txList.map((item) => ( diff --git a/src/i18n/locales/en/core.json b/src/i18n/locales/en/core.json index c819a38..91b7f48 100644 --- a/src/i18n/locales/en/core.json +++ b/src/i18n/locales/en/core.json @@ -27,7 +27,8 @@ }, "notify": "notify", "post": "post", - "post_message": "post message" + "post_message": "post message", + "publish": "publish" }, "admin": "admin", "core": { diff --git a/src/i18n/locales/en/group.json b/src/i18n/locales/en/group.json index 6fba714..5e0db18 100644 --- a/src/i18n/locales/en/group.json +++ b/src/i18n/locales/en/group.json @@ -1,5 +1,6 @@ { "action": { + "add_promotion": "add promotion", "ban": "ban member from group", "cancel_ban": "cancel ban", "copy_private_key": "copy private key", @@ -16,9 +17,11 @@ "load_members": "load members with names", "make_admin": "make an admin", "manage_members": "manage members", + "promote_group": "promote your group to non-members", "refetch_page": "refetch page", "remove_admin": "remove as admin", - "return_to_thread": "return to threads" + "return_to_thread": "return to threads", + "select_group": "select a group" }, "advanced_options": "advanced options", "approval_threshold": "group Approval Threshold (number / percentage of Admins that must approve a transaction)", @@ -36,6 +39,9 @@ "member_number": "number of members", "name": "group name", "open": "open (public)", + "private": "private group", + "promotions": "group promotions", + "public": "public group", "type": "group type" }, "invitation_expiry": "invitation Expiry Time", @@ -46,12 +52,16 @@ "latest_mails": "latest Q-Mails", "message": { "generic": { + "admin_only": "only groups where you are an admin will be shown", "already_in_group": "you are already in this group!", "closed_group": "this is a closed/private group, so you will need to wait until an admin accepts your request", "descrypt_wallet": "decrypting wallet...", "encryption_key": "the group's first common encryption key is in the process of creation. Please wait a few minutes for it to be retrieved by the network. Checking every 2 minutes...", "group_invited_you": "{{group}} has invited you", + "invalid_data": "error loading content: Invalid Data", + "latest_promotion": "only the latest promotion from the week will be shown for your group.", "loading_members": "loading member list with names... please wait.", + "max_chars": " Max 200 characters. Publish Fee", "no_display": "nothing to display", "no_selection": "no group selected", "not_part_group": "you are not part of the encrypted group of members. Wait until an admin re-encrypts the keys.", @@ -66,6 +76,7 @@ "descrypt_wallet": "error decrypting wallet {{ :errorMessage }}", "description_required": "please provide a description", "group_info": "cannot access group information", + "group_promotion": "error publishing the promotion. Please try again", "group_secret_key": "cannot get group secret key", "name_required": "please provide a name", "notify_admins": "try notifying an admin from the list of admins below:", @@ -87,6 +98,7 @@ "group_leave_name": "left group {{group_name}}: awaiting confirmation", "group_leave_label": "left group {{name}}: success!", "group_member_admin": "successfully made member an admin. It may take a couple of minutes for the changes to propagate", + "group_promotion": "successfully published promotion. It may take a couple of minutes for the promotion to appear", "group_remove_member": "successfully removed member as an admin. It may take a couple of minutes for the changes to propagate", "invitation_cancellation": "successfully canceled invitation. It may take a couple of minutes for the changes to propagate", "invitation_request": "accepted join request: awaiting confirmation", From a0bddf338d9e6dad63ef277b6fcea049df8c12b2 Mon Sep 17 00:00:00 2001 From: Nicola Benaglia Date: Sat, 10 May 2025 19:02:38 +0200 Subject: [PATCH 02/15] Add comment --- src/components/Apps/AppInfo.tsx | 3 +-- src/components/Apps/AppInfoSnippet.tsx | 2 +- src/components/Apps/AppPublish.tsx | 1 + src/components/Apps/AppRating.tsx | 1 + src/components/Apps/Apps-styles.tsx | 2 +- src/components/Apps/AppsCategoryDesktop.tsx | 20 ++------------------ src/components/Apps/AppsDesktop.tsx | 13 +++++-------- src/components/Apps/AppsDevMode.tsx | 5 ++++- src/components/Apps/AppsDevModeNavBar.tsx | 6 +++--- src/components/Apps/AppsHomeDesktop.tsx | 2 +- src/components/Apps/AppsLibraryDesktop.tsx | 2 +- 11 files changed, 21 insertions(+), 36 deletions(-) diff --git a/src/components/Apps/AppInfo.tsx b/src/components/Apps/AppInfo.tsx index 6dc8c37..abca186 100644 --- a/src/components/Apps/AppInfo.tsx +++ b/src/components/Apps/AppInfo.tsx @@ -1,4 +1,3 @@ -import React, { useEffect, useMemo, useState } from 'react'; import { AppCircle, AppCircleContainer, @@ -172,7 +171,7 @@ export const AppInfo = ({ app, myName }) => { }} > - {isSelectedAppPinned + {isSelectedAppPinned // TODO translate ? 'Unpin from dashboard' : 'Pin to dashboard'} diff --git a/src/components/Apps/AppInfoSnippet.tsx b/src/components/Apps/AppInfoSnippet.tsx index 22903d9..ba71164 100644 --- a/src/components/Apps/AppInfoSnippet.tsx +++ b/src/components/Apps/AppInfoSnippet.tsx @@ -166,7 +166,7 @@ export const AppInfoSnippet = ({ sx={{ backgroundColor: theme.palette.background.paper, opacity: isSelectedAppPinned ? 0.6 : 1, - }} + }} // TODO translate > {' '} diff --git a/src/components/Apps/AppPublish.tsx b/src/components/Apps/AppPublish.tsx index 486fd8a..8875c0b 100644 --- a/src/components/Apps/AppPublish.tsx +++ b/src/components/Apps/AppPublish.tsx @@ -185,6 +185,7 @@ export const AppPublish = ({ names, categories }) => { const fee = await getFee('ARBITRARY'); await show({ + // TODO translate message: 'Would you like to publish this app?', publishFee: fee.fee + ' QORT', }); diff --git a/src/components/Apps/AppRating.tsx b/src/components/Apps/AppRating.tsx index 191650d..881cfec 100644 --- a/src/components/Apps/AppRating.tsx +++ b/src/components/Apps/AppRating.tsx @@ -106,6 +106,7 @@ export const AppRating = ({ app, myName, ratingCountPosition = 'right' }) => { const fee = await getFee('CREATE_POLL'); await show({ + // TODO translate message: `Would you like to rate this app a rating of ${newValue}?. It will create a POLL tx.`, publishFee: fee.fee + ' QORT', }); diff --git a/src/components/Apps/Apps-styles.tsx b/src/components/Apps/Apps-styles.tsx index dd10e36..abbbaba 100644 --- a/src/components/Apps/Apps-styles.tsx +++ b/src/components/Apps/Apps-styles.tsx @@ -351,7 +351,7 @@ export const PublishQAppChoseFile = styled(ButtonBase)(({ theme }) => ({ justifyContent: 'center', width: '120px', '&:hover': { - backgroundColor: 'action.hover', // background on hover + backgroundColor: 'action.hover', }, })); diff --git a/src/components/Apps/AppsCategoryDesktop.tsx b/src/components/Apps/AppsCategoryDesktop.tsx index 39f12ee..7fc853a 100644 --- a/src/components/Apps/AppsCategoryDesktop.tsx +++ b/src/components/Apps/AppsCategoryDesktop.tsx @@ -16,20 +16,6 @@ import { Spacer } from '../../common/Spacer'; import { AppInfoSnippet } from './AppInfoSnippet'; import { Virtuoso } from 'react-virtuoso'; -const ScrollerStyled = styled('div')({ - // Hide scrollbar for WebKit browsers (Chrome, Safari) - '::-webkit-scrollbar': { - width: '0px', - height: '0px', - }, - - // Hide scrollbar for Firefox - scrollbarWidth: 'none', - - // Hide scrollbar for IE and older Edge - msOverflowStyle: 'none', -}); - const StyledVirtuosoContainer = styled('div')({ position: 'relative', width: '100%', @@ -99,7 +85,8 @@ export const AppsCategoryDesktop = ({ }, [debouncedValue, categoryList]); const rowRenderer = (index) => { - let app = searchedList[index]; + const app = searchedList[index]; + return ( diff --git a/src/components/Apps/AppsDesktop.tsx b/src/components/Apps/AppsDesktop.tsx index 80f57b9..d1268fb 100644 --- a/src/components/Apps/AppsDesktop.tsx +++ b/src/components/Apps/AppsDesktop.tsx @@ -17,7 +17,6 @@ import { AppsCategoryDesktop } from './AppsCategoryDesktop'; import { AppsNavBarDesktop } from './AppsNavBarDesktop'; import { Box, ButtonBase, useTheme } from '@mui/material'; import { HomeIcon } from '../../assets/Icons/HomeIcon'; -import { MessagingIcon } from '../../assets/Icons/MessagingIcon'; import { Save } from '../Save/Save'; import { IconWrapper } from '../Desktop/DesktopFooter'; import { enabledDevModeAtom } from '../../atoms/global'; @@ -98,8 +97,6 @@ export const AppsDesktop = ({ setCategories(responseData); } catch (error) { console.log(error); - } finally { - // dispatch(setIsLoadingGlobal(false)) } }, []); @@ -135,8 +132,6 @@ export const AppsDesktop = ({ setAvailableQapps(combine); } catch (error) { console.log(error); - } finally { - // dispatch(setIsLoadingGlobal(false)) } }, []); useEffect(() => { @@ -338,13 +333,13 @@ export const AppsDesktop = ({ + {isEnabledDevMode && ( @@ -515,6 +511,7 @@ export const AppsDesktop = ({ }} > + + + { setDesktopViewMode('dev'); @@ -342,10 +344,10 @@ export const AppsDevMode = ({ @@ -387,6 +389,7 @@ export const AppsDevMode = ({ }} > + { return ( Apps Dashboard diff --git a/src/components/Apps/AppsLibraryDesktop.tsx b/src/components/Apps/AppsLibraryDesktop.tsx index 17b9b71..64f1007 100644 --- a/src/components/Apps/AppsLibraryDesktop.tsx +++ b/src/components/Apps/AppsLibraryDesktop.tsx @@ -273,7 +273,7 @@ export const AppsLibraryDesktop = ({ }} onClick={() => { executeEvent('navigateBack', {}); - }} + }} // TODO translate > Return to Apps Dashboard From cbd784db0438258a59308bf6dc0798ad2cfa819e Mon Sep 17 00:00:00 2001 From: Nicola Benaglia Date: Sat, 10 May 2025 23:35:20 +0200 Subject: [PATCH 03/15] Add translation to wallets --- src/Wallets.tsx | 46 ++++++++++++++++++++++++++--------- src/i18n/locales/en/auth.json | 9 +++++++ 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/Wallets.tsx b/src/Wallets.tsx index 20fb5a6..c3f290a 100644 --- a/src/Wallets.tsx +++ b/src/Wallets.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useState } from 'react'; +import { Fragment, useContext, useEffect, useState } from 'react'; import List from '@mui/material/List'; import ListItem from '@mui/material/ListItem'; import Divider from '@mui/material/Divider'; @@ -32,6 +32,7 @@ import { LoadingButton } from '@mui/lab'; import { PasswordField } from './components'; import { HtmlTooltip } from './ExtStates/NotAuthenticated'; import { MyContext } from './App'; +import { useTranslation } from 'react-i18next'; const parsefilenameQortal = (filename) => { return filename.startsWith('qortal_backup_') ? filename.slice(14) : filename; @@ -44,11 +45,11 @@ export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => { const [seedName, setSeedName] = useState(''); const [seedError, setSeedError] = useState(''); const { hasSeenGettingStarted } = useContext(MyContext); - const [password, setPassword] = useState(''); const [isOpenSeedModal, setIsOpenSeedModal] = useState(false); const [isLoadingEncryptSeed, setIsLoadingEncryptSeed] = useState(false); const theme = useTheme(); + const { t } = useTranslation(['core', 'auth']); const { isShow, onCancel, onOk, show } = useModal(); const { getRootProps, getInputProps } = useDropzone({ @@ -83,7 +84,7 @@ export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => { } let error: any = null; - let uniqueInitialMap = new Map(); + const uniqueInitialMap = new Map(); // Only add a message if it doesn't already exist in the Map importedWallets.forEach((wallet) => { @@ -92,7 +93,9 @@ export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => { uniqueInitialMap.set(wallet?.address0, wallet); } }); + const data = Array.from(uniqueInitialMap.values()); + if (data && data?.length > 0) { const uniqueNewWallets = data.filter( (newWallet) => @@ -148,10 +151,19 @@ export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => { setPassword(''); setSeedError(''); } else { - setSeedError('Could not create account.'); + setSeedError( + t('auth:message.error.account_creation', { + postProcess: 'capitalize', + }) + ); } } catch (error) { - setSeedError(error?.message || 'Could not create account.'); + setSeedError( + error?.message || + t('auth:message.error.account_creation', { + postProcess: 'capitalize', + }) + ); } finally { setIsLoadingEncryptSeed(false); } @@ -189,19 +201,29 @@ export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => {
{wallets?.length === 0 || !wallets ? ( <> - No accounts saved + + {t('auth:message.generic.no_account', { + postProcess: 'capitalize', + })} + + ) : ( <> - Your saved accounts + + {t('auth:message.generic.your_accounts', { + postProcess: 'capitalize', + })} + + )} {rawWallet && ( - Selected Account: + Selected Account: // TODO translate {rawWallet?.name && {rawWallet.name}} {rawWallet?.address0 && ( {rawWallet?.address0} @@ -249,7 +271,7 @@ export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => { + { here to access it. This phrase is one of the ways to recover your account. - + } > { + { already made, in order to login with them afterwards. You will need access to your backup JSON file in order to do so. - + } > Date: Sat, 10 May 2025 22:27:56 +0300 Subject: [PATCH 04/15] added event for UI language to qapp --- src/components/Apps/AppViewer.tsx | 43 ++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/src/components/Apps/AppViewer.tsx b/src/components/Apps/AppViewer.tsx index 06a64c5..7ffeecd 100644 --- a/src/components/Apps/AppViewer.tsx +++ b/src/components/Apps/AppViewer.tsx @@ -5,6 +5,7 @@ import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events'; import { useFrame } from 'react-frame-component'; import { useQortalMessageListener } from './useQortalMessageListener'; import { useThemeContext } from '../Theme/ThemeContext'; +import { useTranslation } from 'react-i18next'; export const AppViewer = React.forwardRef( ({ app, hide, isDevMode, skipAuth }, iframeRef) => { @@ -22,11 +23,13 @@ export const AppViewer = React.forwardRef( ); const [url, setUrl] = useState(''); const { themeMode } = useThemeContext(); + const { i18n } = useTranslation(['core']); + const currentLang = i18n.language; useEffect(() => { if (app?.isPreview) return; if (isDevMode) { - setUrl(app?.url); + setUrl(app?.url + `?theme=${themeMode}&lang=${currentLang}`); return; } let hasQueryParam = false; @@ -35,14 +38,14 @@ export const AppViewer = React.forwardRef( } setUrl( - `${getBaseApiReact()}/render/${app?.service}/${app?.name}${app?.path != null ? `/${app?.path}` : ''}${hasQueryParam ? '&' : '?'}theme=${themeMode}&identifier=${app?.identifier != null && app?.identifier != 'null' ? app?.identifier : ''}` + `${getBaseApiReact()}/render/${app?.service}/${app?.name}${app?.path != null ? `/${app?.path}` : ''}${hasQueryParam ? '&' : '?'}theme=${themeMode}&lang=${currentLang}&identifier=${app?.identifier != null && app?.identifier != 'null' ? app?.identifier : ''}` ); }, [app?.service, app?.name, app?.identifier, app?.path, app?.isPreview]); useEffect(() => { if (app?.isPreview && app?.url) { resetHistory(); - setUrl(app.url); + setUrl(app.url + `&theme=${themeMode}&lang=${currentLang}`); } }, [app?.url, app?.isPreview]); const defaultUrl = useMemo(() => { @@ -55,11 +58,14 @@ export const AppViewer = React.forwardRef( if (isDevMode) { resetHistory(); if (!app?.isPreview || app?.isPrivate) { - setUrl(app?.url + `?time=${Date.now()}`); + setUrl( + app?.url + + `?time=${Date.now()}&theme=${themeMode}&lang=${currentLang}` + ); } return; } - const constructUrl = `${getBaseApiReact()}/render/${app?.service}/${app?.name}${path != null ? path : ''}?theme=${themeMode}&identifier=${app?.identifier != null ? app?.identifier : ''}&time=${new Date().getMilliseconds()}`; + const constructUrl = `${getBaseApiReact()}/render/${app?.service}/${app?.name}${path != null ? path : ''}?theme=${themeMode}&lang=${currentLang}&identifier=${app?.identifier != null ? app?.identifier : ''}&time=${new Date().getMilliseconds()}`; setUrl(constructUrl); } }; @@ -70,7 +76,7 @@ export const AppViewer = React.forwardRef( return () => { unsubscribeFromEvent('refreshApp', refreshAppFunc); }; - }, [app, path, isDevMode]); + }, [app, path, isDevMode, themeMode, currentLang]); useEffect(() => { const iframe = iframeRef?.current; @@ -87,6 +93,25 @@ export const AppViewer = React.forwardRef( } }, [themeMode]); + useEffect(() => { + const iframe = iframeRef?.current; + if (!iframe) return; + + try { + const targetOrigin = new URL(iframe.src).origin; + iframe.contentWindow?.postMessage( + { + action: 'LANGUAGE_CHANGED', + language: currentLang, + requestedHandler: 'UI', + }, + targetOrigin + ); + } catch (err) { + console.error('Failed to send theme change to iframe:', err); + } + }, [currentLang]); + const removeTrailingSlash = (str) => str.replace(/\/$/, ''); const copyLinkFunc = (e) => { @@ -181,12 +206,12 @@ export const AppViewer = React.forwardRef( } catch (error) { if (isDevMode) { setUrl( - `${url}${previousPath != null ? previousPath : ''}?theme=${themeMode}&time=${new Date().getMilliseconds()}&isManualNavigation=false` + `${url}${previousPath != null ? previousPath : ''}?theme=${themeMode}&lang=${currentLang}&time=${new Date().getMilliseconds()}&isManualNavigation=false` ); return; } setUrl( - `${getBaseApiReact()}/render/${app?.service}/${app?.name}${previousPath != null ? previousPath : ''}?theme=${themeMode}&identifier=${app?.identifier != null && app?.identifier != 'null' ? app?.identifier : ''}&time=${new Date().getMilliseconds()}&isManualNavigation=false` + `${getBaseApiReact()}/render/${app?.service}/${app?.name}${previousPath != null ? previousPath : ''}?theme=${themeMode}&lang=${currentLang}&identifier=${app?.identifier != null && app?.identifier != 'null' ? app?.identifier : ''}&time=${new Date().getMilliseconds()}&isManualNavigation=false` ); // iframeRef.current.contentWindow.location.href = previousPath; // Fallback URL update } @@ -209,7 +234,7 @@ export const AppViewer = React.forwardRef( navigateBackAppFunc ); }; - }, [app, history]); + }, [app, history, themeMode, currentLang]); // Function to navigate back in iframe const navigateForwardInIframe = async () => { From ccd348100e6135383ed11a72adcaba84016189b6 Mon Sep 17 00:00:00 2001 From: "nico.benaz" <52411515+nbenaglia@users.noreply.github.com> Date: Sat, 10 May 2025 23:09:18 +0200 Subject: [PATCH 05/15] Update src/components/Apps/AppViewer.tsx --- src/components/Apps/AppViewer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Apps/AppViewer.tsx b/src/components/Apps/AppViewer.tsx index 7ffeecd..87f88e8 100644 --- a/src/components/Apps/AppViewer.tsx +++ b/src/components/Apps/AppViewer.tsx @@ -108,7 +108,7 @@ export const AppViewer = React.forwardRef( targetOrigin ); } catch (err) { - console.error('Failed to send theme change to iframe:', err); + console.error('Failed to send language change to iframe:', err); } }, [currentLang]); From 5820f8123ca9060ba7864f28208a2967cf83eb71 Mon Sep 17 00:00:00 2001 From: Nicola Benaglia Date: Sun, 11 May 2025 00:00:36 +0200 Subject: [PATCH 06/15] Organize code --- src/MessageQueueContext.tsx | 142 ++++++++++++--------- src/hooks/useHandlePaymentNotification.tsx | 7 +- src/hooks/useNameSearch.tsx | 2 + 3 files changed, 88 insertions(+), 63 deletions(-) diff --git a/src/MessageQueueContext.tsx b/src/MessageQueueContext.tsx index 7104520..11f356f 100644 --- a/src/MessageQueueContext.tsx +++ b/src/MessageQueueContext.tsx @@ -1,12 +1,17 @@ -import React, { createContext, useContext, useState, useCallback, useRef } from 'react'; -import ShortUniqueId from "short-unique-id"; +import { + createContext, + useContext, + useState, + useCallback, + useRef, +} from 'react'; +import ShortUniqueId from 'short-unique-id'; const MessageQueueContext = createContext(null); +const uid = new ShortUniqueId({ length: 8 }); export const useMessageQueue = () => useContext(MessageQueueContext); -const uid = new ShortUniqueId({ length: 8 }); - export const MessageQueueProvider = ({ children }) => { const messageQueueRef = useRef([]); const [queueChats, setQueueChats] = useState({}); // Stores chats and status for display @@ -21,39 +26,41 @@ export const MessageQueueProvider = ({ children }) => { const processingPromiseRef = useRef(Promise.resolve()); // Function to add a message to the queue - const addToQueue = useCallback((sendMessageFunc, messageObj, type, groupDirectId) => { - const tempId = uid.rnd(); - const chatData = { - ...messageObj, - type, - groupDirectId, - signature: uid.rnd(), - identifier: tempId, - retries: 0, // Retry count for display purposes - status: 'pending' // Initial status is 'pending' - }; + const addToQueue = useCallback( + (sendMessageFunc, messageObj, type, groupDirectId) => { + const tempId = uid.rnd(); + const chatData = { + ...messageObj, + type, + groupDirectId, + signature: uid.rnd(), + identifier: tempId, + retries: 0, // Retry count for display purposes + status: 'pending', // Initial status is 'pending' + }; - // Add chat data to queueChats for status tracking - setQueueChats((prev) => ({ - ...prev, - [groupDirectId]: [...(prev[groupDirectId] || []), chatData] - })); + // Add chat data to queueChats for status tracking + setQueueChats((prev) => ({ + ...prev, + [groupDirectId]: [...(prev[groupDirectId] || []), chatData], + })); - // Add the message to the global messageQueueRef - messageQueueRef.current.push({ - func: sendMessageFunc, - identifier: tempId, - groupDirectId, - specialId: messageObj?.message?.specialId - }); + // Add the message to the global messageQueueRef + messageQueueRef.current.push({ + func: sendMessageFunc, + identifier: tempId, + groupDirectId, + specialId: messageObj?.message?.specialId, + }); - // Start processing the queue - processQueue([], groupDirectId); - }, []); + // Start processing the queue + processQueue([], groupDirectId); + }, + [] + ); // Function to process the message queue const processQueue = useCallback((newMessages = [], groupDirectId) => { - processingPromiseRef.current = processingPromiseRef.current .then(() => processQueueInternal(newMessages, groupDirectId)) .catch((err) => console.error('Error in processQueue:', err)); @@ -62,7 +69,7 @@ export const MessageQueueProvider = ({ children }) => { // Internal function to handle queue processing const processQueueInternal = async (newMessages, groupDirectId) => { // Remove any messages from the queue that match the specialId from newMessages - + // If the queue is empty, no need to process if (messageQueueRef.current.length === 0) return; @@ -92,7 +99,6 @@ export const MessageQueueProvider = ({ children }) => { // Remove the message from the queue after successful sending messageQueueRef.current.shift(); - } catch (error) { console.error('Message sending failed', error); @@ -110,7 +116,8 @@ export const MessageQueueProvider = ({ children }) => { updatedChats[groupDirectId][chatIndex].status = 'failed'; } else { // Max retries reached, set status to 'failed-permanent' - updatedChats[groupDirectId][chatIndex].status = 'failed-permanent'; + updatedChats[groupDirectId][chatIndex].status = + 'failed-permanent'; // Remove the message from the queue after max retries messageQueueRef.current.shift(); @@ -127,33 +134,39 @@ export const MessageQueueProvider = ({ children }) => { // Method to process with new messages and groupDirectId const processWithNewMessages = (newMessages, groupDirectId) => { - let updatedNewMessages = newMessages + let updatedNewMessages = newMessages; if (newMessages.length > 0) { // Remove corresponding entries in queueChats for the provided groupDirectId setQueueChats((prev) => { const updatedChats = { ...prev }; if (updatedChats[groupDirectId]) { - - updatedNewMessages = newMessages?.map((msg)=> { - const findTempMsg = updatedChats[groupDirectId]?.find((msg2)=> msg2?.message?.specialId === msg?.specialId) - if(findTempMsg){ + updatedNewMessages = newMessages?.map((msg) => { + const findTempMsg = updatedChats[groupDirectId]?.find( + (msg2) => msg2?.message?.specialId === msg?.specialId + ); + if (findTempMsg) { return { ...msg, - tempSignature: findTempMsg?.signature - } + tempSignature: findTempMsg?.signature, + }; } - return msg - }) - - - updatedChats[groupDirectId] = updatedChats[groupDirectId].filter((chat) => { - return !newMessages.some(newMsg => newMsg?.specialId === chat?.message?.specialId); + return msg; }); + updatedChats[groupDirectId] = updatedChats[groupDirectId].filter( + (chat) => { + return !newMessages.some( + (newMsg) => newMsg?.specialId === chat?.message?.specialId + ); + } + ); + // Remove messages with status 'failed-permanent' - updatedChats[groupDirectId] = updatedChats[groupDirectId].filter((chat) => { - return chat?.status !== 'failed-permanent'; - }); + updatedChats[groupDirectId] = updatedChats[groupDirectId].filter( + (chat) => { + return chat?.status !== 'failed-permanent'; + } + ); // If no more chats for this group, delete the groupDirectId entry if (updatedChats[groupDirectId].length === 0) { @@ -162,27 +175,36 @@ export const MessageQueueProvider = ({ children }) => { } return updatedChats; }); - } setTimeout(() => { - if(!messageQueueRef.current.find((msg) => msg?.groupDirectId === groupDirectId)){ + if ( + !messageQueueRef.current.find( + (msg) => msg?.groupDirectId === groupDirectId + ) + ) { setQueueChats((prev) => { const updatedChats = { ...prev }; if (updatedChats[groupDirectId]) { - delete updatedChats[groupDirectId] + delete updatedChats[groupDirectId]; } - - return updatedChats - } - ) + + return updatedChats; + }); } }, 300); - - return updatedNewMessages + + return updatedNewMessages; }; return ( - + {children} ); diff --git a/src/hooks/useHandlePaymentNotification.tsx b/src/hooks/useHandlePaymentNotification.tsx index 9ca9ff5..7a13072 100644 --- a/src/hooks/useHandlePaymentNotification.tsx +++ b/src/hooks/useHandlePaymentNotification.tsx @@ -7,11 +7,9 @@ import { subscribeToEvent, unsubscribeFromEvent } from '../utils/events'; import { useAtom } from 'jotai'; export const useHandlePaymentNotification = (address) => { - const [latestTx, setLatestTx] = useState(null); - const nameAddressOfSender = useRef({}); const isFetchingName = useRef({}); - + const [latestTx, setLatestTx] = useState(null); const [lastEnteredTimestampPayment, setLastEnteredTimestampPayment] = useAtom( lastPaymentSeenTimestampAtom ); @@ -63,6 +61,7 @@ export const useHandlePaymentNotification = (address) => { const key = `last-seen-payment-${address}`; const res = await getData(key).catch(() => null); + if (res) { setLastEnteredTimestampPayment(res); } @@ -76,6 +75,7 @@ export const useHandlePaymentNotification = (address) => { const latestTx = responseData.filter( (tx) => tx?.creatorAddress !== address && tx?.recipient === address )[0]; + if (!latestTx) { return; // continue to the next group } @@ -128,6 +128,7 @@ export const useHandlePaymentNotification = (address) => { ); }; }, [setLastEnteredTimestampPaymentEventFunc]); + return { latestTx, getNameOrAddressOfSenderMiddle, diff --git a/src/hooks/useNameSearch.tsx b/src/hooks/useNameSearch.tsx index 6bc86ec..e00fec1 100644 --- a/src/hooks/useNameSearch.tsx +++ b/src/hooks/useNameSearch.tsx @@ -5,6 +5,7 @@ interface NameListItem { name: string; address: string; } + export const useNameSearch = (value: string, limit = 20) => { const [nameList, setNameList] = useState([]); const [isLoading, setIsLoading] = useState(false); @@ -48,6 +49,7 @@ export const useNameSearch = (value: string, limit = 20) => { clearTimeout(handler); }; }, [value, limit, checkIfNameExisits]); + return { isLoading, results: nameList, From 42f870b466f365df06d374d4b8aa03c0f4d8f543 Mon Sep 17 00:00:00 2001 From: Nicola Benaglia Date: Sun, 11 May 2025 00:02:02 +0200 Subject: [PATCH 07/15] Sort --- src/atoms/global.ts | 51 ++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/src/atoms/global.ts b/src/atoms/global.ts index e0b4156..baa96d5 100644 --- a/src/atoms/global.ts +++ b/src/atoms/global.ts @@ -18,37 +18,36 @@ export const sortablePinnedAppsAtom = atomWithReset([ { name: 'Q-Nodecontrol', service: 'APP' }, ]); -export const canSaveSettingToQdnAtom = atomWithReset(false); -export const settingsQDNLastUpdatedAtom = atomWithReset(-100); -export const settingsLocalLastUpdatedAtom = atomWithReset(0); -export const oldPinnedAppsAtom = atomWithReset([]); -export const isUsingImportExportSettingsAtom = atomWithReset(null); -export const fullScreenAtom = atomWithReset(false); -export const hasSettingsChangedAtom = atomWithReset(false); -export const navigationControllerAtom = atomWithReset({}); -export const enabledDevModeAtom = atomWithReset(false); -export const myGroupsWhereIAmAdminAtom = atomWithReset([]); -export const promotionTimeIntervalAtom = atomWithReset(0); -export const promotionsAtom = atomWithReset([]); -export const resourceDownloadControllerAtom = atomWithReset({}); -export const blobControllerAtom = atomWithReset({}); -export const selectedGroupIdAtom = atomWithReset(null); export const addressInfoControllerAtom = atomWithReset({}); +export const blobControllerAtom = atomWithReset({}); +export const canSaveSettingToQdnAtom = atomWithReset(false); +export const enabledDevModeAtom = atomWithReset(false); +export const fullScreenAtom = atomWithReset(false); +export const groupAnnouncementsAtom = atomWithReset({}); +export const groupChatTimestampsAtom = atomWithReset({}); +export const groupsOwnerNamesAtom = atomWithReset({}); +export const groupsPropertiesAtom = atomWithReset({}); +export const hasSettingsChangedAtom = atomWithReset(false); export const isDisabledEditorEnterAtom = atomWithReset(false); -export const qMailLastEnteredTimestampAtom = atomWithReset(null); +export const isOpenBlockedModalAtom = atomWithReset(false); +export const isRunningPublicNodeAtom = atomWithReset(false); +export const isUsingImportExportSettingsAtom = atomWithReset(null); export const lastPaymentSeenTimestampAtom = atomWithReset(null); export const mailsAtom = atomWithReset([]); -export const groupsPropertiesAtom = atomWithReset({}); -export const groupsOwnerNamesAtom = atomWithReset({}); -export const isOpenBlockedModalAtom = atomWithReset(false); -export const groupAnnouncementsAtom = atomWithReset({}); -export const mutedGroupsAtom = atomWithReset([]); -export const groupChatTimestampsAtom = atomWithReset({}); -export const timestampEnterDataAtom = atomWithReset({}); - -export const txListAtom = atomWithReset([]); export const memberGroupsAtom = atomWithReset([]); -export const isRunningPublicNodeAtom = atomWithReset(false); +export const mutedGroupsAtom = atomWithReset([]); +export const myGroupsWhereIAmAdminAtom = atomWithReset([]); +export const navigationControllerAtom = atomWithReset({}); +export const oldPinnedAppsAtom = atomWithReset([]); +export const promotionsAtom = atomWithReset([]); +export const promotionTimeIntervalAtom = atomWithReset(0); +export const qMailLastEnteredTimestampAtom = atomWithReset(null); +export const resourceDownloadControllerAtom = atomWithReset({}); +export const selectedGroupIdAtom = atomWithReset(null); +export const settingsLocalLastUpdatedAtom = atomWithReset(0); +export const settingsQDNLastUpdatedAtom = atomWithReset(-100); +export const timestampEnterDataAtom = atomWithReset({}); +export const txListAtom = atomWithReset([]); // Atom Families (replacing selectorFamily) export const resourceKeySelector = atomFamily((key) => From 8b22e65646fb97a5d80d7fdfe442bfde1d81ed9a Mon Sep 17 00:00:00 2001 From: Nicola Benaglia Date: Mon, 12 May 2025 08:19:05 +0200 Subject: [PATCH 08/15] Add auth action --- src/App.tsx | 10 ++- src/ExtStates/NotAuthenticated.tsx | 6 +- src/Wallets.tsx | 26 +++--- src/components/Auth/DownloadWallet.tsx | 6 +- src/components/Home/NewUsersCTA.tsx | 2 +- src/i18n/locales/en/auth.json | 17 ++-- src/i18n/locales/en/core.json | 6 +- src/utils/queue/queue.ts | 110 +++++++++++++------------ 8 files changed, 98 insertions(+), 85 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index d24a4ae..07b915c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2513,7 +2513,7 @@ function App() { setExtstate('create-wallet'); }} > - {t('auth:create_account', { postProcess: 'capitalize' })} + {t('auth:action.create_account', { postProcess: 'capitalize' })} )} @@ -2610,7 +2610,7 @@ function App() { fontWeight: 600, }} > - {t('auth:authenticate', { postProcess: 'capitalize' })} + {t('auth:action.authenticate', { postProcess: 'capitalize' })} @@ -2662,7 +2662,7 @@ function App() { - {t('auth:authenticate', { postProcess: 'capitalize' })} + {t('auth:action.authenticate', { postProcess: 'capitalize' })} {walletToBeDecryptedError} @@ -2930,7 +2930,9 @@ function App() { - {t('auth:create_account', { postProcess: 'capitalize' })} + {t('auth:action.create_account', { + postProcess: 'capitalize', + })} {walletToBeDownloadedError} diff --git a/src/ExtStates/NotAuthenticated.tsx b/src/ExtStates/NotAuthenticated.tsx index 693cdf0..2a6b090 100644 --- a/src/ExtStates/NotAuthenticated.tsx +++ b/src/ExtStates/NotAuthenticated.tsx @@ -572,7 +572,7 @@ export const NotAuthenticated = ({ }, }} > - {t('auth:create_account', { postProcess: 'capitalize' })} + {t('auth:action.create_account', { postProcess: 'capitalize' })} @@ -953,7 +953,9 @@ export const NotAuthenticated = ({ setCustomNodeToSaveIndex(null); }} > - {t('auth:return_to_list', { postProcess: 'capitalize' })} + {t('auth:action.return_to_list', { + postProcess: 'capitalize', + })} { }} autoFocus > - Add + {t('core:action.add', { + postProcess: 'capitalize', + })} { const [note, setNote] = useState(''); const [isEdit, setIsEdit] = useState(false); const theme = useTheme(); + const { t } = useTranslation(['core', 'auth']); useEffect(() => { if (wallet?.name) { @@ -463,6 +485,7 @@ const WalletItem = ({ wallet, updateWalletItem, idx, setSelectedWallet }) => { + { marginTop: '5px', }} > - Login + {t('core:action.login', { + postProcess: 'capitalize', + })} } @@ -519,7 +544,11 @@ const WalletItem = ({ wallet, updateWalletItem, idx, setSelectedWallet }) => { padding: '8px', }} > - + { - + { variant="contained" onClick={() => setIsEdit(false)} > - Close + {t('core:action.close', { + postProcess: 'capitalize', + })} diff --git a/src/i18n/locales/en/auth.json b/src/i18n/locales/en/auth.json index 09aae5f..8a514fa 100644 --- a/src/i18n/locales/en/auth.json +++ b/src/i18n/locales/en/auth.json @@ -6,9 +6,13 @@ "selected": "selected account" }, "action": { - "add_seed_phrase": "add seed-phrase", + "add": { + "account": "add account", + "seed_phrase": "add seed-phrase" + }, "authenticate": "authenticate", "create_account": "create account", + "choose_password": "choose new password", "download_account": "download account", "return_to_list": "return to list" }, @@ -29,9 +33,11 @@ "generic": { "no_account": "No accounts saved", "keep_secure": "keep your account file secure", + "type_seed": "type or paste in your seed-phrase", "your_accounts": "your saved accounts" } }, + "name": "name", "node": { "choose": "choose custom node", "custom_many": "custom nodes", @@ -40,9 +46,12 @@ "using": "using node", "using_public": "using public node" }, + "note": "note", "password": "password", "password_confirmation": "confirm password", + "seed": "seed phrase", "tips": { + "additional_wallet": "use this option to connect additional Qortal wallets you've already made, in order to login with them afterwards. You will need access to your backup JSON file in order to do so.", "digital_id": "your wallet is like your digital ID on Qortal, and is how you will login to the Qortal User Interface. It holds your public address and the Qortal name you will eventually choose. Every transaction you make is linked to your ID, and this is where you manage all your QORT and other tradeable cryptocurrencies on Qortal.", "existing_account": "already have a Qortal account? Enter your secret backup phrase here to access it. This phrase is one of the ways to recover your account.", "new_account": "creating an account means creating a new wallet and digital ID to start using Qortal. Once you have made your account, you can start doing things like obtaining some QORT, buying a name and avatar, publishing videos and blogs, and much more.", diff --git a/src/i18n/locales/en/core.json b/src/i18n/locales/en/core.json index 6114abc..41ff3c0 100644 --- a/src/i18n/locales/en/core.json +++ b/src/i18n/locales/en/core.json @@ -20,6 +20,7 @@ "import": "import", "invite": "invite", "join": "join", + "login": "login", "logout": "logout", "new": { "post": "new post", @@ -28,7 +29,9 @@ "notify": "notify", "post": "post", "post_message": "post message", - "publish": "publish" + "publish": "publish", + "remove": "remove", + "save": "save" }, "admin": "admin", "core": { From 035142504b14b2711c01d69879896cd56ed4896c Mon Sep 17 00:00:00 2001 From: Nicola Benaglia Date: Mon, 12 May 2025 09:27:24 +0200 Subject: [PATCH 10/15] Move file --- src/components/Chat/AdminSpaceInner.tsx | 2 +- src/components/{ => Chat}/GroupAvatar.tsx | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) rename src/components/{ => Chat}/GroupAvatar.tsx (96%) diff --git a/src/components/Chat/AdminSpaceInner.tsx b/src/components/Chat/AdminSpaceInner.tsx index 9a1cffd..1d1dc26 100644 --- a/src/components/Chat/AdminSpaceInner.tsx +++ b/src/components/Chat/AdminSpaceInner.tsx @@ -15,7 +15,7 @@ import { base64ToUint8Array } from '../../qdn/encryption/group-encryption'; import { uint8ArrayToObject } from '../../backgroundFunctions/encryption'; import { formatTimestampForum } from '../../utils/time'; import { Spacer } from '../../common/Spacer'; -import { GroupAvatar } from '../GroupAvatar'; +import { GroupAvatar } from './GroupAvatar'; export const getPublishesFromAdminsAdminSpace = async ( admins: string[], diff --git a/src/components/GroupAvatar.tsx b/src/components/Chat/GroupAvatar.tsx similarity index 96% rename from src/components/GroupAvatar.tsx rename to src/components/Chat/GroupAvatar.tsx index 3244d5c..1533dbe 100644 --- a/src/components/GroupAvatar.tsx +++ b/src/components/Chat/GroupAvatar.tsx @@ -1,6 +1,10 @@ import { useCallback, useContext, useEffect, useState } from 'react'; import Logo2 from '../assets/svgs/Logo2.svg'; -import { MyContext, getArbitraryEndpointReact, getBaseApiReact } from '../App'; +import { + MyContext, + getArbitraryEndpointReact, + getBaseApiReact, +} from '../../App'; import { Avatar, Box, @@ -10,10 +14,10 @@ import { Typography, useTheme, } from '@mui/material'; -import { Spacer } from '../common/Spacer'; -import ImageUploader from '../common/ImageUploader'; -import { getFee } from '../background'; -import { fileToBase64 } from '../utils/fileReading'; +import { Spacer } from '../../common/Spacer'; +import ImageUploader from '../../common/ImageUploader'; +import { getFee } from '../../background'; +import { fileToBase64 } from '../../utils/fileReading'; import { LoadingButton } from '@mui/lab'; import ErrorIcon from '@mui/icons-material/Error'; From 452c3a0894576c6f3b521a0c72f5a7f6ff55b7ad Mon Sep 17 00:00:00 2001 From: Nicola Benaglia Date: Tue, 13 May 2025 06:15:26 +0200 Subject: [PATCH 11/15] Format code --- src/components/Chat/GroupAvatar.tsx | 2 +- src/utils/chromeStorage.ts | 63 +++++--- src/utils/decode.ts | 54 +++---- src/utils/decryptChatMessage.ts | 75 +++++---- src/utils/decryptWallet.ts | 57 +++---- src/utils/events.ts | 21 +-- src/utils/helpers.ts | 48 +++--- src/utils/indexedDB.ts | 169 +++++++++++--------- src/utils/memeTypes.ts | 230 ++++++++++++++-------------- src/utils/time.ts | 101 ++++++------ src/utils/utils.ts | 102 ++++++------ src/utils/validateAddress.ts | 29 ++-- 12 files changed, 502 insertions(+), 449 deletions(-) diff --git a/src/components/Chat/GroupAvatar.tsx b/src/components/Chat/GroupAvatar.tsx index 1533dbe..8e00911 100644 --- a/src/components/Chat/GroupAvatar.tsx +++ b/src/components/Chat/GroupAvatar.tsx @@ -1,5 +1,5 @@ import { useCallback, useContext, useEffect, useState } from 'react'; -import Logo2 from '../assets/svgs/Logo2.svg'; +import Logo2 from '../../assets/svgs/Logo2.svg'; import { MyContext, getArbitraryEndpointReact, diff --git a/src/utils/chromeStorage.ts b/src/utils/chromeStorage.ts index ebe7578..f2b227c 100644 --- a/src/utils/chromeStorage.ts +++ b/src/utils/chromeStorage.ts @@ -7,22 +7,25 @@ const keysToEncrypt = ['keyPair']; async function initializeKeyAndIV() { if (!inMemoryKey) { - inMemoryKey = await generateKey(); // Generates the key in memory + inMemoryKey = await generateKey(); // Generates the key in memory } } async function generateKey(): Promise { return await crypto.subtle.generateKey( { - name: "AES-GCM", - length: 256 + name: 'AES-GCM', + length: 256, }, true, - ["encrypt", "decrypt"] + ['encrypt', 'decrypt'] ); } -async function encryptData(data: string, key: CryptoKey): Promise<{ iv: Uint8Array; encryptedData: ArrayBuffer }> { +async function encryptData( + data: string, + key: CryptoKey +): Promise<{ iv: Uint8Array; encryptedData: ArrayBuffer }> { const encoder = new TextEncoder(); const encodedData = encoder.encode(data); @@ -31,8 +34,8 @@ async function encryptData(data: string, key: CryptoKey): Promise<{ iv: Uint8Arr const encryptedData = await crypto.subtle.encrypt( { - name: "AES-GCM", - iv: iv + name: 'AES-GCM', + iv: iv, }, key, encodedData @@ -41,11 +44,15 @@ async function encryptData(data: string, key: CryptoKey): Promise<{ iv: Uint8Arr return { iv, encryptedData }; } -async function decryptData(encryptedData: ArrayBuffer, key: CryptoKey, iv: Uint8Array): Promise { +async function decryptData( + encryptedData: ArrayBuffer, + key: CryptoKey, + iv: Uint8Array +): Promise { const decryptedData = await crypto.subtle.decrypt( { - name: "AES-GCM", - iv: iv + name: 'AES-GCM', + iv: iv, }, key, encryptedData @@ -83,7 +90,10 @@ export const storeData = async (key: string, payload: any): Promise => { const { iv, encryptedData } = await encryptData(base64Data, inMemoryKey); // Combine IV and encrypted data into a single Uint8Array - const combinedData = new Uint8Array([...iv, ...new Uint8Array(encryptedData)]); + const combinedData = new Uint8Array([ + ...iv, + ...new Uint8Array(encryptedData), + ]); const encryptedBase64Data = btoa(String.fromCharCode(...combinedData)); await SecureStoragePlugin.set({ key, value: encryptedBase64Data }); } else { @@ -91,10 +101,9 @@ export const storeData = async (key: string, payload: any): Promise => { await SecureStoragePlugin.set({ key, value: base64Data }); } - return "Data saved successfully"; + return 'Data saved successfully'; }; - export const getData = async (key: string): Promise => { await initializeKeyAndIV(); @@ -105,13 +114,17 @@ export const getData = async (key: string): Promise => { if (keysToEncrypt.includes(key) && inMemoryKey) { // Decode the Base64-encoded encrypted data const combinedData = atob(storedDataBase64.value) - .split("") + .split('') .map((c) => c.charCodeAt(0)); const iv = new Uint8Array(combinedData.slice(0, 12)); // First 12 bytes are the IV const encryptedData = new Uint8Array(combinedData.slice(12)).buffer; - const decryptedBase64Data = await decryptData(encryptedData, inMemoryKey, iv); + const decryptedBase64Data = await decryptData( + encryptedData, + inMemoryKey, + iv + ); return base64ToJson(decryptedBase64Data); } else { // Decode non-encrypted data @@ -121,21 +134,21 @@ export const getData = async (key: string): Promise => { return null; } } catch (error) { - return null + return null; } }; - - - - // Remove keys from storage and log out -export async function removeKeysAndLogout(keys: string[], event: MessageEvent, request: any) { +export async function removeKeysAndLogout( + keys: string[], + event: MessageEvent, + request: any +) { try { for (const key of keys) { try { await SecureStoragePlugin.remove({ key }); - await SecureStoragePlugin.remove({ key: `${key}_iv` }); // Remove associated IV + await SecureStoragePlugin.remove({ key: `${key}_iv` }); // Remove associated IV } catch (error) { console.warn(`Key not found: ${key}`); } @@ -144,13 +157,13 @@ export async function removeKeysAndLogout(keys: string[], event: MessageEvent, r event.source.postMessage( { requestId: request.requestId, - action: "logout", + action: 'logout', payload: true, - type: "backgroundMessageResponse", + type: 'backgroundMessageResponse', }, event.origin ); } catch (error) { - console.error("Error removing keys:", error); + console.error('Error removing keys:', error); } } diff --git a/src/utils/decode.ts b/src/utils/decode.ts index 06fdb2b..ba8d87d 100644 --- a/src/utils/decode.ts +++ b/src/utils/decode.ts @@ -1,31 +1,31 @@ export function decodeIfEncoded(input) { - try { - // Check if input is URI-encoded by encoding and decoding - const encoded = encodeURIComponent(decodeURIComponent(input)); - if (encoded === input) { - // Input is URI-encoded, so decode it - return decodeURIComponent(input); - } - } catch (e) { - // decodeURIComponent throws an error if input is not encoded - console.error("Error decoding URI:", e); + try { + // Check if input is URI-encoded by encoding and decoding + const encoded = encodeURIComponent(decodeURIComponent(input)); + if (encoded === input) { + // Input is URI-encoded, so decode it + return decodeURIComponent(input); } - - // Return input as-is if not URI-encoded - return input; + } catch (e) { + // decodeURIComponent throws an error if input is not encoded + console.error('Error decoding URI:', e); } - export const isValidBase64 = (str: string): boolean => { - if (typeof str !== "string" || str.length % 4 !== 0) return false; - - const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/; - return base64Regex.test(str); - }; - - export const isValidBase64WithDecode = (str: string): boolean => { - try { - return isValidBase64(str) && Boolean(atob(str)); - } catch { - return false; - } - }; \ No newline at end of file + // Return input as-is if not URI-encoded + return input; +} + +export const isValidBase64 = (str: string): boolean => { + if (typeof str !== 'string' || str.length % 4 !== 0) return false; + + const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/; + return base64Regex.test(str); +}; + +export const isValidBase64WithDecode = (str: string): boolean => { + try { + return isValidBase64(str) && Boolean(atob(str)); + } catch { + return false; + } +}; diff --git a/src/utils/decryptChatMessage.ts b/src/utils/decryptChatMessage.ts index c50bb45..39085e8 100644 --- a/src/utils/decryptChatMessage.ts +++ b/src/utils/decryptChatMessage.ts @@ -1,38 +1,59 @@ // @ts-nocheck -import Base58 from '../deps/Base58' -import ed2curve from '../deps/ed2curve' -import nacl from '../deps/nacl-fast' -import {Sha256} from 'asmcrypto.js' +import Base58 from '../deps/Base58'; +import ed2curve from '../deps/ed2curve'; +import nacl from '../deps/nacl-fast'; +import { Sha256 } from 'asmcrypto.js'; +export const decryptChatMessage = ( + encryptedMessage, + privateKey, + recipientPublicKey, + lastReference +) => { + const test = encryptedMessage; + let _encryptedMessage = atob(encryptedMessage); + const binaryLength = _encryptedMessage.length; + const bytes = new Uint8Array(binaryLength); -export const decryptChatMessage = (encryptedMessage, privateKey, recipientPublicKey, lastReference) => { - const test = encryptedMessage - let _encryptedMessage = atob(encryptedMessage) - const binaryLength = _encryptedMessage.length - const bytes = new Uint8Array(binaryLength) + for (let i = 0; i < binaryLength; i++) { + bytes[i] = _encryptedMessage.charCodeAt(i); + } - for (let i = 0; i < binaryLength; i++) { - bytes[i] = _encryptedMessage.charCodeAt(i) - } + const _base58RecipientPublicKey = + recipientPublicKey instanceof Uint8Array + ? Base58.encode(recipientPublicKey) + : recipientPublicKey; + const _recipientPublicKey = Base58.decode(_base58RecipientPublicKey); - - const _base58RecipientPublicKey = recipientPublicKey instanceof Uint8Array ? Base58.encode(recipientPublicKey) : recipientPublicKey - const _recipientPublicKey = Base58.decode(_base58RecipientPublicKey) + const _lastReference = + lastReference instanceof Uint8Array + ? lastReference + : Base58.decode(lastReference); - const _lastReference = lastReference instanceof Uint8Array ? lastReference : Base58.decode(lastReference) + const convertedPrivateKey = ed2curve.convertSecretKey(privateKey); + const convertedPublicKey = ed2curve.convertPublicKey(_recipientPublicKey); + const sharedSecret = new Uint8Array(32); + nacl.lowlevel.crypto_scalarmult( + sharedSecret, + convertedPrivateKey, + convertedPublicKey + ); - const convertedPrivateKey = ed2curve.convertSecretKey(privateKey) - const convertedPublicKey = ed2curve.convertPublicKey(_recipientPublicKey) - const sharedSecret = new Uint8Array(32); - nacl.lowlevel.crypto_scalarmult(sharedSecret, convertedPrivateKey, convertedPublicKey) + const _chatEncryptionSeed = new Sha256() + .process(sharedSecret) + .finish().result; + const _decryptedMessage = nacl.secretbox.open( + bytes, + _lastReference.slice(0, 24), + _chatEncryptionSeed + ); - const _chatEncryptionSeed = new Sha256().process(sharedSecret).finish().result - const _decryptedMessage = nacl.secretbox.open(bytes, _lastReference.slice(0, 24), _chatEncryptionSeed) + let decryptedMessage = ''; - let decryptedMessage = '' + _decryptedMessage === false + ? decryptedMessage + : (decryptedMessage = new TextDecoder('utf-8').decode(_decryptedMessage)); - _decryptedMessage === false ? decryptedMessage : decryptedMessage = new TextDecoder('utf-8').decode(_decryptedMessage) - - return decryptedMessage -} \ No newline at end of file + return decryptedMessage; +}; diff --git a/src/utils/decryptWallet.ts b/src/utils/decryptWallet.ts index b091639..11394a2 100644 --- a/src/utils/decryptWallet.ts +++ b/src/utils/decryptWallet.ts @@ -1,33 +1,38 @@ // @ts-nocheck -import { crypto } from '../constants/decryptWallet' -import Base58 from '../deps/Base58' -import {AES_CBC, HmacSha512} from 'asmcrypto.js' -import { doInitWorkers, kdf } from '../deps/kdf' - +import { crypto } from '../constants/decryptWallet'; +import Base58 from '../deps/Base58'; +import { AES_CBC, HmacSha512 } from 'asmcrypto.js'; +import { doInitWorkers, kdf } from '../deps/kdf'; export const decryptStoredWallet = async (password, wallet) => { - const threads = doInitWorkers(crypto.kdfThreads) - const encryptedSeedBytes = Base58.decode(wallet.encryptedSeed) - const iv = Base58.decode(wallet.iv) - const salt = Base58.decode(wallet.salt) - - const key = await kdf(password, salt, threads) - const encryptionKey = key.slice(0, 32) - const macKey = key.slice(32, 63) - const mac = new HmacSha512(macKey).process(encryptedSeedBytes).finish().result - if (Base58.encode(mac) !== wallet.mac) { - throw new Error("Incorrect password") - } - const decryptedBytes = AES_CBC.decrypt(encryptedSeedBytes, encryptionKey, false, iv) - return decryptedBytes -} + const threads = doInitWorkers(crypto.kdfThreads); + const encryptedSeedBytes = Base58.decode(wallet.encryptedSeed); + const iv = Base58.decode(wallet.iv); + const salt = Base58.decode(wallet.salt); + + const key = await kdf(password, salt, threads); + const encryptionKey = key.slice(0, 32); + const macKey = key.slice(32, 63); + const mac = new HmacSha512(macKey) + .process(encryptedSeedBytes) + .finish().result; + if (Base58.encode(mac) !== wallet.mac) { + throw new Error('Incorrect password'); + } + const decryptedBytes = AES_CBC.decrypt( + encryptedSeedBytes, + encryptionKey, + false, + iv + ); + return decryptedBytes; +}; export const decryptStoredWalletFromSeedPhrase = async (password) => { - const threads = doInitWorkers(crypto.kdfThreads) - const salt = new Uint8Array(void 0) + const threads = doInitWorkers(crypto.kdfThreads); + const salt = new Uint8Array(void 0); - - const seed = await kdf(password, salt, threads) - return seed -} \ No newline at end of file + const seed = await kdf(password, salt, threads); + return seed; +}; diff --git a/src/utils/events.ts b/src/utils/events.ts index 94d1c31..5145a93 100644 --- a/src/utils/events.ts +++ b/src/utils/events.ts @@ -1,11 +1,12 @@ -export const executeEvent = (eventName: string, data: any)=> { - const event = new CustomEvent(eventName, {detail: data}) - document.dispatchEvent(event) -} -export const subscribeToEvent = (eventName: string, listener: any)=> { - document.addEventListener(eventName, listener) -} +export const executeEvent = (eventName: string, data: any) => { + const event = new CustomEvent(eventName, { detail: data }); + document.dispatchEvent(event); +}; -export const unsubscribeFromEvent = (eventName: string, listener: any)=> { - document.removeEventListener(eventName, listener) -} \ No newline at end of file +export const subscribeToEvent = (eventName: string, listener: any) => { + document.addEventListener(eventName, listener); +}; + +export const unsubscribeFromEvent = (eventName: string, listener: any) => { + document.removeEventListener(eventName, listener); +}; diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index ff7997c..a60a523 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,34 +1,28 @@ -import moment from "moment"; - -export const delay = (time: number) => new Promise((_, reject) => +export const delay = (time: number) => + new Promise((_, reject) => setTimeout(() => reject(new Error('Request timed out')), time) -); - -// const originalHtml = `

---------- Forwarded message ---------

From: Alex

Date: Mon, Jun 9 2014 9:32 PM

Subject: Batteries

To: Jessica



`; - - -// export function updateMessageDetails(newFrom: string, newDateMillis: number, newTo: string) { -// let htmlString = originalHtml -// // Use Moment.js to format the date from milliseconds -// const formattedDate = moment(newDateMillis).format('ddd, MMM D YYYY h:mm A'); - -// // Replace the From, Date, and To fields in the HTML string -// htmlString = htmlString.replace(/

From:.*?<\/p>/, `

From: ${newFrom}

`); -// htmlString = htmlString.replace(/

Date:.*?<\/p>/, `

Date: ${formattedDate}

`); -// htmlString = htmlString.replace(/

To:.*?<\/p>/, `

To: ${newTo}

`); - -// return htmlString; -// } + ); const originalHtml = `

---------- Forwarded message ---------

From: Alex

Subject: Batteries

To: Jessica



`; +export function updateMessageDetails( + newFrom: string, + newSubject: string, + newTo: string +) { + let htmlString = originalHtml; -export function updateMessageDetails(newFrom: string, newSubject: string, newTo: string) { - let htmlString = originalHtml + htmlString = htmlString.replace( + /

From:.*?<\/p>/, + `

From: ${newFrom}

` + ); - htmlString = htmlString.replace(/

From:.*?<\/p>/, `

From: ${newFrom}

`); - htmlString = htmlString.replace(/

Subject:.*?<\/p>/, `

Subject: ${newSubject}

`); - htmlString = htmlString.replace(/

To:.*?<\/p>/, `

To: ${newTo}

`); + htmlString = htmlString.replace( + /

Subject:.*?<\/p>/, + `

Subject: ${newSubject}

` + ); - return htmlString; -} \ No newline at end of file + htmlString = htmlString.replace(/

To:.*?<\/p>/, `

To: ${newTo}

`); + + return htmlString; +} diff --git a/src/utils/indexedDB.ts b/src/utils/indexedDB.ts index 0364570..3066b6b 100644 --- a/src/utils/indexedDB.ts +++ b/src/utils/indexedDB.ts @@ -1,90 +1,107 @@ -import { openIndexedDB } from "../components/Apps/useQortalMessageListener"; -import { fileToBase64 } from "./fileReading"; +import { openIndexedDB } from '../components/Apps/useQortalMessageListener'; +import { fileToBase64 } from './fileReading'; export async function handleGetFileFromIndexedDB(event) { - try { - const { fileId, requestId } = event.data; - const db = await openIndexedDB(); - const transaction = db.transaction(["files"], "readonly"); - const objectStore = transaction.objectStore("files"); - - const getRequest = objectStore.get(fileId); - - getRequest.onsuccess = async function (event) { - if (getRequest.result) { - const file = getRequest.result.data; - - try { - const base64String = await fileToBase64(file); - - // Create a new transaction to delete the file - const deleteTransaction = db.transaction(["files"], "readwrite"); - const deleteObjectStore = deleteTransaction.objectStore("files"); - const deleteRequest = deleteObjectStore.delete(fileId); - - deleteRequest.onsuccess = function () { - try { - const targetOrigin = window.location.origin; + try { + const { fileId, requestId } = event.data; + const db = await openIndexedDB(); + const transaction = db.transaction(['files'], 'readonly'); + const objectStore = transaction.objectStore('files'); - window.postMessage( - { action: "getFileFromIndexedDBResponse", requestId, result: base64String }, - targetOrigin - ); - } catch (error) { - console.log('error', error) - } - }; - - deleteRequest.onerror = function () { - console.error(`Error deleting file with ID ${fileId} from IndexedDB`); - - }; - } catch (error) { - console.error("Error converting file to Base64:", error); - event.ports[0].postMessage({ - result: null, - error: "Failed to convert file to Base64", - }); - const targetOrigin = window.location.origin; + const getRequest = objectStore.get(fileId); - window.postMessage( - { action: "getFileFromIndexedDBResponse", requestId, result: null, - error: "Failed to convert file to Base64" - }, - targetOrigin + getRequest.onsuccess = async function (event) { + if (getRequest.result) { + const file = getRequest.result.data; + + try { + const base64String = await fileToBase64(file); + + // Create a new transaction to delete the file + const deleteTransaction = db.transaction(['files'], 'readwrite'); + const deleteObjectStore = deleteTransaction.objectStore('files'); + const deleteRequest = deleteObjectStore.delete(fileId); + + deleteRequest.onsuccess = function () { + try { + const targetOrigin = window.location.origin; + + window.postMessage( + { + action: 'getFileFromIndexedDBResponse', + requestId, + result: base64String, + }, + targetOrigin + ); + } catch (error) { + console.log('error', error); + } + }; + + deleteRequest.onerror = function () { + console.error( + `Error deleting file with ID ${fileId} from IndexedDB` ); - } - } else { - console.error(`File with ID ${fileId} not found in IndexedDB`); + }; + } catch (error) { + console.error('Error converting file to Base64:', error); + event.ports[0].postMessage({ + result: null, + error: 'Failed to convert file to Base64', + }); const targetOrigin = window.location.origin; window.postMessage( - { action: "getFileFromIndexedDBResponse", requestId, result: null, - error: 'File not found in IndexedDB' - }, - targetOrigin + { + action: 'getFileFromIndexedDBResponse', + requestId, + result: null, + error: 'Failed to convert file to Base64', + }, + targetOrigin ); } - }; - - getRequest.onerror = function () { - console.error(`Error retrieving file with ID ${fileId} from IndexedDB`); - - event.source.postMessage( - { action: "getFileFromIndexedDBResponse", requestId, result: null, - error: 'Error retrieving file from IndexedDB' - }, - event.origin + } else { + console.error(`File with ID ${fileId} not found in IndexedDB`); + const targetOrigin = window.location.origin; + + window.postMessage( + { + action: 'getFileFromIndexedDBResponse', + requestId, + result: null, + error: 'File not found in IndexedDB', + }, + targetOrigin ); - }; - } catch (error) { - const { requestId } = event.data; - console.error("Error opening IndexedDB:", error); + } + }; + + getRequest.onerror = function () { + console.error(`Error retrieving file with ID ${fileId} from IndexedDB`); + event.source.postMessage( - { action: "getFileFromIndexedDBResponse", requestId, result: null, - error: 'Error opening IndexedDB' - }, + { + action: 'getFileFromIndexedDBResponse', + requestId, + result: null, + error: 'Error retrieving file from IndexedDB', + }, event.origin ); - } - } \ No newline at end of file + }; + } catch (error) { + const { requestId } = event.data; + console.error('Error opening IndexedDB:', error); + event.source.postMessage( + { + action: 'getFileFromIndexedDBResponse', + requestId, + result: null, + error: 'Error opening IndexedDB', + }, + event.origin + ); + } +} diff --git a/src/utils/memeTypes.ts b/src/utils/memeTypes.ts index 2bd8ac0..f87277f 100644 --- a/src/utils/memeTypes.ts +++ b/src/utils/memeTypes.ts @@ -1,123 +1,125 @@ export const mimeToExtensionMap = { - // Documents - "application/pdf": ".pdf", - "application/msword": ".doc", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx", - "application/vnd.ms-excel": ".xls", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx", - "application/vnd.ms-powerpoint": ".ppt", - "application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx", - "application/vnd.oasis.opendocument.text": ".odt", - "application/vnd.oasis.opendocument.spreadsheet": ".ods", - "application/vnd.oasis.opendocument.presentation": ".odp", - "text/plain": ".txt", - "text/csv": ".csv", - "application/xhtml+xml": ".xhtml", - "application/xml": ".xml", - "application/rtf": ".rtf", - "application/vnd.apple.pages": ".pages", - "application/vnd.google-apps.document": ".gdoc", - "application/vnd.google-apps.spreadsheet": ".gsheet", - "application/vnd.google-apps.presentation": ".gslides", + // Documents + 'application/pdf': '.pdf', + 'application/msword': '.doc', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': + '.docx', + 'application/vnd.ms-excel': '.xls', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx', + 'application/vnd.ms-powerpoint': '.ppt', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation': + '.pptx', + 'application/vnd.oasis.opendocument.text': '.odt', + 'application/vnd.oasis.opendocument.spreadsheet': '.ods', + 'application/vnd.oasis.opendocument.presentation': '.odp', + 'text/plain': '.txt', + 'text/csv': '.csv', + 'application/xhtml+xml': '.xhtml', + 'application/xml': '.xml', + 'application/rtf': '.rtf', + 'application/vnd.apple.pages': '.pages', + 'application/vnd.google-apps.document': '.gdoc', + 'application/vnd.google-apps.spreadsheet': '.gsheet', + 'application/vnd.google-apps.presentation': '.gslides', - // Images - "image/jpeg": ".jpg", - "image/png": ".png", - "image/gif": ".gif", - "image/webp": ".webp", - "image/svg+xml": ".svg", - "image/tiff": ".tif", - "image/bmp": ".bmp", - "image/x-icon": ".ico", - "image/heic": ".heic", - "image/heif": ".heif", - "image/apng": ".apng", - "image/avif": ".avif", + // Images + 'image/jpeg': '.jpg', + 'image/png': '.png', + 'image/gif': '.gif', + 'image/webp': '.webp', + 'image/svg+xml': '.svg', + 'image/tiff': '.tif', + 'image/bmp': '.bmp', + 'image/x-icon': '.ico', + 'image/heic': '.heic', + 'image/heif': '.heif', + 'image/apng': '.apng', + 'image/avif': '.avif', - // Audio - "audio/mpeg": ".mp3", - "audio/ogg": ".ogg", - "audio/wav": ".wav", - "audio/webm": ".weba", - "audio/aac": ".aac", - "audio/flac": ".flac", - "audio/x-m4a": ".m4a", - "audio/x-ms-wma": ".wma", - "audio/midi": ".midi", - "audio/x-midi": ".mid", + // Audio + 'audio/mpeg': '.mp3', + 'audio/ogg': '.ogg', + 'audio/wav': '.wav', + 'audio/webm': '.weba', + 'audio/aac': '.aac', + 'audio/flac': '.flac', + 'audio/x-m4a': '.m4a', + 'audio/x-ms-wma': '.wma', + 'audio/midi': '.midi', + 'audio/x-midi': '.mid', - // Video - "video/mp4": ".mp4", - "video/webm": ".webm", - "video/ogg": ".ogv", - "video/x-msvideo": ".avi", - "video/quicktime": ".mov", - "video/x-ms-wmv": ".wmv", - "video/mpeg": ".mpeg", - "video/3gpp": ".3gp", - "video/3gpp2": ".3g2", - "video/x-matroska": ".mkv", - "video/x-flv": ".flv", - "video/x-ms-asf": ".asf", + // Video + 'video/mp4': '.mp4', + 'video/webm': '.webm', + 'video/ogg': '.ogv', + 'video/x-msvideo': '.avi', + 'video/quicktime': '.mov', + 'video/x-ms-wmv': '.wmv', + 'video/mpeg': '.mpeg', + 'video/3gpp': '.3gp', + 'video/3gpp2': '.3g2', + 'video/x-matroska': '.mkv', + 'video/x-flv': '.flv', + 'video/x-ms-asf': '.asf', - // Archives - "application/zip": ".zip", - "application/x-rar-compressed": ".rar", - "application/x-tar": ".tar", - "application/x-7z-compressed": ".7z", - "application/x-gzip": ".gz", - "application/x-bzip2": ".bz2", - "application/x-apple-diskimage": ".dmg", - "application/vnd.android.package-archive": ".apk", - "application/x-iso9660-image": ".iso", + // Archives + 'application/zip': '.zip', + 'application/x-rar-compressed': '.rar', + 'application/x-tar': '.tar', + 'application/x-7z-compressed': '.7z', + 'application/x-gzip': '.gz', + 'application/x-bzip2': '.bz2', + 'application/x-apple-diskimage': '.dmg', + 'application/vnd.android.package-archive': '.apk', + 'application/x-iso9660-image': '.iso', - // Code Files - "text/javascript": ".js", - "text/css": ".css", - "text/html": ".html", - "application/json": ".json", - "text/xml": ".xml", - "application/x-sh": ".sh", - "application/x-csh": ".csh", - "text/x-python": ".py", - "text/x-java-source": ".java", - "application/java-archive": ".jar", - "application/vnd.microsoft.portable-executable": ".exe", - "application/x-msdownload": ".msi", - "text/x-c": ".c", - "text/x-c++": ".cpp", - "text/x-go": ".go", - "application/x-perl": ".pl", - "text/x-php": ".php", - "text/x-ruby": ".rb", - "text/x-sql": ".sql", - "application/x-httpd-php": ".php", - "application/x-python-code": ".pyc", + // Code Files + 'text/javascript': '.js', + 'text/css': '.css', + 'text/html': '.html', + 'application/json': '.json', + 'text/xml': '.xml', + 'application/x-sh': '.sh', + 'application/x-csh': '.csh', + 'text/x-python': '.py', + 'text/x-java-source': '.java', + 'application/java-archive': '.jar', + 'application/vnd.microsoft.portable-executable': '.exe', + 'application/x-msdownload': '.msi', + 'text/x-c': '.c', + 'text/x-c++': '.cpp', + 'text/x-go': '.go', + 'application/x-perl': '.pl', + 'text/x-php': '.php', + 'text/x-ruby': '.rb', + 'text/x-sql': '.sql', + 'application/x-httpd-php': '.php', + 'application/x-python-code': '.pyc', - // ROM Files - "application/x-nintendo-nes-rom": ".nes", - "application/x-snes-rom": ".smc", - "application/x-gameboy-rom": ".gb", - "application/x-gameboy-advance-rom": ".gba", - "application/x-n64-rom": ".n64", - "application/x-sega-genesis-rom": ".gen", - "application/x-sega-master-system-rom": ".sms", - "application/x-psx-rom": ".iso", // PlayStation ROMs - "application/x-bios-rom": ".rom", - "application/x-flash-rom": ".bin", - "application/x-eeprom": ".eep", - "application/x-c64-rom": ".prg", + // ROM Files + 'application/x-nintendo-nes-rom': '.nes', + 'application/x-snes-rom': '.smc', + 'application/x-gameboy-rom': '.gb', + 'application/x-gameboy-advance-rom': '.gba', + 'application/x-n64-rom': '.n64', + 'application/x-sega-genesis-rom': '.gen', + 'application/x-sega-master-system-rom': '.sms', + 'application/x-psx-rom': '.iso', // PlayStation ROMs + 'application/x-bios-rom': '.rom', + 'application/x-flash-rom': '.bin', + 'application/x-eeprom': '.eep', + 'application/x-c64-rom': '.prg', - // Miscellaneous - "application/octet-stream": ".bin", // General binary files - "application/x-shockwave-flash": ".swf", - "application/x-silverlight-app": ".xap", - "application/x-ms-shortcut": ".lnk", - "application/vnd.ms-fontobject": ".eot", - "font/woff": ".woff", - "font/woff2": ".woff2", - "font/ttf": ".ttf", - "font/otf": ".otf", - "application/vnd.visio": ".vsd", - "application/vnd.ms-project": ".mpp", + // Miscellaneous + 'application/octet-stream': '.bin', // General binary files + 'application/x-shockwave-flash': '.swf', + 'application/x-silverlight-app': '.xap', + 'application/x-ms-shortcut': '.lnk', + 'application/vnd.ms-fontobject': '.eot', + 'font/woff': '.woff', + 'font/woff2': '.woff2', + 'font/ttf': '.ttf', + 'font/otf': '.otf', + 'application/vnd.visio': '.vsd', + 'application/vnd.ms-project': '.mpp', }; diff --git a/src/utils/time.ts b/src/utils/time.ts index c89c1cd..e434cfb 100644 --- a/src/utils/time.ts +++ b/src/utils/time.ts @@ -1,56 +1,57 @@ -import moment from "moment" +import moment from 'moment'; export function formatTimestamp(timestamp: number): string { - const now = moment() - const timestampMoment = moment(timestamp) - const elapsedTime = now.diff(timestampMoment, 'minutes') - - if (elapsedTime < 1) { - return 'Just now' - } else if (elapsedTime < 60) { - return `${elapsedTime}m ago` - } else if (elapsedTime < 1440) { - return `${Math.floor(elapsedTime / 60)}h ago` - } else { - return timestampMoment.format('MMM D, YYYY') - } + const now = moment(); + const timestampMoment = moment(timestamp); + const elapsedTime = now.diff(timestampMoment, 'minutes'); + + if (elapsedTime < 1) { + return 'Just now'; + } else if (elapsedTime < 60) { + return `${elapsedTime}m ago`; + } else if (elapsedTime < 1440) { + return `${Math.floor(elapsedTime / 60)}h ago`; + } else { + return timestampMoment.format('MMM D, YYYY'); } - export function formatTimestampForum(timestamp: number): string { - const now = moment(); - const timestampMoment = moment(timestamp); - const elapsedTime = now.diff(timestampMoment, 'minutes'); - - if (elapsedTime < 1) { - return `Just now - ${timestampMoment.format('h:mm A')}`; - } else if (elapsedTime < 60) { - return `${elapsedTime}m ago - ${timestampMoment.format('h:mm A')}`; - } else if (elapsedTime < 1440) { - return `${Math.floor(elapsedTime / 60)}h ago - ${timestampMoment.format('h:mm A')}`; - } else { - return timestampMoment.format('MMM D, YYYY - h:mm A'); - } } - export const formatDate = (unixTimestamp: number): string => { - const date = moment(unixTimestamp, 'x').fromNow() - - return date - } +export function formatTimestampForum(timestamp: number): string { + const now = moment(); + const timestampMoment = moment(timestamp); + const elapsedTime = now.diff(timestampMoment, 'minutes'); - export function sortArrayByTimestampAndGroupName(array) { - return array.sort((a, b) => { - if (a.timestamp && b.timestamp) { - // Both have timestamp, sort by timestamp descending - return b.timestamp - a.timestamp; - } else if (a.timestamp) { - // Only `a` has timestamp, it comes first - return -1; - } else if (b.timestamp) { - // Only `b` has timestamp, it comes first - return 1; - } else { - // Neither has timestamp, sort alphabetically by groupName - return a.groupName.localeCompare(b.groupName); - } - }); - } \ No newline at end of file + if (elapsedTime < 1) { + return `Just now - ${timestampMoment.format('h:mm A')}`; + } else if (elapsedTime < 60) { + return `${elapsedTime}m ago - ${timestampMoment.format('h:mm A')}`; + } else if (elapsedTime < 1440) { + return `${Math.floor(elapsedTime / 60)}h ago - ${timestampMoment.format('h:mm A')}`; + } else { + return timestampMoment.format('MMM D, YYYY - h:mm A'); + } +} + +export const formatDate = (unixTimestamp: number): string => { + const date = moment(unixTimestamp, 'x').fromNow(); + + return date; +}; + +export function sortArrayByTimestampAndGroupName(array) { + return array.sort((a, b) => { + if (a.timestamp && b.timestamp) { + // Both have timestamp, sort by timestamp descending + return b.timestamp - a.timestamp; + } else if (a.timestamp) { + // Only `a` has timestamp, it comes first + return -1; + } else if (b.timestamp) { + // Only `b` has timestamp, it comes first + return 1; + } else { + // Neither has timestamp, sort alphabetically by groupName + return a.groupName.localeCompare(b.groupName); + } + }); +} diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 49912ae..a981fe0 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,55 +1,55 @@ // @ts-nocheck const utils = { - int32ToBytes (word) { - var byteArray = [] - for (var b = 0; b < 32; b += 8) { - byteArray.push((word >>> (24 - b % 32)) & 0xFF) - } - return byteArray - }, - - stringtoUTF8Array (message) { - if (typeof message === 'string') { - var s = unescape(encodeURIComponent(message)) // UTF-8 - message = new Uint8Array(s.length) - for (var i = 0; i < s.length; i++) { - message[i] = s.charCodeAt(i) & 0xff - } - } - return message - }, - // ...buffers then buffers.foreach and append to buffer1 - appendBuffer (buffer1, buffer2) { - buffer1 = new Uint8Array(buffer1) - buffer2 = new Uint8Array(buffer2) - const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength) - tmp.set(buffer1, 0) - tmp.set(buffer2, buffer1.byteLength) - return tmp - }, - - int64ToBytes (int64) { - // we want to represent the input as a 8-bytes array - var byteArray = [0, 0, 0, 0, 0, 0, 0, 0] - - for (var index = 0; index < byteArray.length; index++) { - var byte = int64 & 0xff - byteArray[byteArray.length - index - 1] = byte - int64 = (int64 - byte) / 256 - } - - return byteArray - }, - - equal (buf1, buf2) { - if (buf1.byteLength != buf2.byteLength) return false - var dv1 = new Uint8Array(buf1) - var dv2 = new Uint8Array(buf2) - for (var i = 0; i != buf1.byteLength; i++) { - if (dv1[i] != dv2[i]) return false - } - return true + int32ToBytes(word) { + var byteArray = []; + for (var b = 0; b < 32; b += 8) { + byteArray.push((word >>> (24 - (b % 32))) & 0xff); } -} + return byteArray; + }, -export default utils + stringtoUTF8Array(message) { + if (typeof message === 'string') { + var s = unescape(encodeURIComponent(message)); // UTF-8 + message = new Uint8Array(s.length); + for (var i = 0; i < s.length; i++) { + message[i] = s.charCodeAt(i) & 0xff; + } + } + return message; + }, + // ...buffers then buffers.foreach and append to buffer1 + appendBuffer(buffer1, buffer2) { + buffer1 = new Uint8Array(buffer1); + buffer2 = new Uint8Array(buffer2); + const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength); + tmp.set(buffer1, 0); + tmp.set(buffer2, buffer1.byteLength); + return tmp; + }, + + int64ToBytes(int64) { + // we want to represent the input as a 8-bytes array + var byteArray = [0, 0, 0, 0, 0, 0, 0, 0]; + + for (var index = 0; index < byteArray.length; index++) { + var byte = int64 & 0xff; + byteArray[byteArray.length - index - 1] = byte; + int64 = (int64 - byte) / 256; + } + + return byteArray; + }, + + equal(buf1, buf2) { + if (buf1.byteLength != buf2.byteLength) return false; + var dv1 = new Uint8Array(buf1); + var dv2 = new Uint8Array(buf2); + for (var i = 0; i != buf1.byteLength; i++) { + if (dv1[i] != dv2[i]) return false; + } + return true; + }, +}; + +export default utils; diff --git a/src/utils/validateAddress.ts b/src/utils/validateAddress.ts index 8b359bf..bbab210 100644 --- a/src/utils/validateAddress.ts +++ b/src/utils/validateAddress.ts @@ -1,21 +1,20 @@ // @ts-nocheck -import Base58 from "../deps/Base58" +import Base58 from '../deps/Base58'; export const validateAddress = (address) => { - let isAddress = false - try { - const decodePubKey = Base58.decode(address) + let isAddress = false; + try { + const decodePubKey = Base58.decode(address); - if (!(decodePubKey instanceof Uint8Array && decodePubKey.length == 25)) { - isAddress = false - } else { - isAddress = true - } - - } catch (error) { - - } + if (!(decodePubKey instanceof Uint8Array && decodePubKey.length == 25)) { + isAddress = false; + } else { + isAddress = true; + } + } catch (error) { + console.log(error); + } - return isAddress -} + return isAddress; +}; From 9adddce9fc7a863918e9f4151666d8fd392d99a6 Mon Sep 17 00:00:00 2001 From: Nicola Benaglia Date: Tue, 13 May 2025 06:39:29 +0200 Subject: [PATCH 12/15] Add minting translations --- src/components/Minting/Minting.tsx | 186 +++++++++++++++++++---------- src/i18n/locales/en/core.json | 2 + src/i18n/locales/en/group.json | 9 +- 3 files changed, 132 insertions(+), 65 deletions(-) diff --git a/src/components/Minting/Minting.tsx b/src/components/Minting/Minting.tsx index fe05c5b..c84ffdb 100644 --- a/src/components/Minting/Minting.tsx +++ b/src/components/Minting/Minting.tsx @@ -27,6 +27,7 @@ import { FidgetSpinner } from 'react-loader-spinner'; import { useModal } from '../../common/useModal'; import { useAtom, useSetAtom } from 'jotai'; import { memberGroupsAtom, txListAtom } from '../../atoms/global'; +import { useTranslation } from 'react-i18next'; export const Minting = ({ setIsOpenMinting, myAddress, show }) => { const setTxList = useSetAtom(txListAtom); @@ -44,7 +45,7 @@ export const Minting = ({ setIsOpenMinting, myAddress, show }) => { const { show: showKey, message } = useModal(); const { isShow: isShowNext, onOk, show: showNext } = useModal(); const theme = useTheme(); - + const { t } = useTranslation(['core', 'auth', 'group']); const [info, setInfo] = useState(null); const [names, setNames] = useState({}); const [accountInfos, setAccountInfos] = useState({}); @@ -223,13 +224,21 @@ export const Minting = ({ setIsOpenMinting, myAddress, show }) => { rej({ message: response.error }); }) .catch((error) => { - rej({ message: error.message || 'An error occurred' }); + rej({ + message: + error.message || + t('core:message.error.generic', { postProcess: 'capitalize' }), + }); }); }); } catch (error) { setInfo({ type: 'error', - message: error?.message || 'Unable to add minting account', + message: + error?.message || + t('core:message.error.minting_account_add', { + postProcess: 'capitalize', + }), }); setOpenSnack(true); } finally { @@ -263,13 +272,21 @@ export const Minting = ({ setIsOpenMinting, myAddress, show }) => { rej({ message: response.error }); }) .catch((error) => { - rej({ message: error.message || 'An error occurred' }); + rej({ + message: + error.message || + t('core:message.error.generic', { postProcess: 'capitalize' }), + }); }); }); } catch (error) { setInfo({ type: 'error', - message: error?.message || 'Unable to remove minting account', + message: + error?.message || + t('core:message.error.minting_account_remove', { + postProcess: 'capitalize', + }), }); setOpenSnack(true); } finally { @@ -278,9 +295,13 @@ export const Minting = ({ setIsOpenMinting, myAddress, show }) => { }, []); const createRewardShare = useCallback(async (publicKey, recipient) => { - const fee = await getFee('REWARD_SHARE'); // TODO translate + const fee = await getFee('REWARD_SHARE'); await show({ - message: 'Would you like to perform an REWARD_SHARE transaction?', + message: t('group:question.perform_transaction', { + // TODO move from group into core namespace + action: 'REWARD_SHARE', + postProcess: 'capitalize', + }), publishFee: fee.fee + ' QORT', }); return await new Promise((res, rej) => { @@ -295,8 +316,12 @@ export const Minting = ({ setIsOpenMinting, myAddress, show }) => { recipient, ...response, type: 'add-rewardShare', - label: `Add rewardshare: awaiting confirmation`, - labelDone: `Add rewardshare: success!`, + label: t('group:message.success.rewardshare_add', { + postProcess: 'capitalize', + }), + labelDone: t('group:message.success.rewardshare_add_label', { + postProcess: 'capitalize', + }), done: false, }, ...prev, @@ -307,7 +332,11 @@ export const Minting = ({ setIsOpenMinting, myAddress, show }) => { rej({ message: response.error }); }) .catch((error) => { - rej({ message: error.message || 'An error occurred' }); + rej({ + message: + error.message || + t('core:message.error.generic', { postProcess: 'capitalize' }), + }); }); }); }, []); @@ -326,7 +355,11 @@ export const Minting = ({ setIsOpenMinting, myAddress, show }) => { rej({ message: response.error }); }) .catch((error) => { - rej({ message: error.message || 'An error occurred' }); + rej({ + message: + error.message || + t('core:message.error.generic', { postProcess: 'capitalize' }), + }); }); }); }, []); @@ -350,7 +383,9 @@ export const Minting = ({ setIsOpenMinting, myAddress, show }) => { await sleep(pollingInterval); // Wait before the next poll } - throw new Error('Timeout waiting for reward share confirmation'); + throw new Error( + t('group:message.error.timeout_reward', { postProcess: 'capitalize' }) + ); }; const startMinting = async () => { @@ -382,7 +417,11 @@ export const Minting = ({ setIsOpenMinting, myAddress, show }) => { setShowWaitDialog(false); setInfo({ type: 'error', - message: error?.message || 'Unable to start minting', + message: + error?.message || + t('group:message.error.unable_minting', { + postProcess: 'capitalize', + }), }); setOpenSnack(true); } finally { @@ -420,8 +459,12 @@ export const Minting = ({ setIsOpenMinting, myAddress, show }) => { ...rewardShare, ...response, type: 'remove-rewardShare', - label: `Remove rewardshare: awaiting confirmation`, - labelDone: `Remove rewardshare: success!`, + label: t('group:message.success.rewardshare_remove', { + postProcess: 'capitalize', + }), + labelDone: t('group:message.success.rewardshare_remove_label', { + postProcess: 'capitalize', + }), done: false, }, ...prev, @@ -431,59 +474,65 @@ export const Minting = ({ setIsOpenMinting, myAddress, show }) => { rej({ message: response.error }); }) .catch((error) => { - rej({ message: error.message || 'An error occurred' }); + rej({ + message: + error.message || + t('core:message.error.generic', { postProcess: 'capitalize' }), + }); }); }); }, []); - const handleRemoveRewardShare = async (rewardShare) => { - try { - setIsLoading(true); + // TODO unused functions. Remove?? - const privateRewardShare = await removeRewardShare(rewardShare); - } catch (error) { - setInfo({ - type: 'error', - message: error?.message || 'Unable to remove reward share', - }); - setOpenSnack(true); - } finally { - setIsLoading(false); - } - }; + // const handleRemoveRewardShare = async (rewardShare) => { + // try { + // setIsLoading(true); - const createRewardShareForPotentialMinter = async (receiver) => { - try { - setIsLoading(true); - const confirmReceiver = await getNameOrAddress(receiver); - if (confirmReceiver.error) - throw new Error('Invalid receiver address or name'); - const isInMinterGroup = await checkIfMinterGroup(confirmReceiver); - if (!isInMinterGroup) throw new Error('Account not in Minter Group'); - const publicKey = await getPublicKeyFromAddress(confirmReceiver); - const findRewardShare = rewardShares?.find( - (item) => - item?.recipient === confirmReceiver && - item?.mintingAccount === myAddress - ); - if (findRewardShare) { - const privateRewardShare = await getRewardSharePrivateKey(publicKey); - setRewardsharekey(privateRewardShare); - } else { - await createRewardShare(publicKey, confirmReceiver); - const privateRewardShare = await getRewardSharePrivateKey(publicKey); - setRewardsharekey(privateRewardShare); - } - } catch (error) { - setInfo({ - type: 'error', - message: error?.message || 'Unable to create reward share', - }); - setOpenSnack(true); - } finally { - setIsLoading(false); - } - }; + // const privateRewardShare = await removeRewardShare(rewardShare); + // } catch (error) { + // setInfo({ + // type: 'error', + // message: error?.message || 'Unable to remove reward share', + // }); + // setOpenSnack(true); + // } finally { + // setIsLoading(false); + // } + // }; + + // const createRewardShareForPotentialMinter = async (receiver) => { + // try { + // setIsLoading(true); + // const confirmReceiver = await getNameOrAddress(receiver); + // if (confirmReceiver.error) + // throw new Error('Invalid receiver address or name'); + // const isInMinterGroup = await checkIfMinterGroup(confirmReceiver); + // if (!isInMinterGroup) throw new Error('Account not in Minter Group'); + // const publicKey = await getPublicKeyFromAddress(confirmReceiver); + // const findRewardShare = rewardShares?.find( + // (item) => + // item?.recipient === confirmReceiver && + // item?.mintingAccount === myAddress + // ); + // if (findRewardShare) { + // const privateRewardShare = await getRewardSharePrivateKey(publicKey); + // setRewardsharekey(privateRewardShare); + // } else { + // await createRewardShare(publicKey, confirmReceiver); + // const privateRewardShare = await getRewardSharePrivateKey(publicKey); + // setRewardsharekey(privateRewardShare); + // } + // } catch (error) { + // setInfo({ + // type: 'error', + // message: error?.message || 'Unable to create reward share', + // }); + // setOpenSnack(true); + // } finally { + // setIsLoading(false); + // } + // }; useEffect(() => { getNodeInfos(); @@ -558,7 +607,12 @@ export const Minting = ({ setIsOpenMinting, myAddress, show }) => { }, }} > - {'Manage your minting'} + + {t('group:message.generic.manage_minting', { + postProcess: 'capitalize', + })} + + { sx={{ backgroundColor: theme.palette.background.default, padding: '10px', - }} + }} // TODO translate > Account: {handleNames(accountInfo?.address)} @@ -618,7 +672,9 @@ export const Minting = ({ setIsOpenMinting, myAddress, show }) => { This node is minting: {nodeInfos?.isMintingPossible?.toString()} + + {isPartOfMintingGroup && !accountIsMinting && ( { )} )} + + {mintingAccounts?.length > 0 && ( Node's minting accounts )} diff --git a/src/i18n/locales/en/core.json b/src/i18n/locales/en/core.json index 41ff3c0..df7eb80 100644 --- a/src/i18n/locales/en/core.json +++ b/src/i18n/locales/en/core.json @@ -67,6 +67,8 @@ "error": { "generic": "an error occurred", "incorrect_password": "incorrect password", + "minting_account_add": "unable to add minting account", + "minting_account_remove": "unable to remove minting account", "missing_field": "missing: {{ field }}", "save_qdn": "unable to save to QDN" }, diff --git a/src/i18n/locales/en/group.json b/src/i18n/locales/en/group.json index 5e0db18..6e0c8c7 100644 --- a/src/i18n/locales/en/group.json +++ b/src/i18n/locales/en/group.json @@ -62,6 +62,7 @@ "latest_promotion": "only the latest promotion from the week will be shown for your group.", "loading_members": "loading member list with names... please wait.", "max_chars": " Max 200 characters. Publish Fee", + "manage_minting": "manage your minting", "no_display": "nothing to display", "no_selection": "no group selected", "not_part_group": "you are not part of the encrypted group of members. Wait until an admin re-encrypts the keys.", @@ -80,7 +81,9 @@ "group_secret_key": "cannot get group secret key", "name_required": "please provide a name", "notify_admins": "try notifying an admin from the list of admins below:", - "thread_id": "unable to locate thread Id" + "timeout_reward": "timeout waiting for reward share confirmation", + "thread_id": "unable to locate thread Id", + "unable_minting": "unable to start minting" }, "success": { "group_ban": "successfully banned member from group. It may take a couple of minutes for the changes to propagate", @@ -104,6 +107,10 @@ "invitation_request": "accepted join request: awaiting confirmation", "loading_threads": "loading threads... please wait.", "post_creation": "successfully created post. It may take some time for the publish to propagate", + "rewardshare_add": "add rewardshare: awaiting confirmation", + "rewardshare_add_label": "add rewardshare: success!", + "rewardshare_remove": "remove rewardshare: awaiting confirmation", + "rewardshare_remove_label": "remove rewardshare: success!", "thread_creation": "successfully created thread. It may take some time for the publish to propagate", "unbanned_user": "successfully unbanned user. It may take a couple of minutes for the changes to propagate", "user_joined": "user successfully joined!" From c5ff69b2f7d3137107b721efecae35337993312e Mon Sep 17 00:00:00 2001 From: Nicola Benaglia Date: Wed, 14 May 2025 21:47:39 +0200 Subject: [PATCH 13/15] Add minting translations --- src/App.tsx | 1 + src/components/Group/AddGroup.tsx | 2 +- src/components/Minting/Minting.tsx | 46 ++++++++++++++++++++++-------- src/i18n/locales/en/core.json | 4 ++- src/i18n/locales/en/group.json | 3 ++ 5 files changed, 42 insertions(+), 14 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 07b915c..17dea23 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1778,6 +1778,7 @@ function App() { + {extState === 'authenticated' && ( )} diff --git a/src/components/Group/AddGroup.tsx b/src/components/Group/AddGroup.tsx index f9cbbf8..8b91ebe 100644 --- a/src/components/Group/AddGroup.tsx +++ b/src/components/Group/AddGroup.tsx @@ -536,7 +536,7 @@ export const AddGroup = ({ address, open, setOpen }) => { {t('core:time.hour', { count: 1 })} - 3{t('core:time.hour', { count: 3 })} + {t('core:time.hour', { count: 3 })} {t('core:time.hour', { count: 5 })} diff --git a/src/components/Minting/Minting.tsx b/src/components/Minting/Minting.tsx index c84ffdb..2ee2508 100644 --- a/src/components/Minting/Minting.tsx +++ b/src/components/Minting/Minting.tsx @@ -21,7 +21,7 @@ import { subscribeToEvent, unsubscribeFromEvent, } from '../../utils/events'; -import { getFee, getNameOrAddress } from '../../background'; +import { getFee } from '../../background'; import { Spacer } from '../../common/Spacer'; import { FidgetSpinner } from 'react-loader-spinner'; import { useModal } from '../../common/useModal'; @@ -658,18 +658,34 @@ export const Minting = ({ setIsOpenMinting, myAddress, show }) => { sx={{ backgroundColor: theme.palette.background.default, padding: '10px', - }} // TODO translate + }} > - Account: {handleNames(accountInfo?.address)} - - Level: {accountInfo?.level} - - blocks remaining until next level: {_levelUpBlocks()} + {t('auth:account.account_one', { + postProcess: 'capitalize', + })} + : {handleNames(accountInfo?.address)} - This node is minting: {nodeInfos?.isMintingPossible?.toString()} + {t('core:level', { + postProcess: 'capitalize', + })} + : {accountInfo?.level} + + + + {t('group:message.generic.next_level', { + postProcess: 'capitalize', + })}{' '} + {_levelUpBlocks()} + + + + {t('group:message.generic.node_minting', { + postProcess: 'capitalize', + })}{' '} + {nodeInfos?.isMintingPossible?.toString()} @@ -706,12 +722,16 @@ export const Minting = ({ setIsOpenMinting, myAddress, show }) => { }} variant="contained" > - Start minting + {t('core:action.start_minting', { + postProcess: 'capitalize', + })} + {mintingAccounts?.length > 1 && ( - Only 2 minting keys are allowed per node. Please remove one if - you would like to mint with this account. + {t('group:message.generic.minting_keys_per_node', { + postProcess: 'capitalize', + })} )} @@ -720,7 +740,7 @@ export const Minting = ({ setIsOpenMinting, myAddress, show }) => { {mintingAccounts?.length > 0 && ( - Node's minting accounts + Node's minting accounts // TODO translate )} { )} + + {mintingAccounts?.map((acct) => ( Date: Thu, 15 May 2025 08:23:40 +0200 Subject: [PATCH 14/15] Translations for minting file --- src/components/Minting/Minting.tsx | 57 ++++++++++++++++++++++-------- src/i18n/locales/en/group.json | 14 ++++++-- 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/src/components/Minting/Minting.tsx b/src/components/Minting/Minting.tsx index 2ee2508..d7a8d88 100644 --- a/src/components/Minting/Minting.tsx +++ b/src/components/Minting/Minting.tsx @@ -740,7 +740,11 @@ export const Minting = ({ setIsOpenMinting, myAddress, show }) => { {mintingAccounts?.length > 0 && ( - Node's minting accounts // TODO translate + + {t('group:message.generic.node_minting_account', { + postProcess: 'capitalize', + })} + )} { }} > - You currently have a minting key for this account attached to - this node + {t('group:message.generic.node_minting_key', { + postProcess: 'capitalize', + })} )} @@ -775,8 +780,12 @@ export const Minting = ({ setIsOpenMinting, myAddress, show }) => { }} > - Minting account: {handleNames(acct?.mintingAccount)} + {t('group:message.generic.minting_account', { + postProcess: 'capitalize', + })}{' '} + {handleNames(acct?.mintingAccount)} + @@ -808,13 +819,15 @@ export const Minting = ({ setIsOpenMinting, myAddress, show }) => { {mintingAccounts?.length > 1 && ( - Only 2 minting keys are allowed per node. Please remove one if you - would like to add a different account. + {t('group:message.generic.minting_keys_per_node_different', { + postProcess: 'capitalize', + })} )} + {!isPartOfMintingGroup && ( { }} > - You are currently not part of the MINTER group + {t('group:message.generic.minter_group', { + postProcess: 'capitalize', + })} + - Visit the Q-Mintership app to apply to be a minter + {t('group:message.generic.mintership_app', { + postProcess: 'capitalize', + })} + + @@ -880,13 +902,16 @@ export const Minting = ({ setIsOpenMinting, myAddress, show }) => { {!isShowNext && ( - Confirming creation of rewardshare on chain. Please be - patient, this could take up to 90 seconds. + {t('group:message.success.rewardshare_creation', { + postProcess: 'capitalize', + })} )} {isShowNext && ( - Rewardshare confirmed. Please click Next. + {t('group:message.success.rewardshare_confirmed', { + postProcess: 'capitalize', + })} )} @@ -898,21 +923,23 @@ export const Minting = ({ setIsOpenMinting, myAddress, show }) => { onClick={onOk} autoFocus > - Next + {t('core:page.next', { postProcess: 'capitalize' })} )} + + Date: Thu, 15 May 2025 08:55:57 +0200 Subject: [PATCH 15/15] Translate register_name page --- src/components/RegisterName.tsx | 100 ++++++++++++++++++++++++++------ src/i18n/locales/en/core.json | 13 +++++ src/i18n/locales/en/group.json | 6 +- 3 files changed, 99 insertions(+), 20 deletions(-) diff --git a/src/components/RegisterName.tsx b/src/components/RegisterName.tsx index adb8198..f11e9ee 100644 --- a/src/components/RegisterName.tsx +++ b/src/components/RegisterName.tsx @@ -25,6 +25,7 @@ import CheckIcon from '@mui/icons-material/Check'; import ErrorIcon from '@mui/icons-material/Error'; import { useSetAtom } from 'jotai'; import { txListAtom } from '../atoms/global'; +import { useTranslation } from 'react-i18next'; enum Availability { NULL = 'null', @@ -50,6 +51,7 @@ export const RegisterName = ({ ); const [nameFee, setNameFee] = useState(null); const theme = useTheme(); + const { t } = useTranslation(['core', 'auth', 'group']); const checkIfNameExisits = async (name) => { if (!name?.trim()) { setIsNameAvailable(Availability.NULL); @@ -110,12 +112,24 @@ export const RegisterName = ({ const registerName = async () => { try { - if (!userInfo?.address) throw new Error('Your address was not found'); - if (!registerNameValue) throw new Error('Enter a name'); + if (!userInfo?.address) + throw new Error( + t('core:message.error.address_not_found', { + postProcess: 'capitalize', + }) + ); + if (!registerNameValue) + throw new Error( + t('core:action.enter_name', { + postProcess: 'capitalize', + }) + ); const fee = await getFee('REGISTER_NAME'); await show({ - message: 'Would you like to register this name?', + message: t('group:question.register_name', { + postProcess: 'capitalize', + }), publishFee: fee.fee + ' QORT', }); setIsLoadingRegisterName(true); @@ -130,8 +144,9 @@ export const RegisterName = ({ setIsLoadingRegisterName(false); setInfoSnack({ type: 'success', - message: - 'Successfully registered. It may take a couple of minutes for the changes to propagate', + message: t('group:message.success.registered_name', { + postProcess: 'capitalize', + }), }); setIsOpen(false); setRegisterNameValue(''); @@ -140,8 +155,15 @@ export const RegisterName = ({ { ...response, type: 'register-name', - label: `Registered name: awaiting confirmation. This may take a couple minutes.`, - labelDone: `Registered name: success!`, + label: t('group:message.success.registered_name_label', { + postProcess: 'capitalize', + }), + labelDone: t( + 'group:message.success.registered_name_success', + { + postProcess: 'capitalize', + } + ), done: false, }, ...prev.filter((item) => !item.done), @@ -158,7 +180,9 @@ export const RegisterName = ({ .catch((error) => { setInfoSnack({ type: 'error', - message: error.message || 'An error occurred', + message: + error.message || + t('core:message.error.generic', { postProcess: 'capitalize' }), }); setOpenSnack(true); rej(error); @@ -199,7 +223,9 @@ export const RegisterName = ({ width: '400px', }} > - // TODO: translate + + - Your balance is {balance ?? 0} QORT. A name registration - requires a {nameFee} QORT fee + {t('core:message.generic.name_registration', { + balance: balance ?? 0, + fee: { nameFee }, + postProcess: 'capitalize', + })} )} + + {isNameAvailable === Availability.AVAILABLE && ( - {registerNameValue} is available + + {t('core:message.generic.name_available', { + name: registerNameValue, + postProcess: 'capitalize', + })} + )} + {isNameAvailable === Availability.NOT_AVAILABLE && ( - {registerNameValue} is unavailable + + {t('core:message.generic.name_unavailable', { + name: registerNameValue, + postProcess: 'capitalize', + })} + )} + {isNameAvailable === Availability.LOADING && ( - Checking if name already existis + + + {t('core:message.generic.name_checking', { + postProcess: 'capitalize', + })} + )} + + - Benefits of a name + {t('core:message.generic.name_benefits', { + postProcess: 'capitalize', + })} + - + @@ -307,7 +365,11 @@ export const RegisterName = ({ }} /> - + @@ -322,7 +384,7 @@ export const RegisterName = ({ setRegisterNameValue(''); }} > - Close + {t('core:action.close', { postProcess: 'capitalize' })} diff --git a/src/i18n/locales/en/core.json b/src/i18n/locales/en/core.json index 2b0d173..b33a4bf 100644 --- a/src/i18n/locales/en/core.json +++ b/src/i18n/locales/en/core.json @@ -13,9 +13,11 @@ "continue": "continue", "continue_logout": "continue to logout", "create_thread": "create thread", + "choose_name": "choose a name", "decline": "decline", "decrypt": "decrypt", "edit": "edit", + "enter_name": "enter a name", "export": "export", "import": "import", "invite": "invite", @@ -30,6 +32,7 @@ "post": "post", "post_message": "post message", "publish": "publish", + "register_name": "register name", "remove": "remove", "save": "save", "start_minting": "start minting" @@ -67,6 +70,7 @@ "message_us": "please message us on Telegram or Discord if you need 4 QORT to start chatting without any limitations", "message": { "error": { + "address_not_found": "your address was not found", "generic": "an error occurred", "incorrect_password": "incorrect password", "minting_account_add": "unable to add minting account", @@ -74,6 +78,15 @@ "missing_field": "missing: {{ field }}", "save_qdn": "unable to save to QDN" }, + "generic": { + "name_available": "{{ name }} is available", + "name_benefits": "benefits of a name", + "name_checking": "checking if name already exists", + "name_registration": "your balance is {{ balance }} QORT. A name registration requires a {{ fee }} QORT fee", + "name_unavailable": "{{ name }} is unavailable", + "publish_data": "publish data to Qortal: anything from apps to videos. Fully decentralized!", + "secure_ownership": "secure ownership of data published by your name. You can even sell your name, along with your data to a third party." + }, "question": { "new_user": "are you a new user?" }, diff --git a/src/i18n/locales/en/group.json b/src/i18n/locales/en/group.json index f8a7d41..7a8cc23 100644 --- a/src/i18n/locales/en/group.json +++ b/src/i18n/locales/en/group.json @@ -118,6 +118,9 @@ "invitation_request": "accepted join request: awaiting confirmation", "loading_threads": "loading threads... please wait.", "post_creation": "successfully created post. It may take some time for the publish to propagate", + "registered_name": "successfully registered. It may take a couple of minutes for the changes to propagate", + "registered_name_label": "registered name: awaiting confirmation. This may take a couple minutes.", + "registered_name_success": "registered name: success!", "rewardshare_add": "add rewardshare: awaiting confirmation", "rewardshare_add_label": "add rewardshare: success!", "rewardshare_creation": "confirming creation of rewardshare on chain. Please be patient, this could take up to 90 seconds.", @@ -131,7 +134,8 @@ }, "question": { "perform_transaction": "would you like to perform a {{action}} transaction?", - "provide_thread": "please provide a thread title" + "provide_thread": "please provide a thread title", + "register_name": "would you like to register this name?" }, "thread_posts": "new thread posts" }