Merge pull request #25 from nbenaglia/feature/i18n-groups

I18N: add group namespace
This commit is contained in:
nico.benaz 2025-04-26 17:14:20 +02:00 committed by GitHub
commit 4fa9aa3c91
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 995 additions and 507 deletions

13
i18n.js
View File

@ -19,6 +19,15 @@ const capitalize = {
},
};
export const supportedLanguages = {
de: { name: 'Deutsch', flag: '🇩🇪' },
en: { name: 'English', flag: '🇺🇸' },
es: { name: 'Español', flag: '🇪🇸' },
fr: { name: 'Français', flag: '🇫🇷' },
it: { name: 'Italiano', flag: '🇮🇹' },
ru: { name: 'Русский', flag: '🇷🇺' },
};
i18n
.use(HttpApi)
.use(LanguageDetector)
@ -42,8 +51,8 @@ i18n
escapeValue: false,
},
lng: navigator.language,
ns: ['auth', 'core', 'tutorial'],
supportedLngs: ['en', 'it', 'es', 'fr', 'de', 'ru'],
ns: ['auth', 'core', 'group', 'tutorial'],
supportedLngs: Object.keys(supportedLanguages),
});
export default i18n;

View File

@ -29,6 +29,11 @@
"password": "password",
"password_confirmation": "confirm password",
"return_to_list": "return to list",
"tips": {
"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.",
"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.",
"new_users": "new users start here!"
},
"wallet": {
"password_confirmation": "confirm wallet password",
"password": "wallet password",

View File

@ -1,27 +1,57 @@
{
"action": {
"add": "add",
"accept": "accept",
"backup_account": "backup account",
"backup_wallet": "backup wallet",
"cancel": "cancel",
"change": "change",
"change_language": "change language",
"choose": "choose",
"close": "close",
"continue": "continue",
"continue_logout": "continue to logout",
"decline": "decline",
"edit": "edit",
"export": "export",
"import": "import",
"invite": "invite",
"join": "join",
"logout": "logout",
"notify": "notify"
},
"core": {
"block_height": "block height",
"information": "core information",
"peers": "connected peers",
"version": "core version"
},
"count": {
"none": "none",
"one": "one"
},
"description": "description",
"edit": "edit",
"export": "export",
"import": "import",
"fee": {
"payment": "payment fee",
"publish": "publish fee"
},
"page": {
"last": "last",
"first": "first",
"next": "next",
"previous": "previous"
},
"downloading_qdn": "downloading from QDN",
"last_height": "last height",
"loading": "loading...",
"logout": "logout",
"loading_posts": "loading posts... please wait.",
"message_us": "please message us on Telegram or Discord if you need 4 QORT to start chatting without any limitations",
"minting_status": "minting status",
"new_user": "are you a new user?",
"payment_notification": "payment notification",
"price": "price",
"q_mail": "q-mail",
"result": {
"message": {
"error": {
"generic": "an error occurred",
"incorrect_password": "incorrect password",
@ -34,7 +64,10 @@
"synchronizing": "synchronizing"
},
"success": {
"publish_qdn": "successfully published to QDN"
"order_submitted": "your buy order was submitted",
"publish_qdn": "successfully published to QDN",
"request_read": "I have read this request",
"transfer": "the transfer was succesful!"
}
},
"save_options": {
@ -59,11 +92,18 @@
"dark": "dark mode",
"light": "light mode"
},
"time": {
"day_one": "{{count}} day",
"day_other": "{{count}} days",
"hour_one": "{{count}} hour",
"hour_other": "{{count}} hours",
"minute_one": "{{count}} minute",
"minute_other": "{{count}} minutes"
},
"title": "title",
"tutorial": "tutorial",
"user_lookup": "user lookup",
"wallet": {
"backup_wallet": "backup wallet",
"wallet": "wallet",
"wallet_other": "wallets"
},

View File

@ -0,0 +1,65 @@
{
"action": {
"cancel_ban": "cancel ban",
"create_group": "create group",
"find_group": "find group",
"join_group": "join group",
"invite_member": "invite member",
"refetch_page": "refetch page",
"return_to_thread": "return to threads"
},
"advanced_options": "advanced options",
"approval_threshold": "group Approval Threshold (number / percentage of Admins that must approve a transaction)",
"ban_list": "ban list",
"block_delay": {
"minimum": "minimum Block delay for Group Transaction Approvals",
"maximum": "maximum Block delay for Group Transaction Approvals"
},
"group": {
"closed": "closed (private) - users need permission to join",
"description": "description of group",
"invites": "group invites",
"management": "group management",
"name": "name of group",
"open": "open (public)",
"type": "group type"
},
"invitation_expiry": "invitation Expiry Time",
"join_requests": "join requests",
"question": {
"cancel_ban": "would you like to perform a CANCEL_GROUP_BAN transaction?",
"create_group": "would you like to perform an CREATE_GROUP transaction?",
"group_invite": "would you like to perform a GROUP_INVITE transaction?",
"join_group": "would you like to perform an JOIN_GROUP transaction?",
"provide_thread": "please provide a thread title"
},
"message": {
"generic": {
"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",
"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.",
"only_encrypted": "only unencrypted messages will be displayed.",
"setting_group": "setting up group... please wait."
},
"error": {
"access_name": "cannot send a message without a access to your name",
"description_required": "please provide a description",
"group_info": "cannot access group information",
"name_required": "please provide a name",
"notify_admins": "try notifying an admin from the list of admins below:"
},
"success": {
"group_creation": "successfully created group. It may take a couple of minutes for the changes to propagate",
"group_creation_name": "created group {{group_name}}: awaiting confirmation",
"group_creation_label": "created group {{name}}: success!",
"group_invite": "successfully invited {{value}}. It may take a couple of minutes for the changes to propagate",
"join_creation": "successfully requested to join group. It may take a couple of minutes for the changes to propagate",
"group_join_name": "joined group {{group_name}}: awaiting confirmation",
"group_join_label": "joined group {{name}}: success!",
"loading_threads": "loading threads... please wait.",
"unbanned_user": "successfully unbanned user. It may take a couple of minutes for the changes to propagate"
}
}
}

View File

@ -6,38 +6,43 @@
},
"advanced_users": "per utenti avanzati",
"apikey": {
"alternative": "alternativa: seleziona un file",
"change": "cambia la chiave API",
"enter": "inserisci la chiave API",
"import": "importa chiave API",
"alternative": "alternativa: selezione file",
"change": "cambia APIkey",
"enter": "inserisci APIkey",
"import": "importa APIkey",
"key": "chiave API",
"select_valid": "selezione una chiave API valida"
"select_valid": "seleziona una APIkey valida"
},
"authenticate": "autenticazione",
"authenticate": "autentica",
"build_version": "versione build",
"create_account": "crea un account",
"create_account": "crea account",
"download_account": "scarica account",
"keep_secure": "metti al sicuro il file del tuo account",
"keep_secure": "mantieni sicuro il file del tuo account",
"node": {
"choose": "scegli un nodo custom",
"custom_many": "nodi custom",
"use_custom": "use nodo custom",
"choose": "scegli nodo personalizzato",
"custom_many": "nodi personalizzati",
"use_custom": "usa nodo personalizzato",
"use_local": "usa nodo locale",
"using": "nodo in uso",
"using": "utilizzo nodo",
"using_public": "utilizzo nodo pubblico"
},
"password_confirmation": "confirma la password",
"password": "password",
"password_confirmation": "conferma password",
"return_to_list": "torna alla lista",
"tips": {
"digital_id": "il tuo wallet è come la tua identità digitale su Qortal ed è il modo in cui accederai all'interfaccia utente di Qortal. Contiene il tuo indirizzo pubblico e il nome Qortal che sceglierai. Ogni transazione che esegui è collegata alla tua identità ed è qui che gestisci tutti i tuoi QORT e altre criptovalute scambiabili su Qortal.",
"new_account": "creare un account significa creare un nuovo wallet e un'identità digitale per iniziare a usare Qortal. Una volta creato l'account, potrai iniziare a ottenere QORT, acquistare un nome e un avatar, pubblicare video e blog, e molto altro.",
"new_users": "i nuovi utenti iniziano qui!"
},
"wallet": {
"password_confirmation": "conferma la password del wallet",
"password_confirmation": "conferma password del wallet",
"password": "password del wallet",
"keep_password": "mantieni la password attuale",
"keep_password": "mantieni password corrente",
"new_password": "nuova password",
"error": {
"missing_new_password": "per favore inserisci una nuova password",
"missing_password": "per favore inserisci la tua password"
}
},
"return_to_list": "ritorna alla lista",
"welcome": "benvenuto in"
}

View File

@ -1,71 +1,111 @@
{
"action": {
"add": "aggiungi",
"accept": "accetta",
"backup_account": "backup account",
"backup_wallet": "backup wallet",
"cancel": "annulla",
"change": "cambia",
"change_language": "cambia lingua",
"choose": "scegli",
"close": "chiudi",
"continue": "continua",
"continue_logout": "continua con il logout",
"decline": "rifiuta",
"edit": "modifica",
"export": "esporta",
"import": "importa",
"invite": "invita",
"join": "unisciti",
"logout": "esci",
"notify": "notifica"
},
"core": {
"block_height": "altezza del blocco",
"block_height": "altezza blocco",
"information": "informazioni core",
"peers": "peer connessi",
"version": "versione core"
},
"count": {
"none": "nessuno",
"one": "uno"
},
"description": "descrizione",
"edit": "modifica",
"export": "esporta",
"import": "importa",
"fee": {
"payment": "commissione di pagamento",
"publish": "commissione di pubblicazione"
},
"page": {
"last": "ultimo",
"first": "primo",
"next": "successivo",
"previous": "precedente"
},
"downloading_qdn": "scaricamento da QDN",
"last_height": "ultima altezza",
"loading": "caricamento...",
"logout": "disconnetti",
"minting_status": "stato del conio",
"loading_posts": "caricamento post... attendere prego.",
"message_us": "per favore scrivici su Telegram o Discord se hai bisogno di 4 QORT per iniziare a chattare senza limitazioni",
"minting_status": "stato minting",
"new_user": "sei un nuovo utente?",
"payment_notification": "notifica di pagamento",
"price": "prezzo",
"q_mail": "q-mail",
"result": {
"message": {
"error": {
"generic": "si è verificato un errore",
"incorrect_password": "password errata",
"save_qdn": "impossibile salvare su QDN"
},
"status": {
"minting": "(conio in corso)",
"not_minting": "(conio non attivo)",
"minting": "(minting)",
"not_minting": "(non minting)",
"synchronized": "sincronizzato",
"synchronizing": "sincronizzazione in corso"
},
"success": {
"publish_qdn": "pubblicato con successo su QDN"
"order_submitted": "il tuo ordine di acquisto è stato inviato",
"publish_qdn": "pubblicato su QDN con successo",
"request_read": "ho letto questa richiesta",
"transfer": "il trasferimento è stato effettuato con successo!"
}
},
"save_options": {
"no_pinned_changes": "attualmente non hai modifiche alle tue app appuntate",
"overwrite_changes": "l'app non è riuscita a scaricare le tue app appuntate salvate su QDN. Vuoi sovrascrivere queste modifiche?",
"overwrite_changes": "l'app non è riuscita a scaricare le tue app appuntate salvate su QDN. Vuoi sovrascrivere le modifiche?",
"overwrite_qdn": "sovrascrivi su QDN",
"publish_qdn": "vuoi pubblicare le tue impostazioni su QDN (crittografate)?",
"qdn": "usa il salvataggio QDN",
"register_name": "hai bisogno di un nome Qortal registrato per salvare le tue app appuntate su QDN.",
"publish_qdn": "vuoi pubblicare le tue impostazioni su QDN (crittografato)?",
"qdn": "usa il salvataggio su QDN",
"register_name": "devi avere un nome Qortal registrato per salvare le tue app appuntate su QDN.",
"reset_pinned": "non ti piacciono le modifiche locali attuali? Vuoi ripristinare le app appuntate predefinite?",
"reset_qdn": "non ti piacciono le modifiche locali attuali? Vuoi ripristinare le tue app appuntate salvate su QDN?",
"revert_default": "ripristina predefinite",
"reset_qdn": "non ti piacciono le modifiche locali attuali? Vuoi ripristinare le app appuntate salvate su QDN?",
"revert_default": "ripristina predefinito",
"revert_qdn": "ripristina da QDN",
"save_qdn": "salva su QDN",
"save": "salva",
"settings": "stai utilizzando il metodo esporta/importa per salvare le impostazioni.",
"settings": "stai usando il metodo di esportazione/importazione per salvare le impostazioni.",
"unsaved_changes": "hai modifiche non salvate alle tue app appuntate. Salvale su QDN."
},
"settings": "impostazioni",
"supply": "offerta",
"supply": "disponibilità",
"theme": {
"dark": "modalità scura",
"light": "modalità chiara"
},
"time": {
"day_one": "{{count}} giorno",
"day_other": "{{count}} giorni",
"hour_one": "{{count}} ora",
"hour_other": "{{count}} ore",
"minute_one": "{{count}} minuto",
"minute_other": "{{count}} minuti"
},
"title": "titolo",
"tutorial": "tutorial",
"user_lookup": "ricerca utente",
"wallet": {
"backup_wallet": "backup portafoglio",
"wallet": "portafoglio",
"wallet_other": "portafogli"
"wallet": "wallet",
"wallet_other": "wallet"
},
"welcome": "benvenuto"
}

View File

@ -0,0 +1,65 @@
{
"action": {
"cancel_ban": "annulla ban",
"create_group": "crea gruppo",
"find_group": "trova gruppo",
"join_group": "unisciti al gruppo",
"invite_member": "invita membro",
"refetch_page": "ricarica pagina",
"return_to_thread": "torna ai thread"
},
"advanced_options": "opzioni avanzate",
"approval_threshold": "soglia di Approvazione del gruppo (numero/percentuale di Admin che devono approvare una transazione)",
"ban_list": "lista ban",
"block_delay": {
"minimum": "ritardo minimo dei blocchi per Approvazione di Transazioni di Gruppo",
"maximum": "ritardo massimo dei blocchi per Approvazione di Transazioni di Gruppo"
},
"group": {
"closed": "chiuso (privato) - gli utenti necessitano di permesso per unirsi",
"description": "descrizione del gruppo",
"invites": "inviti del gruppo",
"management": "gestione del gruppo",
"name": "nome del gruppo",
"open": "aperto (pubblico)",
"type": "tipo di gruppo"
},
"invitation_expiry": "tempo di scadenza dell'invito",
"join_requests": "richieste di adesione",
"question": {
"cancel_ban": "vuoi eseguire una transazione CANCEL_GROUP_BAN?",
"create_group": "vuoi eseguire una transazione CREATE_GROUP?",
"group_invite": "vuoi eseguire una transazione GROUP_INVITE?",
"join_group": "vuoi eseguire una transazione JOIN_GROUP?",
"provide_thread": "per favore fornisci un titolo per il thread"
},
"message": {
"generic": {
"encryption_key": "la prima chiave di cifratura comune del gruppo è in fase di creazione. Attendere alcuni minuti affinché venga recuperata dalla rete. Controllo ogni 2 minuti...",
"group_invited_you": "{{group}} ti ha invitato",
"no_display": "niente da visualizzare",
"no_selection": "nessun gruppo selezionato",
"not_part_group": "non fai parte del gruppo cifrato dei membri. Attendi che un amministratore ri-codifichi le chiavi.",
"only_encrypted": "verranno visualizzati solo i messaggi non cifrati.",
"setting_group": "configurazione del gruppo... attendere prego."
},
"error": {
"access_name": "impossibile inviare un messaggio senza accesso al tuo nome",
"description_required": "per favore fornisci una descrizione",
"group_info": "impossibile accedere alle informazioni del gruppo",
"name_required": "per favore fornisci un nome",
"notify_admins": "prova a notificare un admin dalla lista di amministratori qui sotto:"
},
"success": {
"group_creation": "gruppo creato con successo. Potrebbero volerci alcuni minuti affinché le modifiche si propaghino",
"group_creation_name": "gruppo {{group_name}} creato: in attesa di conferma",
"group_creation_label": "gruppo {{name}} creato: successo!",
"group_invite": "invito inviato con successo a {{value}}. Potrebbero volerci alcuni minuti affinché le modifiche si propaghino",
"join_creation": "richiesta di adesione al gruppo inviata con successo. Potrebbero volerci alcuni minuti affinché le modifiche si propaghino",
"group_join_name": "entrato nel gruppo {{group_name}}: in attesa di conferma",
"group_join_label": "entrato nel gruppo {{name}}: successo!",
"loading_threads": "caricamento thread... attendere prego.",
"unbanned_user": "utente sbannato con successo. Potrebbero volerci alcuni minuti affinché le modifiche si propaghino"
}
}
}

View File

@ -135,6 +135,7 @@ import { GeneralNotifications } from './components/GeneralNotifications';
import { PdfViewer } from './common/PdfViewer';
import ThemeSelector from './components/Theme/ThemeSelector.tsx';
import { useTranslation } from 'react-i18next';
import LanguageSelector from './components/Language/LanguageSelector.tsx';
import { DownloadWallet } from './components/Auth/DownloadWallet.tsx';
type extStates =
@ -255,14 +256,7 @@ export const getBaseApiReact = (customApi?: string) => {
return groupApi;
}
};
// export const getArbitraryEndpointReact = () => {
// if (globalApiKey) {
// return `/arbitrary/resources/search`;
// } else {
// return `/arbitrary/resources/searchsimple`;
// }
// };
export const getArbitraryEndpointReact = () => {
if (globalApiKey) {
return `/arbitrary/resources/searchsimple`;
@ -571,26 +565,6 @@ function App() {
isFocusedRef.current = isFocused;
}, [isFocused]);
// const checkIfUserHasLocalNode = useCallback(async () => {
// try {
// const url = `http://127.0.0.1:12391/admin/status`;
// const response = await fetch(url, {
// method: "GET",
// headers: {
// "Content-Type": "application/json",
// },
// });
// const data = await response.json();
// if (data?.isSynchronizing === false && data?.syncPercent === 100) {
// setHasLocalNode(true);
// }
// } catch (error) {}
// }, []);
// useEffect(() => {
// checkIfUserHasLocalNode();
// }, [extState]);
const address = useMemo(() => {
if (!rawWallet?.address0) return '';
return rawWallet.address0;
@ -1007,7 +981,7 @@ function App() {
await showUnsavedChanges({
message:
'Your settings have changed. If you logout you will lose your changes. Click on the save button in the header to keep your changed settings.',
});
}); // TODO translate
} else if (extState === 'authenticated') {
await showUnsavedChanges({
message: 'Are you sure you would like to logout?',
@ -1311,19 +1285,24 @@ function App() {
</Tooltip>
)}
</Box>
<Spacer height="48px" />
{authenticatedMode === 'ltc' ? (
<>
<img src={ltcLogo} />
<Spacer height="32px" />
<CopyToClipboard text={rawWallet?.ltcAddress}>
<AddressBox>
{rawWallet?.ltcAddress?.slice(0, 6)}...
{rawWallet?.ltcAddress?.slice(-4)} <img src={Copy} />
</AddressBox>
</CopyToClipboard>
<Spacer height="10px" />
{ltcBalanceLoading && (
<CircularProgress color="success" size={16} />
)}
@ -1345,6 +1324,7 @@ function App() {
>
{ltcBalance} LTC
</TextP>
<RefreshIcon
onClick={getLtcBalanceFunc}
sx={{
@ -1364,7 +1344,9 @@ function App() {
myName={userInfo?.name}
balance={balance}
/>
<Spacer height="32px" />
<TextP
sx={{
textAlign: 'center',
@ -1374,7 +1356,9 @@ function App() {
>
{userInfo?.name}
</TextP>
<Spacer height="10px" />
<CopyToClipboard text={rawWallet?.address0}>
<AddressBox>
{rawWallet?.address0?.slice(0, 6)}...
@ -1514,7 +1498,7 @@ function App() {
textTransform: 'uppercase',
}}
>
{t('core:logout')}
{t('core:action.logout')}
</span>
}
placement="left"
@ -1869,7 +1853,7 @@ function App() {
textTransform: 'uppercase',
}}
>
{t('core:backup_wallet')}
{t('core:action.backup_wallet')}
</span>
}
placement="left"
@ -1903,10 +1887,6 @@ function App() {
<AppContainer
sx={{
height: '100vh',
// backgroundImage: desktopViewMode === "apps" && 'url("appsBg.svg")',
// backgroundSize: desktopViewMode === "apps" && "cover",
// backgroundPosition: desktopViewMode === "apps" && "center",
// backgroundRepeat: desktopViewMode === "apps" && "no-repeat",
}}
>
<PdfViewer />
@ -2036,7 +2016,6 @@ function App() {
/>
</Box>
)}
{isShowQortalRequest && !isMainWindow && (
<>
<Spacer height="120px" />
@ -2319,7 +2298,6 @@ function App() {
<ErrorText>{sendPaymentError}</ErrorText>
</>
)}
{extState === 'web-app-request-payment' && !isMainWindow && (
<>
<Spacer height="100px" />
@ -2953,7 +2931,9 @@ function App() {
});
}}
>
Backup Account
{t('core:action.backup_account', {
postProcess: 'capitalize',
})}
</CustomButton>
</>
)}
@ -2981,7 +2961,9 @@ function App() {
lineHeight: '15px',
}}
>
The transfer was succesful!
{t('core:message.success.transfer', {
postProcess: 'capitalize',
})}
</TextP>
<Spacer height="100px" />
<CustomButton
@ -2989,7 +2971,7 @@ function App() {
returnToMain();
}}
>
Continue
{t('core:action.continue', { postProcess: 'capitalize' })}
</CustomButton>
</Box>
)}
@ -3004,7 +2986,9 @@ function App() {
lineHeight: '15px',
}}
>
The transfer was succesful!
{t('core:message.success.transfer', {
postProcess: 'capitalize',
})}
</TextP>
<Spacer height="100px" />
<CustomButton
@ -3012,7 +2996,7 @@ function App() {
window.close();
}}
>
Continue
{t('core:action.continue', { postProcess: 'capitalize' })}
</CustomButton>
</>
)}
@ -3027,7 +3011,9 @@ function App() {
lineHeight: '15px',
}}
>
Your buy order was submitted
{t('core:message.success.order_submitted', {
postProcess: 'capitalize',
})}
</TextP>
<Spacer height="100px" />
<CustomButton
@ -3035,10 +3021,11 @@ function App() {
window.close();
}}
>
Close
{t('core:action.close', { postProcess: 'capitalize' })}
</CustomButton>
</>
)}
{countdown && (
<Box
style={{
@ -3082,12 +3069,18 @@ function App() {
</DialogContentText>
{message?.paymentFee && (
<DialogContentText id="alert-dialog-description2">
payment fee: {message.paymentFee}
{t('core:fee.payment', {
postProcess: 'capitalize',
})}
: {message.paymentFee}
</DialogContentText>
)}
{message?.publishFee && (
<DialogContentText id="alert-dialog-description2">
publish fee: {message.publishFee}
{t('core:fee.publish', {
postProcess: 'capitalize',
})}
: {message.publishFee}
</DialogContentText>
)}
</DialogContent>
@ -3108,7 +3101,9 @@ function App() {
onClick={onOk}
autoFocus
>
accept
{t('core:action.accept', {
postProcess: 'capitalize',
})}
</Button>
<Button
sx={{
@ -3125,7 +3120,9 @@ function App() {
variant="contained"
onClick={onCancel}
>
decline
{t('core:action.decline', {
postProcess: 'capitalize',
})}
</Button>
</DialogActions>
</Dialog>
@ -3146,7 +3143,9 @@ function App() {
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={onOkInfo} autoFocus>
Close
{t('core:action.close', {
postProcess: 'capitalize',
})}
</Button>
</DialogActions>
</Dialog>
@ -3165,14 +3164,18 @@ function App() {
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={onCancelUnsavedChanges}>
Cancel
{t('core:action.cancel', {
postProcess: 'capitalize',
})}
</Button>
<Button
variant="contained"
onClick={onOkUnsavedChanges}
autoFocus
>
Continue to Logout
{t('core:action.decline', {
postProcess: 'capitalize',
})}
</Button>
</DialogActions>
</Dialog>
@ -3443,7 +3446,9 @@ function App() {
label={
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Typography sx={{ fontSize: '14px' }}>
I have read this request
{t('core:message.success.request_read', {
postProcess: 'capitalize',
})}
</Typography>
<PriorityHighIcon color="warning" />
</Box>
@ -3454,8 +3459,8 @@ function App() {
<Spacer height="29px" />
<Box
sx={{
display: 'flex',
alignItems: 'center',
display: 'flex',
gap: '14px',
}}
>
@ -3491,7 +3496,9 @@ function App() {
onOkQortalRequestExtension('accepted');
}}
>
accept
{t('core:action.accept', {
postProcess: 'capitalize',
})}
</CustomButtonAccept>
<CustomButtonAccept
color="black"
@ -3501,7 +3508,9 @@ function App() {
}}
onClick={() => onCancelQortalRequestExtension()}
>
decline
{t('core:action.decline', {
postProcess: 'capitalize',
})}
</CustomButtonAccept>
</Box>
<ErrorText>{sendPaymentError}</ErrorText>
@ -3566,6 +3575,7 @@ function App() {
/>
)}
<LanguageSelector />
<ThemeSelector />
</AppContainer>
);

View File

@ -31,6 +31,7 @@ import { GlobalContext } from '../App';
import Tooltip, { TooltipProps, tooltipClasses } from '@mui/material/Tooltip';
import ThemeSelector from '../components/Theme/ThemeSelector';
import { useTranslation } from 'react-i18next';
import LanguageSelector from '../components/Language/LanguageSelector';
const manifestData = {
version: '0.5.3',
@ -510,14 +511,8 @@ export const NotAuthenticated = ({
fontSize: '16px',
}}
>
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.
</Typography>{' '}
// TODO translate
{t('auth:tips.digital_id', { postProcess: 'capitalize' })}
</Typography>
</React.Fragment>
}
>
@ -547,9 +542,8 @@ export const NotAuthenticated = ({
fontSize: '18px',
}}
>
New users start here!
</Typography>{' '}
// TODO translate
{t('auth:tips.new_users', { postProcess: 'capitalize' })}
</Typography>
<Spacer height="10px" />
<Typography
color="inherit"
@ -557,12 +551,8 @@ export const NotAuthenticated = ({
fontSize: '16px',
}}
>
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.
</Typography>{' '}
// TODO translate
{t('auth:tips.new_account', { postProcess: 'capitalize' })}
</Typography>
</React.Fragment>
}
>
@ -816,7 +806,7 @@ export const NotAuthenticated = ({
}}
variant="contained"
>
{t('core:choose', { postProcess: 'capitalize' })}
{t('core:action.choose', { postProcess: 'capitalize' })}
</Button>
</Box>
</Box>
@ -875,7 +865,9 @@ export const NotAuthenticated = ({
}}
variant="contained"
>
{t('core:choose', { postProcess: 'capitalize' })}
{t('core:action.choose', {
postProcess: 'capitalize',
})}
</Button>
<Button
@ -888,7 +880,9 @@ export const NotAuthenticated = ({
}}
variant="contained"
>
{t('core:edit', { postProcess: 'capitalize' })}
{t('core:action.edit', {
postProcess: 'capitalize',
})}
</Button>
<Button
@ -940,7 +934,7 @@ export const NotAuthenticated = ({
<DialogActions>
{mode === 'list' && (
<Button variant="contained" onClick={addCustomNode}>
{t('core:add', { postProcess: 'capitalize' })}
{t('core:action.add', { postProcess: 'capitalize' })}
</Button>
)}
@ -953,7 +947,7 @@ export const NotAuthenticated = ({
}}
autoFocus
>
{t('core:close', { postProcess: 'capitalize' })}
{t('core:action.close', { postProcess: 'capitalize' })}
</Button>
</>
)}
@ -1075,7 +1069,7 @@ export const NotAuthenticated = ({
setShowSelectApiKey(false);
}}
>
{t('core:close', { postProcess: 'capitalize' })}
{t('core:action.close', { postProcess: 'capitalize' })}
</Button>
</DialogActions>
</Dialog>
@ -1097,6 +1091,7 @@ export const NotAuthenticated = ({
/>
</ButtonBase>
<LanguageSelector />
<ThemeSelector />
</>
);

View File

@ -15,6 +15,7 @@ import { extractComponents } from '../Chat/MessageDisplay';
import ArrowOutwardIcon from '@mui/icons-material/ArrowOutward';
import { AppsPrivate } from './AppsPrivate';
import ThemeSelector from '../Theme/ThemeSelector';
import LanguageSelector from '../Language/LanguageSelector';
export const AppsHomeDesktop = ({
setMode,
@ -157,6 +158,7 @@ export const AppsHomeDesktop = ({
/>
</AppsContainer>
<LanguageSelector />
<ThemeSelector />
</>
);

View File

@ -79,21 +79,21 @@ export const CoreSyncStatus = () => {
if (isMintingPossible && !isUsingGateway) {
imagePath = syncedMintingImg;
message = `${t(`core:result.status.${isSynchronizing ? 'synchronizing' : 'synchronized'}`, { postProcess: 'capitalize' })} ${t('core:result.status.minting')}`;
message = `${t(`core:message.status.${isSynchronizing ? 'synchronizing' : 'synchronized'}`, { postProcess: 'capitalize' })} ${t('core:message.status.minting')}`;
} else if (isSynchronizing === true && syncPercent === 99) {
imagePath = syncingImg;
} else if (isSynchronizing && !isMintingPossible && syncPercent === 100) {
imagePath = syncingImg;
message = `${t('core:result.status.synchronizing', { postProcess: 'capitalize' })} ${!isUsingGateway ? t('core:result.status.not_minting') : ''}`;
message = `${t('core:message.status.synchronizing', { postProcess: 'capitalize' })} ${!isUsingGateway ? t('core:message.status.not_minting') : ''}`;
} else if (!isSynchronizing && !isMintingPossible && syncPercent === 100) {
imagePath = syncedImg;
message = `${t('core:result.status.synchronized', { postProcess: 'capitalize' })} ${!isUsingGateway ? t('core:result.status.not_minting') : ''}`;
message = `${t('core:message.status.synchronized', { postProcess: 'capitalize' })} ${!isUsingGateway ? t('core:message.status.not_minting') : ''}`;
} else if (isSynchronizing && isMintingPossible && syncPercent === 100) {
imagePath = syncingImg;
message = `${t('core:result.status.synchronizing', { postProcess: 'capitalize' })} ${!isUsingGateway ? t('core:result.status.minting') : ''}`;
message = `${t('core:message.status.synchronizing', { postProcess: 'capitalize' })} ${!isUsingGateway ? t('core:message.status.minting') : ''}`;
} else if (!isSynchronizing && isMintingPossible && syncPercent === 100) {
imagePath = syncedMintingImg;
message = `${t('core:result.status.synchronized', { postProcess: 'capitalize' })} ${!isUsingGateway ? t('core:result.status.minting') : ''}`;
message = `${t('core:message.status.synchronized', { postProcess: 'capitalize' })} ${!isUsingGateway ? t('core:message.status.minting') : ''}`;
}
return (

View File

@ -8,6 +8,7 @@ import { enabledDevModeAtom } from '../atoms/global';
import { AppsIcon } from '../assets/Icons/AppsIcon';
import ThemeSelector from './Theme/ThemeSelector';
import { CoreSyncStatus } from './CoreSyncStatus';
import LanguageSelector from './Language/LanguageSelector';
export const DesktopSideBar = ({
goToHome,
@ -143,6 +144,7 @@ export const DesktopSideBar = ({
</ButtonBase>
)}
<LanguageSelector />
<ThemeSelector />
</Box>
);

View File

@ -1,4 +1,13 @@
import * as React from 'react';
import {
forwardRef,
Fragment,
ReactElement,
Ref,
SyntheticEvent,
useContext,
useEffect,
useState,
} from 'react';
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import AppBar from '@mui/material/AppBar';
@ -28,6 +37,7 @@ import { CustomizedSnackbars } from '../Snackbar/Snackbar';
import { getFee } from '../../background';
import { MyContext } from '../../App';
import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events';
import { useTranslation } from 'react-i18next';
export const Label = styled('label')`
display: block;
@ -37,30 +47,29 @@ export const Label = styled('label')`
margin-bottom: 4px;
`;
const Transition = React.forwardRef(function Transition(
const Transition = forwardRef(function Transition(
props: TransitionProps & {
children: React.ReactElement;
children: ReactElement;
},
ref: React.Ref<unknown>
ref: Ref<unknown>
) {
return <Slide direction="up" ref={ref} {...props} />;
});
export const AddGroup = ({ address, open, setOpen }) => {
const { show, setTxList } = React.useContext(MyContext);
const [tab, setTab] = React.useState('create');
const [openAdvance, setOpenAdvance] = React.useState(false);
const [name, setName] = React.useState('');
const [description, setDescription] = React.useState('');
const [groupType, setGroupType] = React.useState('1');
const [approvalThreshold, setApprovalThreshold] = React.useState('40');
const [minBlock, setMinBlock] = React.useState('5');
const [maxBlock, setMaxBlock] = React.useState('21600');
const [value, setValue] = React.useState(0);
const [openSnack, setOpenSnack] = React.useState(false);
const [infoSnack, setInfoSnack] = React.useState(null);
const { show, setTxList } = useContext(MyContext);
const [openAdvance, setOpenAdvance] = useState(false);
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [groupType, setGroupType] = useState('1');
const [approvalThreshold, setApprovalThreshold] = useState('40');
const [minBlock, setMinBlock] = useState('5');
const [maxBlock, setMaxBlock] = useState('21600');
const [value, setValue] = useState(0);
const [openSnack, setOpenSnack] = useState(false);
const [infoSnack, setInfoSnack] = useState(null);
const handleChange = (event: React.SyntheticEvent, newValue: number) => {
const handleChange = (event: SyntheticEvent, newValue: number) => {
setValue(newValue);
};
@ -84,16 +93,30 @@ export const AddGroup = ({ address, open, setOpen }) => {
setMaxBlock(event.target.value as string);
};
const { t } = useTranslation(['core', 'group']);
const theme = useTheme();
const handleCreateGroup = async () => {
try {
if (!name) throw new Error('Please provide a name');
if (!description) throw new Error('Please provide a description');
if (!name)
throw new Error(
t('group:message.error.name_required', {
postProcess: 'capitalize',
})
);
if (!description)
throw new Error(
t('group:message.error.description_required', {
postProcess: 'capitalize',
})
);
const fee = await getFee('CREATE_GROUP');
const fee = await getFee('CREATE_GROUP'); // TODO translate
await show({
message: 'Would you like to perform an CREATE_GROUP transaction?',
message: t('group:question.create_group', {
postProcess: 'capitalize',
}),
publishFee: fee.fee + ' QORT',
});
@ -111,16 +134,23 @@ export const AddGroup = ({ address, open, setOpen }) => {
if (!response?.error) {
setInfoSnack({
type: 'success',
message:
'Successfully created group. It may take a couple of minutes for the changes to propagate',
message: t('group:message.success.group_creation', {
postProcess: 'capitalize',
}),
});
setOpenSnack(true);
setTxList((prev) => [
{
...response,
type: 'created-group',
label: `Created group ${name}: awaiting confirmation`,
labelDone: `Created group ${name}: success!`,
label: t('group:message.success.group_creation_name', {
group_name: name,
postProcess: 'capitalize',
}),
labelDone: t('group:message.success.group_creation_label', {
group_name: name,
postProcess: 'capitalize',
}),
done: false,
},
...prev,
@ -131,7 +161,11 @@ export const AddGroup = ({ address, open, setOpen }) => {
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) {
@ -143,22 +177,6 @@ export const AddGroup = ({ address, open, setOpen }) => {
}
};
function CustomTabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && <Box sx={{ p: 3 }}>{children}</Box>}
</div>
);
}
function a11yProps(index: number) {
return {
id: `simple-tab-${index}`,
@ -170,7 +188,7 @@ export const AddGroup = ({ address, open, setOpen }) => {
setValue(2);
};
React.useEffect(() => {
useEffect(() => {
subscribeToEvent('openGroupInvitesRequest', openGroupInvitesRequestFunc);
return () => {
@ -182,7 +200,7 @@ export const AddGroup = ({ address, open, setOpen }) => {
}, []);
return (
<React.Fragment>
<Fragment>
<Dialog
fullScreen
open={open}
@ -197,7 +215,7 @@ export const AddGroup = ({ address, open, setOpen }) => {
>
<Toolbar>
<Typography sx={{ ml: 2, flex: 1 }} variant="h4" component="div">
Group Management
{t('group:group.management', { postProcess: 'capitalize' })}
</Typography>
<IconButton
@ -208,12 +226,9 @@ export const AddGroup = ({ address, open, setOpen }) => {
>
<CloseIcon />
</IconButton>
{/* <Button autoFocus color="inherit" onClick={handleClose}>
save
</Button> */}
</Toolbar>
</AppBar>
<Box
sx={{
bgcolor: theme.palette.background.default,
@ -241,7 +256,9 @@ export const AddGroup = ({ address, open, setOpen }) => {
}}
>
<Tab
label="Create Group"
label={t('group:action.create_group', {
postProcess: 'capitalize',
})}
{...a11yProps(0)}
sx={{
'&.Mui-selected': {
@ -251,7 +268,9 @@ export const AddGroup = ({ address, open, setOpen }) => {
}}
/>
<Tab
label="Find Group"
label={t('group:action.find_group', {
postProcess: 'capitalize',
})}
{...a11yProps(1)}
sx={{
'&.Mui-selected': {
@ -261,7 +280,9 @@ export const AddGroup = ({ address, open, setOpen }) => {
}}
/>
<Tab
label="Group Invites"
label={t('group:group.invites', {
postProcess: 'capitalize',
})}
{...a11yProps(2)}
sx={{
'&.Mui-selected': {
@ -295,9 +316,15 @@ export const AddGroup = ({ address, open, setOpen }) => {
gap: '5px',
}}
>
<Label>Name of group</Label>
<Label>
{t('group:group.name', {
postProcess: 'capitalize',
})}
</Label>
<Input
placeholder="Name of group"
placeholder={t('group:group.name', {
postProcess: 'capitalize',
})}
value={name}
onChange={(e) => setName(e.target.value)}
/>
@ -309,14 +336,21 @@ export const AddGroup = ({ address, open, setOpen }) => {
gap: '5px',
}}
>
<Label>Description of group</Label>
<Label>
{t('group:group.description', {
postProcess: 'capitalize',
})}
</Label>
<Input
placeholder="Description of group"
placeholder={t('group:group.description', {
postProcess: 'capitalize',
})}
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</Box>
<Box
sx={{
display: 'flex',
@ -324,7 +358,13 @@ export const AddGroup = ({ address, open, setOpen }) => {
gap: '5px',
}}
>
<Label>Group type</Label>
<Label>
{' '}
{t('group:group.type', {
postProcess: 'capitalize',
})}
</Label>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
@ -332,12 +372,19 @@ export const AddGroup = ({ address, open, setOpen }) => {
label="Group Type"
onChange={handleChangeGroupType}
>
<MenuItem value={1}>Open (public)</MenuItem>
<MenuItem value={1}>
{t('group:group.open', {
postProcess: 'capitalize',
})}
</MenuItem>
<MenuItem value={0}>
Closed (private) - users need permission to join
{t('group:group.closed', {
postProcess: 'capitalize',
})}
</MenuItem>
</Select>
</Box>
<Box
sx={{
display: 'flex',
@ -347,10 +394,15 @@ export const AddGroup = ({ address, open, setOpen }) => {
}}
onClick={() => setOpenAdvance((prev) => !prev)}
>
<Typography>Advanced options</Typography>
<Typography>
{t('group:advanced_options', {
postProcess: 'capitalize',
})}
</Typography>
{openAdvance ? <ExpandLess /> : <ExpandMore />}
</Box>
<Collapse in={openAdvance} timeout="auto" unmountOnExit>
<Box
sx={{
@ -360,8 +412,9 @@ export const AddGroup = ({ address, open, setOpen }) => {
}}
>
<Label>
Group Approval Threshold (number / percentage of Admins
that must approve a transaction)
{t('group:approval_threshold', {
postProcess: 'capitalize',
})}
</Label>
<Select
labelId="demo-simple-select-label"
@ -370,14 +423,21 @@ export const AddGroup = ({ address, open, setOpen }) => {
label="Group Approval Threshold"
onChange={handleChangeApprovalThreshold}
>
<MenuItem value={0}>NONE</MenuItem>
<MenuItem value={1}>ONE </MenuItem>
<MenuItem value={20}>20% </MenuItem>
<MenuItem value={40}>40% </MenuItem>
<MenuItem value={60}>60% </MenuItem>
<MenuItem value={80}>80% </MenuItem>
<MenuItem value={100}>100% </MenuItem>
<MenuItem value={0}>
{t('core.count.none', {
postProcess: 'capitalize',
})}
</MenuItem>
<MenuItem value={1}>
{t('core.count.one', {
postProcess: 'capitalize',
})}
</MenuItem>
<MenuItem value={20}>20%</MenuItem>
<MenuItem value={40}>40%</MenuItem>
<MenuItem value={60}>60%</MenuItem>
<MenuItem value={80}>80%</MenuItem>
<MenuItem value={100}>100%</MenuItem>
</Select>
</Box>
<Box
@ -388,7 +448,9 @@ export const AddGroup = ({ address, open, setOpen }) => {
}}
>
<Label>
Minimum Block delay for Group Transaction Approvals
{t('group.block_delay.minimum', {
postProcess: 'capitalize',
})}
</Label>
<Select
labelId="demo-simple-select-label"
@ -397,18 +459,42 @@ export const AddGroup = ({ address, open, setOpen }) => {
label="Minimum Block delay"
onChange={handleChangeMinBlock}
>
<MenuItem value={5}>5 minutes</MenuItem>
<MenuItem value={10}>10 minutes</MenuItem>
<MenuItem value={30}>30 minutes</MenuItem>
<MenuItem value={60}>1 hour</MenuItem>
<MenuItem value={180}>3 hours</MenuItem>
<MenuItem value={300}>5 hours</MenuItem>
<MenuItem value={420}>7 hours</MenuItem>
<MenuItem value={720}>12 hours</MenuItem>
<MenuItem value={1440}>1 day</MenuItem>
<MenuItem value={4320}>3 days</MenuItem>
<MenuItem value={7200}>5 days</MenuItem>
<MenuItem value={10080}>7 days</MenuItem>
<MenuItem value={5}>
{t('core.time.minute', { count: 5 })}
</MenuItem>
<MenuItem value={10}>
{t('core.time.minute', { count: 10 })}
</MenuItem>
<MenuItem value={30}>
{t('core.time.minute', { count: 30 })}
</MenuItem>
<MenuItem value={60}>
{t('core.time.hour', { count: 1 })}
</MenuItem>
<MenuItem value={180}>
{t('core.time.hour', { count: 3 })}
</MenuItem>
<MenuItem value={300}>
{t('core.time.hour', { count: 5 })}
</MenuItem>
<MenuItem value={420}>
{t('core.time.hour', { count: 7 })}
</MenuItem>
<MenuItem value={720}>
{t('core.time.hour', { count: 12 })}
</MenuItem>
<MenuItem value={1440}>
{t('core.time.day', { count: 1 })}
</MenuItem>
<MenuItem value={4320}>
{t('core.time.day', { count: 3 })}
</MenuItem>
<MenuItem value={7200}>
{t('core.time.day', { count: 5 })}
</MenuItem>
<MenuItem value={10080}>
{t('core.time.day', { count: 7 })}
</MenuItem>
</Select>
</Box>
<Box
@ -419,7 +505,9 @@ export const AddGroup = ({ address, open, setOpen }) => {
}}
>
<Label>
Maximum Block delay for Group Transaction Approvals
{t('group.block_delay.maximum', {
postProcess: 'capitalize',
})}
</Label>
<Select
labelId="demo-simple-select-label"
@ -428,17 +516,39 @@ export const AddGroup = ({ address, open, setOpen }) => {
label="Maximum Block delay"
onChange={handleChangeMaxBlock}
>
<MenuItem value={60}>1 hour</MenuItem>
<MenuItem value={180}>3 hours</MenuItem>
<MenuItem value={300}>5 hours</MenuItem>
<MenuItem value={420}>7 hours</MenuItem>
<MenuItem value={720}>12 hours</MenuItem>
<MenuItem value={1440}>1 day</MenuItem>
<MenuItem value={4320}>3 days</MenuItem>
<MenuItem value={7200}>5 days</MenuItem>
<MenuItem value={10080}>7 days</MenuItem>
<MenuItem value={14400}>10 days</MenuItem>
<MenuItem value={21600}>15 days</MenuItem>
<MenuItem value={60}>
{t('core.time.hour', { count: 1 })}
</MenuItem>
<MenuItem value={180}>
3{t('core.time.hour', { count: 3 })}
</MenuItem>
<MenuItem value={300}>
{t('core.time.hour', { count: 5 })}
</MenuItem>
<MenuItem value={420}>
{t('core.time.hour', { count: 7 })}
</MenuItem>
<MenuItem value={720}>
{t('core.time.hour', { count: 12 })}
</MenuItem>
<MenuItem value={1440}>
{t('core.time.day', { count: 1 })}
</MenuItem>
<MenuItem value={4320}>
{t('core.time.day', { count: 3 })}
</MenuItem>
<MenuItem value={7200}>
{t('core.time.day', { count: 5 })}
</MenuItem>
<MenuItem value={10080}>
{t('core.time.day', { count: 7 })}
</MenuItem>
<MenuItem value={14400}>
{t('core.time.day', { count: 10 })}
</MenuItem>
<MenuItem value={21600}>
{t('core.time.day', { count: 15 })}
</MenuItem>
</Select>
</Box>
</Collapse>
@ -454,7 +564,9 @@ export const AddGroup = ({ address, open, setOpen }) => {
color="primary"
onClick={handleCreateGroup}
>
Create Group
{t('group.action.create', {
postProcess: 'capitalize',
})}
</Button>
</Box>
</Box>
@ -503,6 +615,6 @@ export const AddGroup = ({ address, open, setOpen }) => {
setInfo={setInfoSnack}
/>
</Dialog>
</React.Fragment>
</Fragment>
);
};

View File

@ -1,6 +1,5 @@
import {
Box,
Button,
ListItem,
ListItemButton,
ListItemText,
@ -8,7 +7,7 @@ import {
TextField,
Typography,
} from '@mui/material';
import React, {
import {
useCallback,
useContext,
useEffect,
@ -25,10 +24,12 @@ import {
import _ from 'lodash';
import { MyContext, getBaseApiReact } from '../../App';
import { LoadingButton } from '@mui/lab';
import { getBaseApi, getFee } from '../../background';
import { getFee } from '../../background';
import LockIcon from '@mui/icons-material/Lock';
import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred';
import { Spacer } from '../../common/Spacer';
import { useTranslation } from 'react-i18next';
const cache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 50,
@ -36,7 +37,7 @@ const cache = new CellMeasurerCache({
export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => {
const { memberGroups, show, setTxList } = useContext(MyContext);
const { t } = useTranslation(['core', 'group']);
const [groups, setGroups] = useState([]);
const [popoverAnchor, setPopoverAnchor] = useState(null); // Track which list item the popover is anchored to
const [openPopoverIndex, setOpenPopoverIndex] = useState(null); // Track which list item has the popover open
@ -101,12 +102,17 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => {
const handleJoinGroup = async (group, isOpen) => {
try {
const groupId = group.groupId;
const fee = await getFee('JOIN_GROUP'); // TODO translate
const fee = await getFee('JOIN_GROUP');
await show({
message: 'Would you like to perform an JOIN_GROUP transaction?',
message: t('group:question.join_group', {
postProcess: 'capitalize',
}),
publishFee: fee.fee + ' QORT',
});
setIsLoading(true);
await new Promise((res, rej) => {
window
.sendMessage('joinGroup', {
@ -116,8 +122,9 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => {
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.join_group', {
postProcess: 'capitalize',
}),
});
if (isOpen) {
@ -125,8 +132,14 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => {
{
...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,
},
@ -215,7 +228,10 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => {
padding: '10px',
}}
>
<Typography>Join {group?.groupName}</Typography>
<Typography>
{t('core:action.join', { postProcess: 'capitalize' })}{' '}
{group?.groupName}
</Typography>
<Typography>
{group?.isOpen === false &&
'This is a closed/private group, so you will need to wait until an admin accepts your request'}
@ -226,7 +242,9 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => {
variant="contained"
onClick={() => handleJoinGroup(group, group?.isOpen)}
>
Join group
{t('group:action.join_group', {
postProcess: 'capitalize',
})}
</LoadingButton>
</Box>
</Popover>

View File

@ -129,7 +129,7 @@ export const BlockedUsersModal = () => {
executeEvent('updateChatMessagesWithBlocks', true);
}
} catch (error) {
setOpenSnackGlobal(true); // TODO translate
setOpenSnackGlobal(true);
setInfoSnackCustom({
type: 'error',
message: error?.message || 'Unable to block user',

View File

@ -1,5 +1,4 @@
import React, {
FC,
useCallback,
useEffect,
useMemo,
@ -17,7 +16,6 @@ import {
ComposeIcon,
ComposeP,
GroupContainer,
GroupNameP,
InstanceFooter,
InstanceListContainer,
InstanceListContainerRow,
@ -58,10 +56,12 @@ import { executeEvent } from '../../../utils/events';
import RefreshIcon from '@mui/icons-material/Refresh';
import { getArbitraryEndpointReact, getBaseApiReact } from '../../../App';
import { addDataPublishesFunc, getDataPublishesFunc } from '../Group';
import { useTranslation } from 'react-i18next';
const filterOptions = ['Recently active', 'Newest', 'Oldest'];
export const threadIdentifier = 'DOCUMENT';
export const GroupMail = ({
selectedGroup,
userInfo,
@ -82,6 +82,7 @@ export const GroupMail = ({
const anchorElInstanceFilter = useRef<any>(null);
const [tempPublishedList, setTempPublishedList] = useState([]);
const dataPublishes = useRef({});
const { t } = useTranslation(['core']);
const [isLoading, setIsLoading] = useState(false);
const groupIdRef = useRef<any>(null);
@ -120,7 +121,9 @@ export const GroupMail = ({
});
setTempPublishedList(tempData);
}
} catch (error) {}
} catch (error) {
console.log(error);
}
};
const getEncryptedResource = async (
@ -627,9 +630,9 @@ export const GroupMail = ({
<ThreadContainer>
<Box
sx={{
alignItems: 'center',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<NewThread
@ -667,8 +670,8 @@ export const GroupMail = ({
<Spacer height="30px" />
<Box
sx={{
display: 'flex',
alignItems: 'center',
display: 'flex',
justifyContent: 'space-between',
}}
>
@ -682,6 +685,7 @@ export const GroupMail = ({
}}
/>
</Box>
<Spacer height="30px" />
{combinedListTempAndReal.map((thread) => {
@ -754,8 +758,8 @@ export const GroupMail = ({
{filterMode === 'Recently active' && (
<div
style={{
display: 'flex',
alignItems: 'center',
display: 'flex',
}}
>
<ThreadSingleLastMessageP>
@ -776,16 +780,16 @@ export const GroupMail = ({
}, 300);
}}
sx={{
position: 'absolute',
bottom: '2px',
right: '2px',
borderRadius: '5px',
alignItems: 'center',
backgroundColor: '#27282c',
borderRadius: '5px',
bottom: '2px',
cursor: 'pointer',
display: 'flex',
gap: '10px',
alignItems: 'center',
padding: '5px',
cursor: 'pointer',
position: 'absolute',
right: '2px',
'&:hover': {
background: 'rgba(255, 255, 255, 0.60)',
},
@ -795,9 +799,11 @@ export const GroupMail = ({
sx={{
color: 'white',
fontSize: '12px',
}} // TODO translate
}}
>
Last page
{t('core:page.last', {
postProcess: 'capitalize',
})}
</Typography>
<ArrowForwardIosIcon
sx={{
@ -828,7 +834,9 @@ export const GroupMail = ({
<LoadingSnackbar
open={isLoading}
info={{
message: 'Loading threads... please wait.',
message: t('group:message.success.loading_threads', {
postProcess: 'capitalize',
}),
}}
/>
</GroupContainer>

View File

@ -1,11 +1,9 @@
import React, { useEffect, useRef, useState } from 'react';
import { Box, CircularProgress, Input } from '@mui/material';
import ShortUniqueId from 'short-unique-id';
import CloseIcon from '@mui/icons-material/Close';
import ModalCloseSVG from '../../../assets/svgs/ModalClose.svg';
import ComposeIconSVG from '../../../assets/svgs/ComposeIcon.svg';
import {
AttachmentContainer,
CloseContainer,
ComposeContainer,
ComposeIcon,
@ -30,6 +28,7 @@ import TipTap from '../../Chat/TipTap';
import { MessageDisplay } from '../../Chat/MessageDisplay';
import { CustomizedSnackbars } from '../../Snackbar/Snackbar';
import { saveTempPublish } from '../../Chat/GroupAnnouncements';
import { useTranslation } from 'react-i18next';
const uid = new ShortUniqueId({ length: 8 });
@ -129,6 +128,7 @@ export const encryptSingleFunc = async (data: string, secretKeyObject: any) => {
console.log(error);
}
};
export const NewThread = ({
groupInfo,
members,
@ -143,8 +143,8 @@ export const NewThread = ({
setPostReply,
isPrivate,
}: NewMessageProps) => {
const { t } = useTranslation(['core', 'group']);
const { show } = React.useContext(MyContext);
const [isOpen, setIsOpen] = useState<boolean>(false);
const [value, setValue] = useState('');
const [isSending, setIsSending] = useState(false);
@ -183,21 +183,28 @@ export const NewThread = ({
const missingFields: string[] = [];
if (!isMessage && !threadTitle) {
errorMsg = 'Please provide a thread title';
errorMsg = t('group:question.provide_thread', {
postProcess: 'capitalize',
});
}
if (!name) {
errorMsg = 'Cannot send a message without a access to your name';
errorMsg = t('group:message.error.access_name', {
postProcess: 'capitalize',
});
}
if (!groupInfo) {
errorMsg = 'Cannot access group information';
} // TODO translate
errorMsg = t('group:message.error.group_info', {
postProcess: 'capitalize',
});
}
// if (!description) missingFields.push('subject')
if (missingFields.length > 0) {
const missingFieldsString = missingFields.join(', ');
const errMsg = `Missing: ${missingFieldsString}`;
errorMsg = errMsg;
errorMsg = errMsg; // TODO translate
}
if (errorMsg) {

View File

@ -3,7 +3,6 @@ import { Avatar, Box, IconButton } from '@mui/material';
import DOMPurify from 'dompurify';
import FormatQuoteIcon from '@mui/icons-material/FormatQuote';
import MoreSVG from '../../../assets/svgs/More.svg';
import {
MoreImg,
MoreP,
@ -38,16 +37,16 @@ export const ShowMessage = ({ message, openNewPostWithQuote, myName }: any) => {
>
<Box
sx={{
alignItems: 'flex-start',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
width: '100%',
}}
>
<Box
sx={{
display: 'flex',
alignItems: 'flex-start',
display: 'flex',
gap: '10px',
}}
>
@ -67,6 +66,7 @@ export const ShowMessage = ({ message, openNewPostWithQuote, myName }: any) => {
{message?.name?.charAt(0)}
</Avatar>
</WrapperUserAction>
<ThreadInfoColumn>
<WrapperUserAction
disabled={myName === message?.name}
@ -75,6 +75,7 @@ export const ShowMessage = ({ message, openNewPostWithQuote, myName }: any) => {
>
<ThreadInfoColumnNameP>{message?.name}</ThreadInfoColumnNameP>
</WrapperUserAction>
<ThreadInfoColumnTime>
{formatTimestampForum(message?.created)}
</ThreadInfoColumnTime>
@ -205,6 +206,7 @@ export const ShowMessage = ({ message, openNewPostWithQuote, myName }: any) => {
>
{message?.reply?.name?.charAt(0)}
</Avatar>
<ThreadInfoColumn>
<ThreadInfoColumnNameP
sx={{
@ -215,6 +217,7 @@ export const ShowMessage = ({ message, openNewPostWithQuote, myName }: any) => {
</ThreadInfoColumnNameP>
</ThreadInfoColumn>
</Box>
<MessageDisplay htmlContent={message?.reply?.textContentV2} />
</Box>
<Spacer height="20px" />

View File

@ -1,20 +1,11 @@
import React, {
FC,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import {
Avatar,
Box,
Button,
ButtonBase,
IconButton,
Skeleton,
Typography,
} from '@mui/material';
import { Avatar, Box, Button, ButtonBase, Typography } from '@mui/material';
import { ShowMessage } from './ShowMessageWithoutModal';
import {
ComposeP,
@ -51,8 +42,12 @@ import { RequestQueueWithPromise } from '../../../utils/queue/queue';
import { CustomLoader } from '../../../common/CustomLoader';
import { WrapperUserAction } from '../../WrapperUserAction';
import { formatTimestampForum } from '../../../utils/time';
import { useTranslation } from 'react-i18next';
const requestQueueSaveToLocal = new RequestQueueWithPromise(1);
const requestQueueDownloadPost = new RequestQueueWithPromise(3);
interface ThreadProps {
currentThread: any;
groupInfo: any;
@ -120,6 +115,7 @@ export const Thread = ({
const [isLoading, setIsLoading] = useState(true);
const [postReply, setPostReply] = useState(null);
const [hasLastPage, setHasLastPage] = useState(false);
const { t } = useTranslation(['core']);
// Update: Use a new ref for the scrollable container
const threadContainerRef = useRef(null);
@ -251,6 +247,7 @@ export const Thread = ({
'Content-Type': 'application/json',
},
});
const responseData = await response.json();
let fullArrayMsg = [...responseData];
@ -431,6 +428,7 @@ export const Thread = ({
}
const newArray = responseData.slice(0, findMessage).reverse();
let fullArrayMsg = [...messages];
for (const message of newArray) {
try {
const responseDataMessage = await getEncryptedResource({
@ -468,7 +466,6 @@ export const Thread = ({
setMessages(fullArrayMsg);
} catch (error) {
console.log(error);
} finally {
}
},
[messages]
@ -565,20 +562,20 @@ export const Thread = ({
return (
<GroupContainer
sx={{
position: 'relative',
width: '100%',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
position: 'relative',
width: '100%',
}}
// Removed the ref from here since the scrollable area has changed
>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
display: 'flex',
flexShrink: 0, // Corrected property name
justifyContent: 'space-between',
}}
>
<NewThread
@ -598,9 +595,9 @@ export const Thread = ({
/>
<Box
sx={{
alignItems: 'center',
display: 'flex',
gap: '35px',
alignItems: 'center',
}}
>
<ShowMessageReturnButton
@ -610,7 +607,11 @@ export const Thread = ({
}}
>
<MailIconImg src={ReturnSVG} />
<ComposeP>Return to Threads</ComposeP>
<ComposeP>
{t('group:action.return_to_thread', {
postProcess: 'capitalize',
})}
</ComposeP>
</ShowMessageReturnButton>
{/* Conditionally render the scroll buttons */}
{showScrollButton &&
@ -658,6 +659,7 @@ export const Thread = ({
>
<GroupNameP>{currentThread?.threadData?.title}</GroupNameP>
</Box>
<Spacer height={'15px'} />
<Box
@ -685,8 +687,9 @@ export const Thread = ({
disabled={!hasFirstPage}
variant="contained"
>
First
{t('core:page.first', { postProcess: 'capitalize' })}
</Button>
<Button
sx={{
textTransformation: 'capitalize',
@ -701,9 +704,9 @@ export const Thread = ({
);
}}
disabled={!hasPreviousPage}
variant="contained" // TODO translate
variant="contained"
>
Previous
{t('core:page.previous', { postProcess: 'capitalize' })}
</Button>
<Button
sx={{
@ -721,7 +724,7 @@ export const Thread = ({
disabled={!hasNextPage}
variant="contained"
>
Next
{t('core:page.next', { postProcess: 'capitalize' })}
</Button>
<Button
sx={{
@ -739,7 +742,7 @@ export const Thread = ({
disabled={!hasLastPage}
variant="contained"
>
Last
{t('core:page.last', { postProcess: 'capitalize' })}
</Button>
</Box>
@ -925,7 +928,7 @@ export const Thread = ({
color: 'white',
}}
>
Downloading from QDN
{t('core:downloading_qdn', { postProcess: 'capitalize' })}
</Typography>
</Box>
</Box>
@ -959,7 +962,9 @@ export const Thread = ({
color: 'white',
}}
>
Refetch page
{t('group:action.refetch_page', {
postProcess: 'capitalize',
})}
</Button>
</Box>
</>
@ -997,7 +1002,7 @@ export const Thread = ({
disabled={!hasFirstPage}
variant="contained"
>
First
{t('core:page.first', { postProcess: 'capitalize' })}
</Button>
<Button
sx={{
@ -1015,7 +1020,7 @@ export const Thread = ({
disabled={!hasPreviousPage}
variant="contained"
>
Previous
{t('core:page.previous', { postProcess: 'capitalize' })}
</Button>
<Button
sx={{
@ -1033,7 +1038,7 @@ export const Thread = ({
disabled={!hasNextPage}
variant="contained"
>
Next
{t('core:page.next', { postProcess: 'capitalize' })}
</Button>
<Button
sx={{
@ -1051,7 +1056,7 @@ export const Thread = ({
disabled={!hasLastPage}
variant="contained"
>
Last
{t('core:page.last', { postProcess: 'capitalize' })}
</Button>
</Box>
<Spacer height="30px" />
@ -1063,7 +1068,7 @@ export const Thread = ({
<LoadingSnackbar
open={isLoading}
info={{
message: 'Loading posts... please wait.',
message: t('core:loading_posts', { postProcess: 'capitalize' }),
}}
/>
</GroupContainer>

View File

@ -55,8 +55,6 @@ import { RequestQueueWithPromise } from '../../utils/queue/queue';
import { WebSocketActive } from './WebsocketActive';
import { useMessageQueue } from '../../MessageQueueContext';
import { ContextMenu } from '../ContextMenu';
import { ReturnIcon } from '../../assets/Icons/ReturnIcon';
import { ExitIcon } from '../../assets/Icons/ExitIcon';
import { HomeDesktop } from './HomeDesktop';
import { IconWrapper } from '../Desktop/DesktopFooter';
import { DesktopHeader } from '../Desktop/DesktopHeader';
@ -80,6 +78,7 @@ import LockIcon from '@mui/icons-material/Lock';
import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred';
import { BlockedUsersModal } from './BlockedUsersModal';
import { WalletsAppWrapper } from './WalletsAppWrapper';
import { useTranslation } from 'react-i18next';
export const getPublishesFromAdmins = async (admins: string[], groupId) => {
const queryString = admins.map((name) => `name=${name}`).join('&');
@ -450,6 +449,7 @@ export const Group = ({
const [isOpenSideViewGroups, setIsOpenSideViewGroups] = useState(false);
const [isForceShowCreationKeyPopup, setIsForceShowCreationKeyPopup] =
useState(false);
const { t } = useTranslation(['core', 'group']);
const [groupsProperties, setGroupsProperties] =
useRecoilState(groupsPropertiesAtom);
@ -2219,9 +2219,10 @@ export const Group = ({
color: theme.palette.text.primary,
}}
>
No group selected
</Typography>{' '}
// TODO translate
{t('group:message.generic.no_selection', {
postProcess: 'capitalize',
})}
</Typography>
</Box>
)}
@ -2317,9 +2318,9 @@ export const Group = ({
>
{' '}
<Typography>
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...
{t('group:message.generic.encryption_key', {
postProcess: 'capitalize',
})}
</Typography>
</div>
)}
@ -2343,18 +2344,23 @@ export const Group = ({
>
{' '}
<Typography>
You are not part of the encrypted group of members. Wait
until an admin re-encrypts the keys.
{t('group:message.generic.not_part_group', {
postProcess: 'capitalize',
})}
</Typography>
<Spacer height="25px" />
<Typography>
<strong>
Only unencrypted messages will be displayed.
{t('group:message.generic.only_encrypted', {
postProcess: 'capitalize',
})}
</strong>
</Typography>
<Spacer height="25px" />
<Typography>
Try notifying an admin from the list of admins below:
{t('group:message.generic.notify_admins', {
postProcess: 'capitalize',
})}
</Typography>
<Spacer height="25px" />
{adminsWithNames.map((admin) => {
@ -2374,7 +2380,9 @@ export const Group = ({
variant="contained"
onClick={() => notifyAdmin(admin)}
>
Notify
{t('core:action.notify', {
postProcess: 'capitalize',
})}
</LoadingButton>
</Box>
);
@ -2594,14 +2602,19 @@ export const Group = ({
open={isLoadingGroup}
info={{
message:
isLoadingGroupMessage || 'Setting up group... please wait.',
isLoadingGroupMessage ||
t('group:message.generic.setting_group', {
postProcess: 'capitalize',
}),
}}
/>
<LoadingSnackbar
open={isLoadingGroups}
info={{
message: 'Setting up groups... please wait.',
message: t('group:message.generic.setting_group', {
postProcess: 'capitalize',
}),
}}
/>
<WalletsAppWrapper />

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import { useEffect, useState } from 'react';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
@ -12,14 +12,13 @@ import { CustomLoader } from '../../common/CustomLoader';
import { getBaseApiReact } from '../../App';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import { useTranslation } from 'react-i18next';
export const GroupInvites = ({ myAddress, setOpenAddGroup }) => {
const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState(
[]
);
const [isExpanded, setIsExpanded] = React.useState(false);
const [groupsWithJoinRequests, setGroupsWithJoinRequests] = useState([]);
const [isExpanded, setIsExpanded] = useState(false);
const [loading, setLoading] = React.useState(true);
const [loading, setLoading] = useState(true);
const getJoinRequests = async () => {
try {
@ -38,9 +37,10 @@ export const GroupInvites = ({ myAddress, setOpenAddGroup }) => {
}
};
const { t } = useTranslation(['core', 'group']);
const theme = useTheme();
React.useEffect(() => {
useEffect(() => {
if (myAddress) {
getJoinRequests();
}
@ -69,9 +69,9 @@ export const GroupInvites = ({ myAddress, setOpenAddGroup }) => {
<Typography
sx={{
fontSize: '1rem',
}} // TODO translate
}}
>
Group Invites{' '}
{t('group:group_invites', { postProcess: 'capitalize' })}{' '}
{groupsWithJoinRequests?.length > 0 &&
` (${groupsWithJoinRequests?.length})`}
</Typography>
@ -130,7 +130,9 @@ export const GroupInvites = ({ myAddress, setOpenAddGroup }) => {
fontWeight: 400,
}}
>
Nothing to display
{t('group:message.generic.no_display', {
postProcess: 'capitalize',
})}
</Typography>
</Box>
)}
@ -177,7 +179,10 @@ export const GroupInvites = ({ myAddress, setOpenAddGroup }) => {
fontWeight: 400,
},
}}
primary={`${group?.groupName} has invited you`}
primary={t('group:message.generic.group_invited_you', {
group: group?.groupName,
postProcess: 'capitalize',
})}
/>
</ListItemButton>
</ListItem>

View File

@ -14,6 +14,7 @@ import { myGroupsWhereIAmAdminAtom } from '../../atoms/global';
import { useSetRecoilState } from 'recoil';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import { useTranslation } from 'react-i18next';
export const requestQueueGroupJoinRequests = new RequestQueueWithPromise(2);
export const GroupJoinRequests = ({
@ -27,7 +28,7 @@ export const GroupJoinRequests = ({
setDesktopViewMode,
}) => {
const [isExpanded, setIsExpanded] = React.useState(false);
const { t } = useTranslation(['core', 'group']);
const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState(
[]
);
@ -139,9 +140,9 @@ export const GroupJoinRequests = ({
<Typography
sx={{
fontSize: '1rem',
}} // TODO translate
}}
>
Join Requests{' '}
{t('group:join_requests', { postProcess: 'capitalize' })}{' '}
{filteredJoinRequests?.filter((group) => group?.data?.length > 0)
?.length > 0 &&
` (${filteredJoinRequests?.filter((group) => group?.data?.length > 0)?.length})`}
@ -163,14 +164,13 @@ export const GroupJoinRequests = ({
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
<Box
sx={{
width: '322px',
height: '250px',
bgcolor: 'background.paper',
borderRadius: '19px',
display: 'flex',
flexDirection: 'column',
bgcolor: 'background.paper',
height: '250px',
padding: '20px',
borderRadius: '19px',
width: '322px',
}}
>
{loading && filteredJoinRequests.length === 0 && (
@ -204,18 +204,20 @@ export const GroupJoinRequests = ({
color: 'rgba(255, 255, 255, 0.2)',
}}
>
Nothing to display
{t('group:message.generic.no_display', {
postProcess: 'capitalize',
})}
</Typography>
</Box>
)}
<List
className="scrollable-container"
sx={{
width: '100%',
maxWidth: 360,
bgcolor: 'background.paper',
maxHeight: '300px',
maxWidth: 360,
overflow: 'auto',
width: '100%',
}}
>
{filteredJoinRequests?.map((group) => {

View File

@ -4,16 +4,21 @@ import { useState } from 'react';
import { Spacer } from '../../common/Spacer';
import { Label } from './AddGroup';
import { getFee } from '../../background';
import { useTranslation } from 'react-i18next';
export const InviteMember = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
const [value, setValue] = useState('');
const [expiryTime, setExpiryTime] = useState<string>('259200');
const [isLoadingInvite, setIsLoadingInvite] = useState(false);
const { t } = useTranslation(['core', 'group']);
const inviteMember = async () => {
try {
const fee = await getFee('GROUP_INVITE');
await show({
message: 'Would you like to perform a GROUP_INVITE transaction?',
message: t('group:question.group_invite', {
postProcess: 'capitalize',
}),
publishFee: fee.fee + ' QORT',
});
setIsLoadingInvite(true);
@ -27,10 +32,12 @@ export const InviteMember = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
})
.then((response) => {
if (!response?.error) {
// TODO translate
setInfoSnack({
type: 'success',
message: `Successfully invited ${value}. It may take a couple of minutes for the changes to propagate`,
message: t('group:message.success.group_invite', {
value: value,
postProcess: 'capitalize',
}),
});
setOpenSnack(true);
res(response);
@ -72,7 +79,7 @@ export const InviteMember = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
flexDirection: 'column',
}}
>
Invite member
{t('group:action.invite_member', { postProcess: 'capitalize' })}
<Spacer height="20px" />
<Input
value={value}
@ -80,24 +87,26 @@ export const InviteMember = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
onChange={(e) => setValue(e.target.value)}
/>
<Spacer height="20px" />
<Label>Invitation Expiry Time</Label>
<Label>
{t('group:invitation_expiry', { postProcess: 'capitalize' })}
</Label>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={expiryTime}
label="Invitation Expiry Time"
label={t('group:invitation_expiry', { postProcess: 'capitalize' })}
onChange={handleChange}
>
<MenuItem value={10800}>3 hours</MenuItem>
<MenuItem value={21600}>6 hours</MenuItem>
<MenuItem value={43200}>12 hours</MenuItem>
<MenuItem value={86400}>1 day</MenuItem>
<MenuItem value={259200}>3 days</MenuItem>
<MenuItem value={432000}>5 days</MenuItem>
<MenuItem value={604800}>7 days</MenuItem>
<MenuItem value={864000}>10 days</MenuItem>
<MenuItem value={1296000}>15 days</MenuItem>
<MenuItem value={2592000}>30 days</MenuItem>
<MenuItem value={10800}>{t('core.time.hour', { count: 3 })}</MenuItem>
<MenuItem value={21600}>{t('core.time.hour', { count: 6 })}</MenuItem>
<MenuItem value={43200}>{t('core.time.hour', { count: 12 })}</MenuItem>
<MenuItem value={86400}>{t('core.time.day', { count: 1 })}</MenuItem>
<MenuItem value={259200}>{t('core.time.day', { count: 3 })}</MenuItem>
<MenuItem value={432000}>{t('core.time.day', { count: 5 })}</MenuItem>
<MenuItem value={604800}>{t('core.time.day', { count: 7 })}</MenuItem>
<MenuItem value={864000}>{t('core.time.day', { count: 10 })}</MenuItem>
<MenuItem value={1296000}>{t('core.time.day', { count: 15 })}</MenuItem>
<MenuItem value={2592000}>{t('core.time.day', { count: 30 })}</MenuItem>
</Select>
<Spacer height="20px" />
<LoadingButton
@ -106,7 +115,7 @@ export const InviteMember = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
loading={isLoadingInvite}
onClick={inviteMember}
>
Invite
{t('core:action.invite', { postProcess: 'capitalize' })}
</LoadingButton>
</Box>
);

View File

@ -18,6 +18,7 @@ import { getNameInfo } from './Group';
import { getFee } from '../../background';
import { LoadingButton } from '@mui/lab';
import { getBaseApiReact } from '../../App';
import { useTranslation } from 'react-i18next';
export const getMemberInvites = async (groupNumber) => {
const response = await fetch(
@ -55,6 +56,7 @@ export const ListOfBans = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
const [openPopoverIndex, setOpenPopoverIndex] = useState(null); // Track which list item has the popover open
const listRef = useRef();
const [isLoadingUnban, setIsLoadingUnban] = useState(false);
const { t } = useTranslation(['core', 'group']);
const getInvites = async (groupId) => {
try {
@ -84,10 +86,9 @@ export const ListOfBans = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
const handleCancelBan = async (address) => {
try {
// TODO translate
const fee = await getFee('CANCEL_GROUP_BAN');
await show({
message: 'Would you like to perform a CANCEL_GROUP_BAN transaction?',
message: t('group:question.cancel_ban', { postProcess: 'capitalize' }),
publishFee: fee.fee + ' QORT',
});
setIsLoadingUnban(true);
@ -103,8 +104,9 @@ export const ListOfBans = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
setIsLoadingUnban(false);
setInfoSnack({
type: 'success',
message:
'Successfully unbanned user. It may take a couple of minutes for the changes to propagate',
message: t('group:message.success.unbanned_user', {
postProcess: 'capitalize',
}),
});
handlePopoverClose();
setOpenSnack(true);
@ -127,6 +129,7 @@ export const ListOfBans = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
});
});
} catch (error) {
console.log(error);
} finally {
setIsLoadingUnban(false);
}
@ -177,10 +180,13 @@ export const ListOfBans = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
variant="contained"
onClick={() => handleCancelBan(member?.offender)}
>
Cancel Ban
{t('group:action.cancel_ban', {
postProcess: 'capitalize',
})}
</LoadingButton>
</Box>
</Popover>
<ListItemButton
onClick={(event) => handlePopoverOpen(event, index)}
>
@ -205,7 +211,7 @@ export const ListOfBans = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
return (
<div>
<p>Ban list</p>
<p>{t('group:ban_list', { postProcess: 'capitalize' })}</p>
<div
style={{
position: 'relative',

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import { forwardRef, Fragment, ReactElement, Ref, useEffect } from 'react';
import Dialog from '@mui/material/Dialog';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
@ -11,13 +11,6 @@ import { Box, FormControlLabel, Switch, styled, useTheme } from '@mui/material';
import { enabledDevModeAtom } from '../../atoms/global';
import { useRecoilState } from 'recoil';
function a11yProps(index: number) {
return {
id: `simple-tab-${index}`,
'aria-controls': `simple-tabpanel-${index}`,
};
}
const LocalNodeSwitch = styled(Switch)(({ theme }) => ({
padding: 8,
'& .MuiSwitch-track': {
@ -51,11 +44,11 @@ const LocalNodeSwitch = styled(Switch)(({ theme }) => ({
},
}));
const Transition = React.forwardRef(function Transition(
const Transition = forwardRef(function Transition(
props: TransitionProps & {
children: React.ReactElement;
children: ReactElement;
},
ref: React.Ref<unknown>
ref: Ref<unknown>
) {
return <Slide direction="up" ref={ref} {...props} />;
});
@ -118,12 +111,12 @@ export const Settings = ({ address, open, setOpen }) => {
}
};
React.useEffect(() => {
useEffect(() => {
getUserSettings();
}, []);
return (
<React.Fragment>
<Fragment>
<Dialog
fullScreen
open={open}
@ -192,6 +185,6 @@ export const Settings = ({ address, open, setOpen }) => {
)}
</Box>
</Dialog>
</React.Fragment>
</Fragment>
);
};

View File

@ -141,21 +141,6 @@ export const ThingsToDoInitial = ({
outline: '1px solid rgba(9, 182, 232, 1)',
}}
/>
{/* <Checkbox
edge="start"
checked={checked1}
tabIndex={-1}
disableRipple
disabled={true}
sx={{
"&.Mui-checked": {
color: "white", // Customize the color when checked
},
"& .MuiSvgIcon-root": {
color: "white",
},
}}
/> */}
</ListItemIcon>
</ListItemButton>
</ListItem>
@ -163,15 +148,6 @@ export const ThingsToDoInitial = ({
sx={{
marginBottom: '20px',
}}
// secondaryAction={
// <IconButton edge="end" aria-label="comments">
// <InfoIcon
// sx={{
// color: "white",
// }}
// />
// </IconButton>
// }
disablePadding
>
<ListItemButton
@ -215,34 +191,6 @@ export const ThingsToDoInitial = ({
</ListItemIcon>
</ListItemButton>
</ListItem>
{/* <ListItem
disablePadding
>
<ListItemButton sx={{
padding: "0px",
}} disableRipple role={undefined} dense>
<ListItemText sx={{
"& .MuiTypography-root": {
fontSize: "13px",
fontWeight: 400,
},
}} primary={`Join a group`} />
<ListItemIcon sx={{
justifyContent: "flex-end",
}}>
<Box
sx={{
height: "18px",
width: "18px",
borderRadius: "50%",
backgroundColor: checked3 ? "rgba(9, 182, 232, 1)" : "transparent",
outline: "1px solid rgba(9, 182, 232, 1)",
}}
/>
</ListItemIcon>
</ListItemButton>
</ListItem> */}
</List>
)}
</Box>

View File

@ -88,7 +88,7 @@ export const WalletsAppWrapper = () => {
justifyContent: 'space-between',
}}
>
<Typography>Q-Wallets</Typography> // TODO translate
<Typography>Q-Wallets</Typography>
<ButtonBase onClick={handleClose}>
<CloseIcon
sx={{

View File

@ -107,7 +107,6 @@ export const WebSocketActive = ({ myAddress, setIsLoadingGroups }) => {
directs: sortedDirects,
})
.catch((error) => {
// TODO translate
console.error(
'Failed to handle active group data from socket:',
error.message || 'An error occurred'

View File

@ -17,7 +17,7 @@ export const useBlockedAddresses = () => {
if (userBlockedRef.current[address]) return true;
return false;
} catch (error) {
//error
console.log(error);
}
}, []);
@ -42,10 +42,13 @@ export const useBlockedAddresses = () => {
console.error('Failed qortalRequest', error);
});
});
const blockedUsers = {};
response?.forEach((item) => {
blockedUsers[item] = true;
});
userBlockedRef.current = blockedUsers;
const response2 = await new Promise((res, rej) => {
@ -66,10 +69,13 @@ export const useBlockedAddresses = () => {
console.error('Failed qortalRequest', error);
});
});
const blockedUsers2 = {};
response2?.forEach((item) => {
blockedUsers2[item] = true;
});
userNamesBlockedRef.current = blockedUsers2;
} catch (error) {
console.error(error);

View File

@ -22,7 +22,7 @@ export const useHandleUserInfo = () => {
};
return data?.level;
} catch (error) {
//error
console.log(error);
}
}, []);

View File

@ -1,46 +1,52 @@
import { Box, ButtonBase, Typography } from '@mui/material';
import { Spacer } from '../../common/Spacer';
import { useTranslation } from 'react-i18next';
export const NewUsersCTA = ({ balance }) => {
const { t } = useTranslation(['core']);
if (balance === undefined || +balance > 0) return null;
return (
<Box
sx={{
width: '100%',
alignItems: 'center',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
width: '100%',
}}
>
<Spacer height="40px" />
<Box
sx={{
width: '320px',
justifyContent: 'center',
flexDirection: 'column',
alignItems: 'center',
padding: '15px',
outline: '1px solid gray',
borderRadius: '4px',
flexDirection: 'column',
justifyContent: 'center',
outline: '1px solid gray',
padding: '15px',
width: '320px',
}}
>
<Typography
sx={{
textAlign: 'center',
fontSize: '1.2rem',
fontWeight: 'bold',
textAlign: 'center',
}}
>
Are you a new user?
</Typography>{' '}
// TODO translate
<Spacer height="20px" />
<Typography>
Please message us on Telegram or Discord if you need 4 QORT to start
chatting without any limitations
{t('core:new_user', { postProcess: 'capitalize' })}
</Typography>
<Spacer height="20px" />
<Typography>
{t('core:message_us', { postProcess: 'capitalize' })}
</Typography>
<Spacer height="20px" />
<Box
sx={{
width: '100%',
@ -68,6 +74,7 @@ export const NewUsersCTA = ({ balance }) => {
>
Telegram
</ButtonBase>
<ButtonBase
sx={{
textDecoration: 'underline',

View File

@ -0,0 +1,92 @@
import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { supportedLanguages } from '../../../i18n';
import { Tooltip, useTheme } from '@mui/material';
const LanguageSelector = () => {
const { i18n, t } = useTranslation(['core']);
const [showSelect, setShowSelect] = useState(false);
const theme = useTheme();
const selectorRef = useRef(null);
const handleChange = (e) => {
const newLang = e.target.value;
i18n.changeLanguage(newLang);
setShowSelect(false);
};
const currentLang = i18n.language;
const { name, flag } =
supportedLanguages[currentLang] || supportedLanguages['en'];
// Detect clicks outside the component
useEffect(() => {
const handleClickOutside = (event) => {
if (selectorRef.current && !selectorRef.current.contains(event.target)) {
setShowSelect(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
return (
<div
ref={selectorRef}
style={{
bottom: '5%',
display: 'flex',
gap: '12px',
left: '1.5vh',
position: 'absolute',
}}
>
<Tooltip
title={t('core:action.change_language', {
postProcess: 'capitalize',
})}
>
{showSelect ? (
<select
style={{
fontSize: '1rem',
border: '2px',
background: theme.palette.background.default,
color: theme.palette.text.primary,
cursor: 'pointer',
position: 'relative',
bottom: '7px',
}}
value={currentLang}
onChange={handleChange}
autoFocus
>
{Object.entries(supportedLanguages).map(([code, { name }]) => (
<option key={code} value={code}>
{code.toUpperCase()} - {name}
</option>
))}
</select>
) : (
<button
onClick={() => setShowSelect(true)}
style={{
fontSize: '1.5rem',
border: 'none',
background: 'none',
cursor: 'pointer',
}}
aria-label={`Current language: ${name}`}
>
{showSelect ? undefined : flag}
</button>
)}
</Tooltip>
</div>
);
};
export default LanguageSelector;

View File

@ -265,7 +265,7 @@ export const Minting = ({
rej({ message: response.error });
})
.catch((error) => {
rej({ message: error.message || 'An error occurred' }); //TODO translate
rej({ message: error.message || 'An error occurred' });
});
});
} catch (error) {
@ -280,7 +280,7 @@ export const Minting = ({
}, []);
const createRewardShare = useCallback(async (publicKey, recipient) => {
const fee = await getFee('REWARD_SHARE');
const fee = await getFee('REWARD_SHARE'); // TODO translate
await show({
message: 'Would you like to perform an REWARD_SHARE transaction?',
publishFee: fee.fee + ' QORT',

View File

@ -62,6 +62,7 @@ export const QMailStatus = () => {
color: theme.palette.text.primary,
fontSize: '14px',
fontWeight: 700,
textTransform: 'uppercase',
}}
>
{t('core:q_mail', {

View File

@ -176,7 +176,7 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
.catch((error) => {
rej(
error.message ||
t('core:result.error.generic', { postProcess: 'capitalize' })
t('core:message.error.generic', { postProcess: 'capitalize' })
);
});
});
@ -185,7 +185,7 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
setSettingsQdnLastUpdated(Date.now());
setInfoSnack({
type: 'success',
message: t('core:result.success.publish_qdn', {
message: t('core:message.success.publish_qdn', {
postProcess: 'capitalize',
}),
});
@ -198,7 +198,7 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
type: 'error',
message:
error?.message ||
t('core:result.error.save_qdn', {
t('core:message.error.save_qdn', {
postProcess: 'capitalize',
}),
});
@ -591,7 +591,7 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
}
}}
>
{t('core:import', {
{t('core:action.import', {
postProcess: 'capitalize',
})}
</ButtonBase>
@ -616,7 +616,7 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
}
}}
>
{t('core:export', {
{t('core:action.export', {
postProcess: 'capitalize',
})}
</ButtonBase>

View File

@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
const ThemeSelector = () => {
const { t } = useTranslation(['core']);
const { themeMode, toggleTheme } = useThemeContext();
return (
@ -14,7 +15,7 @@ const ThemeSelector = () => {
bottom: '1%',
display: 'flex',
gap: '12px',
left: '1.5vh',
left: '1.2vh',
position: 'absolute',
}}
>

View File

@ -91,7 +91,7 @@ export const Tutorials = () => {
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={handleClose}>
{t('core:close', { postProcess: 'capitalize' })}
{t('core:action.close', { postProcess: 'capitalize' })}
</Button>
</DialogActions>
</Dialog>
@ -138,7 +138,7 @@ export const Tutorials = () => {
<DialogActions>
<Button variant="contained" onClick={handleClose}>
{t('core:close', { postProcess: 'capitalize' })}
{t('core:action.close', { postProcess: 'capitalize' })}
</Button>
</DialogActions>
</Dialog>

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect } from 'react';
import { useCallback, useEffect } from 'react';
import { useRecoilState, useSetRecoilState } from 'recoil';
import {
canSaveSettingToQdnAtom,
@ -46,6 +46,7 @@ const getPublishRecord = async (myName) => {
return { hasPublishRecord: false };
};
const getPublish = async (myName) => {
try {
let data;
@ -57,7 +58,6 @@ const getPublish = async (myName) => {
if (!data) throw new Error('Unable to fetch publish');
const decryptedKey: any = await decryptResource(data);
const dataint8Array = base64ToUint8Array(decryptedKey.data);
const decryptedKeyToObject = uint8ArrayToObject(dataint8Array);
return decryptedKeyToObject;
@ -112,6 +112,7 @@ export const useQortalGetSaveSettings = (myName, isAuthenticated) => {
},
[]
);
useEffect(() => {
if (
!myName ||

View File

@ -1,6 +1,12 @@
import React, { useCallback, useEffect } from 'react'
import { useCallback, useEffect } from 'react';
import { useSetRecoilState } from 'recoil';
import { isUsingImportExportSettingsAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from './atoms/global';
import {
isUsingImportExportSettingsAtom,
oldPinnedAppsAtom,
settingsLocalLastUpdatedAtom,
settingsQDNLastUpdatedAtom,
sortablePinnedAppsAtom,
} from './atoms/global';
function fetchFromLocalStorage(key) {
try {
@ -17,39 +23,47 @@ function fetchFromLocalStorage(key) {
export const useRetrieveDataLocalStorage = (address) => {
const setSortablePinnedApps = useSetRecoilState(sortablePinnedAppsAtom);
const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom);
const setIsUsingImportExportSettings = useSetRecoilState(isUsingImportExportSettingsAtom)
const setSettingsQDNLastUpdated = useSetRecoilState(settingsQDNLastUpdatedAtom);
const setOldPinnedApps = useSetRecoilState(oldPinnedAppsAtom)
const getSortablePinnedApps = useCallback(()=> {
const pinnedAppsLocal = fetchFromLocalStorage('ext_saved_settings')
if(pinnedAppsLocal?.sortablePinnedApps){
setSortablePinnedApps(pinnedAppsLocal?.sortablePinnedApps)
setSettingsLocalLastUpdated(pinnedAppsLocal?.timestamp || -1)
const setSettingsLocalLastUpdated = useSetRecoilState(
settingsLocalLastUpdatedAtom
);
const setIsUsingImportExportSettings = useSetRecoilState(
isUsingImportExportSettingsAtom
);
const setSettingsQDNLastUpdated = useSetRecoilState(
settingsQDNLastUpdatedAtom
);
const setOldPinnedApps = useSetRecoilState(oldPinnedAppsAtom);
const getSortablePinnedApps = useCallback(() => {
const pinnedAppsLocal = fetchFromLocalStorage('ext_saved_settings');
if (pinnedAppsLocal?.sortablePinnedApps) {
setSortablePinnedApps(pinnedAppsLocal?.sortablePinnedApps);
setSettingsLocalLastUpdated(pinnedAppsLocal?.timestamp || -1);
} else {
setSettingsLocalLastUpdated(-1)
setSettingsLocalLastUpdated(-1);
}
}, []);
}, [])
const getSortablePinnedAppsImportExport = useCallback(()=> {
const pinnedAppsLocal = fetchFromLocalStorage('ext_saved_settings_import_export')
if(pinnedAppsLocal?.sortablePinnedApps){
setOldPinnedApps(pinnedAppsLocal?.sortablePinnedApps)
setIsUsingImportExportSettings(true)
setSettingsQDNLastUpdated(pinnedAppsLocal?.timestamp || 0)
const getSortablePinnedAppsImportExport = useCallback(() => {
const pinnedAppsLocal = fetchFromLocalStorage(
'ext_saved_settings_import_export'
);
if (pinnedAppsLocal?.sortablePinnedApps) {
setOldPinnedApps(pinnedAppsLocal?.sortablePinnedApps);
setIsUsingImportExportSettings(true);
setSettingsQDNLastUpdated(pinnedAppsLocal?.timestamp || 0);
} else {
setIsUsingImportExportSettings(false)
setIsUsingImportExportSettings(false);
}
}, []);
}, [])
useEffect(()=> {
getSortablePinnedApps()
getSortablePinnedAppsImportExport()
}, [getSortablePinnedApps, address])
}
useEffect(() => {
getSortablePinnedApps();
getSortablePinnedAppsImportExport();
}, [getSortablePinnedApps, address]);
};