mirror of
https://github.com/Qortal/Qortal-Hub.git
synced 2025-07-23 04:36:52 +00:00
Merge branch 'develop' into chat-image
This commit is contained in:
42
src/App.tsx
42
src/App.tsx
@@ -1028,12 +1028,7 @@ function App() {
|
||||
|
||||
const logoutFunc = useCallback(async () => {
|
||||
try {
|
||||
if (hasSettingsChanged) {
|
||||
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') {
|
||||
if (extState === 'authenticated') {
|
||||
await showUnsavedChanges({
|
||||
message: 'Are you sure you would like to logout?',
|
||||
});
|
||||
@@ -2074,6 +2069,8 @@ function App() {
|
||||
lineHeight: 1.2,
|
||||
maxWidth: '90%',
|
||||
textAlign: 'center',
|
||||
fontSize: '16px',
|
||||
marginBottom: '10px',
|
||||
}}
|
||||
>
|
||||
{messageQortalRequest?.text1}
|
||||
@@ -3012,13 +3009,16 @@ function App() {
|
||||
})}
|
||||
</TextP>
|
||||
<Spacer height="100px" />
|
||||
<CustomButton
|
||||
<ButtonBase
|
||||
autoFocus
|
||||
onClick={() => {
|
||||
returnToMain();
|
||||
}}
|
||||
>
|
||||
{t('core:action.continue', { postProcess: 'capitalize' })}
|
||||
</CustomButton>
|
||||
<CustomButton>
|
||||
{t('core:action.continue', { postProcess: 'capitalize' })}
|
||||
</CustomButton>
|
||||
</ButtonBase>
|
||||
</Box>
|
||||
)}
|
||||
{extState === 'transfer-success-request' && (
|
||||
@@ -3268,6 +3268,8 @@ function App() {
|
||||
lineHeight: 1.2,
|
||||
maxWidth: '90%',
|
||||
textAlign: 'center',
|
||||
fontSize: '16px',
|
||||
marginBottom: '10px',
|
||||
}}
|
||||
>
|
||||
{messageQortalRequestExtension?.text1}
|
||||
@@ -3314,8 +3316,8 @@ function App() {
|
||||
>
|
||||
{messageQortalRequestExtension?.text3}
|
||||
</TextP>
|
||||
<Spacer height="15px" />
|
||||
</Box>
|
||||
<Spacer height="15px" />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -3340,11 +3342,15 @@ function App() {
|
||||
)}
|
||||
|
||||
{messageQortalRequestExtension?.html && (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: messageQortalRequestExtension?.html,
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<Spacer height="15px" />
|
||||
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: messageQortalRequestExtension?.html,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Spacer height="15px" />
|
||||
|
||||
@@ -3564,7 +3570,11 @@ function App() {
|
||||
</Dialog>
|
||||
)}
|
||||
{isSettingsOpen && (
|
||||
<Settings open={isSettingsOpen} setOpen={setIsSettingsOpen} />
|
||||
<Settings
|
||||
open={isSettingsOpen}
|
||||
setOpen={setIsSettingsOpen}
|
||||
rawWallet={rawWallet}
|
||||
/>
|
||||
)}
|
||||
<CustomizedSnackbars
|
||||
open={openSnack}
|
||||
|
@@ -33,7 +33,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import LanguageSelector from '../components/Language/LanguageSelector';
|
||||
import { MyContext } from '../App';
|
||||
|
||||
const manifestData = {
|
||||
export const manifestData = {
|
||||
version: '0.5.4',
|
||||
};
|
||||
|
||||
|
@@ -1,10 +1,9 @@
|
||||
import { useTheme } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
export const CopyIcon = ({ color, height = 11, width = 10 }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const setColor = color ? color : theme.palette.text.primary;
|
||||
|
||||
return (
|
||||
<svg
|
||||
width={width}
|
||||
@@ -14,11 +13,11 @@ export const CopyIcon = ({ color, height = 11, width = 10 }) => {
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3.92857 0.5H8.57143C9.36071 0.5 10 1.13929 10 1.92857V6.57143C10 7.36071 9.36071 8 8.57143 8H8.21429V4.42857C8.21429 3.24643 7.25357 2.28571 6.07143 2.28571H2.5V1.92857C2.5 1.13929 3.13929 0.5 3.92857 0.5ZM1.42857 3H6.07143C6.86041 3 7.5 3.63959 7.5 4.42857V9.07143C7.5 9.86041 6.86041 10.5 6.07143 10.5H1.42857C0.639593 10.5 0 9.86041 0 9.07143V4.42857C0 3.63959 0.639593 3 1.42857 3Z"
|
||||
fill={setColor}
|
||||
fill-opacity="0.5"
|
||||
fillOpacity="0.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
@@ -937,6 +937,29 @@ export async function getBalanceInfo() {
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getAssetBalanceInfo(assetId: number) {
|
||||
const wallet = await getSaveWallet();
|
||||
const address = wallet.address0;
|
||||
const validApi = await getBaseApi();
|
||||
const response = await fetch(
|
||||
validApi +
|
||||
`/assets/balances?address=${address}&assetid=${assetId}&ordering=ASSET_BALANCE_ACCOUNT&limit=1`
|
||||
);
|
||||
|
||||
if (!response?.ok) throw new Error('Cannot fetch asset balance');
|
||||
const data = await response.json();
|
||||
return +data?.[0]?.balance;
|
||||
}
|
||||
|
||||
export async function getAssetInfo(assetId: number) {
|
||||
const validApi = await getBaseApi();
|
||||
const response = await fetch(validApi + `/assets/info?assetId=${assetId}`);
|
||||
|
||||
if (!response?.ok) throw new Error('Cannot fetch asset info');
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getLTCBalance() {
|
||||
const wallet = await getSaveWallet();
|
||||
let _url = `${buyTradeNodeBaseUrl}/crosschain/ltc/walletbalance`;
|
||||
@@ -2288,6 +2311,34 @@ export async function kickFromGroup({
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function transferAsset({ amount, recipient, assetId }) {
|
||||
const lastReference = await getLastRef();
|
||||
const resKeyPair = await getKeyPair();
|
||||
const parsedData = resKeyPair;
|
||||
const uint8PrivateKey = Base58.decode(parsedData.privateKey);
|
||||
const uint8PublicKey = Base58.decode(parsedData.publicKey);
|
||||
const keyPair = {
|
||||
privateKey: uint8PrivateKey,
|
||||
publicKey: uint8PublicKey,
|
||||
};
|
||||
const feeres = await getFee('TRANSFER_ASSET');
|
||||
|
||||
const tx = await createTransaction(12, keyPair, {
|
||||
fee: feeres.fee,
|
||||
recipient: recipient,
|
||||
amount: amount,
|
||||
assetId: assetId,
|
||||
lastReference: lastReference,
|
||||
});
|
||||
|
||||
const signedBytes = Base58.encode(tx.signedBytes);
|
||||
|
||||
const res = await processTransactionVersion2(signedBytes);
|
||||
if (!res?.signature)
|
||||
throw new Error(res?.message || 'Transaction was not able to be processed');
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function createGroup({
|
||||
groupName,
|
||||
groupDescription,
|
||||
@@ -3638,7 +3689,7 @@ export const checkThreads = async (bringBack) => {
|
||||
dataToBringBack.push(thread);
|
||||
}
|
||||
} catch (error) {
|
||||
conosle.log({ error });
|
||||
console.log({ error });
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import './customloader.css';
|
||||
import { Box, useTheme } from '@mui/material';
|
||||
|
||||
export const CustomLoader = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
|
@@ -1,17 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
export const CustomSvg = ({ src, color = 'black', size = 24 }) => {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{ fill: color }}
|
||||
>
|
||||
{src}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{ fill: color }}
|
||||
>
|
||||
{src}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
@@ -36,6 +36,7 @@
|
||||
left: 56px;
|
||||
animation: lds-ellipsis3 0.6s infinite;
|
||||
}
|
||||
|
||||
@keyframes lds-ellipsis1 {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { resourceDownloadControllerAtom } from '../atoms/global';
|
||||
import { getBaseApiReact } from '../App';
|
||||
import { useSetAtom } from 'jotai';
|
||||
|
@@ -56,7 +56,7 @@ export const AppsCategoryDesktop = ({
|
||||
isShow,
|
||||
}) => {
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const virtuosoRef = useRef();
|
||||
const virtuosoRef = useRef(null);
|
||||
const theme = useTheme();
|
||||
|
||||
const categoryList = useMemo(() => {
|
||||
|
@@ -414,8 +414,23 @@ export const AppsDesktop = ({
|
||||
setDesktopViewMode('dev');
|
||||
}}
|
||||
>
|
||||
<IconWrapper label="Dev" disableWidth>
|
||||
<AppsIcon height={30} />
|
||||
<IconWrapper
|
||||
color={
|
||||
desktopViewMode === 'dev'
|
||||
? theme.palette.text.primary
|
||||
: theme.palette.text.secondary
|
||||
}
|
||||
label="Dev"
|
||||
disableWidth
|
||||
>
|
||||
<AppsIcon
|
||||
color={
|
||||
desktopViewMode === 'dev'
|
||||
? theme.palette.text.primary
|
||||
: theme.palette.text.secondary
|
||||
}
|
||||
height={30}
|
||||
/>
|
||||
</IconWrapper>
|
||||
</ButtonBase>
|
||||
)}
|
||||
|
@@ -180,7 +180,7 @@ export const AppsDevModeNavBar = () => {
|
||||
>
|
||||
<RefreshIcon
|
||||
sx={{
|
||||
color: 'rgba(250, 250, 250, 0.5)',
|
||||
color: theme.palette.text.primary,
|
||||
width: '40px',
|
||||
height: 'auto',
|
||||
}}
|
||||
|
@@ -102,7 +102,7 @@ export const AppsLibraryDesktop = ({
|
||||
getQapps,
|
||||
}) => {
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const virtuosoRef = useRef();
|
||||
const virtuosoRef = useRef(null);
|
||||
const theme = useTheme();
|
||||
|
||||
const officialApps = useMemo(() => {
|
||||
|
@@ -253,6 +253,9 @@ export const listOfAllQortalRequests = [
|
||||
'SELL_NAME',
|
||||
'CANCEL_SELL_NAME',
|
||||
'BUY_NAME',
|
||||
'SIGN_FOREIGN_FEES',
|
||||
'MULTI_ASSET_PAYMENT_WITH_PRIVATE_DATA',
|
||||
'TRANSFER_ASSET',
|
||||
];
|
||||
|
||||
export const UIQortalRequests = [
|
||||
@@ -313,6 +316,9 @@ export const UIQortalRequests = [
|
||||
'SELL_NAME',
|
||||
'CANCEL_SELL_NAME',
|
||||
'BUY_NAME',
|
||||
'SIGN_FOREIGN_FEES',
|
||||
'MULTI_ASSET_PAYMENT_WITH_PRIVATE_DATA',
|
||||
'TRANSFER_ASSET',
|
||||
];
|
||||
|
||||
async function retrieveFileFromIndexedDB(fileId) {
|
||||
|
@@ -63,6 +63,7 @@ export const DownloadWallet = ({
|
||||
wallet,
|
||||
qortAddress: rawWallet.address0,
|
||||
});
|
||||
|
||||
return {
|
||||
wallet,
|
||||
qortAddress: rawWallet.address0,
|
||||
|
@@ -18,7 +18,7 @@ export const AnnouncementList = ({
|
||||
loadMore,
|
||||
myName,
|
||||
}) => {
|
||||
const listRef = useRef();
|
||||
const listRef = useRef(null);
|
||||
const [messages, setMessages] = useState(initialMessages);
|
||||
|
||||
useEffect(() => {
|
||||
|
@@ -23,7 +23,7 @@ export const ChatList = ({
|
||||
hasSecretKey,
|
||||
isPrivate,
|
||||
}) => {
|
||||
const parentRef = useRef();
|
||||
const parentRef = useRef(null);
|
||||
const [messages, setMessages] = useState(initialMessages);
|
||||
const [showScrollButton, setShowScrollButton] = useState(false);
|
||||
const [showScrollDownButton, setShowScrollDownButton] = useState(false);
|
||||
|
@@ -60,8 +60,8 @@ export const ChatOptions = ({
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [selectedMember, setSelectedMember] = useState(0);
|
||||
const theme = useTheme();
|
||||
const parentRef = useRef();
|
||||
const parentRefMentions = useRef();
|
||||
const parentRef = useRef(null);
|
||||
const parentRefMentions = useRef(null);
|
||||
const [lastMentionTimestamp, setLastMentionTimestamp] = useState(null);
|
||||
const [debouncedValue, setDebouncedValue] = useState(''); // Debounced value
|
||||
const messages = useMemo(() => {
|
||||
|
@@ -31,6 +31,7 @@ import {
|
||||
import { RequestQueueWithPromise } from '../../utils/queue/queue';
|
||||
import { CustomizedSnackbars } from '../Snackbar/Snackbar';
|
||||
import { addDataPublishesFunc, getDataPublishesFunc } from '../Group/Group';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const uid = new ShortUniqueId({ length: 8 });
|
||||
|
||||
@@ -101,6 +102,7 @@ export const decryptPublishes = async (encryptedMessages: any[], secretKey) => {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const handleUnencryptedPublishes = (publishes) => {
|
||||
let publishesData = [];
|
||||
publishes.forEach((pub) => {
|
||||
@@ -149,6 +151,7 @@ export const GroupAnnouncements = ({
|
||||
editorRef.current = editorInstance;
|
||||
};
|
||||
const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
|
||||
const { t } = useTranslation(['core', 'group']);
|
||||
|
||||
const triggerRerender = () => {
|
||||
forceUpdate(); // Trigger re-render by updating the state
|
||||
@@ -209,7 +212,6 @@ export const GroupAnnouncements = ({
|
||||
)
|
||||
return;
|
||||
setIsLoading(true);
|
||||
// initWebsocketMessageGroup()
|
||||
hasInitializedWebsocket.current = true;
|
||||
}, [secretKey, isPrivate]);
|
||||
|
||||
@@ -287,7 +289,10 @@ export const GroupAnnouncements = ({
|
||||
const fee = await getFee('ARBITRARY');
|
||||
|
||||
await show({
|
||||
message: 'Would you like to perform a ARBITRARY transaction?',
|
||||
message: t('group:question.perform_transaction', {
|
||||
action: 'ARBITRARY',
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
publishFee: fee.fee + ' QORT',
|
||||
});
|
||||
|
||||
@@ -329,7 +334,7 @@ export const GroupAnnouncements = ({
|
||||
setTempData(selectedGroup);
|
||||
clearEditorContent();
|
||||
}
|
||||
// send chat message
|
||||
// TODO send chat message
|
||||
} catch (error) {
|
||||
if (!error) return;
|
||||
setInfoSnack({
|
||||
|
@@ -6,6 +6,7 @@ import { getBaseApiReact } from '../App';
|
||||
import '../styles/CoreSyncStatus.css';
|
||||
import { useTheme } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { manifestData } from '../ExtStates/NotAuthenticated';
|
||||
|
||||
export const CoreSyncStatus = () => {
|
||||
const [nodeInfos, setNodeInfos] = useState({});
|
||||
@@ -75,7 +76,9 @@ export const CoreSyncStatus = () => {
|
||||
: '';
|
||||
|
||||
let imagePath = syncingImg;
|
||||
let message = t('core:status.synchronizing', { postProcess: 'capitalize' });
|
||||
let message = t('core:message.status.synchronizing', {
|
||||
postProcess: 'capitalize',
|
||||
});
|
||||
|
||||
if (isMintingPossible && !isUsingGateway) {
|
||||
imagePath = syncedMintingImg;
|
||||
@@ -111,7 +114,7 @@ export const CoreSyncStatus = () => {
|
||||
</span>
|
||||
|
||||
<div
|
||||
className="bottom"
|
||||
className="core-panel"
|
||||
style={{
|
||||
right: 'unset',
|
||||
left: '55px',
|
||||
@@ -119,27 +122,37 @@ export const CoreSyncStatus = () => {
|
||||
}}
|
||||
>
|
||||
<h3>{t('core:core.information', { postProcess: 'capitalize' })}</h3>
|
||||
|
||||
<h4 className="lineHeight">
|
||||
{t('core:core.version', { postProcess: 'capitalize' })}:{' '}
|
||||
<span style={{ color: '#03a9f4' }}>{buildVersion}</span>
|
||||
</h4>
|
||||
|
||||
<h4 className="lineHeight">{message}</h4>
|
||||
|
||||
<h4 className="lineHeight">
|
||||
{t('core:core.block_height', { postProcess: 'capitalize' })}:{' '}
|
||||
<span style={{ color: '#03a9f4' }}>{height || ''}</span>
|
||||
</h4>
|
||||
|
||||
<h4 className="lineHeight">
|
||||
{t('core:core.peers', { postProcess: 'capitalize' })}:{' '}
|
||||
<span style={{ color: '#03a9f4' }}>
|
||||
{numberOfConnections || ''}
|
||||
</span>
|
||||
</h4>
|
||||
|
||||
<h4 className="lineHeight">
|
||||
{t('auth:node.using_public', { postProcess: 'capitalize' })}:{' '}
|
||||
<span style={{ color: '#03a9f4' }}>
|
||||
{isUsingGateway?.toString()}
|
||||
</span>
|
||||
</h4>
|
||||
|
||||
<h4 className="lineHeight">
|
||||
{t('core:ui.version', { postProcess: 'capitalize' })}:{' '}
|
||||
<span style={{ color: '#03a9f4' }}>{manifestData.version}</span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@@ -3,10 +3,8 @@ import Box from '@mui/material/Box';
|
||||
import { HubsIcon } from '../../assets/Icons/HubsIcon';
|
||||
import { MessagingIcon } from '../../assets/Icons/MessagingIcon';
|
||||
import AppIcon from '../../assets/svgs/AppIcon.svg';
|
||||
|
||||
import { HomeIcon } from '../../assets/Icons/HomeIcon';
|
||||
import { Save } from '../Save/Save';
|
||||
|
||||
import { enabledDevModeAtom } from '../../atoms/global';
|
||||
import { useAtom } from 'jotai';
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { ButtonBase, Typography, useTheme } from '@mui/material';
|
||||
import Box from '@mui/material/Box';
|
||||
import { NotificationIcon2 } from '../../assets/Icons/NotificationIcon2';
|
||||
@@ -81,18 +81,18 @@ export const DesktopHeader = ({
|
||||
setGroupSection,
|
||||
isPrivate,
|
||||
}) => {
|
||||
const [value, setValue] = React.useState(0);
|
||||
const [value, setValue] = useState(0);
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
height: '70px', // Footer height
|
||||
zIndex: 1,
|
||||
justifyContent: 'space-between',
|
||||
padding: '10px',
|
||||
width: '100%',
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
@@ -126,11 +126,12 @@ export const DesktopHeader = ({
|
||||
: selectedGroup?.groupName}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
gap: '20px',
|
||||
alignItems: 'center',
|
||||
visibility: selectedGroup?.groupId === '0' ? 'hidden' : 'visibile',
|
||||
}}
|
||||
>
|
||||
@@ -219,6 +220,7 @@ export const DesktopHeader = ({
|
||||
/>
|
||||
</IconWrapper>
|
||||
</ButtonBase>
|
||||
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setOpenManageMembers(true);
|
||||
@@ -226,17 +228,18 @@ export const DesktopHeader = ({
|
||||
>
|
||||
<IconWrapper
|
||||
color={theme.palette.text.secondary}
|
||||
customHeight="55px"
|
||||
label="Members"
|
||||
selected={false}
|
||||
customHeight="55px"
|
||||
>
|
||||
<MembersIcon
|
||||
color={theme.palette.text.secondary}
|
||||
height={25}
|
||||
width={20}
|
||||
color={theme.palette.text.secondary}
|
||||
/>
|
||||
</IconWrapper>
|
||||
</ButtonBase>
|
||||
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setGroupSection('adminSpace');
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import Box from '@mui/material/Box';
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
|
||||
export const DrawerComponent = ({ open, setOpen, children }) => {
|
||||
const toggleDrawer = (newOpen: boolean) => () => {
|
||||
setOpen(newOpen);
|
||||
|
@@ -40,6 +40,7 @@ export const Explore = ({ setDesktopViewMode }) => {
|
||||
}}
|
||||
src={qTradeLogo}
|
||||
/>
|
||||
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '1rem',
|
||||
@@ -66,6 +67,7 @@ export const Explore = ({ setDesktopViewMode }) => {
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '1rem',
|
||||
@@ -94,6 +96,7 @@ export const Explore = ({ setDesktopViewMode }) => {
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '1rem',
|
||||
@@ -102,6 +105,7 @@ export const Explore = ({ setDesktopViewMode }) => {
|
||||
{t('tutorial:initial.general_chat', { postProcess: 'capitalize' })}
|
||||
</Typography>
|
||||
</ButtonBase>
|
||||
|
||||
<ButtonBase
|
||||
sx={{
|
||||
'&:hover': { backgroundColor: theme.palette.background.paper },
|
||||
@@ -119,6 +123,7 @@ export const Explore = ({ setDesktopViewMode }) => {
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '1rem',
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { JoinGroup } from './JoinGroup';
|
||||
|
||||
export const GlobalActions = () => {
|
||||
|
@@ -1,8 +1,7 @@
|
||||
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ButtonBase,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
@@ -11,13 +10,14 @@ import {
|
||||
Typography,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
import { CustomButton, CustomButtonAccept } from '../../styles/App-styles';
|
||||
import { CustomButtonAccept } from '../../styles/App-styles';
|
||||
import { getBaseApiReact, MyContext } from '../../App';
|
||||
import { getFee } from '../../background';
|
||||
import { CustomizedSnackbars } from '../Snackbar/Snackbar';
|
||||
import { FidgetSpinner } from 'react-loader-spinner';
|
||||
import { useAtom, useSetAtom } from 'jotai';
|
||||
import { memberGroupsAtom, txListAtom } from '../../atoms/global';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const JoinGroup = () => {
|
||||
const { show } = useContext(MyContext);
|
||||
@@ -29,7 +29,9 @@ export const JoinGroup = () => {
|
||||
const [isLoadingInfo, setIsLoadingInfo] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation(['core', 'group']);
|
||||
const [isLoadingJoinGroup, setIsLoadingJoinGroup] = useState(false);
|
||||
|
||||
const handleJoinGroup = async (e) => {
|
||||
setGroupInfo(null);
|
||||
const groupId = e?.detail?.groupId;
|
||||
@@ -41,6 +43,7 @@ export const JoinGroup = () => {
|
||||
const groupData = await response.json();
|
||||
setGroupInfo(groupData);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsLoadingInfo(false);
|
||||
}
|
||||
@@ -60,15 +63,22 @@ export const JoinGroup = () => {
|
||||
(item) => +item?.groupId === +groupInfo?.groupId
|
||||
);
|
||||
}, [memberGroups, groupInfo]);
|
||||
|
||||
const joinGroup = async (group, isOpen) => {
|
||||
try {
|
||||
const groupId = group.groupId;
|
||||
const fee = await getFee('JOIN_GROUP');
|
||||
|
||||
await show({
|
||||
message: 'Would you like to perform an JOIN_GROUP transaction?',
|
||||
message: t('group:question.perform_transaction', {
|
||||
action: 'JOIN_GROUP',
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
publishFee: fee.fee + ' QORT',
|
||||
});
|
||||
|
||||
setIsLoadingJoinGroup(true);
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
window
|
||||
.sendMessage('joinGroup', {
|
||||
@@ -78,8 +88,9 @@ export const JoinGroup = () => {
|
||||
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) {
|
||||
@@ -87,8 +98,14 @@ export const JoinGroup = () => {
|
||||
{
|
||||
...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,
|
||||
},
|
||||
@@ -99,15 +116,20 @@ export const JoinGroup = () => {
|
||||
{
|
||||
...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);
|
||||
res(response);
|
||||
return;
|
||||
@@ -123,7 +145,9 @@ export const JoinGroup = () => {
|
||||
.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);
|
||||
@@ -131,10 +155,12 @@ export const JoinGroup = () => {
|
||||
});
|
||||
setIsLoadingJoinGroup(false);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsLoadingJoinGroup(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
@@ -146,32 +172,31 @@ export const JoinGroup = () => {
|
||||
{!groupInfo && (
|
||||
<Box
|
||||
sx={{
|
||||
width: '325px',
|
||||
height: '150px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
height: '150px',
|
||||
justifyContent: 'center',
|
||||
width: '325px',
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
<CircularProgress
|
||||
size={25}
|
||||
sx={{
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
/>{' '}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
width: '325px',
|
||||
height: 'auto',
|
||||
maxHeight: '400px',
|
||||
alignItems: 'center',
|
||||
display: !groupInfo ? 'none' : 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
height: 'auto',
|
||||
maxHeight: '400px',
|
||||
padding: '10px',
|
||||
width: '325px',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
@@ -180,16 +205,20 @@ export const JoinGroup = () => {
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Group name: {` ${groupInfo?.groupName}`}
|
||||
{t('group:group.name', { postProcess: 'capitalize' })}:{' '}
|
||||
{` ${groupInfo?.groupName}`}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '15px',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Number of members: {` ${groupInfo?.memberCount}`}
|
||||
{t('group:group.member_number', { postProcess: 'capitalize' })}:{' '}
|
||||
{` ${groupInfo?.memberCount}`}
|
||||
</Typography>
|
||||
|
||||
{groupInfo?.description && (
|
||||
<Typography
|
||||
sx={{
|
||||
@@ -207,7 +236,9 @@ export const JoinGroup = () => {
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
*You are already in this group!
|
||||
{t('group:message.generic.already_in_group', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</Typography>
|
||||
)}
|
||||
{!isInGroup && groupInfo?.isOpen === false && (
|
||||
@@ -217,12 +248,14 @@ export const JoinGroup = () => {
|
||||
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',
|
||||
})}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
@@ -242,7 +275,9 @@ export const JoinGroup = () => {
|
||||
opacity: isInGroup ? 0.1 : 1,
|
||||
}}
|
||||
>
|
||||
Join
|
||||
{t('core:action.join', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</CustomButtonAccept>
|
||||
</ButtonBase>
|
||||
|
||||
@@ -255,7 +290,9 @@ export const JoinGroup = () => {
|
||||
}}
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
Close
|
||||
{t('core:action.close', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</CustomButtonAccept>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
@@ -269,14 +306,14 @@ export const JoinGroup = () => {
|
||||
{isLoadingJoinGroup && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
alignItems: 'center',
|
||||
bottom: 0,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
}}
|
||||
>
|
||||
<FidgetSpinner
|
||||
|
@@ -118,7 +118,8 @@ export const AddGroup = ({ address, open, setOpen }) => {
|
||||
const fee = await getFee('CREATE_GROUP');
|
||||
|
||||
await show({
|
||||
message: t('group:question.create_group', {
|
||||
message: t('group:question.perform_transaction', {
|
||||
action: 'CREATE_GROUP',
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
publishFee: fee.fee + ' QORT',
|
||||
@@ -159,6 +160,9 @@ export const AddGroup = ({ address, open, setOpen }) => {
|
||||
},
|
||||
...prev,
|
||||
]);
|
||||
setName('');
|
||||
setDescription('');
|
||||
setGroupType('1');
|
||||
res(response);
|
||||
return;
|
||||
}
|
||||
@@ -327,6 +331,7 @@ export const AddGroup = ({ address, open, setOpen }) => {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</Label>
|
||||
|
||||
<Input
|
||||
placeholder={t('group:group.name', {
|
||||
postProcess: 'capitalize',
|
||||
@@ -335,6 +340,7 @@ export const AddGroup = ({ address, open, setOpen }) => {
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -430,12 +436,12 @@ export const AddGroup = ({ address, open, setOpen }) => {
|
||||
onChange={handleChangeApprovalThreshold}
|
||||
>
|
||||
<MenuItem value={0}>
|
||||
{t('core.count.none', {
|
||||
{t('core:count.none', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</MenuItem>
|
||||
<MenuItem value={1}>
|
||||
{t('core.count.one', {
|
||||
{t('core:count.one', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</MenuItem>
|
||||
@@ -446,6 +452,7 @@ export const AddGroup = ({ address, open, setOpen }) => {
|
||||
<MenuItem value={100}>100%</MenuItem>
|
||||
</Select>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -454,10 +461,11 @@ export const AddGroup = ({ address, open, setOpen }) => {
|
||||
}}
|
||||
>
|
||||
<Label>
|
||||
{t('group.block_delay.minimum', {
|
||||
{t('group:block_delay.minimum', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</Label>
|
||||
|
||||
<Select
|
||||
labelId="demo-simple-select-label"
|
||||
id="demo-simple-select"
|
||||
@@ -466,43 +474,44 @@ export const AddGroup = ({ address, open, setOpen }) => {
|
||||
onChange={handleChangeMinBlock}
|
||||
>
|
||||
<MenuItem value={5}>
|
||||
{t('core.time.minute', { count: 5 })}
|
||||
{t('core:time.minute', { count: 5 })}
|
||||
</MenuItem>
|
||||
<MenuItem value={10}>
|
||||
{t('core.time.minute', { count: 10 })}
|
||||
{t('core:time.minute', { count: 10 })}
|
||||
</MenuItem>
|
||||
<MenuItem value={30}>
|
||||
{t('core.time.minute', { count: 30 })}
|
||||
{t('core:time.minute', { count: 30 })}
|
||||
</MenuItem>
|
||||
<MenuItem value={60}>
|
||||
{t('core.time.hour', { count: 1 })}
|
||||
{t('core:time.hour', { count: 1 })}
|
||||
</MenuItem>
|
||||
<MenuItem value={180}>
|
||||
{t('core.time.hour', { count: 3 })}
|
||||
{t('core:time.hour', { count: 3 })}
|
||||
</MenuItem>
|
||||
<MenuItem value={300}>
|
||||
{t('core.time.hour', { count: 5 })}
|
||||
{t('core:time.hour', { count: 5 })}
|
||||
</MenuItem>
|
||||
<MenuItem value={420}>
|
||||
{t('core.time.hour', { count: 7 })}
|
||||
{t('core:time.hour', { count: 7 })}
|
||||
</MenuItem>
|
||||
<MenuItem value={720}>
|
||||
{t('core.time.hour', { count: 12 })}
|
||||
{t('core:time.hour', { count: 12 })}
|
||||
</MenuItem>
|
||||
<MenuItem value={1440}>
|
||||
{t('core.time.day', { count: 1 })}
|
||||
{t('core:time.day', { count: 1 })}
|
||||
</MenuItem>
|
||||
<MenuItem value={4320}>
|
||||
{t('core.time.day', { count: 3 })}
|
||||
{t('core:time.day', { count: 3 })}
|
||||
</MenuItem>
|
||||
<MenuItem value={7200}>
|
||||
{t('core.time.day', { count: 5 })}
|
||||
{t('core:time.day', { count: 5 })}
|
||||
</MenuItem>
|
||||
<MenuItem value={10080}>
|
||||
{t('core.time.day', { count: 7 })}
|
||||
{t('core:time.day', { count: 7 })}
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -511,10 +520,11 @@ export const AddGroup = ({ address, open, setOpen }) => {
|
||||
}}
|
||||
>
|
||||
<Label>
|
||||
{t('group.block_delay.maximum', {
|
||||
{t('group:block_delay.maximum', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</Label>
|
||||
|
||||
<Select
|
||||
labelId="demo-simple-select-label"
|
||||
id="demo-simple-select"
|
||||
@@ -523,41 +533,42 @@ export const AddGroup = ({ address, open, setOpen }) => {
|
||||
onChange={handleChangeMaxBlock}
|
||||
>
|
||||
<MenuItem value={60}>
|
||||
{t('core.time.hour', { count: 1 })}
|
||||
{t('core:time.hour', { count: 1 })}
|
||||
</MenuItem>
|
||||
<MenuItem value={180}>
|
||||
3{t('core.time.hour', { count: 3 })}
|
||||
3{t('core:time.hour', { count: 3 })}
|
||||
</MenuItem>
|
||||
<MenuItem value={300}>
|
||||
{t('core.time.hour', { count: 5 })}
|
||||
{t('core:time.hour', { count: 5 })}
|
||||
</MenuItem>
|
||||
<MenuItem value={420}>
|
||||
{t('core.time.hour', { count: 7 })}
|
||||
{t('core:time.hour', { count: 7 })}
|
||||
</MenuItem>
|
||||
<MenuItem value={720}>
|
||||
{t('core.time.hour', { count: 12 })}
|
||||
{t('core:time.hour', { count: 12 })}
|
||||
</MenuItem>
|
||||
<MenuItem value={1440}>
|
||||
{t('core.time.day', { count: 1 })}
|
||||
{t('core:time.day', { count: 1 })}
|
||||
</MenuItem>
|
||||
<MenuItem value={4320}>
|
||||
{t('core.time.day', { count: 3 })}
|
||||
{t('core:time.day', { count: 3 })}
|
||||
</MenuItem>
|
||||
<MenuItem value={7200}>
|
||||
{t('core.time.day', { count: 5 })}
|
||||
{t('core:time.day', { count: 5 })}
|
||||
</MenuItem>
|
||||
<MenuItem value={10080}>
|
||||
{t('core.time.day', { count: 7 })}
|
||||
{t('core:time.day', { count: 7 })}
|
||||
</MenuItem>
|
||||
<MenuItem value={14400}>
|
||||
{t('core.time.day', { count: 10 })}
|
||||
{t('core:time.day', { count: 10 })}
|
||||
</MenuItem>
|
||||
<MenuItem value={21600}>
|
||||
{t('core.time.day', { count: 15 })}
|
||||
{t('core:time.day', { count: 15 })}
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</Box>
|
||||
</Collapse>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -570,7 +581,7 @@ export const AddGroup = ({ address, open, setOpen }) => {
|
||||
color="primary"
|
||||
onClick={handleCreateGroup}
|
||||
>
|
||||
{t('group.action.create', {
|
||||
{t('group:action.create_group', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</Button>
|
||||
|
@@ -48,7 +48,7 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => {
|
||||
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
|
||||
const listRef = useRef();
|
||||
const listRef = useRef(null);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [filteredItems, setFilteredItems] = useState(groups);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -113,7 +113,8 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => {
|
||||
const fee = await getFee('JOIN_GROUP');
|
||||
|
||||
await show({
|
||||
message: t('group:question.join_group', {
|
||||
message: t('group:question.perform_transaction', {
|
||||
action: 'JOIN_GROUP',
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
publishFee: fee.fee + ' QORT',
|
||||
@@ -157,8 +158,14 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => {
|
||||
{
|
||||
...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,
|
||||
},
|
||||
|
@@ -1,26 +1,17 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Avatar, Box, Popover, Typography, useTheme } from '@mui/material';
|
||||
// import { MAIL_SERVICE_TYPE, THREAD_SERVICE_TYPE } from "../../constants/mail";
|
||||
import { Thread } from './Thread';
|
||||
import {
|
||||
AllThreadP,
|
||||
ArrowDownIcon,
|
||||
ComposeContainer,
|
||||
ComposeContainerBlank,
|
||||
ComposeIcon,
|
||||
ComposeP,
|
||||
GroupContainer,
|
||||
InstanceFooter,
|
||||
InstanceListContainer,
|
||||
InstanceListContainerRow,
|
||||
InstanceListContainerRowCheck,
|
||||
InstanceListContainerRowCheckIcon,
|
||||
InstanceListContainerRowMain,
|
||||
InstanceListContainerRowMainP,
|
||||
InstanceListHeader,
|
||||
@@ -48,7 +39,6 @@ import {
|
||||
getTempPublish,
|
||||
handleUnencryptedPublishes,
|
||||
} from '../../Chat/GroupAnnouncements';
|
||||
import CheckSVG from '../../../assets/svgs/Check.svg';
|
||||
import ArrowDownSVG from '../../../assets/svgs/ArrowDown.svg';
|
||||
import { LoadingSnackbar } from '../../Snackbar/LoadingSnackbar';
|
||||
import { executeEvent } from '../../../utils/events';
|
||||
@@ -73,9 +63,9 @@ export const GroupMail = ({
|
||||
hide,
|
||||
isPrivate,
|
||||
}) => {
|
||||
const [viewedThreads, setViewedThreads] = React.useState<any>({});
|
||||
const [viewedThreads, setViewedThreads] = useState<any>({});
|
||||
const [filterMode, setFilterMode] = useState<string>('Recently active');
|
||||
const [currentThread, setCurrentThread] = React.useState(null);
|
||||
const [currentThread, setCurrentThread] = useState(null);
|
||||
const [recentThreads, setRecentThreads] = useState<any[]>([]);
|
||||
const [allThreads, setAllThreads] = useState<any[]>([]);
|
||||
const [members, setMembers] = useState<any>(null);
|
||||
@@ -178,7 +168,10 @@ export const GroupMail = ({
|
||||
rej(response.error);
|
||||
})
|
||||
.catch((error) => {
|
||||
rej(error.message || 'An error occurred');
|
||||
rej(
|
||||
error.message ||
|
||||
t('core:message.error.generic', { postProcess: 'capitalize' })
|
||||
);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -186,7 +179,7 @@ export const GroupMail = ({
|
||||
}
|
||||
};
|
||||
|
||||
const getAllThreads = React.useCallback(
|
||||
const getAllThreads = useCallback(
|
||||
async (groupId: string, mode: string, isInitial?: boolean) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
@@ -206,7 +199,7 @@ export const GroupMail = ({
|
||||
});
|
||||
const responseData = await response.json();
|
||||
|
||||
let fullArrayMsg = isInitial ? [] : [...allThreads];
|
||||
const fullArrayMsg = isInitial ? [] : [...allThreads];
|
||||
const getMessageForThreads = responseData.map(async (message: any) => {
|
||||
let fullObject: any = null;
|
||||
if (message?.metadata?.description) {
|
||||
@@ -271,13 +264,12 @@ export const GroupMail = ({
|
||||
} finally {
|
||||
if (isInitial) {
|
||||
setIsLoading(false);
|
||||
// dispatch(setIsLoadingCustom(null));
|
||||
}
|
||||
}
|
||||
},
|
||||
[allThreads, isPrivate]
|
||||
);
|
||||
const getMailMessages = React.useCallback(
|
||||
const getMailMessages = useCallback(
|
||||
async (groupId: string, members: any) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
@@ -315,7 +307,7 @@ export const GroupMail = ({
|
||||
.sort((a, b) => b.created - a.created)
|
||||
.slice(0, 10);
|
||||
|
||||
let fullThreadArray: any = [];
|
||||
const fullThreadArray: any = [];
|
||||
const getMessageForThreads = newArray.map(async (message: any) => {
|
||||
try {
|
||||
const identifierQuery = message.threadId;
|
||||
@@ -327,6 +319,7 @@ export const GroupMail = ({
|
||||
},
|
||||
});
|
||||
const responseData = await response.json();
|
||||
|
||||
if (responseData.length > 0) {
|
||||
const thread = responseData[0];
|
||||
if (thread?.metadata?.description) {
|
||||
@@ -342,7 +335,7 @@ export const GroupMail = ({
|
||||
};
|
||||
fullThreadArray.push(fullObject);
|
||||
} else {
|
||||
let threadRes = await Promise.race([
|
||||
const threadRes = await Promise.race([
|
||||
getEncryptedResource(
|
||||
{
|
||||
name: thread.name,
|
||||
@@ -377,13 +370,12 @@ export const GroupMail = ({
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
// dispatch(setIsLoadingCustom(null));
|
||||
}
|
||||
},
|
||||
[secretKey, isPrivate]
|
||||
);
|
||||
|
||||
const getMessages = React.useCallback(async () => {
|
||||
const getMessages = useCallback(async () => {
|
||||
// if ( !groupId || members?.length === 0) return;
|
||||
if (!groupId || isPrivate === null) return;
|
||||
|
||||
@@ -400,7 +392,6 @@ export const GroupMail = ({
|
||||
if (filterModeRef.current !== filterMode) {
|
||||
firstMount.current = false;
|
||||
}
|
||||
// if (groupId && !firstMount.current && members.length > 0) {
|
||||
if (groupId && !firstMount.current && isPrivate !== null) {
|
||||
if (filterMode === 'Recently active') {
|
||||
getMessages();
|
||||
@@ -427,11 +418,6 @@ export const GroupMail = ({
|
||||
if (groupData && Array.isArray(groupData?.members)) {
|
||||
for (const member of groupData.members) {
|
||||
if (member.member) {
|
||||
// const res = await getNameInfo(member.member);
|
||||
// const resAddress = await qortalRequest({
|
||||
// action: "GET_ACCOUNT_DATA",
|
||||
// address: member.member,
|
||||
// });
|
||||
const name = res;
|
||||
const publicKey = resAddress.publicKey;
|
||||
if (name) {
|
||||
@@ -465,16 +451,6 @@ export const GroupMail = ({
|
||||
[filterMode]
|
||||
);
|
||||
|
||||
// useEffect(()=> {
|
||||
// if(user?.name){
|
||||
// const threads = JSON.parse(
|
||||
// localStorage.getItem(`qmail_threads_viewedtimestamp_${user.name}`) || "{}"
|
||||
// );
|
||||
// setViewedThreads(threads)
|
||||
|
||||
// }
|
||||
// }, [user?.name, currentThread])
|
||||
|
||||
const handleCloseThreadFilterList = () => {
|
||||
setIsOpenFilterList(false);
|
||||
};
|
||||
@@ -596,7 +572,7 @@ export const GroupMail = ({
|
||||
padding: '0px',
|
||||
}}
|
||||
>
|
||||
<InstanceListHeader></InstanceListHeader>
|
||||
<InstanceListHeader />
|
||||
<InstanceListContainer>
|
||||
{filterOptions?.map((filter) => {
|
||||
return (
|
||||
@@ -621,6 +597,7 @@ export const GroupMail = ({
|
||||
/>
|
||||
)}
|
||||
</InstanceListContainerRowCheck>
|
||||
|
||||
<InstanceListContainerRowMain>
|
||||
<InstanceListContainerRowMainP>
|
||||
{filter}
|
||||
@@ -630,9 +607,10 @@ export const GroupMail = ({
|
||||
);
|
||||
})}
|
||||
</InstanceListContainer>
|
||||
<InstanceFooter></InstanceFooter>
|
||||
<InstanceFooter />
|
||||
</InstanceListParent>
|
||||
</Popover>
|
||||
|
||||
<ThreadContainerFullWidth>
|
||||
<ThreadContainer>
|
||||
<Box
|
||||
@@ -674,7 +652,9 @@ export const GroupMail = ({
|
||||
)}
|
||||
</ComposeContainerBlank>
|
||||
</Box>
|
||||
|
||||
<Spacer height="30px" />
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
@@ -700,10 +680,12 @@ export const GroupMail = ({
|
||||
viewedThreads[
|
||||
`qmail_threads_${thread?.threadData?.groupId}_${thread?.threadId}`
|
||||
];
|
||||
|
||||
const shouldAppearLighter =
|
||||
hasViewedRecent &&
|
||||
filterMode === 'Recently active' &&
|
||||
thread?.threadData?.createdAt < hasViewedRecent?.timestamp;
|
||||
|
||||
return (
|
||||
<SingleThreadParent
|
||||
sx={{
|
||||
@@ -771,13 +753,17 @@ export const GroupMail = ({
|
||||
>
|
||||
<ThreadSingleLastMessageP>
|
||||
<ThreadSingleLastMessageSpanP>
|
||||
last message:{' '}
|
||||
{t('group:last_message', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
:{' '}
|
||||
</ThreadSingleLastMessageSpanP>
|
||||
{formatDate(thread?.created)}
|
||||
</ThreadSingleLastMessageP>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<CustomButton
|
||||
onClick={() => {
|
||||
setTimeout(() => {
|
||||
@@ -834,6 +820,7 @@ export const GroupMail = ({
|
||||
</Box>
|
||||
</ThreadContainer>
|
||||
</ThreadContainerFullWidth>
|
||||
|
||||
<LoadingSnackbar
|
||||
open={isLoading}
|
||||
info={{
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useContext, useEffect, useRef, useState } from 'react';
|
||||
import { Box, CircularProgress, Input, useTheme } from '@mui/material';
|
||||
import ShortUniqueId from 'short-unique-id';
|
||||
import {
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
InstanceFooter,
|
||||
InstanceListContainer,
|
||||
InstanceListHeader,
|
||||
NewMessageCloseImg,
|
||||
NewMessageHeaderP,
|
||||
NewMessageInputRow,
|
||||
NewMessageSendButton,
|
||||
@@ -143,13 +142,13 @@ export const NewThread = ({
|
||||
isPrivate,
|
||||
}: NewMessageProps) => {
|
||||
const { t } = useTranslation(['core', 'group']);
|
||||
const { show } = React.useContext(MyContext);
|
||||
const { show } = useContext(MyContext);
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [value, setValue] = useState('');
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
const [threadTitle, setThreadTitle] = useState<string>('');
|
||||
const [openSnack, setOpenSnack] = React.useState(false);
|
||||
const [infoSnack, setInfoSnack] = React.useState(null);
|
||||
const [openSnack, setOpenSnack] = useState(false);
|
||||
const [infoSnack, setInfoSnack] = useState(null);
|
||||
const editorRef = useRef(null);
|
||||
const theme = useTheme();
|
||||
const setEditorRef = (editorInstance) => {
|
||||
@@ -203,31 +202,37 @@ export const NewThread = ({
|
||||
// if (!description) missingFields.push('subject')
|
||||
if (missingFields.length > 0) {
|
||||
const missingFieldsString = missingFields.join(', ');
|
||||
const errMsg = `Missing: ${missingFieldsString}`;
|
||||
errorMsg = errMsg; // TODO translate
|
||||
const errMsg = t('group:message.error.missing_field', {
|
||||
field: missingFieldsString,
|
||||
postProcess: 'capitalize',
|
||||
});
|
||||
errorMsg = errMsg;
|
||||
}
|
||||
|
||||
if (errorMsg) {
|
||||
// dispatch(
|
||||
// setNotification({
|
||||
// msg: errorMsg,
|
||||
// alertType: "error",
|
||||
// })
|
||||
// );
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
const htmlContent = editorRef.current.getHTML();
|
||||
|
||||
if (!htmlContent?.trim() || htmlContent?.trim() === '<p></p>')
|
||||
throw new Error('Please provide a first message to the thread');
|
||||
if (!htmlContent?.trim() || htmlContent?.trim() === '<p></p>') {
|
||||
const errMsg = t('group:message.generic.provide_message', {
|
||||
postProcess: 'capitalize',
|
||||
});
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
|
||||
const fee = await getFee('ARBITRARY');
|
||||
let feeToShow = fee.fee;
|
||||
|
||||
if (!isMessage) {
|
||||
feeToShow = +feeToShow * 2;
|
||||
}
|
||||
await show({
|
||||
message: 'Would you like to perform a ARBITRARY transaction?',
|
||||
message: t('group:question.perform_transaction', {
|
||||
action: 'ARBITRARY',
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
publishFee: feeToShow + ' QORT',
|
||||
});
|
||||
|
||||
@@ -238,6 +243,7 @@ export const NewThread = ({
|
||||
delete reply.reply;
|
||||
}
|
||||
}
|
||||
|
||||
const mailObject: any = {
|
||||
createdAt: Date.now(),
|
||||
version: 1,
|
||||
@@ -250,7 +256,10 @@ export const NewThread = ({
|
||||
const secretKey =
|
||||
isPrivate === false ? null : await getSecretKey(false, true);
|
||||
if (!secretKey && isPrivate) {
|
||||
throw new Error('Cannot get group secret key');
|
||||
const errMsg = t('group:message.error.group_secret_key', {
|
||||
postProcess: 'capitalize',
|
||||
});
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
|
||||
if (!isMessage) {
|
||||
@@ -273,17 +282,18 @@ export const NewThread = ({
|
||||
isPrivate === false
|
||||
? threadToBase64
|
||||
: await encryptSingleFunc(threadToBase64, secretKey);
|
||||
let identifierThread = `grp-${groupInfo.groupId}-thread-${idThread}`;
|
||||
const identifierThread = `grp-${groupInfo.groupId}-thread-${idThread}`;
|
||||
await publishGroupEncryptedResource({
|
||||
identifier: identifierThread,
|
||||
encryptedData: encryptSingleThread,
|
||||
});
|
||||
|
||||
let identifierPost = `thmsg-${identifierThread}-${idMsg}`;
|
||||
const identifierPost = `thmsg-${identifierThread}-${idMsg}`;
|
||||
await publishGroupEncryptedResource({
|
||||
identifier: identifierPost,
|
||||
encryptedData: encryptSingleFirstPost,
|
||||
});
|
||||
|
||||
const dataToSaveToStorage = {
|
||||
name: myName,
|
||||
identifier: identifierThread,
|
||||
@@ -292,6 +302,7 @@ export const NewThread = ({
|
||||
created: Date.now(),
|
||||
groupId: groupInfo.groupId,
|
||||
};
|
||||
|
||||
const dataToSaveToStoragePost = {
|
||||
name: myName,
|
||||
identifier: identifierPost,
|
||||
@@ -300,6 +311,7 @@ export const NewThread = ({
|
||||
created: Date.now(),
|
||||
threadId: identifierThread,
|
||||
};
|
||||
|
||||
await saveTempPublish({ data: dataToSaveToStorage, key: 'thread' });
|
||||
await saveTempPublish({
|
||||
data: dataToSaveToStoragePost,
|
||||
@@ -307,36 +319,32 @@ export const NewThread = ({
|
||||
});
|
||||
setInfoSnack({
|
||||
type: 'success',
|
||||
message:
|
||||
'Successfully created thread. It may take some time for the publish to propagate',
|
||||
message: t('group:message.success.thread_creation', {
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
});
|
||||
setOpenSnack(true);
|
||||
|
||||
// dispatch(
|
||||
// setNotification({
|
||||
// msg: "Message sent",
|
||||
// alertType: "success",
|
||||
// })
|
||||
// );
|
||||
if (publishCallback) {
|
||||
publishCallback();
|
||||
}
|
||||
closeModal();
|
||||
} else {
|
||||
if (!currentThread) throw new Error('unable to locate thread Id');
|
||||
if (!currentThread) {
|
||||
const errMsg = t('group:message.error.thread_id', {
|
||||
postProcess: 'capitalize',
|
||||
});
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
const idThread = currentThread.threadId;
|
||||
const messageToBase64 = await objectToBase64(mailObject);
|
||||
const encryptSinglePost =
|
||||
isPrivate === false
|
||||
? messageToBase64
|
||||
: await encryptSingleFunc(messageToBase64, secretKey);
|
||||
const idMsg = uid.rnd();
|
||||
let identifier = `thmsg-${idThread}-${idMsg}`;
|
||||
const res = await publishGroupEncryptedResource({
|
||||
identifier: identifier,
|
||||
encryptedData: encryptSinglePost,
|
||||
});
|
||||
|
||||
const idMsg = uid.rnd();
|
||||
const identifier = `thmsg-${idThread}-${idMsg}`;
|
||||
const dataToSaveToStoragePost = {
|
||||
threadId: idThread,
|
||||
name: myName,
|
||||
@@ -349,32 +357,17 @@ export const NewThread = ({
|
||||
data: dataToSaveToStoragePost,
|
||||
key: 'thread-post',
|
||||
});
|
||||
// await qortalRequest(multiplePublishMsg);
|
||||
// dispatch(
|
||||
// setNotification({
|
||||
// msg: "Message sent",
|
||||
// alertType: "success",
|
||||
// })
|
||||
// );
|
||||
setInfoSnack({
|
||||
type: 'success',
|
||||
message:
|
||||
'Successfully created post. It may take some time for the publish to propagate',
|
||||
message: t('group:message.success.post_creation', {
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
});
|
||||
setOpenSnack(true);
|
||||
if (publishCallback) {
|
||||
publishCallback();
|
||||
}
|
||||
// messageCallback({
|
||||
// identifier,
|
||||
// id: identifier,
|
||||
// name,
|
||||
// service: MAIL_SERVICE_TYPE,
|
||||
// created: Date.now(),
|
||||
// ...mailObject,
|
||||
// });
|
||||
}
|
||||
|
||||
closeModal();
|
||||
} catch (error: any) {
|
||||
if (error?.message) {
|
||||
@@ -393,6 +386,7 @@ export const NewThread = ({
|
||||
const sendMail = () => {
|
||||
publishQDNResource();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -407,7 +401,15 @@ export const NewThread = ({
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<ComposeIcon />
|
||||
<ComposeP>{currentThread ? 'New Post' : 'New Thread'}</ComposeP>
|
||||
<ComposeP>
|
||||
{currentThread
|
||||
? t('core:action.new.post', {
|
||||
postProcess: 'capitalize',
|
||||
})
|
||||
: t('core:action.new.thread', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</ComposeP>
|
||||
</ComposeContainer>
|
||||
|
||||
<ReusableModal
|
||||
@@ -433,8 +435,15 @@ export const NewThread = ({
|
||||
}}
|
||||
>
|
||||
<NewMessageHeaderP>
|
||||
{isMessage ? 'Post Message' : 'New Thread'}
|
||||
{isMessage
|
||||
? t('core:action.post_message', {
|
||||
postProcess: 'capitalize',
|
||||
})
|
||||
: t('core:action.new.thread', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</NewMessageHeaderP>
|
||||
|
||||
<CloseContainer
|
||||
sx={{
|
||||
height: '40px',
|
||||
@@ -448,6 +457,7 @@ export const NewThread = ({
|
||||
/>
|
||||
</CloseContainer>
|
||||
</InstanceListHeader>
|
||||
|
||||
<InstanceListContainer
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
@@ -459,6 +469,7 @@ export const NewThread = ({
|
||||
{!isMessage && (
|
||||
<>
|
||||
<Spacer height="10px" />
|
||||
|
||||
<NewMessageInputRow>
|
||||
<Input
|
||||
id="standard-adornment-name"
|
||||
@@ -516,28 +527,27 @@ export const NewThread = ({
|
||||
overrideMobile
|
||||
customEditorHeight="240px"
|
||||
/>
|
||||
|
||||
</Box>
|
||||
</InstanceListContainer>
|
||||
|
||||
<InstanceFooter
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
padding: '20px 42px',
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
height: '90px',
|
||||
padding: '20px 42px',
|
||||
}}
|
||||
>
|
||||
<NewMessageSendButton onClick={sendMail}>
|
||||
{isSending && (
|
||||
<Box
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<CircularProgress
|
||||
@@ -550,7 +560,13 @@ export const NewThread = ({
|
||||
)}
|
||||
|
||||
<NewMessageSendP>
|
||||
{isMessage ? 'Post' : 'Create Thread'}
|
||||
{isMessage
|
||||
? t('core:action.post', {
|
||||
postProcess: 'capitalize',
|
||||
})
|
||||
: t('core:action.create_thread', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</NewMessageSendP>
|
||||
|
||||
{isMessage ? (
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { createEditor } from 'slate';
|
||||
import {
|
||||
withReact,
|
||||
@@ -96,7 +96,7 @@ interface ReadOnlySlateProps {
|
||||
content: any;
|
||||
mode?: string;
|
||||
}
|
||||
const ReadOnlySlate: React.FC<ReadOnlySlateProps> = ({ content, mode }) => {
|
||||
const ReadOnlySlate: FC<ReadOnlySlateProps> = ({ content, mode }) => {
|
||||
const [load, setLoad] = useState(false);
|
||||
const editor = useMemo(() => withReact(createEditor()), []);
|
||||
const value = useMemo(() => content, [content]);
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import { FC } from 'react';
|
||||
import { Box, Modal, useTheme } from '@mui/material';
|
||||
|
||||
interface MyModalProps {
|
||||
@@ -9,7 +9,7 @@ interface MyModalProps {
|
||||
customStyles?: any;
|
||||
}
|
||||
|
||||
export const ReusableModal: React.FC<MyModalProps> = ({
|
||||
export const ReusableModal: FC<MyModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
|
@@ -118,27 +118,6 @@ export const ShowMessage = ({ message, openNewPostWithQuote, myName }: any) => {
|
||||
width: 'auto',
|
||||
}}
|
||||
>
|
||||
{/* <FileElement
|
||||
fileInfo={{ ...file, mimeTypeSaved: file?.type }}
|
||||
title={file?.filename}
|
||||
mode="mail"
|
||||
otherUser={message?.user}
|
||||
>
|
||||
<MailAttachmentImg src={AttachmentMailSVG} />
|
||||
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "16px",
|
||||
transition: '0.2s all',
|
||||
"&:hover": {
|
||||
color: 'rgba(255, 255, 255, 0.90)',
|
||||
textDecoration: 'underline'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{file?.originalFilename || file?.filename}
|
||||
</Typography>
|
||||
</FileElement> */}
|
||||
{message?.attachments?.length > 1 && isFirst && (
|
||||
<Box
|
||||
sx={{
|
||||
|
@@ -1,10 +1,4 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
@@ -18,7 +12,6 @@ import {
|
||||
ComposeP,
|
||||
GroupContainer,
|
||||
GroupNameP,
|
||||
MailIconImg,
|
||||
ShowMessageReturnButton,
|
||||
SingleThreadParent,
|
||||
ThreadContainer,
|
||||
@@ -222,7 +215,7 @@ export const Thread = ({
|
||||
}
|
||||
};
|
||||
|
||||
const getMailMessages = React.useCallback(
|
||||
const getMailMessages = useCallback(
|
||||
async (groupInfo: any, before, after, isReverse, groupId) => {
|
||||
try {
|
||||
setTempPublishedList([]);
|
||||
@@ -328,7 +321,7 @@ export const Thread = ({
|
||||
},
|
||||
[messages, secretKey]
|
||||
);
|
||||
const getMessages = React.useCallback(async () => {
|
||||
const getMessages = useCallback(async () => {
|
||||
if (
|
||||
!currentThread ||
|
||||
(!secretKey && isPrivate) ||
|
||||
@@ -410,7 +403,7 @@ export const Thread = ({
|
||||
|
||||
const interval = useRef<any>(null);
|
||||
|
||||
const checkNewMessages = React.useCallback(
|
||||
const checkNewMessages = useCallback(
|
||||
async (groupInfo: any) => {
|
||||
try {
|
||||
let threadId = groupInfo.threadId;
|
||||
@@ -494,7 +487,7 @@ export const Thread = ({
|
||||
firstMount.current = true;
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
subscribeToEvent('threadFetchMode', threadFetchModeFunc);
|
||||
|
||||
return () => {
|
||||
@@ -656,6 +649,7 @@ export const Thread = ({
|
||||
<div ref={threadBeginningRef} />
|
||||
<ThreadContainer>
|
||||
<Spacer height={'30px'} />
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
@@ -715,6 +709,7 @@ export const Thread = ({
|
||||
>
|
||||
{t('core:page.previous', { postProcess: 'capitalize' })}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
sx={{
|
||||
textTransformation: 'capitalize',
|
||||
@@ -733,6 +728,7 @@ export const Thread = ({
|
||||
>
|
||||
{t('core:page.next', { postProcess: 'capitalize' })}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
sx={{
|
||||
textTransformation: 'capitalize',
|
||||
@@ -1006,6 +1002,7 @@ export const Thread = ({
|
||||
>
|
||||
{t('core:page.first', { postProcess: 'capitalize' })}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
sx={{
|
||||
textTransformation: 'capitalize',
|
||||
@@ -1024,6 +1021,7 @@ export const Thread = ({
|
||||
>
|
||||
{t('core:page.previous', { postProcess: 'capitalize' })}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
sx={{
|
||||
textTransformation: 'capitalize',
|
||||
@@ -1042,6 +1040,7 @@ export const Thread = ({
|
||||
>
|
||||
{t('core:page.next', { postProcess: 'capitalize' })}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
sx={{
|
||||
textTransformation: 'capitalize',
|
||||
@@ -1061,12 +1060,14 @@ export const Thread = ({
|
||||
{t('core:page.last', { postProcess: 'capitalize' })}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Spacer height="30px" />
|
||||
</Box>
|
||||
|
||||
<div ref={containerRef} />
|
||||
</ThreadContainer>
|
||||
</ThreadContainerFullWidth>
|
||||
|
||||
<LoadingSnackbar
|
||||
open={isLoading}
|
||||
info={{
|
||||
|
@@ -9,20 +9,12 @@ import {
|
||||
Typography,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ChatGroup } from '../Chat/ChatGroup';
|
||||
import { CreateCommonSecret } from '../Chat/CreateCommonSecret';
|
||||
import { base64ToUint8Array } from '../../qdn/encryption/group-encryption';
|
||||
import { uint8ArrayToObject } from '../../backgroundFunctions/encryption';
|
||||
import CampaignIcon from '@mui/icons-material/Campaign';
|
||||
import { AddGroup } from './AddGroup';
|
||||
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
|
||||
import CreateIcon from '@mui/icons-material/Create';
|
||||
import {
|
||||
AuthenticatedContainerInnerRight,
|
||||
@@ -52,7 +44,6 @@ import {
|
||||
import { RequestQueueWithPromise } from '../../utils/queue/queue';
|
||||
import { WebSocketActive } from './WebsocketActive';
|
||||
import { useMessageQueue } from '../../MessageQueueContext';
|
||||
import { ContextMenu } from '../ContextMenu';
|
||||
import { HomeDesktop } from './HomeDesktop';
|
||||
import { IconWrapper } from '../Desktop/DesktopFooter';
|
||||
import { DesktopHeader } from '../Desktop/DesktopHeader';
|
||||
@@ -63,7 +54,6 @@ import { HubsIcon } from '../../assets/Icons/HubsIcon';
|
||||
import { MessagingIcon } from '../../assets/Icons/MessagingIcon';
|
||||
import { formatEmailDate } from './QMailMessages';
|
||||
import { AdminSpace } from '../Chat/AdminSpace';
|
||||
|
||||
import {
|
||||
addressInfoControllerAtom,
|
||||
groupAnnouncementsAtom,
|
||||
@@ -77,9 +67,6 @@ import {
|
||||
timestampEnterDataAtom,
|
||||
} from '../../atoms/global';
|
||||
import { sortArrayByTimestampAndGroupName } from '../../utils/time';
|
||||
import PersonOffIcon from '@mui/icons-material/PersonOff';
|
||||
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';
|
||||
@@ -92,6 +79,7 @@ export const getPublishesFromAdmins = async (admins: string[], groupId) => {
|
||||
groupId
|
||||
}&exactmatchnames=true&limit=0&reverse=true&${queryString}&prefix=true`;
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('network error');
|
||||
}
|
||||
@@ -100,9 +88,11 @@ export const getPublishesFromAdmins = async (admins: string[], groupId) => {
|
||||
const filterId = adminData.filter(
|
||||
(data: any) => data.identifier === `symmetric-qchat-group-${groupId}`
|
||||
);
|
||||
|
||||
if (filterId?.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const sortedData = filterId.sort((a: any, b: any) => {
|
||||
// Get the most recent date for both a and b
|
||||
const dateA = a.updated ? new Date(a.updated) : new Date(a.created);
|
||||
@@ -114,24 +104,18 @@ export const getPublishesFromAdmins = async (admins: string[], groupId) => {
|
||||
|
||||
return sortedData[0];
|
||||
};
|
||||
|
||||
interface GroupProps {
|
||||
myAddress: string;
|
||||
isFocused: boolean;
|
||||
userInfo: any;
|
||||
balance: number;
|
||||
isFocused: boolean;
|
||||
myAddress: string;
|
||||
userInfo: any;
|
||||
}
|
||||
|
||||
export const timeDifferenceForNotificationChats = 900000;
|
||||
|
||||
export const requestQueueMemberNames = new RequestQueueWithPromise(5);
|
||||
export const requestQueueAdminMemberNames = new RequestQueueWithPromise(5);
|
||||
|
||||
// const audio = new Audio(chrome.runtime?.getURL("msg-not1.wav"));
|
||||
|
||||
export const getGroupAdminsAddress = async (groupNumber: number) => {
|
||||
// const validApi = await findUsableApi();
|
||||
|
||||
const response = await fetch(
|
||||
`${getBaseApiReact()}/groups/members/${groupNumber}?limit=0&onlyAdmins=true`
|
||||
);
|
||||
@@ -422,27 +406,24 @@ export const Group = ({
|
||||
|
||||
const [chatMode, setChatMode] = useState('groups');
|
||||
const [newChat, setNewChat] = useState(false);
|
||||
const [openSnack, setOpenSnack] = React.useState(false);
|
||||
const [infoSnack, setInfoSnack] = React.useState(null);
|
||||
const [isLoadingNotifyAdmin, setIsLoadingNotifyAdmin] = React.useState(false);
|
||||
const [isLoadingGroups, setIsLoadingGroups] = React.useState(true);
|
||||
const [isLoadingGroup, setIsLoadingGroup] = React.useState(false);
|
||||
const [openSnack, setOpenSnack] = useState(false);
|
||||
const [infoSnack, setInfoSnack] = useState(null);
|
||||
const [isLoadingNotifyAdmin, setIsLoadingNotifyAdmin] = useState(false);
|
||||
const [isLoadingGroups, setIsLoadingGroups] = useState(true);
|
||||
const [isLoadingGroup, setIsLoadingGroup] = useState(false);
|
||||
const [firstSecretKeyInCreation, setFirstSecretKeyInCreation] =
|
||||
React.useState(false);
|
||||
const [groupSection, setGroupSection] = React.useState('home');
|
||||
useState(false);
|
||||
const [groupSection, setGroupSection] = useState('home');
|
||||
const [groupAnnouncements, setGroupAnnouncements] = useAtom(
|
||||
groupAnnouncementsAtom
|
||||
);
|
||||
|
||||
const [defaultThread, setDefaultThread] = React.useState(null);
|
||||
const [isOpenDrawer, setIsOpenDrawer] = React.useState(false);
|
||||
const [defaultThread, setDefaultThread] = useState(null);
|
||||
const [isOpenDrawer, setIsOpenDrawer] = useState(false);
|
||||
const setIsOpenBlockedUserModal = useSetAtom(isOpenBlockedModalAtom);
|
||||
|
||||
const [hideCommonKeyPopup, setHideCommonKeyPopup] = React.useState(false);
|
||||
const [isLoadingGroupMessage, setIsLoadingGroupMessage] = React.useState('');
|
||||
const [drawerMode, setDrawerMode] = React.useState('groups');
|
||||
const [hideCommonKeyPopup, setHideCommonKeyPopup] = useState(false);
|
||||
const [isLoadingGroupMessage, setIsLoadingGroupMessage] = useState('');
|
||||
const [drawerMode, setDrawerMode] = useState('groups');
|
||||
const setMutedGroups = useSetAtom(mutedGroupsAtom);
|
||||
|
||||
const [mobileViewMode, setMobileViewMode] = useState('home');
|
||||
const [mobileViewModeKeepOpen, setMobileViewModeKeepOpen] = useState('');
|
||||
const isFocusedRef = useRef(true);
|
||||
@@ -531,7 +512,10 @@ export const Group = ({
|
||||
rej(response.error);
|
||||
})
|
||||
.catch((error) => {
|
||||
rej(error.message || 'An error occurred');
|
||||
rej(
|
||||
error.message ||
|
||||
t('core:message.error.generic', { postProcess: 'capitalize' })
|
||||
);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -557,7 +541,10 @@ export const Group = ({
|
||||
rej(response.error);
|
||||
})
|
||||
.catch((error) => {
|
||||
rej(error.message || 'An error occurred');
|
||||
rej(
|
||||
error.message ||
|
||||
t('core:message.error.generic', { postProcess: 'capitalize' })
|
||||
);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -586,7 +573,10 @@ export const Group = ({
|
||||
rej(response.error);
|
||||
})
|
||||
.catch((error) => {
|
||||
rej(error.message || 'An error occurred');
|
||||
rej(
|
||||
error.message ||
|
||||
t('core:message.error.generic', { postProcess: 'capitalize' })
|
||||
);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -1106,7 +1096,10 @@ export const Group = ({
|
||||
rej(response.error);
|
||||
})
|
||||
.catch((error) => {
|
||||
rej(error.message || 'An error occurred');
|
||||
rej(
|
||||
error.message ||
|
||||
t('core:message.error.generic', { postProcess: 'capitalize' })
|
||||
);
|
||||
});
|
||||
});
|
||||
setInfoSnack({
|
||||
|
@@ -16,7 +16,8 @@ export const InviteMember = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
|
||||
try {
|
||||
const fee = await getFee('GROUP_INVITE');
|
||||
await show({
|
||||
message: t('group:question.group_invite', {
|
||||
message: t('group:question.perform_transaction', {
|
||||
action: 'GROUP_INVITE',
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
publishFee: fee.fee + ' QORT',
|
||||
@@ -97,16 +98,16 @@ export const InviteMember = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
|
||||
label={t('group:invitation_expiry', { postProcess: 'capitalize' })}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<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>
|
||||
<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
|
||||
|
@@ -54,7 +54,7 @@ export const ListOfBans = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
|
||||
const [bans, setBans] = 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
|
||||
const listRef = useRef();
|
||||
const listRef = useRef(null);
|
||||
const [isLoadingUnban, setIsLoadingUnban] = useState(false);
|
||||
const { t } = useTranslation(['core', 'group']);
|
||||
|
||||
@@ -88,7 +88,10 @@ export const ListOfBans = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
|
||||
try {
|
||||
const fee = await getFee('CANCEL_GROUP_BAN');
|
||||
await show({
|
||||
message: t('group:question.cancel_ban', { postProcess: 'capitalize' }),
|
||||
message: t('group:question.perform_transaction', {
|
||||
action: 'CANCEL_GROUP_BAN',
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
publishFee: fee.fee + ' QORT',
|
||||
});
|
||||
setIsLoadingUnban(true);
|
||||
@@ -165,13 +168,13 @@ export const ListOfBans = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '325px',
|
||||
height: '250px',
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
height: '250px',
|
||||
padding: '10px',
|
||||
width: '325px',
|
||||
}}
|
||||
>
|
||||
<LoadingButton
|
||||
@@ -214,12 +217,12 @@ export const ListOfBans = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
|
||||
<p>{t('group:ban_list', { postProcess: 'capitalize' })}</p>
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
height: '500px',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexShrink: 1,
|
||||
height: '500px',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<AutoSizer>
|
||||
|
@@ -51,6 +51,11 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import { getFee } from '../../background';
|
||||
import { useAtom, useSetAtom } from 'jotai';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const THIRTY_MINUTES = 30 * 60 * 1000; // 30 minutes in milliseconds
|
||||
const uid = new ShortUniqueId({ length: 8 });
|
||||
|
||||
export const requestQueuePromos = new RequestQueueWithPromise(3);
|
||||
|
||||
export function utf8ToBase64(inputString: string): string {
|
||||
@@ -65,13 +70,11 @@ export function utf8ToBase64(inputString: string): string {
|
||||
return base64String;
|
||||
}
|
||||
|
||||
const uid = new ShortUniqueId({ length: 8 });
|
||||
|
||||
export function getGroupId(str) {
|
||||
const match = str.match(/group-(\d+)-/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
const THIRTY_MINUTES = 30 * 60 * 1000; // 30 minutes in milliseconds
|
||||
|
||||
export const ListOfGroupPromotions = () => {
|
||||
const [popoverAnchor, setPopoverAnchor] = useState(null);
|
||||
const [openPopoverIndex, setOpenPopoverIndex] = useState(null);
|
||||
@@ -98,7 +101,8 @@ export const ListOfGroupPromotions = () => {
|
||||
const setTxList = useSetAtom(txListAtom);
|
||||
|
||||
const theme = useTheme();
|
||||
const listRef = useRef();
|
||||
const { t } = useTranslation(['core', 'group']);
|
||||
const listRef = useRef(null);
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: promotions.length,
|
||||
getItemKey: React.useCallback(
|
||||
@@ -120,6 +124,7 @@ export const ListOfGroupPromotions = () => {
|
||||
console.log(error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getPromotions = useCallback(async () => {
|
||||
try {
|
||||
setPromotionTimeInterval(Date.now());
|
||||
@@ -135,6 +140,7 @@ export const ListOfGroupPromotions = () => {
|
||||
let data: any[] = [];
|
||||
const uniqueGroupIds = new Set();
|
||||
const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
const getPromos = responseData?.map(async (promo: any) => {
|
||||
if (promo?.size < 200 && promo.created > oneWeekAgo) {
|
||||
const name = await requestQueuePromos.enqueue(async () => {
|
||||
@@ -213,6 +219,7 @@ export const ListOfGroupPromotions = () => {
|
||||
setPopoverAnchor(null);
|
||||
setOpenPopoverIndex(null);
|
||||
};
|
||||
|
||||
const publishPromo = async () => {
|
||||
try {
|
||||
setIsLoadingPublish(true);
|
||||
@@ -235,9 +242,12 @@ export const ListOfGroupPromotions = () => {
|
||||
rej(response.error);
|
||||
})
|
||||
.catch((error) => {
|
||||
rej(error.message || 'An error occurred');
|
||||
rej(
|
||||
error.message ||
|
||||
t('core:message.error.generic', { postProcess: 'capitalize' })
|
||||
);
|
||||
});
|
||||
}); // TODO translate
|
||||
});
|
||||
setInfoSnack({
|
||||
type: 'success',
|
||||
message:
|
||||
@@ -264,7 +274,10 @@ export const ListOfGroupPromotions = () => {
|
||||
const groupId = group.groupId;
|
||||
const fee = await getFee('JOIN_GROUP');
|
||||
await show({
|
||||
message: 'Would you like to perform an JOIN_GROUP transaction?',
|
||||
message: t('group:question.perform_transaction', {
|
||||
action: 'JOIN_GROUP',
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
publishFee: fee.fee + ' QORT',
|
||||
});
|
||||
setIsLoadingJoinGroup(true);
|
||||
@@ -331,6 +344,7 @@ export const ListOfGroupPromotions = () => {
|
||||
});
|
||||
setIsLoadingJoinGroup(false);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsLoadingJoinGroup(false);
|
||||
}
|
||||
@@ -339,30 +353,30 @@ export const ListOfGroupPromotions = () => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
marginTop: '20px',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
marginTop: '20px',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: '20px',
|
||||
width: '100%',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<ButtonBase
|
||||
sx={{
|
||||
alignSelf: isExpanded && 'flex-start',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
padding: `0px ${isExpanded ? '24px' : '20px'}`,
|
||||
gap: '10px',
|
||||
justifyContent: 'flex-start',
|
||||
alignSelf: isExpanded && 'flex-start',
|
||||
padding: `0px ${isExpanded ? '24px' : '20px'}`,
|
||||
}}
|
||||
onClick={() => setIsExpanded((prev) => !prev)}
|
||||
>
|
||||
@@ -374,6 +388,7 @@ export const ListOfGroupPromotions = () => {
|
||||
Group promotions{' '}
|
||||
{promotions.length > 0 && ` (${promotions.length})`}
|
||||
</Typography>
|
||||
|
||||
{isExpanded ? (
|
||||
<ExpandLessIcon
|
||||
sx={{
|
||||
@@ -400,19 +415,19 @@ export const ListOfGroupPromotions = () => {
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
width: '750px',
|
||||
maxWidth: '90%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
maxWidth: '90%',
|
||||
padding: '0px 20px',
|
||||
width: '750px',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
|
@@ -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(
|
||||
@@ -59,8 +60,8 @@ export const ListOfInvites = ({
|
||||
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
|
||||
const [isLoadingCancelInvite, setIsLoadingCancelInvite] = useState(false);
|
||||
|
||||
const listRef = useRef();
|
||||
const { t } = useTranslation(['core', 'group']);
|
||||
const listRef = useRef(null);
|
||||
|
||||
const getInvites = async (groupId) => {
|
||||
try {
|
||||
@@ -90,13 +91,18 @@ export const ListOfInvites = ({
|
||||
|
||||
const handleCancelInvitation = async (address) => {
|
||||
try {
|
||||
// TODO translate
|
||||
const fee = await getFee('CANCEL_GROUP_INVITE');
|
||||
|
||||
await show({
|
||||
message: 'Would you like to perform a CANCEL_GROUP_INVITE transaction?',
|
||||
message: t('group:question.perform_transaction', {
|
||||
action: 'CANCEL_GROUP_INVITE',
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
publishFee: fee.fee + ' QORT',
|
||||
});
|
||||
|
||||
setIsLoadingCancelInvite(true);
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
window
|
||||
.sendMessage('cancelInvitationToGroup', {
|
||||
@@ -107,8 +113,9 @@ export const ListOfInvites = ({
|
||||
if (!response?.error) {
|
||||
setInfoSnack({
|
||||
type: 'success',
|
||||
message:
|
||||
'Successfully canceled invitation. It may take a couple of minutes for the changes to propagate',
|
||||
message: t('group:message.success.invitation_cancellation', {
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
});
|
||||
setOpenSnack(true);
|
||||
handlePopoverClose();
|
||||
@@ -126,13 +133,18 @@ export const ListOfInvites = ({
|
||||
.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);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsLoadingCancelInvite(false);
|
||||
}
|
||||
@@ -168,13 +180,13 @@ export const ListOfInvites = ({
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '325px',
|
||||
height: '250px',
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
height: '250px',
|
||||
padding: '10px',
|
||||
width: '325px',
|
||||
}}
|
||||
>
|
||||
<LoadingButton
|
||||
@@ -183,10 +195,13 @@ export const ListOfInvites = ({
|
||||
variant="contained"
|
||||
onClick={() => handleCancelInvitation(member?.invitee)}
|
||||
>
|
||||
Cancel Invitation
|
||||
{t('core:action.cancel_invitation', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Popover>
|
||||
|
||||
<ListItemButton
|
||||
onClick={(event) => handlePopoverOpen(event, index)}
|
||||
>
|
||||
@@ -200,6 +215,7 @@ export const ListOfInvites = ({
|
||||
}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
|
||||
<ListItemText primary={member?.name || member?.invitee} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
@@ -211,15 +227,19 @@ export const ListOfInvites = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Invitees list</p>
|
||||
<p>
|
||||
{t('group:invitees_list', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
height: '500px',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexShrink: 1,
|
||||
height: '500px',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<AutoSizer>
|
||||
|
@@ -15,11 +15,12 @@ import {
|
||||
List,
|
||||
} from 'react-virtualized';
|
||||
import { getNameInfo } from './Group';
|
||||
import { getBaseApi, getFee } from '../../background';
|
||||
import { getFee } from '../../background';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import { getBaseApiReact } from '../../App';
|
||||
import { txListAtom } from '../../atoms/global';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const getMemberInvites = async (groupNumber) => {
|
||||
const response = await fetch(
|
||||
@@ -59,11 +60,11 @@ export const ListOfJoinRequests = ({
|
||||
}) => {
|
||||
const [invites, setInvites] = useState([]);
|
||||
const [txList, setTxList] = useAtom(txListAtom);
|
||||
|
||||
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
|
||||
const listRef = useRef();
|
||||
const listRef = useRef(null);
|
||||
const [isLoadingAccept, setIsLoadingAccept] = useState(false);
|
||||
const { t } = useTranslation(['core', 'group']);
|
||||
|
||||
const getInvites = async (groupId) => {
|
||||
try {
|
||||
@@ -93,12 +94,18 @@ export const ListOfJoinRequests = ({
|
||||
|
||||
const handleAcceptJoinRequest = async (address) => {
|
||||
try {
|
||||
const fee = await getFee('GROUP_INVITE'); // TODO translate
|
||||
const fee = await getFee('GROUP_INVITE');
|
||||
|
||||
await show({
|
||||
message: 'Would you like to perform a GROUP_INVITE transaction?',
|
||||
message: t('group:question.perform_transaction', {
|
||||
action: 'GROUP_INVITE',
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
publishFee: fee.fee + ' QORT',
|
||||
});
|
||||
|
||||
setIsLoadingAccept(true);
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
window
|
||||
.sendMessage('inviteToGroup', {
|
||||
@@ -111,19 +118,23 @@ export const ListOfJoinRequests = ({
|
||||
setIsLoadingAccept(false);
|
||||
setInfoSnack({
|
||||
type: 'success',
|
||||
message:
|
||||
'Successfully accepted join request. It may take a couple of minutes for the changes to propagate',
|
||||
message: t('group:message.success,group_join', {
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
});
|
||||
setOpenSnack(true);
|
||||
handlePopoverClose();
|
||||
res(response);
|
||||
|
||||
setTxList((prev) => [
|
||||
{
|
||||
...response,
|
||||
type: 'join-request-accept',
|
||||
label: `Accepted join request: awaiting confirmation`,
|
||||
labelDone: `User successfully joined!`,
|
||||
label: t('group:message.success,invitation_request', {
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
labelDone: t('group:message.success,user_joined', {
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
done: false,
|
||||
groupId,
|
||||
qortalAddress: address,
|
||||
@@ -144,13 +155,16 @@ export const ListOfJoinRequests = ({
|
||||
.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);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsLoadingAccept(false);
|
||||
}
|
||||
@@ -158,13 +172,15 @@ export const ListOfJoinRequests = ({
|
||||
|
||||
const rowRenderer = ({ index, key, parent, style }) => {
|
||||
const member = invites[index];
|
||||
const findJoinRequsetInTxList = txList?.find(
|
||||
const findJoinRequestInTxList = txList?.find(
|
||||
(tx) =>
|
||||
tx?.groupId === groupId &&
|
||||
tx?.qortalAddress === member?.joiner &&
|
||||
tx?.type === 'join-request-accept'
|
||||
);
|
||||
if (findJoinRequsetInTxList) return null;
|
||||
|
||||
if (findJoinRequestInTxList) return null;
|
||||
|
||||
return (
|
||||
<CellMeasurer
|
||||
key={key}
|
||||
@@ -207,10 +223,11 @@ export const ListOfJoinRequests = ({
|
||||
variant="contained"
|
||||
onClick={() => handleAcceptJoinRequest(member?.joiner)}
|
||||
>
|
||||
Accept
|
||||
{t('core:action.accept', { postProcess: 'capitalize' })}
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Popover>
|
||||
|
||||
<ListItemButton
|
||||
onClick={(event) => handlePopoverOpen(event, index)}
|
||||
>
|
||||
@@ -235,7 +252,7 @@ export const ListOfJoinRequests = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Join request list</p>
|
||||
<p>{t('core:list.join_request', { postProcess: 'capitalize' })}</p>
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
|
@@ -19,6 +19,7 @@ import {
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import { getFee } from '../../background';
|
||||
import { getBaseApiReact } from '../../App';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const cache = new CellMeasurerCache({
|
||||
fixedWidth: true,
|
||||
@@ -41,7 +42,8 @@ const ListOfMembers = ({
|
||||
const [isLoadingMakeAdmin, setIsLoadingMakeAdmin] = useState(false);
|
||||
const [isLoadingRemoveAdmin, setIsLoadingRemoveAdmin] = useState(false);
|
||||
const theme = useTheme();
|
||||
const listRef = useRef();
|
||||
const { t } = useTranslation(['core', 'group']);
|
||||
const listRef = useRef(null);
|
||||
|
||||
const handlePopoverOpen = (event, index) => {
|
||||
setPopoverAnchor(event.currentTarget);
|
||||
@@ -57,7 +59,10 @@ const ListOfMembers = ({
|
||||
try {
|
||||
const fee = await getFee('GROUP_KICK');
|
||||
await show({
|
||||
message: 'Would you like to perform a GROUP_KICK transaction?',
|
||||
message: t('group:question.perform_transaction', {
|
||||
action: 'GROUP_KICK',
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
publishFee: fee.fee + ' QORT',
|
||||
});
|
||||
|
||||
@@ -72,8 +77,9 @@ const ListOfMembers = ({
|
||||
if (!response?.error) {
|
||||
setInfoSnack({
|
||||
type: 'success',
|
||||
message:
|
||||
'Successfully kicked member from group. It may take a couple of minutes for the changes to propagate',
|
||||
message: t('group:message.success.group_kick', {
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
});
|
||||
setOpenSnack(true);
|
||||
handlePopoverClose();
|
||||
@@ -90,7 +96,9 @@ const ListOfMembers = ({
|
||||
.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);
|
||||
@@ -104,12 +112,18 @@ const ListOfMembers = ({
|
||||
};
|
||||
const handleBan = async (address) => {
|
||||
try {
|
||||
const fee = await getFee('GROUP_BAN'); // TODO translate
|
||||
const fee = await getFee('GROUP_BAN');
|
||||
|
||||
await show({
|
||||
message: 'Would you like to perform a GROUP_BAN transaction?',
|
||||
message: t('group:question.perform_transaction', {
|
||||
action: 'GROUP_BAN',
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
publishFee: fee.fee + ' QORT',
|
||||
});
|
||||
|
||||
setIsLoadingBan(true);
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
window
|
||||
.sendMessage('banFromGroup', {
|
||||
@@ -121,8 +135,9 @@ const ListOfMembers = ({
|
||||
if (!response?.error) {
|
||||
setInfoSnack({
|
||||
type: 'success',
|
||||
message:
|
||||
'Successfully banned member from group. It may take a couple of minutes for the changes to propagate',
|
||||
message: t('group:message.success.group_ban', {
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
});
|
||||
setOpenSnack(true);
|
||||
handlePopoverClose();
|
||||
@@ -139,13 +154,16 @@ const ListOfMembers = ({
|
||||
.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);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsLoadingBan(false);
|
||||
}
|
||||
@@ -155,7 +173,10 @@ const ListOfMembers = ({
|
||||
try {
|
||||
const fee = await getFee('ADD_GROUP_ADMIN');
|
||||
await show({
|
||||
message: 'Would you like to perform a ADD_GROUP_ADMIN transaction?',
|
||||
message: t('group:question.perform_transaction', {
|
||||
action: 'ADD_GROUP_ADMIN',
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
publishFee: fee.fee + ' QORT',
|
||||
});
|
||||
setIsLoadingMakeAdmin(true);
|
||||
@@ -169,8 +190,9 @@ const ListOfMembers = ({
|
||||
if (!response?.error) {
|
||||
setInfoSnack({
|
||||
type: 'success',
|
||||
message:
|
||||
'Successfully made member an admin. It may take a couple of minutes for the changes to propagate',
|
||||
message: t('group:message.success.group_member_admin', {
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
});
|
||||
setOpenSnack(true);
|
||||
handlePopoverClose();
|
||||
@@ -187,13 +209,16 @@ const ListOfMembers = ({
|
||||
.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);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsLoadingMakeAdmin(false);
|
||||
}
|
||||
@@ -203,7 +228,10 @@ const ListOfMembers = ({
|
||||
try {
|
||||
const fee = await getFee('REMOVE_GROUP_ADMIN');
|
||||
await show({
|
||||
message: 'Would you like to perform a REMOVE_GROUP_ADMIN transaction?',
|
||||
message: t('group:question.perform_transaction', {
|
||||
action: 'REMOVE_GROUP_ADMIN',
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
publishFee: fee.fee + ' QORT',
|
||||
});
|
||||
setIsLoadingRemoveAdmin(true);
|
||||
@@ -217,8 +245,9 @@ const ListOfMembers = ({
|
||||
if (!response?.error) {
|
||||
setInfoSnack({
|
||||
type: 'success',
|
||||
message:
|
||||
'Successfully removed member as an admin. It may take a couple of minutes for the changes to propagate',
|
||||
message: t('group:message.success.group_remove_member', {
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
});
|
||||
setOpenSnack(true);
|
||||
handlePopoverClose();
|
||||
@@ -235,13 +264,16 @@ const ListOfMembers = ({
|
||||
.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);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsLoadingRemoveAdmin(false);
|
||||
}
|
||||
@@ -276,13 +308,13 @@ const ListOfMembers = ({
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '325px',
|
||||
height: '250px',
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
height: '250px',
|
||||
padding: '10px',
|
||||
width: '325px',
|
||||
}}
|
||||
>
|
||||
{isOwner && (
|
||||
@@ -293,48 +325,49 @@ const ListOfMembers = ({
|
||||
variant="contained"
|
||||
onClick={() => handleKick(member?.member)}
|
||||
>
|
||||
Kick member from group
|
||||
{t('group:action.kick_member', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</LoadingButton>
|
||||
|
||||
<LoadingButton
|
||||
loading={isLoadingBan}
|
||||
loadingPosition="start"
|
||||
variant="contained"
|
||||
onClick={() => handleBan(member?.member)}
|
||||
>
|
||||
Ban member from group
|
||||
{t('group:action.ban', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</LoadingButton>
|
||||
|
||||
<LoadingButton
|
||||
loading={isLoadingMakeAdmin}
|
||||
loadingPosition="start"
|
||||
variant="contained"
|
||||
onClick={() => makeAdmin(member?.member)}
|
||||
>
|
||||
Make an admin
|
||||
{t('group:action.make_admin', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</LoadingButton>
|
||||
|
||||
<LoadingButton
|
||||
loading={isLoadingRemoveAdmin}
|
||||
loadingPosition="start"
|
||||
variant="contained"
|
||||
onClick={() => removeAdmin(member?.member)}
|
||||
>
|
||||
Remove as admin
|
||||
{t('group:action.remove_admin', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</LoadingButton>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Popover>
|
||||
<ListItem
|
||||
key={member?.member}
|
||||
// secondaryAction={
|
||||
// <Checkbox
|
||||
// edge="end"
|
||||
// onChange={handleToggle(value)}
|
||||
// checked={checked.indexOf(value) !== -1}
|
||||
// inputProps={{ 'aria-labelledby': labelId }}
|
||||
// />
|
||||
// }
|
||||
disablePadding
|
||||
>
|
||||
|
||||
<ListItem key={member?.member} disablePadding>
|
||||
<ListItemButton
|
||||
onClick={(event) => handlePopoverOpen(event, index)}
|
||||
>
|
||||
@@ -348,6 +381,7 @@ const ListOfMembers = ({
|
||||
}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
|
||||
<ListItemText
|
||||
id={''}
|
||||
primary={member?.name || member?.member}
|
||||
@@ -359,7 +393,9 @@ const ListOfMembers = ({
|
||||
marginLeft: 'auto',
|
||||
}}
|
||||
>
|
||||
Admin
|
||||
{t('core:admin', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</Typography>
|
||||
)}
|
||||
</ListItemButton>
|
||||
@@ -372,28 +408,31 @@ const ListOfMembers = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Member list</p>
|
||||
<p>
|
||||
{t('core:list.member', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
height: '500px',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexShrink: 1,
|
||||
height: '500px',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<List
|
||||
ref={listRef}
|
||||
width={width}
|
||||
deferredMeasurementCache={cache}
|
||||
height={height}
|
||||
ref={listRef}
|
||||
rowCount={members.length}
|
||||
rowHeight={cache.rowHeight}
|
||||
rowRenderer={rowRenderer}
|
||||
// onScroll={handleScroll}
|
||||
deferredMeasurementCache={cache}
|
||||
width={width}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
|
@@ -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';
|
||||
@@ -9,10 +9,12 @@ import { Box, Typography } from '@mui/material';
|
||||
import { Spacer } from '../../common/Spacer';
|
||||
import { CustomLoader } from '../../common/CustomLoader';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const ListOfThreadPostsWatched = () => {
|
||||
const [posts, setPosts] = React.useState([]);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [posts, setPosts] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { t } = useTranslation(['core', 'group']);
|
||||
|
||||
const getPosts = async () => {
|
||||
try {
|
||||
@@ -42,34 +44,38 @@ export const ListOfThreadPostsWatched = () => {
|
||||
rej(response.error);
|
||||
})
|
||||
.catch((error) => {
|
||||
rej(error.message || 'An error occurred'); // TODO translate
|
||||
rej(
|
||||
error.message ||
|
||||
t('core:message.error.generic', { postProcess: 'capitalize' })
|
||||
);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
getPosts();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '322px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: '0px 20px',
|
||||
width: '322px',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
@@ -78,8 +84,12 @@ export const ListOfThreadPostsWatched = () => {
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
New Thread Posts:
|
||||
{t('group:thread_posts', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
:
|
||||
</Typography>
|
||||
|
||||
<Spacer height="10px" />
|
||||
</Box>
|
||||
|
||||
@@ -97,9 +107,9 @@ export const ListOfThreadPostsWatched = () => {
|
||||
{loading && posts.length === 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<CustomLoader />
|
||||
@@ -108,11 +118,11 @@ export const ListOfThreadPostsWatched = () => {
|
||||
{!loading && posts.length === 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
@@ -122,7 +132,9 @@ export const ListOfThreadPostsWatched = () => {
|
||||
color: 'rgba(255, 255, 255, 0.2)',
|
||||
}}
|
||||
>
|
||||
Nothing to display
|
||||
{t('group:message.generic.no_display', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
@@ -130,11 +142,11 @@ export const ListOfThreadPostsWatched = () => {
|
||||
<List
|
||||
className="scrollable-container"
|
||||
sx={{
|
||||
width: '100%',
|
||||
maxWidth: 360,
|
||||
bgcolor: 'background.paper',
|
||||
maxHeight: '300px',
|
||||
maxWidth: 360,
|
||||
overflow: 'auto',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{posts?.map((post) => {
|
||||
|
@@ -1,4 +1,14 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
forwardRef,
|
||||
Fragment,
|
||||
ReactElement,
|
||||
Ref,
|
||||
SyntheticEvent,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
@@ -25,6 +35,7 @@ import { Spacer } from '../../common/Spacer';
|
||||
import InsertLinkIcon from '@mui/icons-material/InsertLink';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { txListAtom } from '../../atoms/global';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function a11yProps(index: number) {
|
||||
return {
|
||||
@@ -33,37 +44,35 @@ function a11yProps(index: number) {
|
||||
};
|
||||
}
|
||||
|
||||
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 ManageMembers = ({
|
||||
address,
|
||||
open,
|
||||
setOpen,
|
||||
selectedGroup,
|
||||
|
||||
isAdmin,
|
||||
isOwner,
|
||||
}) => {
|
||||
const [membersWithNames, setMembersWithNames] = React.useState([]);
|
||||
const [tab, setTab] = React.useState('create');
|
||||
const [value, setValue] = React.useState(0);
|
||||
const [openSnack, setOpenSnack] = React.useState(false);
|
||||
const [infoSnack, setInfoSnack] = React.useState(null);
|
||||
const [isLoadingMembers, setIsLoadingMembers] = React.useState(false);
|
||||
const [isLoadingLeave, setIsLoadingLeave] = React.useState(false);
|
||||
const [groupInfo, setGroupInfo] = React.useState(null);
|
||||
const handleChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||
const [membersWithNames, setMembersWithNames] = useState([]);
|
||||
const [value, setValue] = useState(0);
|
||||
const [openSnack, setOpenSnack] = useState(false);
|
||||
const [infoSnack, setInfoSnack] = useState(null);
|
||||
const [isLoadingMembers, setIsLoadingMembers] = useState(false);
|
||||
const [isLoadingLeave, setIsLoadingLeave] = useState(false);
|
||||
const [groupInfo, setGroupInfo] = useState(null);
|
||||
const handleChange = (event: SyntheticEvent, newValue: number) => {
|
||||
setValue(newValue);
|
||||
};
|
||||
const theme = useTheme();
|
||||
const { show } = React.useContext(MyContext);
|
||||
const { t } = useTranslation(['core', 'group']);
|
||||
const { show } = useContext(MyContext);
|
||||
const setTxList = useSetAtom(txListAtom);
|
||||
|
||||
const handleClose = () => {
|
||||
@@ -75,7 +84,10 @@ export const ManageMembers = ({
|
||||
setIsLoadingLeave(true);
|
||||
const fee = await getFee('LEAVE_GROUP');
|
||||
await show({
|
||||
message: 'Would you like to perform an LEAVE_GROUP transaction?',
|
||||
message: t('group:question.perform_transaction', {
|
||||
action: 'LEAVE_GROUP',
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
publishFee: fee.fee + ' QORT',
|
||||
});
|
||||
|
||||
@@ -90,8 +102,14 @@ export const ManageMembers = ({
|
||||
{
|
||||
...response,
|
||||
type: 'leave-group',
|
||||
label: `Left Group ${selectedGroup?.groupName}: awaiting confirmation`,
|
||||
labelDone: `Left Group ${selectedGroup?.groupName}: success!`,
|
||||
label: t('group:message.success.group_leave_name', {
|
||||
group_name: selectedGroup?.groupName,
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
labelDone: t('group:message.success.group_leave_label', {
|
||||
group_name: selectedGroup?.groupName,
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
done: false,
|
||||
groupId: selectedGroup?.groupId,
|
||||
},
|
||||
@@ -100,8 +118,9 @@ export const ManageMembers = ({
|
||||
res(response);
|
||||
setInfoSnack({
|
||||
type: 'success',
|
||||
message:
|
||||
'Successfully requested to leave group. It may take a couple of minutes for the changes to propagate',
|
||||
message: t('group:message.success.group_leave', {
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
});
|
||||
setOpenSnack(true);
|
||||
return;
|
||||
@@ -109,7 +128,10 @@ export const ManageMembers = ({
|
||||
rej(response.error);
|
||||
})
|
||||
.catch((error) => {
|
||||
rej(error.message || 'An error occurred'); // TODO translate
|
||||
rej(
|
||||
error.message ||
|
||||
t('core:message.error.generic', { postProcess: 'capitalize' })
|
||||
);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -119,7 +141,7 @@ export const ManageMembers = ({
|
||||
}
|
||||
};
|
||||
|
||||
const getMembersWithNames = React.useCallback(async (groupId) => {
|
||||
const getMembersWithNames = useCallback(async (groupId) => {
|
||||
try {
|
||||
setIsLoadingMembers(true);
|
||||
const res = await getGroupMembers(groupId);
|
||||
@@ -139,6 +161,7 @@ export const ManageMembers = ({
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const getGroupInfo = async (groupId) => {
|
||||
try {
|
||||
const response = await fetch(`${getBaseApiReact()}/groups/${groupId}`);
|
||||
@@ -149,7 +172,7 @@ export const ManageMembers = ({
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (selectedGroup?.groupId) {
|
||||
getMembers(selectedGroup?.groupId);
|
||||
getGroupInfo(selectedGroup?.groupId);
|
||||
@@ -160,7 +183,7 @@ export const ManageMembers = ({
|
||||
setValue(4);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
subscribeToEvent('openGroupJoinRequest', openGroupJoinRequestFunc);
|
||||
|
||||
return () => {
|
||||
@@ -169,7 +192,7 @@ export const ManageMembers = ({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Fragment>
|
||||
<Dialog
|
||||
fullScreen
|
||||
open={open}
|
||||
@@ -184,18 +207,20 @@ export const ManageMembers = ({
|
||||
>
|
||||
<Toolbar>
|
||||
<Typography sx={{ ml: 2, flex: 1 }} variant="h4" component="div">
|
||||
Manage Members
|
||||
{t('group:action.manage_members', { postProcess: 'capitalize' })}
|
||||
</Typography>
|
||||
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
onClick={handleClose}
|
||||
aria-label="close"
|
||||
color="inherit"
|
||||
edge="start"
|
||||
onClick={handleClose}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: theme.palette.background.default,
|
||||
@@ -284,12 +309,19 @@ export const ManageMembers = ({
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography>GroupId: {groupInfo?.groupId}</Typography>
|
||||
|
||||
<Typography>GroupName: {groupInfo?.groupName}</Typography>
|
||||
<Typography>
|
||||
{t('group:group.id', { postProcess: 'capitalize' })}:{' '}
|
||||
{groupInfo?.groupId}
|
||||
</Typography>
|
||||
|
||||
<Typography>
|
||||
Number of members: {groupInfo?.memberCount}
|
||||
{t('group:group.name', { postProcess: 'capitalize' })}:{' '}
|
||||
{groupInfo?.groupName}
|
||||
</Typography>
|
||||
|
||||
<Typography>
|
||||
{t('group:group.member_number', { postProcess: 'capitalize' })}:{' '}
|
||||
{groupInfo?.memberCount}
|
||||
</Typography>
|
||||
|
||||
<ButtonBase
|
||||
@@ -301,7 +333,11 @@ export const ManageMembers = ({
|
||||
await navigator.clipboard.writeText(link);
|
||||
}}
|
||||
>
|
||||
<InsertLinkIcon /> <Typography>Join Group Link</Typography>
|
||||
<InsertLinkIcon />
|
||||
|
||||
<Typography>
|
||||
{t('group:join_link', { postProcess: 'capitalize' })}
|
||||
</Typography>
|
||||
</ButtonBase>
|
||||
</Box>
|
||||
|
||||
@@ -315,10 +351,11 @@ export const ManageMembers = ({
|
||||
variant="contained"
|
||||
onClick={handleLeaveGroup}
|
||||
>
|
||||
Leave Group
|
||||
{t('group:action.leave_group', { postProcess: 'capitalize' })}
|
||||
</LoadingButton>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{value === 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -331,7 +368,7 @@ export const ManageMembers = ({
|
||||
variant="contained"
|
||||
onClick={() => getMembersWithNames(selectedGroup?.groupId)}
|
||||
>
|
||||
Load members with names
|
||||
{t('group:action.load_members', { postProcess: 'capitalize' })}
|
||||
</Button>
|
||||
|
||||
<Spacer height="10px" />
|
||||
@@ -347,6 +384,7 @@ export const ManageMembers = ({
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{value === 1 && (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -426,10 +464,12 @@ export const ManageMembers = ({
|
||||
<LoadingSnackbar
|
||||
open={isLoadingMembers}
|
||||
info={{
|
||||
message: 'Loading member list with names... please wait.',
|
||||
message: t('group:message.generic.loading_members', {
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</Dialog>
|
||||
</React.Fragment>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
@@ -11,12 +11,12 @@ import MailIcon from '@mui/icons-material/Mail';
|
||||
import MailOutlineIcon from '@mui/icons-material/MailOutline';
|
||||
import { executeEvent } from '../../utils/events';
|
||||
import { CustomLoader } from '../../common/CustomLoader';
|
||||
|
||||
import { mailsAtom, qMailLastEnteredTimestampAtom } from '../../atoms/global';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import MarkEmailUnreadIcon from '@mui/icons-material/MarkEmailUnread';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const isLessThanOneWeekOld = (timestamp) => {
|
||||
// Current time in milliseconds
|
||||
@@ -54,6 +54,7 @@ export const QMailMessages = ({ userName, userAddress }) => {
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation(['core', 'group']);
|
||||
|
||||
const getMails = useCallback(async () => {
|
||||
try {
|
||||
@@ -89,7 +90,10 @@ export const QMailMessages = ({ userName, userAddress }) => {
|
||||
rej(response.error);
|
||||
})
|
||||
.catch((error) => {
|
||||
rej(error.message || 'An error occurred'); // TODO translate
|
||||
rej(
|
||||
error.message ||
|
||||
t('core:message.error.generic', { postProcess: 'capitalize' })
|
||||
);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -129,20 +133,20 @@ export const QMailMessages = ({ userName, userAddress }) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<ButtonBase
|
||||
sx={{
|
||||
width: '322px',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: '10px',
|
||||
padding: '0px 20px',
|
||||
justifyContent: 'flex-start',
|
||||
padding: '0px 20px',
|
||||
width: '322px',
|
||||
}}
|
||||
onClick={() => setIsExpanded((prev) => !prev)}
|
||||
>
|
||||
@@ -151,8 +155,9 @@ export const QMailMessages = ({ userName, userAddress }) => {
|
||||
fontSize: '1rem',
|
||||
}}
|
||||
>
|
||||
Latest Q-Mails
|
||||
{t('group:latest_mails', { postProcess: 'capitalize' })}
|
||||
</Typography>
|
||||
|
||||
<MarkEmailUnreadIcon
|
||||
sx={{
|
||||
color: anyUnread
|
||||
@@ -195,9 +200,9 @@ export const QMailMessages = ({ userName, userAddress }) => {
|
||||
{loading && mails.length === 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<CustomLoader />
|
||||
@@ -206,11 +211,11 @@ export const QMailMessages = ({ userName, userAddress }) => {
|
||||
{!loading && mails.length === 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
@@ -220,7 +225,9 @@ export const QMailMessages = ({ userName, userAddress }) => {
|
||||
color: theme.palette.primary,
|
||||
}}
|
||||
>
|
||||
Nothing to display
|
||||
{t('group:message.generic.no_display', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
@@ -4,6 +4,7 @@ import {
|
||||
Fragment,
|
||||
ReactElement,
|
||||
Ref,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
@@ -15,11 +16,30 @@ import Typography from '@mui/material/Typography';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import Slide from '@mui/material/Slide';
|
||||
import { TransitionProps } from '@mui/material/transitions';
|
||||
import { Box, FormControlLabel, Switch, styled, useTheme } from '@mui/material';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
FormControlLabel,
|
||||
Switch,
|
||||
TextField,
|
||||
styled,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
import { enabledDevModeAtom } from '../../atoms/global';
|
||||
|
||||
import ThemeManager from '../Theme/ThemeManager';
|
||||
import { useAtom } from 'jotai';
|
||||
import { decryptStoredWallet } from '../../utils/decryptWallet';
|
||||
import { Spacer } from '../../common/Spacer';
|
||||
import PhraseWallet from '../../utils/generateWallet/phrase-wallet';
|
||||
import { walletVersion } from '../../background';
|
||||
import Base58 from '../../deps/Base58';
|
||||
import { MyContext } from '../../App';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const LocalNodeSwitch = styled(Switch)(({ theme }) => ({
|
||||
padding: 8,
|
||||
@@ -63,11 +83,11 @@ const Transition = forwardRef(function Transition(
|
||||
return <Slide direction="up" ref={ref} {...props} />;
|
||||
});
|
||||
|
||||
export const Settings = ({ address, open, setOpen }) => {
|
||||
export const Settings = ({ open, setOpen, rawWallet }) => {
|
||||
const [checked, setChecked] = useState(false);
|
||||
const [isEnabledDevMode, setIsEnabledDevMode] = useAtom(enabledDevModeAtom);
|
||||
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation(['core', 'group']);
|
||||
|
||||
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setChecked(event.target.checked);
|
||||
@@ -82,7 +102,7 @@ export const Settings = ({ address, open, setOpen }) => {
|
||||
if (response?.error) {
|
||||
console.error('Error adding user settings:', response.error);
|
||||
} else {
|
||||
console.log('User settings added successfully'); // TODO translate
|
||||
console.log('User settings added successfully');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -113,7 +133,10 @@ export const Settings = ({ address, open, setOpen }) => {
|
||||
rej(response.error);
|
||||
})
|
||||
.catch((error) => {
|
||||
rej(error.message || 'An error occurred');
|
||||
rej(
|
||||
error.message ||
|
||||
t('core:message.error.generic', { postProcess: 'capitalize' })
|
||||
);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -136,7 +159,9 @@ export const Settings = ({ address, open, setOpen }) => {
|
||||
<AppBar sx={{ position: 'relative' }}>
|
||||
<Toolbar>
|
||||
<Typography sx={{ ml: 2, flex: 1 }} variant="h4" component="div">
|
||||
General Settings
|
||||
{t('core:general_settings', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</Typography>
|
||||
|
||||
<IconButton
|
||||
@@ -152,13 +177,13 @@ export const Settings = ({ address, open, setOpen }) => {
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
overflowY: 'auto',
|
||||
color: theme.palette.text.primary,
|
||||
padding: '20px',
|
||||
flexDirection: 'column',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
gap: '20px',
|
||||
overflowY: 'auto',
|
||||
padding: '20px',
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
@@ -168,7 +193,9 @@ export const Settings = ({ address, open, setOpen }) => {
|
||||
control={
|
||||
<LocalNodeSwitch checked={checked} onChange={handleChange} />
|
||||
}
|
||||
label="Disable all push notifications"
|
||||
label={t('group:action.disable_push_notifications', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
/>
|
||||
{window?.electronAPI && (
|
||||
<FormControlLabel
|
||||
@@ -184,12 +211,157 @@ export const Settings = ({ address, open, setOpen }) => {
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="Enable dev mode"
|
||||
label={t('group:action.enable_dev_mode', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{isEnabledDevMode && <ExportPrivateKey rawWallet={rawWallet} />}
|
||||
<ThemeManager />
|
||||
</Box>
|
||||
</Dialog>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const ExportPrivateKey = ({ rawWallet }) => {
|
||||
const [password, setPassword] = useState('');
|
||||
const [privateKey, setPrivateKey] = useState('');
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { setOpenSnackGlobal, setInfoSnackCustom } = useContext(MyContext);
|
||||
const { t } = useTranslation(['core', 'group']);
|
||||
|
||||
const exportPrivateKeyFunc = async () => {
|
||||
try {
|
||||
setInfoSnackCustom({
|
||||
type: 'info',
|
||||
message: t('group:message.generic.descrypt_wallet', {
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
});
|
||||
|
||||
setOpenSnackGlobal(true);
|
||||
const wallet = structuredClone(rawWallet);
|
||||
|
||||
const res = await decryptStoredWallet(password, wallet);
|
||||
const wallet2 = new PhraseWallet(res, wallet?.version || walletVersion);
|
||||
|
||||
const keyPair = Base58.encode(wallet2._addresses[0].keyPair.privateKey);
|
||||
setPrivateKey(keyPair);
|
||||
setInfoSnackCustom({
|
||||
type: '',
|
||||
message: '',
|
||||
});
|
||||
|
||||
setOpenSnackGlobal(false);
|
||||
} catch (error) {
|
||||
setInfoSnackCustom({
|
||||
type: 'error',
|
||||
message: error?.message
|
||||
? t('group:message.error.decrypt_wallet', {
|
||||
errorMessage: error?.message,
|
||||
postProcess: 'capitalize',
|
||||
})
|
||||
: t('group:message.error.descrypt_wallet', {
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
});
|
||||
|
||||
setOpenSnackGlobal(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{
|
||||
width: '200px',
|
||||
}}
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
{t('group:action.export_private_key', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</Button>
|
||||
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">
|
||||
{t('group:action.export_password', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent
|
||||
sx={{
|
||||
flexDirection: 'column',
|
||||
display: 'flex',
|
||||
gap: '10px',
|
||||
}}
|
||||
>
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
{t('group:message.generic.secure_place', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</DialogContentText>
|
||||
|
||||
<Spacer height="20px" />
|
||||
|
||||
<TextField
|
||||
autoFocus
|
||||
type="password"
|
||||
value={password}
|
||||
autoComplete="off"
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
{privateKey && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(privateKey);
|
||||
setInfoSnackCustom({
|
||||
type: 'success',
|
||||
message: t('group:message.generic.private_key_copied', {
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
});
|
||||
|
||||
setOpenSnackGlobal(true);
|
||||
}}
|
||||
>
|
||||
{t('group:action.copy_private_key', {
|
||||
postProcess: 'capitalize',
|
||||
})}{' '}
|
||||
<ContentCopyIcon color="primary" />
|
||||
</Button>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
setPassword('');
|
||||
setPrivateKey('');
|
||||
}}
|
||||
>
|
||||
{t('group:action.cancel', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</Button>
|
||||
|
||||
<Button variant="contained" onClick={exportPrivateKeyFunc}>
|
||||
{t('group:action.decrypt', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
@@ -17,27 +17,27 @@ export const ThingsToDoInitial = ({
|
||||
balance,
|
||||
userInfo,
|
||||
}) => {
|
||||
const [checked1, setChecked1] = React.useState(false);
|
||||
const [checked2, setChecked2] = React.useState(false);
|
||||
const [checked1, setChecked1] = useState(false);
|
||||
const [checked2, setChecked2] = useState(false);
|
||||
const { t } = useTranslation(['core', 'tutorial']);
|
||||
const theme = useTheme();
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (balance && +balance >= 6) {
|
||||
setChecked1(true);
|
||||
}
|
||||
}, [balance]);
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (name) setChecked2(true);
|
||||
}, [name]);
|
||||
|
||||
const isLoaded = React.useMemo(() => {
|
||||
const isLoaded = useMemo(() => {
|
||||
if (userInfo !== null) return true;
|
||||
return false;
|
||||
}, [userInfo]);
|
||||
|
||||
const hasDoneNameAndBalanceAndIsLoaded = React.useMemo(() => {
|
||||
const hasDoneNameAndBalanceAndIsLoaded = useMemo(() => {
|
||||
if (isLoaded && checked1 && checked2) return true;
|
||||
return false;
|
||||
}, [checked1, isLoaded, checked2]);
|
||||
@@ -55,18 +55,18 @@ export const ThingsToDoInitial = ({
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '322px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: '0px 20px',
|
||||
width: '322px',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
@@ -125,6 +125,7 @@ export const ThingsToDoInitial = ({
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
/>
|
||||
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
justifyContent: 'flex-end',
|
||||
@@ -144,6 +145,7 @@ export const ThingsToDoInitial = ({
|
||||
</ListItemIcon>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
sx={{
|
||||
marginBottom: '20px',
|
||||
|
@@ -22,6 +22,7 @@ import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmail
|
||||
import { Spacer } from '../../common/Spacer';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { txListAtom } from '../../atoms/global';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const cache = new CellMeasurerCache({
|
||||
fixedWidth: true,
|
||||
@@ -60,9 +61,10 @@ export const UserListOfInvites = ({
|
||||
const [invites, setInvites] = useState<any[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation(['core', 'group']);
|
||||
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
|
||||
const listRef = useRef();
|
||||
const listRef = useRef(null);
|
||||
|
||||
const getRequests = async () => {
|
||||
try {
|
||||
@@ -94,9 +96,13 @@ export const UserListOfInvites = ({
|
||||
|
||||
const handleJoinGroup = async (groupId, groupName) => {
|
||||
try {
|
||||
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.perform_transaction', {
|
||||
action: 'JOIN_GROUP',
|
||||
postProcess: 'capitalize',
|
||||
}),
|
||||
publishFee: fee.fee + ' QORT',
|
||||
});
|
||||
|
||||
@@ -123,8 +129,9 @@ export const UserListOfInvites = ({
|
||||
res(response);
|
||||
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',
|
||||
}),
|
||||
});
|
||||
setOpenSnack(true);
|
||||
handlePopoverClose();
|
||||
@@ -140,13 +147,16 @@ export const UserListOfInvites = ({
|
||||
.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);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -182,16 +192,22 @@ export const UserListOfInvites = ({
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '325px',
|
||||
height: '250px',
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
height: '250px',
|
||||
padding: '10px',
|
||||
width: '325px',
|
||||
}}
|
||||
>
|
||||
<Typography>Join {invite?.groupName}</Typography>
|
||||
<Typography>
|
||||
{t('core:action.join', {
|
||||
postProcess: 'capitalize',
|
||||
})}{' '}
|
||||
{invite?.groupName}
|
||||
</Typography>
|
||||
|
||||
<LoadingButton
|
||||
loading={isLoading}
|
||||
loadingPosition="start"
|
||||
@@ -200,10 +216,13 @@ export const UserListOfInvites = ({
|
||||
handleJoinGroup(invite?.groupId, invite?.groupName)
|
||||
}
|
||||
>
|
||||
Join group
|
||||
{t('group:action.join_group', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Popover>
|
||||
|
||||
<ListItemButton
|
||||
onClick={(event) => handlePopoverOpen(event, index)}
|
||||
>
|
||||
@@ -221,7 +240,9 @@ export const UserListOfInvites = ({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Spacer width="15px" />
|
||||
|
||||
<ListItemText
|
||||
primary={invite?.groupName}
|
||||
secondary={invite?.description}
|
||||
@@ -242,14 +263,19 @@ export const UserListOfInvites = ({
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
<p>Invite list</p>
|
||||
<p>
|
||||
{t('core:list.invite', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
</p>
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<AutoSizer>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Box, ButtonBase, Divider, Typography, useTheme } from '@mui/material';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Box, ButtonBase, Divider, Typography, useTheme } from '@mui/material';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import AppViewerContainer from '../Apps/AppViewerContainer';
|
||||
import {
|
||||
@@ -19,7 +19,6 @@ export const WalletsAppWrapper = () => {
|
||||
const [navigationController, setNavigationController] = useAtom(
|
||||
navigationControllerAtom
|
||||
);
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState({
|
||||
tabId: '5558589',
|
||||
name: 'Q-Wallets',
|
||||
@@ -60,17 +59,17 @@ export const WalletsAppWrapper = () => {
|
||||
{isOpen && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
height: '100vh',
|
||||
width: '100vw',
|
||||
backgroundColor: theme.palette.background.paper, // TODO: set color theme
|
||||
zIndex: 100,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderTopLeftRadius: '10px',
|
||||
borderTopRightRadius: '10px',
|
||||
bottom: 0,
|
||||
boxShadow: 4,
|
||||
height: '100vh',
|
||||
overflow: 'hidden',
|
||||
position: 'fixed',
|
||||
right: 0,
|
||||
width: '100vw',
|
||||
zIndex: 100,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
@@ -85,11 +84,11 @@ export const WalletsAppWrapper = () => {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '5px',
|
||||
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Typography>Q-Wallets</Typography>
|
||||
|
||||
<ButtonBase onClick={handleClose}>
|
||||
<CloseIcon
|
||||
sx={{
|
||||
@@ -108,6 +107,7 @@ export const WalletsAppWrapper = () => {
|
||||
ref={iframeRef}
|
||||
skipAuth={true}
|
||||
/>
|
||||
|
||||
<AppsNavBarParent>
|
||||
<AppsNavBarLeft
|
||||
sx={{
|
||||
@@ -126,6 +126,7 @@ export const WalletsAppWrapper = () => {
|
||||
>
|
||||
<NavBack />
|
||||
</ButtonBase>
|
||||
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
if (selectedTab?.refreshFunc) {
|
||||
|
@@ -164,6 +164,7 @@ export const useBlockedAddresses = () => {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (address) {
|
||||
await new Promise((res, rej) => {
|
||||
window
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import Logo2 from '../assets/svgs/Logo2.svg';
|
||||
import { MyContext, getArbitraryEndpointReact, getBaseApiReact } from '../App';
|
||||
import {
|
||||
|
@@ -36,7 +36,7 @@ export const NewUsersCTA = ({ balance }) => {
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{t('core:new_user', { postProcess: 'capitalize' })}
|
||||
{t('core:question.new_user', { postProcess: 'capitalize' })}
|
||||
</Typography>
|
||||
|
||||
<Spacer height="20px" />
|
||||
|
@@ -168,6 +168,7 @@ export const QortPrice = () => {
|
||||
)}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -198,6 +199,7 @@ export const QortPrice = () => {
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Tooltip
|
||||
title={
|
||||
<span style={{ fontSize: '14px', fontWeight: 700 }}>
|
||||
|
@@ -1,7 +1,13 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { supportedLanguages } from '../../../i18n';
|
||||
import { Tooltip, useTheme } from '@mui/material';
|
||||
import { supportedLanguages } from '../../i18n/i18n';
|
||||
import {
|
||||
FormControl,
|
||||
MenuItem,
|
||||
Select,
|
||||
Tooltip,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
|
||||
const LanguageSelector = () => {
|
||||
const { i18n, t } = useTranslation(['core']);
|
||||
@@ -19,20 +25,6 @@ const LanguageSelector = () => {
|
||||
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}
|
||||
@@ -44,33 +36,13 @@ const LanguageSelector = () => {
|
||||
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>
|
||||
) : (
|
||||
{!showSelect && (
|
||||
<Tooltip
|
||||
key={currentLang}
|
||||
title={t('core:action.change_language', {
|
||||
postProcess: 'capitalize',
|
||||
})}
|
||||
>
|
||||
<button
|
||||
onClick={() => setShowSelect(true)}
|
||||
style={{
|
||||
@@ -81,10 +53,36 @@ const LanguageSelector = () => {
|
||||
}}
|
||||
aria-label={`Current language: ${name}`}
|
||||
>
|
||||
{showSelect ? undefined : flag}
|
||||
{flag}
|
||||
</button>
|
||||
)}
|
||||
</Tooltip>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{showSelect && (
|
||||
<FormControl
|
||||
size="small"
|
||||
sx={{
|
||||
minWidth: 120,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
}}
|
||||
>
|
||||
<Select
|
||||
open
|
||||
labelId="language-select-label"
|
||||
id="language-select"
|
||||
value={currentLang}
|
||||
onChange={handleChange}
|
||||
autoFocus
|
||||
onClose={() => setShowSelect(false)}
|
||||
>
|
||||
{Object.entries(supportedLanguages).map(([code, { name }]) => (
|
||||
<MenuItem key={code} value={code}>
|
||||
{code.toUpperCase()} – {name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import React from 'react'
|
||||
import { Box, CircularProgress } from "@mui/material";
|
||||
import { Box, CircularProgress } from '@mui/material';
|
||||
|
||||
export const Loader = () => {
|
||||
return (
|
||||
<Box sx={{
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
@@ -11,13 +11,14 @@ export const Loader = () => {
|
||||
height: '100%',
|
||||
position: 'fixed',
|
||||
top: '0px',
|
||||
left:'0px',
|
||||
left: '0px',
|
||||
right: '0px',
|
||||
bottom: '0px',
|
||||
zIndex: 10,
|
||||
background: 'rgba(0, 0, 0, 0.4)'
|
||||
}}>
|
||||
<CircularProgress color="success" size={25} />
|
||||
background: 'rgba(0, 0, 0, 0.4)',
|
||||
}}
|
||||
>
|
||||
<CircularProgress color="success" size={25} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import Logo2 from '../assets/svgs/Logo2.svg';
|
||||
import { MyContext, getArbitraryEndpointReact, getBaseApiReact } from '../App';
|
||||
import {
|
||||
|
@@ -158,6 +158,12 @@ export const QortPayment = ({ balance, show, onSuccess, defaultPaymentTo }) => {
|
||||
value={paymentPassword}
|
||||
onChange={(e) => setPaymentPassword(e.target.value)}
|
||||
autoComplete="off"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
if (isLoadingSendCoin) return;
|
||||
sendCoinFunc();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
@@ -27,15 +27,25 @@ export const ReactionPicker = ({ onReaction }) => {
|
||||
if (showPicker) {
|
||||
setShowPicker(false);
|
||||
} else {
|
||||
// Get the button's position
|
||||
const buttonRect = buttonRef.current.getBoundingClientRect();
|
||||
const pickerWidth = 350;
|
||||
const pickerHeight = 400; // Match Picker height prop
|
||||
|
||||
// Calculate position to align the right edge of the picker with the button's right edge
|
||||
setPickerPosition({
|
||||
top: buttonRect.bottom + window.scrollY, // Position below the button
|
||||
left: buttonRect.right + window.scrollX - pickerWidth, // Align right edges
|
||||
});
|
||||
// Initial position (below the button)
|
||||
let top = buttonRect.bottom + window.scrollY;
|
||||
let left = buttonRect.right + window.scrollX - pickerWidth;
|
||||
|
||||
// If picker would overflow bottom, show it above the button
|
||||
const overflowBottom =
|
||||
top + pickerHeight > window.innerHeight + window.scrollY;
|
||||
if (overflowBottom) {
|
||||
top = buttonRect.top + window.scrollY - pickerHeight;
|
||||
}
|
||||
|
||||
// Optional: prevent overflow on the left too
|
||||
if (left < 0) left = 0;
|
||||
|
||||
setPickerPosition({ top, left });
|
||||
setShowPicker(true);
|
||||
}
|
||||
};
|
||||
@@ -92,12 +102,13 @@ export const ReactionPicker = ({ onReaction }) => {
|
||||
allowExpandReactions={true}
|
||||
autoFocusSearch={false}
|
||||
emojiStyle={EmojiStyle.NATIVE}
|
||||
height="450"
|
||||
height={400}
|
||||
onEmojiClick={handlePicker}
|
||||
onReactionClick={handleReaction}
|
||||
reactionsDefaultOpen={true}
|
||||
// reactionsDefaultOpen={true}
|
||||
// open={true}
|
||||
theme={Theme.DARK}
|
||||
width="350"
|
||||
width={350}
|
||||
/>
|
||||
</div>,
|
||||
document.body
|
||||
|
@@ -1,33 +1,22 @@
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
ButtonBase,
|
||||
Collapse,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
Input,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
List,
|
||||
MenuItem,
|
||||
Popover,
|
||||
Select,
|
||||
TextField,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
import { Label } from './Group/AddGroup';
|
||||
import { Spacer } from '../common/Spacer';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import { getBaseApiReact, MyContext } from '../App';
|
||||
import { getBaseApiReact } from '../App';
|
||||
import { getFee } from '../background';
|
||||
import RadioButtonCheckedIcon from '@mui/icons-material/RadioButtonChecked';
|
||||
import { subscribeToEvent, unsubscribeFromEvent } from '../utils/events';
|
||||
@@ -43,6 +32,7 @@ enum Availability {
|
||||
AVAILABLE = 'available',
|
||||
NOT_AVAILABLE = 'not-available',
|
||||
}
|
||||
|
||||
export const RegisterName = ({
|
||||
setOpenSnack,
|
||||
setInfoSnack,
|
||||
@@ -77,7 +67,6 @@ export const RegisterName = ({
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
}
|
||||
};
|
||||
// Debounce logic
|
||||
@@ -195,21 +184,22 @@ export const RegisterName = ({
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">{'Register name'}</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<Box
|
||||
sx={{
|
||||
width: '400px',
|
||||
maxWidth: '90vw',
|
||||
height: '500px',
|
||||
maxHeight: '90vh',
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
height: '500px',
|
||||
maxHeight: '90vh',
|
||||
maxWidth: '90vw',
|
||||
padding: '10px',
|
||||
width: '400px',
|
||||
}}
|
||||
>
|
||||
<Label>Choose a name</Label>
|
||||
<Label>Choose a name</Label> // TODO: translate
|
||||
<TextField
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
@@ -237,6 +227,7 @@ export const RegisterName = ({
|
||||
requires a {nameFee} QORT fee
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Spacer height="10px" />
|
||||
</>
|
||||
)}
|
||||
@@ -307,6 +298,7 @@ export const RegisterName = ({
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Publish data to Qortal: anything from apps to videos. Fully decentralized!" />
|
||||
</ListItem>
|
||||
|
||||
<ListItem disablePadding>
|
||||
<ListItemIcon>
|
||||
<RadioButtonCheckedIcon
|
||||
@@ -320,6 +312,7 @@ export const RegisterName = ({
|
||||
</List>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button
|
||||
disabled={isLoadingRegisterName}
|
||||
@@ -331,6 +324,7 @@ export const RegisterName = ({
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
disabled={
|
||||
!registerNameValue.trim() ||
|
||||
|
@@ -25,7 +25,7 @@ const ThemeContext = createContext({
|
||||
toggleTheme: () => {},
|
||||
userThemes: [defaultTheme],
|
||||
addUserTheme: (themes) => {},
|
||||
setUserTheme: (theme) => {},
|
||||
setUserTheme: (theme, themes) => {},
|
||||
currentThemeId: 'default',
|
||||
});
|
||||
|
||||
@@ -83,13 +83,13 @@ export const ThemeProvider = ({ children }) => {
|
||||
saveSettings(themes);
|
||||
};
|
||||
|
||||
const setUserTheme = (theme) => {
|
||||
const setUserTheme = (theme, themes) => {
|
||||
if (theme.id === 'default') {
|
||||
setCurrentThemeId('default');
|
||||
saveSettings(userThemes, themeMode, 'default');
|
||||
saveSettings(themes || userThemes, themeMode, 'default');
|
||||
} else {
|
||||
setCurrentThemeId(theme.id);
|
||||
saveSettings(userThemes, themeMode, theme.id);
|
||||
saveSettings(themes || userThemes, themeMode, theme.id);
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -119,7 +119,7 @@ export default function ThemeManager() {
|
||||
const newTheme = { ...themeDraft, id: uid.rnd() };
|
||||
const updatedThemes = [...userThemes, newTheme];
|
||||
addUserTheme(updatedThemes);
|
||||
setUserTheme(newTheme);
|
||||
setUserTheme(newTheme, updatedThemes);
|
||||
}
|
||||
setOpenEditor(false);
|
||||
};
|
||||
@@ -135,19 +135,22 @@ export default function ThemeManager() {
|
||||
);
|
||||
|
||||
if (defaultTheme) {
|
||||
setUserTheme(defaultTheme);
|
||||
setUserTheme(defaultTheme, updatedThemes);
|
||||
} else {
|
||||
// Emergency fallback
|
||||
setUserTheme({
|
||||
light: lightThemeOptions,
|
||||
dark: darkThemeOptions,
|
||||
});
|
||||
setUserTheme(
|
||||
{
|
||||
light: lightThemeOptions,
|
||||
dark: darkThemeOptions,
|
||||
},
|
||||
updatedThemes
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleApplyTheme = (theme) => {
|
||||
setUserTheme(theme);
|
||||
setUserTheme(theme, null);
|
||||
};
|
||||
|
||||
const handleColorChange = (mode, fieldPath, color) => {
|
||||
@@ -210,7 +213,8 @@ export default function ThemeManager() {
|
||||
const newTheme = { ...importedTheme, id: uid.rnd() };
|
||||
const updatedThemes = [...userThemes, newTheme];
|
||||
addUserTheme(updatedThemes);
|
||||
setUserTheme(newTheme);
|
||||
|
||||
setUserTheme(newTheme, updatedThemes);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
54
src/i18n/i18n.ts
Normal file
54
src/i18n/i18n.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
|
||||
const capitalize = {
|
||||
type: 'postProcessor',
|
||||
name: 'capitalize',
|
||||
process: (value: string) => value.charAt(0).toUpperCase() + value.slice(1),
|
||||
};
|
||||
|
||||
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: '🇷🇺' },
|
||||
};
|
||||
|
||||
// Load all JSON files under locales/**/*
|
||||
const modules = import.meta.glob('./locales/**/*.json', {
|
||||
eager: true,
|
||||
}) as Record<string, any>;
|
||||
|
||||
// Construct i18n resources object
|
||||
const resources: Record<string, Record<string, any>> = {};
|
||||
|
||||
for (const path in modules) {
|
||||
// Path format: './locales/en/core.json'
|
||||
const match = path.match(/\.\/locales\/([^/]+)\/([^/]+)\.json$/);
|
||||
if (!match) continue;
|
||||
|
||||
const [, lang, ns] = match;
|
||||
resources[lang] = resources[lang] || {};
|
||||
resources[lang][ns] = modules[path].default;
|
||||
}
|
||||
|
||||
i18n
|
||||
.use(initReactI18next)
|
||||
.use(LanguageDetector)
|
||||
.use(capitalize as any)
|
||||
.init({
|
||||
resources,
|
||||
fallbackLng: 'en',
|
||||
lng: navigator.language,
|
||||
supportedLngs: Object.keys(supportedLanguages),
|
||||
ns: ['core', 'auth', 'group', 'tutorial'],
|
||||
defaultNS: 'core',
|
||||
interpolation: { escapeValue: false },
|
||||
react: { useSuspense: false },
|
||||
debug: import.meta.env.MODE === 'development',
|
||||
});
|
||||
|
||||
export default i18n;
|
43
src/i18n/locales/de/auth.json
Normal file
43
src/i18n/locales/de/auth.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"account": {
|
||||
"your": "ihr Konto",
|
||||
"account_many": "Konten",
|
||||
"account_one": "Konto"
|
||||
},
|
||||
"advanced_users": "für fortgeschrittene Benutzer",
|
||||
"apikey": {
|
||||
"alternative": "Alternative: Datei auswählen",
|
||||
"change": "API-Schlüssel ändern",
|
||||
"enter": "API-Schlüssel eingeben",
|
||||
"import": "API-Schlüssel importieren",
|
||||
"key": "API-Schlüssel",
|
||||
"select_valid": "gültigen API-Schlüssel auswählen"
|
||||
},
|
||||
"authenticate": "authentifizieren",
|
||||
"build_version": "Build-Version",
|
||||
"create_account": "Konto erstellen",
|
||||
"download_account": "Konto herunterladen",
|
||||
"keep_secure": "Bewahren Sie Ihre Kontodatei sicher auf",
|
||||
"node": {
|
||||
"choose": "benutzerdefinierten Node auswählen",
|
||||
"custom_many": "benutzerdefinierte Nodes",
|
||||
"use_custom": "benutzerdefinierten Node verwenden",
|
||||
"use_local": "lokalen Node verwenden",
|
||||
"using": "verwende Node",
|
||||
"using_public": "öffentlichen Node verwenden"
|
||||
},
|
||||
"password": "Passwort",
|
||||
"password_confirmation": "Passwort bestätigen",
|
||||
"return_to_list": "zurück zur Liste",
|
||||
"wallet": {
|
||||
"password_confirmation": "Wallet-Passwort bestätigen",
|
||||
"password": "Wallet-Passwort",
|
||||
"keep_password": "aktuelles Passwort beibehalten",
|
||||
"new_password": "neues Passwort",
|
||||
"error": {
|
||||
"missing_new_password": "bitte neues Passwort eingeben",
|
||||
"missing_password": "bitte Passwort eingeben"
|
||||
}
|
||||
},
|
||||
"welcome": "willkommen bei"
|
||||
}
|
133
src/i18n/locales/de/core.json
Normal file
133
src/i18n/locales/de/core.json
Normal file
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"action": {
|
||||
"add": "hinzufügen",
|
||||
"accept": "akzeptieren",
|
||||
"backup_account": "Konto sichern",
|
||||
"backup_wallet": "Wallet sichern",
|
||||
"cancel": "abbrechen",
|
||||
"cancel_invitation": "Einladung abbrechen",
|
||||
"change": "ändern",
|
||||
"change_language": "Sprache ändern",
|
||||
"choose": "wählen",
|
||||
"close": "schließen",
|
||||
"continue": "fortfahren",
|
||||
"continue_logout": "mit dem Abmelden fortfahren",
|
||||
"create_thread": "Thread erstellen",
|
||||
"decline": "ablehnen",
|
||||
"decrypt": "entschlüsseln",
|
||||
"edit": "bearbeiten",
|
||||
"export": "exportieren",
|
||||
"import": "importieren",
|
||||
"invite": "einladen",
|
||||
"join": "beitreten",
|
||||
"logout": "abmelden",
|
||||
"new": {
|
||||
"post": "neuer Beitrag",
|
||||
"thread": "neuer Thread"
|
||||
},
|
||||
"notify": "benachrichtigen",
|
||||
"post": "veröffentlichen",
|
||||
"post_message": "Nachricht senden"
|
||||
},
|
||||
"admin": "Administrator",
|
||||
"core": {
|
||||
"block_height": "Blockhöhe",
|
||||
"information": "Kerninformationen",
|
||||
"peers": "verbundene Peers",
|
||||
"version": "Core-Version"
|
||||
},
|
||||
"ui": {
|
||||
"version": "UI-Version"
|
||||
},
|
||||
"count": {
|
||||
"none": "keine",
|
||||
"one": "eins"
|
||||
},
|
||||
"description": "Beschreibung",
|
||||
"downloading_qdn": "Herunterladen von QDN",
|
||||
"fee": {
|
||||
"payment": "Zahlungsgebühr",
|
||||
"publish": "Veröffentlichungsgebühr"
|
||||
},
|
||||
"general_settings": "allgemeine Einstellungen",
|
||||
"last_height": "letzte Höhe",
|
||||
"list": {
|
||||
"invite": "Einladungsliste",
|
||||
"join_request": "Beitrittsanfragenliste",
|
||||
"member": "Mitgliederliste"
|
||||
},
|
||||
"loading": "Lädt...",
|
||||
"loading_posts": "Beiträge werden geladen... bitte warten.",
|
||||
"message_us": "Bitte kontaktiere uns über Telegram oder Discord, wenn du 4 QORT benötigst, um ohne Einschränkungen zu chatten",
|
||||
"message": {
|
||||
"error": {
|
||||
"generic": "Ein Fehler ist aufgetreten",
|
||||
"incorrect_password": "falsches Passwort",
|
||||
"missing_field": "fehlt: {{ field }}",
|
||||
"save_qdn": "Speichern in QDN nicht möglich"
|
||||
},
|
||||
"status": {
|
||||
"minting": "(minting)",
|
||||
"not_minting": "(kein minting)",
|
||||
"synchronized": "synchronisiert",
|
||||
"synchronizing": "synchronisiere"
|
||||
},
|
||||
"success": {
|
||||
"order_submitted": "Deine Kauforder wurde übermittelt",
|
||||
"publish_qdn": "Erfolgreich in QDN veröffentlicht",
|
||||
"request_read": "Ich habe diese Anfrage gelesen",
|
||||
"transfer": "Die Übertragung war erfolgreich!"
|
||||
}
|
||||
},
|
||||
"minting_status": "Minting-Status",
|
||||
"page": {
|
||||
"last": "letzte",
|
||||
"first": "erste",
|
||||
"next": "nächste",
|
||||
"previous": "vorherige"
|
||||
},
|
||||
"payment_notification": "Zahlungsbenachrichtigung",
|
||||
"price": "Preis",
|
||||
"q_mail": "Q-Mail",
|
||||
"question": {
|
||||
"new_user": "Bist du ein neuer Benutzer?"
|
||||
},
|
||||
"save_options": {
|
||||
"no_pinned_changes": "Du hast derzeit keine Änderungen an deinen angehefteten Apps",
|
||||
"overwrite_changes": "Die App konnte deine gespeicherten angehefteten QDN-Apps nicht laden. Möchtest du diese Änderungen überschreiben?",
|
||||
"overwrite_qdn": "in QDN überschreiben",
|
||||
"publish_qdn": "Möchtest du deine Einstellungen verschlüsselt in QDN veröffentlichen?",
|
||||
"qdn": "QDN-Speicherung verwenden",
|
||||
"register_name": "Du brauchst einen registrierten Qortal-Namen, um deine angehefteten Apps in QDN zu speichern.",
|
||||
"reset_pinned": "Gefällt dir deine lokale Änderung nicht? Möchtest du auf die Standard-Apps zurücksetzen?",
|
||||
"reset_qdn": "Gefällt dir deine lokale Änderung nicht? Möchtest du auf deine in QDN gespeicherten Apps zurücksetzen?",
|
||||
"revert_default": "auf Standard zurücksetzen",
|
||||
"revert_qdn": "auf QDN zurücksetzen",
|
||||
"save_qdn": "in QDN speichern",
|
||||
"save": "speichern",
|
||||
"settings": "Du nutzt die Export-/Import-Methode zum Speichern der Einstellungen.",
|
||||
"unsaved_changes": "Du hast nicht gespeicherte Änderungen an deinen angehefteten Apps. Speichere sie in QDN."
|
||||
},
|
||||
"settings": "Einstellungen",
|
||||
"supply": "Versorgung",
|
||||
"theme": {
|
||||
"dark": "dunkler Modus",
|
||||
"light": "heller Modus"
|
||||
},
|
||||
"time": {
|
||||
"day_one": "{{count}} Tag",
|
||||
"day_other": "{{count}} Tage",
|
||||
"hour_one": "{{count}} Stunde",
|
||||
"hour_other": "{{count}} Stunden",
|
||||
"minute_one": "{{count}} Minute",
|
||||
"minute_other": "{{count}} Minuten"
|
||||
},
|
||||
"title": "Titel",
|
||||
"tutorial": "Tutorial",
|
||||
"user_lookup": "Benutzersuche",
|
||||
"wallet": {
|
||||
"wallet": "Wallet",
|
||||
"wallet_other": "Wallets"
|
||||
},
|
||||
"welcome": "Willkommen"
|
||||
}
|
105
src/i18n/locales/de/group.json
Normal file
105
src/i18n/locales/de/group.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"action": {
|
||||
"ban": "Mitglied aus der Gruppe verbannen",
|
||||
"cancel_ban": "Sperre aufheben",
|
||||
"copy_private_key": "Privaten Schlüssel kopieren",
|
||||
"create_group": "Gruppe erstellen",
|
||||
"disable_push_notifications": "Alle Push-Benachrichtigungen deaktivieren",
|
||||
"enable_dev_mode": "Entwicklermodus aktivieren",
|
||||
"export_password": "Passwort exportieren",
|
||||
"export_private_key": "Privaten Schlüssel exportieren",
|
||||
"find_group": "Gruppe finden",
|
||||
"join_group": "Gruppe beitreten",
|
||||
"kick_member": "Mitglied aus der Gruppe entfernen",
|
||||
"invite_member": "Mitglied einladen",
|
||||
"leave_group": "Gruppe verlassen",
|
||||
"load_members": "Mitglieder mit Namen laden",
|
||||
"make_admin": "Zum Administrator ernennen",
|
||||
"manage_members": "Mitglieder verwalten",
|
||||
"refetch_page": "Seite neu laden",
|
||||
"remove_admin": "Als Administrator entfernen",
|
||||
"return_to_thread": "Zu den Threads zurückkehren"
|
||||
},
|
||||
"advanced_options": "Erweiterte Optionen",
|
||||
"approval_threshold": "Genehmigungsschwelle der Gruppe (Anzahl / Prozentsatz der Administratoren, die eine Transaktion genehmigen müssen)",
|
||||
"ban_list": "Sperrliste",
|
||||
"block_delay": {
|
||||
"minimum": "Minimale Blockverzögerung für Gruppen-Transaktionsgenehmigungen",
|
||||
"maximum": "Maximale Blockverzögerung für Gruppen-Transaktionsgenehmigungen"
|
||||
},
|
||||
"group": {
|
||||
"closed": "geschlossen (privat) – Benutzer benötigen eine Erlaubnis zum Beitreten",
|
||||
"description": "Gruppenbeschreibung",
|
||||
"id": "Gruppen-ID",
|
||||
"invites": "Gruppeneinladungen",
|
||||
"management": "Gruppenverwaltung",
|
||||
"member_number": "Anzahl der Mitglieder",
|
||||
"name": "Gruppenname",
|
||||
"open": "offen (öffentlich)",
|
||||
"type": "Gruppentyp"
|
||||
},
|
||||
"invitation_expiry": "Ablaufzeit der Einladung",
|
||||
"invitees_list": "Liste der Eingeladenen",
|
||||
"join_link": "Link zum Beitritt zur Gruppe",
|
||||
"join_requests": "Beitrittsanfragen",
|
||||
"last_message": "Letzte Nachricht",
|
||||
"latest_mails": "Neueste Q-Mails",
|
||||
"message": {
|
||||
"generic": {
|
||||
"already_in_group": "Sie sind bereits in dieser Gruppe!",
|
||||
"closed_group": "Dies ist eine geschlossene/private Gruppe, daher müssen Sie warten, bis ein Administrator Ihre Anfrage akzeptiert",
|
||||
"descrypt_wallet": "Wallet wird entschlüsselt...",
|
||||
"encryption_key": "Der erste gemeinsame Verschlüsselungsschlüssel der Gruppe wird erstellt. Bitte warten Sie ein paar Minuten, bis er vom Netzwerk abgerufen wird. Überprüfung alle 2 Minuten...",
|
||||
"group_invited_you": "{{group}} hat Sie eingeladen",
|
||||
"loading_members": "Mitgliederliste mit Namen wird geladen... bitte warten.",
|
||||
"no_display": "Nichts anzuzeigen",
|
||||
"no_selection": "Keine Gruppe ausgewählt",
|
||||
"not_part_group": "Sie sind nicht Teil der verschlüsselten Mitgliedergruppe. Warten Sie, bis ein Administrator die Schlüssel neu verschlüsselt.",
|
||||
"only_encrypted": "Nur unverschlüsselte Nachrichten werden angezeigt.",
|
||||
"private_key_copied": "Privater Schlüssel kopiert",
|
||||
"provide_message": "Bitte geben Sie eine erste Nachricht für den Thread ein",
|
||||
"secure_place": "Bewahren Sie Ihren privaten Schlüssel an einem sicheren Ort auf. Nicht teilen!",
|
||||
"setting_group": "Gruppe wird eingerichtet... bitte warten."
|
||||
},
|
||||
"error": {
|
||||
"access_name": "Kann keine Nachricht senden, ohne Zugriff auf Ihren Namen",
|
||||
"descrypt_wallet": "Fehler beim Entschlüsseln der Wallet {{ :errorMessage }}",
|
||||
"description_required": "Bitte geben Sie eine Beschreibung an",
|
||||
"group_info": "Kann nicht auf Gruppeninformationen zugreifen",
|
||||
"group_secret_key": "Kann den geheimen Gruppenschlüssel nicht abrufen",
|
||||
"name_required": "Bitte geben Sie einen Namen an",
|
||||
"notify_admins": "Versuchen Sie, einen Administrator aus der untenstehenden Liste zu benachrichtigen:",
|
||||
"thread_id": "Thread-ID konnte nicht gefunden werden"
|
||||
},
|
||||
"success": {
|
||||
"group_ban": "Mitglied erfolgreich aus der Gruppe verbannt. Es kann ein paar Minuten dauern, bis die Änderungen wirksam werden",
|
||||
"group_creation": "Gruppe erfolgreich erstellt. Es kann ein paar Minuten dauern, bis die Änderungen wirksam werden",
|
||||
"group_creation_name": "Gruppe {{group_name}} erstellt: Warte auf Bestätigung",
|
||||
"group_creation_label": "Gruppe {{name}} erstellt: Erfolg!",
|
||||
"group_invite": "{{value}} erfolgreich eingeladen. Es kann ein paar Minuten dauern, bis die Änderungen wirksam werden",
|
||||
"group_join": "Beitritt zur Gruppe erfolgreich angefordert. Es kann ein paar Minuten dauern, bis die Änderungen wirksam werden",
|
||||
"group_join_name": "Gruppe {{group_name}} beigetreten: Warte auf Bestätigung",
|
||||
"group_join_label": "Gruppe {{name}} beigetreten: Erfolg!",
|
||||
"group_join_request": "Beitritt zur Gruppe {{group_name}} angefordert: Warte auf Bestätigung",
|
||||
"group_join_outcome": "Beitritt zur Gruppe {{group_name}} angefordert: Erfolg!",
|
||||
"group_kick": "Mitglied erfolgreich aus der Gruppe entfernt. Es kann ein paar Minuten dauern, bis die Änderungen wirksam werden",
|
||||
"group_leave": "Verlassen der Gruppe erfolgreich angefordert. Es kann ein paar Minuten dauern, bis die Änderungen wirksam werden",
|
||||
"group_leave_name": "Gruppe {{group_name}} verlassen: Warte auf Bestätigung",
|
||||
"group_leave_label": "Gruppe {{name}} verlassen: Erfolg!",
|
||||
"group_member_admin": "Mitglied erfolgreich zum Administrator gemacht. Es kann ein paar Minuten dauern, bis die Änderungen wirksam werden",
|
||||
"group_remove_member": "Mitglied erfolgreich als Administrator entfernt. Es kann ein paar Minuten dauern, bis die Änderungen wirksam werden",
|
||||
"invitation_cancellation": "Einladung erfolgreich storniert. Es kann ein paar Minuten dauern, bis die Änderungen wirksam werden",
|
||||
"invitation_request": "Beitrittsanfrage akzeptiert: Warte auf Bestätigung",
|
||||
"loading_threads": "Threads werden geladen... bitte warten.",
|
||||
"post_creation": "Beitrag erfolgreich erstellt. Es kann einige Zeit dauern, bis die Veröffentlichung verbreitet wird",
|
||||
"thread_creation": "Thread erfolgreich erstellt. Es kann einige Zeit dauern, bis die Veröffentlichung verbreitet wird",
|
||||
"unbanned_user": "Benutzer erfolgreich entsperrt. Es kann ein paar Minuten dauern, bis die Änderungen wirksam werden",
|
||||
"user_joined": "Benutzer erfolgreich beigetreten!"
|
||||
}
|
||||
},
|
||||
"question": {
|
||||
"perform_transaction": "Möchten Sie eine {{action}}-Transaktion durchführen?",
|
||||
"provide_thread": "Bitte geben Sie einen Thread-Titel an"
|
||||
},
|
||||
"thread_posts": "Neue Thread-Beiträge"
|
||||
}
|
21
src/i18n/locales/de/tutorial.json
Normal file
21
src/i18n/locales/de/tutorial.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"1_getting_started": "1. Erste Schritte",
|
||||
"2_overview": "2. Überblick",
|
||||
"3_groups": "3. Qortal-Gruppen",
|
||||
"4_obtain_qort": "4. QORT erhalten",
|
||||
"account_creation": "Kontoerstellung",
|
||||
"important_info": "wichtige Informationen!",
|
||||
"apps": {
|
||||
"dashboard": "1. App-Dashboard",
|
||||
"navigation": "2. App-Navigation"
|
||||
},
|
||||
"initial": {
|
||||
"6_qort": "mindestens 6 QORT im Wallet haben",
|
||||
"explore": "erkunden",
|
||||
"general_chat": "allgemeiner Chat",
|
||||
"getting_started": "erste Schritte",
|
||||
"register_name": "einen Namen registrieren",
|
||||
"see_apps": "apps ansehen",
|
||||
"trade_qort": "QORT handeln"
|
||||
}
|
||||
}
|
48
src/i18n/locales/en/auth.json
Normal file
48
src/i18n/locales/en/auth.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"account": {
|
||||
"your": "your account",
|
||||
"account_many": "accounts",
|
||||
"account_one": "account"
|
||||
},
|
||||
"advanced_users": "for advanced users",
|
||||
"apikey": {
|
||||
"alternative": "alternative: File select",
|
||||
"change": "change APIkey",
|
||||
"enter": "enter APIkey",
|
||||
"import": "import APIkey",
|
||||
"key": "API key",
|
||||
"select_valid": "select a valid apikey"
|
||||
},
|
||||
"authenticate": "authenticate",
|
||||
"build_version": "build version",
|
||||
"create_account": "create account",
|
||||
"download_account": "download account",
|
||||
"keep_secure": "keep your account file secure",
|
||||
"node": {
|
||||
"choose": "choose custom node",
|
||||
"custom_many": "custom nodes",
|
||||
"use_custom": "use custom node",
|
||||
"use_local": "use local node",
|
||||
"using": "using node",
|
||||
"using_public": "using public node"
|
||||
},
|
||||
"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",
|
||||
"keep_password": "keep current password",
|
||||
"new_password": "new password",
|
||||
"error": {
|
||||
"missing_new_password": "please enter a new password",
|
||||
"missing_password": "please enter your password"
|
||||
}
|
||||
},
|
||||
"welcome": "welcome to"
|
||||
}
|
133
src/i18n/locales/en/core.json
Normal file
133
src/i18n/locales/en/core.json
Normal file
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"action": {
|
||||
"add": "add",
|
||||
"accept": "accept",
|
||||
"backup_account": "backup account",
|
||||
"backup_wallet": "backup wallet",
|
||||
"cancel": "cancel",
|
||||
"cancel_invitation": "cancel invitation",
|
||||
"change": "change",
|
||||
"change_language": "change language",
|
||||
"choose": "choose",
|
||||
"close": "close",
|
||||
"continue": "continue",
|
||||
"continue_logout": "continue to logout",
|
||||
"create_thread": "create thread",
|
||||
"decline": "decline",
|
||||
"decrypt": "decrypt",
|
||||
"edit": "edit",
|
||||
"export": "export",
|
||||
"import": "import",
|
||||
"invite": "invite",
|
||||
"join": "join",
|
||||
"logout": "logout",
|
||||
"new": {
|
||||
"post": "new post",
|
||||
"thread": "new thread"
|
||||
},
|
||||
"notify": "notify",
|
||||
"post": "post",
|
||||
"post_message": "post message"
|
||||
},
|
||||
"admin": "admin",
|
||||
"core": {
|
||||
"block_height": "block height",
|
||||
"information": "core information",
|
||||
"peers": "connected peers",
|
||||
"version": "core version"
|
||||
},
|
||||
"ui": {
|
||||
"version": "UI version"
|
||||
},
|
||||
"count": {
|
||||
"none": "none",
|
||||
"one": "one"
|
||||
},
|
||||
"description": "description",
|
||||
"downloading_qdn": "downloading from QDN",
|
||||
"fee": {
|
||||
"payment": "payment fee",
|
||||
"publish": "publish fee"
|
||||
},
|
||||
"general_settings": "general settings",
|
||||
"last_height": "last height",
|
||||
"list": {
|
||||
"invite": "invite list",
|
||||
"join_request": "join request list",
|
||||
"member": "member list"
|
||||
},
|
||||
"loading": "loading...",
|
||||
"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",
|
||||
"message": {
|
||||
"error": {
|
||||
"generic": "an error occurred",
|
||||
"incorrect_password": "incorrect password",
|
||||
"missing_field": "missing: {{ field }}",
|
||||
"save_qdn": "unable to save to QDN"
|
||||
},
|
||||
"status": {
|
||||
"minting": "(minting)",
|
||||
"not_minting": "(not minting)",
|
||||
"synchronized": "synchronized",
|
||||
"synchronizing": "synchronizing"
|
||||
},
|
||||
"success": {
|
||||
"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!"
|
||||
}
|
||||
},
|
||||
"minting_status": "minting status",
|
||||
"page": {
|
||||
"last": "last",
|
||||
"first": "first",
|
||||
"next": "next",
|
||||
"previous": "previous"
|
||||
},
|
||||
"payment_notification": "payment notification",
|
||||
"price": "price",
|
||||
"q_mail": "q-mail",
|
||||
"question": {
|
||||
"new_user": "are you a new user?"
|
||||
},
|
||||
"save_options": {
|
||||
"no_pinned_changes": "you currently do not have any changes to your pinned apps",
|
||||
"overwrite_changes": "the app was unable to download your existing QDN-saved pinned apps. Would you like to overwrite those changes?",
|
||||
"overwrite_qdn": "overwrite to QDN",
|
||||
"publish_qdn": "would you like to publish your settings to QDN (encrypted)?",
|
||||
"qdn": "use QDN saving",
|
||||
"register_name": "you need a registered Qortal name to save your pinned apps to QDN.",
|
||||
"reset_pinned": "don't like your current local changes? Would you like to reset to the default pinned apps?",
|
||||
"reset_qdn": "don't like your current local changes? Would you like to reset to your saved QDN pinned apps?",
|
||||
"revert_default": "revert to default",
|
||||
"revert_qdn": "revert to QDN",
|
||||
"save_qdn": "save to QDN",
|
||||
"save": "save",
|
||||
"settings": "you are using the export/import way of saving settings.",
|
||||
"unsaved_changes": " you have unsaved changes to your pinned apps. Save them to QDN."
|
||||
},
|
||||
"settings": "settings",
|
||||
"supply": "supply",
|
||||
"theme": {
|
||||
"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": {
|
||||
"wallet": "wallet",
|
||||
"wallet_other": "wallets"
|
||||
},
|
||||
"welcome": "welcome"
|
||||
}
|
105
src/i18n/locales/en/group.json
Normal file
105
src/i18n/locales/en/group.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"action": {
|
||||
"ban": "ban member from group",
|
||||
"cancel_ban": "cancel ban",
|
||||
"copy_private_key": "copy private key",
|
||||
"create_group": "create group",
|
||||
"disable_push_notifications": "disable all push notifications",
|
||||
"enable_dev_mode": "enable dev mode",
|
||||
"export_password": "export password",
|
||||
"export_private_key": "export private key",
|
||||
"find_group": "find group",
|
||||
"join_group": "join group",
|
||||
"kick_member": "kick member from group",
|
||||
"invite_member": "invite member",
|
||||
"leave_group": "leave group",
|
||||
"load_members": "load members with names",
|
||||
"make_admin": "make an admin",
|
||||
"manage_members": "manage members",
|
||||
"refetch_page": "refetch page",
|
||||
"remove_admin": "remove as admin",
|
||||
"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",
|
||||
"id": "group id",
|
||||
"invites": "group invites",
|
||||
"management": "group management",
|
||||
"member_number": "number of members",
|
||||
"name": "group name",
|
||||
"open": "open (public)",
|
||||
"type": "group type"
|
||||
},
|
||||
"invitation_expiry": "invitation Expiry Time",
|
||||
"invitees_list": "invitees list",
|
||||
"join_link": "join group link",
|
||||
"join_requests": "join requests",
|
||||
"last_message": "last message",
|
||||
"latest_mails": "latest Q-Mails",
|
||||
"message": {
|
||||
"generic": {
|
||||
"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",
|
||||
"loading_members": "loading member list with names... please wait.",
|
||||
"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.",
|
||||
"private_key_copied": "private key copied",
|
||||
"provide_message": "please provide a first message to the thread",
|
||||
"secure_place": "keep your private key in a secure place. Do not share!",
|
||||
"setting_group": "setting up group... please wait."
|
||||
},
|
||||
"error": {
|
||||
"access_name": "cannot send a message without a access to your name",
|
||||
"descrypt_wallet": "error decrypting wallet {{ :errorMessage }}",
|
||||
"description_required": "please provide a description",
|
||||
"group_info": "cannot access group information",
|
||||
"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"
|
||||
},
|
||||
"success": {
|
||||
"group_ban": "successfully banned member from group. It may take a couple of minutes for the changes to propagate",
|
||||
"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",
|
||||
"group_join": "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!",
|
||||
"group_join_request": "requested to join Group {{group_name}}: awaiting confirmation",
|
||||
"group_join_outcome": "requested to join Group {{group_name}}: success!",
|
||||
"group_kick": "successfully kicked member from group. It may take a couple of minutes for the changes to propagate",
|
||||
"group_leave": "successfully requested to leave group. It may take a couple of minutes for the changes to propagate",
|
||||
"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_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",
|
||||
"loading_threads": "loading threads... please wait.",
|
||||
"post_creation": "successfully created post. It may take some time for the publish to propagate",
|
||||
"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!"
|
||||
}
|
||||
},
|
||||
"question": {
|
||||
"perform_transaction": "would you like to perform a {{action}} transaction?",
|
||||
"provide_thread": "please provide a thread title"
|
||||
},
|
||||
"thread_posts": "new thread posts"
|
||||
}
|
21
src/i18n/locales/en/tutorial.json
Normal file
21
src/i18n/locales/en/tutorial.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"1_getting_started": "1. Getting Started",
|
||||
"2_overview": "2. Overview",
|
||||
"3_groups": "3. Qortal Groups",
|
||||
"4_obtain_qort": "4. Obtaining Qort",
|
||||
"account_creation": "account creation",
|
||||
"important_info": "important information!",
|
||||
"apps": {
|
||||
"dashboard": "1. Apps Dashboard",
|
||||
"navigation": "2. Apps Navigation"
|
||||
},
|
||||
"initial": {
|
||||
"6_qort": "have at least 6 QORT in your wallet",
|
||||
"explore": "explore",
|
||||
"general_chat": "general chat",
|
||||
"getting_started": "getting started",
|
||||
"register_name": "register a name",
|
||||
"see_apps": "see apps",
|
||||
"trade_qort": "trade QORT"
|
||||
}
|
||||
}
|
43
src/i18n/locales/es/auth.json
Normal file
43
src/i18n/locales/es/auth.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"account": {
|
||||
"your": "tu cuenta",
|
||||
"account_many": "cuentas",
|
||||
"account_one": "cuenta"
|
||||
},
|
||||
"advanced_users": "para usuarios avanzados",
|
||||
"apikey": {
|
||||
"alternative": "alternativa: Seleccionar archivo",
|
||||
"change": "cambiar clave API",
|
||||
"enter": "ingresar clave API",
|
||||
"import": "importar clave API",
|
||||
"key": "clave API",
|
||||
"select_valid": "selecciona una clave API válida"
|
||||
},
|
||||
"authenticate": "autenticar",
|
||||
"build_version": "versión de compilación",
|
||||
"create_account": "crear cuenta",
|
||||
"download_account": "descargar cuenta",
|
||||
"keep_secure": "mantén tu archivo de cuenta seguro",
|
||||
"node": {
|
||||
"choose": "elegir nodo personalizado",
|
||||
"custom_many": "nodos personalizados",
|
||||
"use_custom": "usar nodo personalizado",
|
||||
"use_local": "usar nodo local",
|
||||
"using": "usando nodo",
|
||||
"using_public": "usando nodo público"
|
||||
},
|
||||
"password": "contraseña",
|
||||
"password_confirmation": "confirmar contraseña",
|
||||
"return_to_list": "volver a la lista",
|
||||
"wallet": {
|
||||
"password_confirmation": "confirmar contraseña del monedero",
|
||||
"password": "contraseña del monedero",
|
||||
"keep_password": "mantener la contraseña actual",
|
||||
"new_password": "nueva contraseña",
|
||||
"error": {
|
||||
"missing_new_password": "por favor ingresa una nueva contraseña",
|
||||
"missing_password": "por favor ingresa tu contraseña"
|
||||
}
|
||||
},
|
||||
"welcome": "bienvenido a"
|
||||
}
|
133
src/i18n/locales/es/core.json
Normal file
133
src/i18n/locales/es/core.json
Normal file
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"action": {
|
||||
"add": "añadir",
|
||||
"accept": "aceptar",
|
||||
"backup_account": "respaldar cuenta",
|
||||
"backup_wallet": "respaldar monedero",
|
||||
"cancel": "cancelar",
|
||||
"cancel_invitation": "cancelar invitación",
|
||||
"change": "cambiar",
|
||||
"change_language": "cambiar idioma",
|
||||
"choose": "elegir",
|
||||
"close": "cerrar",
|
||||
"continue": "continuar",
|
||||
"continue_logout": "continuar para cerrar sesión",
|
||||
"create_thread": "crear hilo",
|
||||
"decline": "rechazar",
|
||||
"decrypt": "descifrar",
|
||||
"edit": "editar",
|
||||
"export": "exportar",
|
||||
"import": "importar",
|
||||
"invite": "invitar",
|
||||
"join": "unirse",
|
||||
"logout": "cerrar sesión",
|
||||
"new": {
|
||||
"post": "nueva publicación",
|
||||
"thread": "nuevo hilo"
|
||||
},
|
||||
"notify": "notificar",
|
||||
"post": "publicar",
|
||||
"post_message": "enviar mensaje"
|
||||
},
|
||||
"admin": "administrador",
|
||||
"core": {
|
||||
"block_height": "altura del bloque",
|
||||
"information": "información del núcleo",
|
||||
"peers": "pares conectados",
|
||||
"version": "versión del núcleo"
|
||||
},
|
||||
"ui": {
|
||||
"version": "versión de la interfaz"
|
||||
},
|
||||
"count": {
|
||||
"none": "ninguno",
|
||||
"one": "uno"
|
||||
},
|
||||
"description": "descripción",
|
||||
"downloading_qdn": "descargando desde QDN",
|
||||
"fee": {
|
||||
"payment": "tarifa de pago",
|
||||
"publish": "tarifa de publicación"
|
||||
},
|
||||
"general_settings": "configuración general",
|
||||
"last_height": "última altura",
|
||||
"list": {
|
||||
"invite": "lista de invitaciones",
|
||||
"join_request": "lista de solicitudes de unión",
|
||||
"member": "lista de miembros"
|
||||
},
|
||||
"loading": "cargando...",
|
||||
"loading_posts": "cargando publicaciones... por favor espera.",
|
||||
"message_us": "por favor contáctanos en Telegram o Discord si necesitas 4 QORT para comenzar a chatear sin limitaciones",
|
||||
"message": {
|
||||
"error": {
|
||||
"generic": "ocurrió un error",
|
||||
"incorrect_password": "contraseña incorrecta",
|
||||
"missing_field": "falta: {{ field }}",
|
||||
"save_qdn": "no se pudo guardar en QDN"
|
||||
},
|
||||
"status": {
|
||||
"minting": "(generando)",
|
||||
"not_minting": "(no generando)",
|
||||
"synchronized": "sincronizado",
|
||||
"synchronizing": "sincronizando"
|
||||
},
|
||||
"success": {
|
||||
"order_submitted": "tu orden de compra fue enviada",
|
||||
"publish_qdn": "publicado correctamente en QDN",
|
||||
"request_read": "he leído esta solicitud",
|
||||
"transfer": "¡la transferencia fue exitosa!"
|
||||
}
|
||||
},
|
||||
"minting_status": "estado de generación",
|
||||
"page": {
|
||||
"last": "última",
|
||||
"first": "primera",
|
||||
"next": "siguiente",
|
||||
"previous": "anterior"
|
||||
},
|
||||
"payment_notification": "notificación de pago",
|
||||
"price": "precio",
|
||||
"q_mail": "q-mail",
|
||||
"question": {
|
||||
"new_user": "¿eres un usuario nuevo?"
|
||||
},
|
||||
"save_options": {
|
||||
"no_pinned_changes": "actualmente no tienes cambios en tus aplicaciones fijadas",
|
||||
"overwrite_changes": "la aplicación no pudo descargar tus aplicaciones fijadas guardadas en QDN. ¿Deseas sobrescribir esos cambios?",
|
||||
"overwrite_qdn": "sobrescribir en QDN",
|
||||
"publish_qdn": "¿quieres publicar tu configuración en QDN (cifrada)?",
|
||||
"qdn": "usar guardado QDN",
|
||||
"register_name": "necesitas un nombre de Qortal registrado para guardar tus aplicaciones fijadas en QDN.",
|
||||
"reset_pinned": "¿no te gustan tus cambios locales actuales? ¿Quieres restablecer las aplicaciones fijadas por defecto?",
|
||||
"reset_qdn": "¿no te gustan tus cambios locales actuales? ¿Quieres restablecer tus aplicaciones fijadas guardadas en QDN?",
|
||||
"revert_default": "restablecer a valores predeterminados",
|
||||
"revert_qdn": "restablecer desde QDN",
|
||||
"save_qdn": "guardar en QDN",
|
||||
"save": "guardar",
|
||||
"settings": "estás usando el método de exportación/importación para guardar la configuración.",
|
||||
"unsaved_changes": "tienes cambios no guardados en tus aplicaciones fijadas. Guárdalos en QDN."
|
||||
},
|
||||
"settings": "configuración",
|
||||
"supply": "oferta",
|
||||
"theme": {
|
||||
"dark": "modo oscuro",
|
||||
"light": "modo claro"
|
||||
},
|
||||
"time": {
|
||||
"day_one": "{{count}} día",
|
||||
"day_other": "{{count}} días",
|
||||
"hour_one": "{{count}} hora",
|
||||
"hour_other": "{{count}} horas",
|
||||
"minute_one": "{{count}} minuto",
|
||||
"minute_other": "{{count}} minutos"
|
||||
},
|
||||
"title": "título",
|
||||
"tutorial": "tutorial",
|
||||
"user_lookup": "búsqueda de usuario",
|
||||
"wallet": {
|
||||
"wallet": "monedero",
|
||||
"wallet_other": "monederos"
|
||||
},
|
||||
"welcome": "bienvenido"
|
||||
}
|
105
src/i18n/locales/es/group.json
Normal file
105
src/i18n/locales/es/group.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"action": {
|
||||
"ban": "expulsar miembro del grupo",
|
||||
"cancel_ban": "cancelar expulsión",
|
||||
"copy_private_key": "copiar clave privada",
|
||||
"create_group": "crear grupo",
|
||||
"disable_push_notifications": "desactivar todas las notificaciones push",
|
||||
"enable_dev_mode": "activar modo desarrollador",
|
||||
"export_password": "exportar contraseña",
|
||||
"export_private_key": "exportar clave privada",
|
||||
"find_group": "encontrar grupo",
|
||||
"join_group": "unirse al grupo",
|
||||
"kick_member": "expulsar miembro del grupo",
|
||||
"invite_member": "invitar miembro",
|
||||
"leave_group": "salir del grupo",
|
||||
"load_members": "cargar miembros con nombres",
|
||||
"make_admin": "hacer administrador",
|
||||
"manage_members": "administrar miembros",
|
||||
"refetch_page": "recargar página",
|
||||
"remove_admin": "quitar como administrador",
|
||||
"return_to_thread": "volver a los hilos"
|
||||
},
|
||||
"advanced_options": "opciones avanzadas",
|
||||
"approval_threshold": "umbral de aprobación del grupo (número / porcentaje de administradores que deben aprobar una transacción)",
|
||||
"ban_list": "lista de expulsados",
|
||||
"block_delay": {
|
||||
"minimum": "retardo mínimo de bloque para aprobaciones de transacciones de grupo",
|
||||
"maximum": "retardo máximo de bloque para aprobaciones de transacciones de grupo"
|
||||
},
|
||||
"group": {
|
||||
"closed": "cerrado (privado) - los usuarios necesitan permiso para unirse",
|
||||
"description": "descripción del grupo",
|
||||
"id": "ID del grupo",
|
||||
"invites": "invitaciones del grupo",
|
||||
"management": "gestión del grupo",
|
||||
"member_number": "número de miembros",
|
||||
"name": "nombre del grupo",
|
||||
"open": "abierto (público)",
|
||||
"type": "tipo de grupo"
|
||||
},
|
||||
"invitation_expiry": "tiempo de expiración de la invitación",
|
||||
"invitees_list": "lista de invitados",
|
||||
"join_link": "enlace para unirse al grupo",
|
||||
"join_requests": "solicitudes de ingreso",
|
||||
"last_message": "último mensaje",
|
||||
"latest_mails": "últimos Q-Mails",
|
||||
"message": {
|
||||
"generic": {
|
||||
"already_in_group": "¡ya estás en este grupo!",
|
||||
"closed_group": "este es un grupo cerrado/privado, debes esperar a que un administrador acepte tu solicitud",
|
||||
"descrypt_wallet": "descifrando billetera...",
|
||||
"encryption_key": "la primera clave de cifrado común del grupo se está creando. Por favor, espera unos minutos a que la red la recupere. Verificando cada 2 minutos...",
|
||||
"group_invited_you": "{{group}} te ha invitado",
|
||||
"loading_members": "cargando lista de miembros con nombres... por favor espera.",
|
||||
"no_display": "nada que mostrar",
|
||||
"no_selection": "ningún grupo seleccionado",
|
||||
"not_part_group": "no eres parte del grupo cifrado de miembros. Espera a que un administrador vuelva a cifrar las claves.",
|
||||
"only_encrypted": "solo se mostrarán mensajes no cifrados.",
|
||||
"private_key_copied": "clave privada copiada",
|
||||
"provide_message": "por favor proporciona un primer mensaje para el hilo",
|
||||
"secure_place": "guarda tu clave privada en un lugar seguro. ¡No la compartas!",
|
||||
"setting_group": "configurando grupo... por favor espera."
|
||||
},
|
||||
"error": {
|
||||
"access_name": "no se puede enviar un mensaje sin acceso a tu nombre",
|
||||
"descrypt_wallet": "error al descifrar la billetera {{ :errorMessage }}",
|
||||
"description_required": "por favor proporciona una descripción",
|
||||
"group_info": "no se puede acceder a la información del grupo",
|
||||
"group_secret_key": "no se puede obtener la clave secreta del grupo",
|
||||
"name_required": "por favor proporciona un nombre",
|
||||
"notify_admins": "intenta notificar a un administrador de la lista a continuación:",
|
||||
"thread_id": "no se puede encontrar el ID del hilo"
|
||||
},
|
||||
"success": {
|
||||
"group_ban": "miembro expulsado del grupo con éxito. Puede tardar unos minutos en propagarse",
|
||||
"group_creation": "grupo creado con éxito. Puede tardar unos minutos en propagarse",
|
||||
"group_creation_name": "grupo {{group_name}} creado: esperando confirmación",
|
||||
"group_creation_label": "grupo {{name}} creado: ¡éxito!",
|
||||
"group_invite": "{{value}} invitado con éxito. Puede tardar unos minutos en propagarse",
|
||||
"group_join": "solicitud de ingreso enviada con éxito. Puede tardar unos minutos en propagarse",
|
||||
"group_join_name": "unido al grupo {{group_name}}: esperando confirmación",
|
||||
"group_join_label": "unido al grupo {{name}}: ¡éxito!",
|
||||
"group_join_request": "solicitud de ingreso al grupo {{group_name}}: esperando confirmación",
|
||||
"group_join_outcome": "solicitud de ingreso al grupo {{group_name}}: ¡éxito!",
|
||||
"group_kick": "miembro expulsado del grupo con éxito. Puede tardar unos minutos en propagarse",
|
||||
"group_leave": "solicitud de salida del grupo enviada con éxito. Puede tardar unos minutos en propagarse",
|
||||
"group_leave_name": "salido del grupo {{group_name}}: esperando confirmación",
|
||||
"group_leave_label": "salido del grupo {{name}}: ¡éxito!",
|
||||
"group_member_admin": "miembro convertido en administrador con éxito. Puede tardar unos minutos en propagarse",
|
||||
"group_remove_member": "miembro eliminado como administrador con éxito. Puede tardar unos minutos en propagarse",
|
||||
"invitation_cancellation": "invitación cancelada con éxito. Puede tardar unos minutos en propagarse",
|
||||
"invitation_request": "solicitud de ingreso aceptada: esperando confirmación",
|
||||
"loading_threads": "cargando hilos... por favor espera.",
|
||||
"post_creation": "publicación creada con éxito. Puede tardar en propagarse",
|
||||
"thread_creation": "hilo creado con éxito. Puede tardar en propagarse",
|
||||
"unbanned_user": "usuario desbloqueado con éxito. Puede tardar unos minutos en propagarse",
|
||||
"user_joined": "¡usuario se ha unido con éxito!"
|
||||
}
|
||||
},
|
||||
"question": {
|
||||
"perform_transaction": "¿quieres realizar una transacción de {{action}}?",
|
||||
"provide_thread": "por favor proporciona un título para el hilo"
|
||||
},
|
||||
"thread_posts": "nuevas publicaciones en el hilo"
|
||||
}
|
21
src/i18n/locales/es/tutorial.json
Normal file
21
src/i18n/locales/es/tutorial.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"1_getting_started": "1. Comenzando",
|
||||
"2_overview": "2. Visión general",
|
||||
"3_groups": "3. Grupos de Qortal",
|
||||
"4_obtain_qort": "4. Obtener QORT",
|
||||
"account_creation": "creación de cuenta",
|
||||
"important_info": "¡información importante!",
|
||||
"apps": {
|
||||
"dashboard": "1. Panel de aplicaciones",
|
||||
"navigation": "2. Navegación de aplicaciones"
|
||||
},
|
||||
"initial": {
|
||||
"6_qort": "tener al menos 6 QORT en tu monedero",
|
||||
"explore": "explorar",
|
||||
"general_chat": "chat general",
|
||||
"getting_started": "comenzando",
|
||||
"register_name": "registrar un nombre",
|
||||
"see_apps": "ver aplicaciones",
|
||||
"trade_qort": "intercambiar QORT"
|
||||
}
|
||||
}
|
43
src/i18n/locales/fr/auth.json
Normal file
43
src/i18n/locales/fr/auth.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"account": {
|
||||
"your": "ton compte",
|
||||
"account_many": "comptes",
|
||||
"account_one": "compte"
|
||||
},
|
||||
"advanced_users": "pour les utilisateurs avancés",
|
||||
"apikey": {
|
||||
"alternative": "alternative : Sélectionner un fichier",
|
||||
"change": "changer la clé API",
|
||||
"enter": "entrer la clé API",
|
||||
"import": "importer la clé API",
|
||||
"key": "clé API",
|
||||
"select_valid": "sélectionnez une clé API valide"
|
||||
},
|
||||
"authenticate": "authentifier",
|
||||
"build_version": "version de build",
|
||||
"create_account": "créer un compte",
|
||||
"download_account": "télécharger le compte",
|
||||
"keep_secure": "Gardez votre fichier de compte en sécurité",
|
||||
"node": {
|
||||
"choose": "choisir un nœud personnalisé",
|
||||
"custom_many": "nœuds personnalisés",
|
||||
"use_custom": "utiliser un nœud personnalisé",
|
||||
"use_local": "utiliser un nœud local",
|
||||
"using": "utilise le nœud",
|
||||
"using_public": "utilise un nœud public"
|
||||
},
|
||||
"password": "mot de passe",
|
||||
"password_confirmation": "confirmer le mot de passe",
|
||||
"return_to_list": "retour à la liste",
|
||||
"wallet": {
|
||||
"password_confirmation": "confirmer le mot de passe du portefeuille",
|
||||
"password": "mot de passe du portefeuille",
|
||||
"keep_password": "garder le mot de passe actuel",
|
||||
"new_password": "nouveau mot de passe",
|
||||
"error": {
|
||||
"missing_new_password": "veuillez entrer un nouveau mot de passe",
|
||||
"missing_password": "veuillez entrer votre mot de passe"
|
||||
}
|
||||
},
|
||||
"welcome": "bienvenue sur"
|
||||
}
|
133
src/i18n/locales/fr/core.json
Normal file
133
src/i18n/locales/fr/core.json
Normal file
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"action": {
|
||||
"add": "ajouter",
|
||||
"accept": "accepter",
|
||||
"backup_account": "sauvegarder le compte",
|
||||
"backup_wallet": "sauvegarder le portefeuille",
|
||||
"cancel": "annuler",
|
||||
"cancel_invitation": "annuler l'invitation",
|
||||
"change": "changer",
|
||||
"change_language": "changer de langue",
|
||||
"choose": "choisir",
|
||||
"close": "fermer",
|
||||
"continue": "continuer",
|
||||
"continue_logout": "continuer la déconnexion",
|
||||
"create_thread": "créer un fil",
|
||||
"decline": "refuser",
|
||||
"decrypt": "déchiffrer",
|
||||
"edit": "modifier",
|
||||
"export": "exporter",
|
||||
"import": "importer",
|
||||
"invite": "inviter",
|
||||
"join": "rejoindre",
|
||||
"logout": "se déconnecter",
|
||||
"new": {
|
||||
"post": "nouveau message",
|
||||
"thread": "nouveau fil"
|
||||
},
|
||||
"notify": "notifier",
|
||||
"post": "publier",
|
||||
"post_message": "envoyer un message"
|
||||
},
|
||||
"admin": "administrateur",
|
||||
"core": {
|
||||
"block_height": "hauteur de bloc",
|
||||
"information": "informations du noyau",
|
||||
"peers": "pairs connectés",
|
||||
"version": "version du noyau"
|
||||
},
|
||||
"ui": {
|
||||
"version": "version de l'interface"
|
||||
},
|
||||
"count": {
|
||||
"none": "aucun",
|
||||
"one": "un"
|
||||
},
|
||||
"description": "description",
|
||||
"downloading_qdn": "téléchargement depuis QDN",
|
||||
"fee": {
|
||||
"payment": "frais de paiement",
|
||||
"publish": "frais de publication"
|
||||
},
|
||||
"general_settings": "paramètres généraux",
|
||||
"last_height": "dernière hauteur",
|
||||
"list": {
|
||||
"invite": "liste d'invitations",
|
||||
"join_request": "liste des demandes d'adhésion",
|
||||
"member": "liste des membres"
|
||||
},
|
||||
"loading": "chargement...",
|
||||
"loading_posts": "chargement des messages... veuillez patienter.",
|
||||
"message_us": "veuillez nous contacter sur Telegram ou Discord si vous avez besoin de 4 QORT pour commencer à discuter sans limites",
|
||||
"message": {
|
||||
"error": {
|
||||
"generic": "une erreur est survenue",
|
||||
"incorrect_password": "mot de passe incorrect",
|
||||
"missing_field": "champ manquant : {{ field }}",
|
||||
"save_qdn": "impossible d'enregistrer dans QDN"
|
||||
},
|
||||
"status": {
|
||||
"minting": "(en cours de minage)",
|
||||
"not_minting": "(non miné)",
|
||||
"synchronized": "synchronisé",
|
||||
"synchronizing": "synchronisation"
|
||||
},
|
||||
"success": {
|
||||
"order_submitted": "votre ordre d'achat a été envoyé",
|
||||
"publish_qdn": "publié avec succès sur QDN",
|
||||
"request_read": "j'ai lu cette demande",
|
||||
"transfer": "le transfert a réussi !"
|
||||
}
|
||||
},
|
||||
"minting_status": "état du minage",
|
||||
"page": {
|
||||
"last": "dernier",
|
||||
"first": "premier",
|
||||
"next": "suivant",
|
||||
"previous": "précédent"
|
||||
},
|
||||
"payment_notification": "notification de paiement",
|
||||
"price": "prix",
|
||||
"q_mail": "q-mail",
|
||||
"question": {
|
||||
"new_user": "êtes-vous un nouvel utilisateur ?"
|
||||
},
|
||||
"save_options": {
|
||||
"no_pinned_changes": "vous n'avez actuellement aucun changement dans vos applications épinglées",
|
||||
"overwrite_changes": "l'application n'a pas pu télécharger vos applications épinglées enregistrées sur QDN. Voulez-vous écraser ces modifications ?",
|
||||
"overwrite_qdn": "écraser sur QDN",
|
||||
"publish_qdn": "voulez-vous publier vos paramètres sur QDN (chiffrés) ?",
|
||||
"qdn": "utiliser l'enregistrement QDN",
|
||||
"register_name": "vous devez avoir un nom Qortal enregistré pour enregistrer vos applications épinglées sur QDN.",
|
||||
"reset_pinned": "vous n'aimez pas vos changements locaux actuels ? Voulez-vous réinitialiser les applications par défaut ?",
|
||||
"reset_qdn": "vous n'aimez pas vos changements locaux actuels ? Voulez-vous restaurer les applications enregistrées sur QDN ?",
|
||||
"revert_default": "réinitialiser aux valeurs par défaut",
|
||||
"revert_qdn": "restaurer depuis QDN",
|
||||
"save_qdn": "enregistrer sur QDN",
|
||||
"save": "enregistrer",
|
||||
"settings": "vous utilisez la méthode d'exportation/importation pour sauvegarder les paramètres.",
|
||||
"unsaved_changes": "vous avez des modifications non enregistrées dans vos applications épinglées. Enregistrez-les sur QDN."
|
||||
},
|
||||
"settings": "paramètres",
|
||||
"supply": "approvisionnement",
|
||||
"theme": {
|
||||
"dark": "mode sombre",
|
||||
"light": "mode clair"
|
||||
},
|
||||
"time": {
|
||||
"day_one": "{{count}} jour",
|
||||
"day_other": "{{count}} jours",
|
||||
"hour_one": "{{count}} heure",
|
||||
"hour_other": "{{count}} heures",
|
||||
"minute_one": "{{count}} minute",
|
||||
"minute_other": "{{count}} minutes"
|
||||
},
|
||||
"title": "titre",
|
||||
"tutorial": "tutoriel",
|
||||
"user_lookup": "recherche d'utilisateur",
|
||||
"wallet": {
|
||||
"wallet": "portefeuille",
|
||||
"wallet_other": "portefeuilles"
|
||||
},
|
||||
"welcome": "bienvenue"
|
||||
}
|
105
src/i18n/locales/fr/group.json
Normal file
105
src/i18n/locales/fr/group.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"action": {
|
||||
"ban": "bannir un membre du groupe",
|
||||
"cancel_ban": "annuler le bannissement",
|
||||
"copy_private_key": "copier la clé privée",
|
||||
"create_group": "créer un groupe",
|
||||
"disable_push_notifications": "désactiver toutes les notifications push",
|
||||
"enable_dev_mode": "activer le mode développeur",
|
||||
"export_password": "exporter le mot de passe",
|
||||
"export_private_key": "exporter la clé privée",
|
||||
"find_group": "trouver un groupe",
|
||||
"join_group": "rejoindre le groupe",
|
||||
"kick_member": "expulser un membre du groupe",
|
||||
"invite_member": "inviter un membre",
|
||||
"leave_group": "quitter le groupe",
|
||||
"load_members": "charger les membres avec noms",
|
||||
"make_admin": "nommer administrateur",
|
||||
"manage_members": "gérer les membres",
|
||||
"refetch_page": "rafraîchir la page",
|
||||
"remove_admin": "retirer les droits d'admin",
|
||||
"return_to_thread": "retourner aux discussions"
|
||||
},
|
||||
"advanced_options": "options avancées",
|
||||
"approval_threshold": "seuil d'approbation du groupe (nombre / pourcentage d'administrateurs devant approuver une transaction)",
|
||||
"ban_list": "liste des bannis",
|
||||
"block_delay": {
|
||||
"minimum": "délai de bloc minimum pour l'approbation des transactions de groupe",
|
||||
"maximum": "délai de bloc maximum pour l'approbation des transactions de groupe"
|
||||
},
|
||||
"group": {
|
||||
"closed": "fermé (privé) – les utilisateurs ont besoin d'une autorisation pour rejoindre",
|
||||
"description": "description du groupe",
|
||||
"id": "ID du groupe",
|
||||
"invites": "invitations du groupe",
|
||||
"management": "gestion du groupe",
|
||||
"member_number": "nombre de membres",
|
||||
"name": "nom du groupe",
|
||||
"open": "ouvert (public)",
|
||||
"type": "type de groupe"
|
||||
},
|
||||
"invitation_expiry": "délai d'expiration de l'invitation",
|
||||
"invitees_list": "liste des invités",
|
||||
"join_link": "lien pour rejoindre le groupe",
|
||||
"join_requests": "demandes d’adhésion",
|
||||
"last_message": "dernier message",
|
||||
"latest_mails": "derniers Q-Mails",
|
||||
"message": {
|
||||
"generic": {
|
||||
"already_in_group": "vous êtes déjà dans ce groupe !",
|
||||
"closed_group": "ce groupe est fermé/privé, vous devez attendre qu’un administrateur accepte votre demande",
|
||||
"descrypt_wallet": "déchiffrement du portefeuille...",
|
||||
"encryption_key": "la première clé de chiffrement partagée du groupe est en cours de création. Veuillez patienter quelques minutes pendant sa récupération par le réseau. Vérification toutes les 2 minutes...",
|
||||
"group_invited_you": "{{group}} vous a invité",
|
||||
"loading_members": "chargement de la liste des membres avec noms... veuillez patienter.",
|
||||
"no_display": "rien à afficher",
|
||||
"no_selection": "aucun groupe sélectionné",
|
||||
"not_part_group": "vous ne faites pas partie du groupe chiffré. Attendez qu’un administrateur ré-encrypte les clés.",
|
||||
"only_encrypted": "seuls les messages non chiffrés seront affichés.",
|
||||
"private_key_copied": "clé privée copiée",
|
||||
"provide_message": "veuillez fournir un premier message pour la discussion",
|
||||
"secure_place": "gardez votre clé privée en lieu sûr. Ne la partagez pas !",
|
||||
"setting_group": "configuration du groupe... veuillez patienter."
|
||||
},
|
||||
"error": {
|
||||
"access_name": "impossible d’envoyer un message sans accès à votre nom",
|
||||
"descrypt_wallet": "erreur lors du déchiffrement du portefeuille {{ :errorMessage }}",
|
||||
"description_required": "veuillez fournir une description",
|
||||
"group_info": "impossible d'accéder aux informations du groupe",
|
||||
"group_secret_key": "impossible d’obtenir la clé secrète du groupe",
|
||||
"name_required": "veuillez fournir un nom",
|
||||
"notify_admins": "essayez de contacter un administrateur dans la liste ci-dessous :",
|
||||
"thread_id": "impossible de localiser l’identifiant de discussion"
|
||||
},
|
||||
"success": {
|
||||
"group_ban": "membre banni avec succès. Les changements peuvent prendre quelques minutes pour se propager",
|
||||
"group_creation": "groupe créé avec succès. Les changements peuvent prendre quelques minutes pour se propager",
|
||||
"group_creation_name": "groupe {{group_name}} créé : en attente de confirmation",
|
||||
"group_creation_label": "groupe {{name}} créé : succès !",
|
||||
"group_invite": "{{value}} invité avec succès. Les changements peuvent prendre quelques minutes pour se propager",
|
||||
"group_join": "demande de rejoindre le groupe envoyée avec succès. Les changements peuvent prendre quelques minutes pour se propager",
|
||||
"group_join_name": "rejoint le groupe {{group_name}} : en attente de confirmation",
|
||||
"group_join_label": "rejoint le groupe {{name}} : succès !",
|
||||
"group_join_request": "demande d’adhésion au groupe {{group_name}} : en attente de confirmation",
|
||||
"group_join_outcome": "adhésion au groupe {{group_name}} : succès !",
|
||||
"group_kick": "membre expulsé avec succès du groupe. Les changements peuvent prendre quelques minutes pour se propager",
|
||||
"group_leave": "demande de quitter le groupe envoyée avec succès. Les changements peuvent prendre quelques minutes pour se propager",
|
||||
"group_leave_name": "a quitté le groupe {{group_name}} : en attente de confirmation",
|
||||
"group_leave_label": "a quitté le groupe {{name}} : succès !",
|
||||
"group_member_admin": "membre promu administrateur avec succès. Les changements peuvent prendre quelques minutes pour se propager",
|
||||
"group_remove_member": "membre retiré en tant qu'administrateur avec succès. Les changements peuvent prendre quelques minutes pour se propager",
|
||||
"invitation_cancellation": "invitation annulée avec succès. Les changements peuvent prendre quelques minutes pour se propager",
|
||||
"invitation_request": "demande d’adhésion acceptée : en attente de confirmation",
|
||||
"loading_threads": "chargement des discussions... veuillez patienter.",
|
||||
"post_creation": "publication créée avec succès. La propagation peut prendre un certain temps",
|
||||
"thread_creation": "discussion créée avec succès. La propagation peut prendre un certain temps",
|
||||
"unbanned_user": "utilisateur débanni avec succès. Les changements peuvent prendre quelques minutes pour se propager",
|
||||
"user_joined": "utilisateur rejoint avec succès !"
|
||||
}
|
||||
},
|
||||
"question": {
|
||||
"perform_transaction": "souhaitez-vous effectuer une transaction de {{action}} ?",
|
||||
"provide_thread": "veuillez fournir un titre pour la discussion"
|
||||
},
|
||||
"thread_posts": "nouveaux messages de discussion"
|
||||
}
|
21
src/i18n/locales/fr/tutorial.json
Normal file
21
src/i18n/locales/fr/tutorial.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"1_getting_started": "1. Démarrer",
|
||||
"2_overview": "2. Aperçu",
|
||||
"3_groups": "3. Groupes Qortal",
|
||||
"4_obtain_qort": "4. Obtenir des QORT",
|
||||
"account_creation": "création de compte",
|
||||
"important_info": "informations importantes !",
|
||||
"apps": {
|
||||
"dashboard": "1. Tableau de bord des applications",
|
||||
"navigation": "2. Navigation des applications"
|
||||
},
|
||||
"initial": {
|
||||
"6_qort": "avoir au moins 6 QORT dans votre portefeuille",
|
||||
"explore": "explorer",
|
||||
"general_chat": "chat général",
|
||||
"getting_started": "démarrer",
|
||||
"register_name": "enregistrer un nom",
|
||||
"see_apps": "voir les applications",
|
||||
"trade_qort": "échanger des QORT"
|
||||
}
|
||||
}
|
48
src/i18n/locales/it/auth.json
Normal file
48
src/i18n/locales/it/auth.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"account": {
|
||||
"your": "il tuo account",
|
||||
"account_many": "account",
|
||||
"account_one": "account"
|
||||
},
|
||||
"advanced_users": "per utenti avanzati",
|
||||
"apikey": {
|
||||
"alternative": "alternativa: selezione file",
|
||||
"change": "cambia APIkey",
|
||||
"enter": "inserisci APIkey",
|
||||
"import": "importa APIkey",
|
||||
"key": "chiave API",
|
||||
"select_valid": "seleziona una APIkey valida"
|
||||
},
|
||||
"authenticate": "autentica",
|
||||
"build_version": "versione build",
|
||||
"create_account": "crea account",
|
||||
"download_account": "scarica account",
|
||||
"keep_secure": "mantieni sicuro il file del tuo account",
|
||||
"node": {
|
||||
"choose": "scegli nodo personalizzato",
|
||||
"custom_many": "nodi personalizzati",
|
||||
"use_custom": "usa nodo personalizzato",
|
||||
"use_local": "usa nodo locale",
|
||||
"using": "utilizzo nodo",
|
||||
"using_public": "utilizzo nodo pubblico"
|
||||
},
|
||||
"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 password del wallet",
|
||||
"password": "password del wallet",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"welcome": "benvenuto in"
|
||||
}
|
133
src/i18n/locales/it/core.json
Normal file
133
src/i18n/locales/it/core.json
Normal file
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"action": {
|
||||
"add": "aggiungi",
|
||||
"accept": "accetta",
|
||||
"backup_account": "esegui backup account",
|
||||
"backup_wallet": "esegui backup portafoglio",
|
||||
"cancel": "annulla",
|
||||
"cancel_invitation": "annulla invito",
|
||||
"change": "cambia",
|
||||
"change_language": "cambia lingua",
|
||||
"choose": "scegli",
|
||||
"close": "chiudi",
|
||||
"continue": "continua",
|
||||
"continue_logout": "continua con il logout",
|
||||
"create_thread": "crea discussione",
|
||||
"decline": "rifiuta",
|
||||
"decrypt": "decifra",
|
||||
"edit": "modifica",
|
||||
"export": "esporta",
|
||||
"import": "importa",
|
||||
"invite": "invita",
|
||||
"join": "unisciti",
|
||||
"logout": "esci",
|
||||
"new": {
|
||||
"post": "nuovo post",
|
||||
"thread": "nuova discussione"
|
||||
},
|
||||
"notify": "notifica",
|
||||
"post": "pubblica",
|
||||
"post_message": "invia messaggio"
|
||||
},
|
||||
"admin": "amministratore",
|
||||
"core": {
|
||||
"block_height": "altezza blocco",
|
||||
"information": "informazioni core",
|
||||
"peers": "peer connessi",
|
||||
"version": "versione core"
|
||||
},
|
||||
"ui": {
|
||||
"version": "versione UI"
|
||||
},
|
||||
"count": {
|
||||
"none": "nessuno",
|
||||
"one": "uno"
|
||||
},
|
||||
"description": "descrizione",
|
||||
"downloading_qdn": "scaricamento da QDN",
|
||||
"fee": {
|
||||
"payment": "commissione di pagamento",
|
||||
"publish": "commissione di pubblicazione"
|
||||
},
|
||||
"general_settings": "impostazioni generali",
|
||||
"last_height": "ultima altezza",
|
||||
"list": {
|
||||
"invite": "lista inviti",
|
||||
"join_request": "lista richieste di adesione",
|
||||
"member": "lista membri"
|
||||
},
|
||||
"loading": "caricamento...",
|
||||
"loading_posts": "caricamento post... attendere.",
|
||||
"message_us": "contattaci su Telegram o Discord se hai bisogno di 4 QORT per iniziare a chattare senza limitazioni",
|
||||
"message": {
|
||||
"error": {
|
||||
"generic": "si è verificato un errore",
|
||||
"incorrect_password": "password errata",
|
||||
"missing_field": "manca: {{ field }}",
|
||||
"save_qdn": "impossibile salvare su QDN"
|
||||
},
|
||||
"status": {
|
||||
"minting": "(in conio)",
|
||||
"not_minting": "(non in conio)",
|
||||
"synchronized": "sincronizzato",
|
||||
"synchronizing": "sincronizzazione"
|
||||
},
|
||||
"success": {
|
||||
"order_submitted": "il tuo ordine di acquisto è stato inviato",
|
||||
"publish_qdn": "pubblicato con successo su QDN",
|
||||
"request_read": "ho letto questa richiesta",
|
||||
"transfer": "il trasferimento è riuscito!"
|
||||
}
|
||||
},
|
||||
"minting_status": "stato conio",
|
||||
"page": {
|
||||
"last": "ultima",
|
||||
"first": "prima",
|
||||
"next": "successiva",
|
||||
"previous": "precedente"
|
||||
},
|
||||
"payment_notification": "notifica di pagamento",
|
||||
"price": "prezzo",
|
||||
"q_mail": "q-mail",
|
||||
"question": {
|
||||
"new_user": "sei un nuovo utente?"
|
||||
},
|
||||
"save_options": {
|
||||
"no_pinned_changes": "attualmente non hai modifiche nelle app fissate",
|
||||
"overwrite_changes": "l'app non è riuscita a scaricare le tue app fissate salvate su QDN. Vuoi sovrascrivere le modifiche?",
|
||||
"overwrite_qdn": "sovrascrivi su QDN",
|
||||
"publish_qdn": "vuoi pubblicare le tue impostazioni su QDN (crittografate)?",
|
||||
"qdn": "usa salvataggio QDN",
|
||||
"register_name": "hai bisogno di un nome Qortal registrato per salvare le app fissate su QDN.",
|
||||
"reset_pinned": "non ti piacciono le modifiche locali? Vuoi ripristinare le app fissate predefinite?",
|
||||
"reset_qdn": "non ti piacciono le modifiche locali? Vuoi ripristinare le app fissate salvate su QDN?",
|
||||
"revert_default": "ripristina ai valori predefiniti",
|
||||
"revert_qdn": "ripristina da QDN",
|
||||
"save_qdn": "salva su QDN",
|
||||
"save": "salva",
|
||||
"settings": "stai usando il metodo di esportazione/importazione per salvare le impostazioni.",
|
||||
"unsaved_changes": "hai modifiche non salvate nelle app fissate. Salvale su QDN."
|
||||
},
|
||||
"settings": "impostazioni",
|
||||
"supply": "offerta",
|
||||
"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": {
|
||||
"wallet": "portafoglio",
|
||||
"wallet_other": "portafogli"
|
||||
},
|
||||
"welcome": "benvenuto"
|
||||
}
|
105
src/i18n/locales/it/group.json
Normal file
105
src/i18n/locales/it/group.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"action": {
|
||||
"ban": "banna membro dal gruppo",
|
||||
"cancel_ban": "annulla ban",
|
||||
"copy_private_key": "copia chiave privata",
|
||||
"create_group": "crea gruppo",
|
||||
"disable_push_notifications": "disattiva tutte le notifiche push",
|
||||
"enable_dev_mode": "attiva modalità sviluppatore",
|
||||
"export_password": "esporta password",
|
||||
"export_private_key": "esporta chiave privata",
|
||||
"find_group": "trova gruppo",
|
||||
"join_group": "unisciti al gruppo",
|
||||
"kick_member": "espelli membro dal gruppo",
|
||||
"invite_member": "invita membro",
|
||||
"leave_group": "lascia il gruppo",
|
||||
"load_members": "carica membri con nomi",
|
||||
"make_admin": "rendi amministratore",
|
||||
"manage_members": "gestisci membri",
|
||||
"refetch_page": "ricarica pagina",
|
||||
"remove_admin": "rimuovi come amministratore",
|
||||
"return_to_thread": "torna alle discussioni"
|
||||
},
|
||||
"advanced_options": "opzioni avanzate",
|
||||
"approval_threshold": "soglia di approvazione del gruppo (numero / percentuale di amministratori che devono approvare una transazione)",
|
||||
"ban_list": "lista bannati",
|
||||
"block_delay": {
|
||||
"minimum": "ritardo minimo dei blocchi per le approvazioni delle transazioni di gruppo",
|
||||
"maximum": "ritardo massimo dei blocchi per le approvazioni delle transazioni di gruppo"
|
||||
},
|
||||
"group": {
|
||||
"closed": "chiuso (privato) – gli utenti hanno bisogno del permesso per entrare",
|
||||
"description": "descrizione del gruppo",
|
||||
"id": "ID gruppo",
|
||||
"invites": "inviti del gruppo",
|
||||
"management": "gestione del gruppo",
|
||||
"member_number": "numero di membri",
|
||||
"name": "nome del gruppo",
|
||||
"open": "aperto (pubblico)",
|
||||
"type": "tipo di gruppo"
|
||||
},
|
||||
"invitation_expiry": "tempo di scadenza dell'invito",
|
||||
"invitees_list": "lista degli invitati",
|
||||
"join_link": "link per unirsi al gruppo",
|
||||
"join_requests": "richieste di adesione",
|
||||
"last_message": "ultimo messaggio",
|
||||
"latest_mails": "ultimi Q-Mails",
|
||||
"message": {
|
||||
"generic": {
|
||||
"already_in_group": "sei già in questo gruppo!",
|
||||
"closed_group": "questo è un gruppo chiuso/privato, devi aspettare che un admin accetti la tua richiesta",
|
||||
"descrypt_wallet": "decrittazione portafoglio in corso...",
|
||||
"encryption_key": "la prima chiave di cifratura comune del gruppo è in fase di creazione. Attendere qualche minuto affinché venga recuperata dalla rete. Controllo ogni 2 minuti...",
|
||||
"group_invited_you": "{{group}} ti ha invitato",
|
||||
"loading_members": "caricamento elenco membri con nomi... attendere.",
|
||||
"no_display": "niente da mostrare",
|
||||
"no_selection": "nessun gruppo selezionato",
|
||||
"not_part_group": "non fai parte del gruppo cifrato. Attendi che un amministratore re-critti le chiavi.",
|
||||
"only_encrypted": "verranno mostrati solo i messaggi non cifrati.",
|
||||
"private_key_copied": "chiave privata copiata",
|
||||
"provide_message": "inserisci un primo messaggio per la discussione",
|
||||
"secure_place": "conserva la tua chiave privata in un luogo sicuro. Non condividerla!",
|
||||
"setting_group": "configurazione del gruppo in corso... attendere."
|
||||
},
|
||||
"error": {
|
||||
"access_name": "impossibile inviare un messaggio senza accesso al tuo nome",
|
||||
"descrypt_wallet": "errore nella decrittazione del portafoglio {{ :errorMessage }}",
|
||||
"description_required": "inserisci una descrizione",
|
||||
"group_info": "impossibile accedere alle informazioni del gruppo",
|
||||
"group_secret_key": "impossibile ottenere la chiave segreta del gruppo",
|
||||
"name_required": "inserisci un nome",
|
||||
"notify_admins": "prova a contattare un amministratore dalla lista qui sotto:",
|
||||
"thread_id": "impossibile trovare l'ID della discussione"
|
||||
},
|
||||
"success": {
|
||||
"group_ban": "membro bannato con successo dal gruppo. Potrebbero volerci alcuni minuti affinché le modifiche si propaghino",
|
||||
"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": "{{value}} invitato con successo. Potrebbero volerci alcuni minuti affinché le modifiche si propaghino",
|
||||
"group_join": "richiesta di accesso 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!",
|
||||
"group_join_request": "richiesta di accesso al gruppo {{group_name}}: in attesa di conferma",
|
||||
"group_join_outcome": "richiesta di accesso al gruppo {{group_name}}: successo!",
|
||||
"group_kick": "membro espulso con successo dal gruppo. Potrebbero volerci alcuni minuti affinché le modifiche si propaghino",
|
||||
"group_leave": "richiesta di uscita dal gruppo inviata con successo. Potrebbero volerci alcuni minuti affinché le modifiche si propaghino",
|
||||
"group_leave_name": "uscito dal gruppo {{group_name}}: in attesa di conferma",
|
||||
"group_leave_label": "uscito dal gruppo {{name}}: successo!",
|
||||
"group_member_admin": "membro nominato amministratore con successo. Potrebbero volerci alcuni minuti affinché le modifiche si propaghino",
|
||||
"group_remove_member": "amministratore rimosso con successo. Potrebbero volerci alcuni minuti affinché le modifiche si propaghino",
|
||||
"invitation_cancellation": "invito annullato con successo. Potrebbero volerci alcuni minuti affinché le modifiche si propaghino",
|
||||
"invitation_request": "richiesta di adesione accettata: in attesa di conferma",
|
||||
"loading_threads": "caricamento discussioni... attendere.",
|
||||
"post_creation": "post creato con successo. Potrebbe volerci un po' per la pubblicazione",
|
||||
"thread_creation": "discussione creata con successo. Potrebbe volerci un po' per la pubblicazione",
|
||||
"unbanned_user": "utente sbannato con successo. Potrebbero volerci alcuni minuti affinché le modifiche si propaghino",
|
||||
"user_joined": "utente entrato con successo!"
|
||||
}
|
||||
},
|
||||
"question": {
|
||||
"perform_transaction": "vuoi eseguire una transazione di tipo {{action}}?",
|
||||
"provide_thread": "inserisci un titolo per la discussione"
|
||||
},
|
||||
"thread_posts": "nuovi messaggi nella discussione"
|
||||
}
|
21
src/i18n/locales/it/tutorial.json
Normal file
21
src/i18n/locales/it/tutorial.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"1_getting_started": "1. Come iniziare",
|
||||
"2_overview": "2. Overview",
|
||||
"3_groups": "3. I gruppi in Qortal",
|
||||
"4_obtain_qort": "4. Ottenere Qort",
|
||||
"account_creation": "creazione account",
|
||||
"important_info": "important information!",
|
||||
"apps": {
|
||||
"dashboard": "1. Dashboard delle app",
|
||||
"navigation": "2. Navigation tra le app"
|
||||
},
|
||||
"initial": {
|
||||
"6_qort": "avere almeno 6 QORT nel proprio wallet",
|
||||
"explore": "esplora",
|
||||
"general_chat": "chat generale",
|
||||
"getting_started": "come iniziare",
|
||||
"register_name": "registra un nome",
|
||||
"see_apps": "vedi le apps",
|
||||
"trade_qort": "scambia i QORT"
|
||||
}
|
||||
}
|
43
src/i18n/locales/ru/auth.json
Normal file
43
src/i18n/locales/ru/auth.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"account": {
|
||||
"your": "Ваш аккаунт",
|
||||
"account_many": "аккаунты",
|
||||
"account_one": "аккаунт"
|
||||
},
|
||||
"advanced_users": "для продвинутых пользователей",
|
||||
"apikey": {
|
||||
"alternative": "альтернатива: выбрать файл",
|
||||
"change": "изменить API-ключ",
|
||||
"enter": "введите API-ключ",
|
||||
"import": "импортировать API-ключ",
|
||||
"key": "API-ключ",
|
||||
"select_valid": "выберите действительный API-ключ"
|
||||
},
|
||||
"authenticate": "аутентификация",
|
||||
"build_version": "версия сборки",
|
||||
"create_account": "создать аккаунт",
|
||||
"download_account": "скачать аккаунт",
|
||||
"keep_secure": "Храните файл аккаунта в безопасности",
|
||||
"node": {
|
||||
"choose": "выбрать пользовательский узел",
|
||||
"custom_many": "пользовательские узлы",
|
||||
"use_custom": "использовать пользовательский узел",
|
||||
"use_local": "использовать локальный узел",
|
||||
"using": "используется узел",
|
||||
"using_public": "используется публичный узел"
|
||||
},
|
||||
"password": "пароль",
|
||||
"password_confirmation": "подтвердите пароль",
|
||||
"return_to_list": "вернуться к списку",
|
||||
"wallet": {
|
||||
"password_confirmation": "подтвердите пароль кошелька",
|
||||
"password": "пароль кошелька",
|
||||
"keep_password": "сохранить текущий пароль",
|
||||
"new_password": "новый пароль",
|
||||
"error": {
|
||||
"missing_new_password": "пожалуйста, введите новый пароль",
|
||||
"missing_password": "пожалуйста, введите ваш пароль"
|
||||
}
|
||||
},
|
||||
"welcome": "добро пожаловать в"
|
||||
}
|
133
src/i18n/locales/ru/core.json
Normal file
133
src/i18n/locales/ru/core.json
Normal file
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"action": {
|
||||
"add": "добавить",
|
||||
"accept": "принять",
|
||||
"backup_account": "резервное копирование аккаунта",
|
||||
"backup_wallet": "резервное копирование кошелька",
|
||||
"cancel": "отменить",
|
||||
"cancel_invitation": "отменить приглашение",
|
||||
"change": "изменить",
|
||||
"change_language": "сменить язык",
|
||||
"choose": "выбрать",
|
||||
"close": "закрыть",
|
||||
"continue": "продолжить",
|
||||
"continue_logout": "продолжить выход",
|
||||
"create_thread": "создать тему",
|
||||
"decline": "отклонить",
|
||||
"decrypt": "расшифровать",
|
||||
"edit": "редактировать",
|
||||
"export": "экспорт",
|
||||
"import": "импорт",
|
||||
"invite": "пригласить",
|
||||
"join": "присоединиться",
|
||||
"logout": "выйти",
|
||||
"new": {
|
||||
"post": "новое сообщение",
|
||||
"thread": "новая тема"
|
||||
},
|
||||
"notify": "уведомить",
|
||||
"post": "опубликовать",
|
||||
"post_message": "отправить сообщение"
|
||||
},
|
||||
"admin": "администратор",
|
||||
"core": {
|
||||
"block_height": "высота блока",
|
||||
"information": "информация ядра",
|
||||
"peers": "подключённые узлы",
|
||||
"version": "версия ядра"
|
||||
},
|
||||
"ui": {
|
||||
"version": "версия интерфейса"
|
||||
},
|
||||
"count": {
|
||||
"none": "нет",
|
||||
"one": "один"
|
||||
},
|
||||
"description": "описание",
|
||||
"downloading_qdn": "загрузка из QDN",
|
||||
"fee": {
|
||||
"payment": "комиссия за платёж",
|
||||
"publish": "комиссия за публикацию"
|
||||
},
|
||||
"general_settings": "общие настройки",
|
||||
"last_height": "последняя высота",
|
||||
"list": {
|
||||
"invite": "список приглашений",
|
||||
"join_request": "список запросов на вступление",
|
||||
"member": "список участников"
|
||||
},
|
||||
"loading": "загрузка...",
|
||||
"loading_posts": "загрузка сообщений... пожалуйста, подождите.",
|
||||
"message_us": "напишите нам в Telegram или Discord, если вам нужно 4 QORT, чтобы начать чат без ограничений",
|
||||
"message": {
|
||||
"error": {
|
||||
"generic": "произошла ошибка",
|
||||
"incorrect_password": "неверный пароль",
|
||||
"missing_field": "отсутствует: {{ field }}",
|
||||
"save_qdn": "не удалось сохранить в QDN"
|
||||
},
|
||||
"status": {
|
||||
"minting": "(выпуск монет)",
|
||||
"not_minting": "(не выпускается)",
|
||||
"synchronized": "синхронизировано",
|
||||
"synchronizing": "синхронизация"
|
||||
},
|
||||
"success": {
|
||||
"order_submitted": "ваш ордер на покупку отправлен",
|
||||
"publish_qdn": "успешно опубликовано в QDN",
|
||||
"request_read": "я прочитал этот запрос",
|
||||
"transfer": "перевод выполнен успешно!"
|
||||
}
|
||||
},
|
||||
"minting_status": "статус выпуска",
|
||||
"page": {
|
||||
"last": "последняя",
|
||||
"first": "первая",
|
||||
"next": "следующая",
|
||||
"previous": "предыдущая"
|
||||
},
|
||||
"payment_notification": "уведомление о платеже",
|
||||
"price": "цена",
|
||||
"q_mail": "q-mail",
|
||||
"question": {
|
||||
"new_user": "вы новый пользователь?"
|
||||
},
|
||||
"save_options": {
|
||||
"no_pinned_changes": "у вас нет изменений в закреплённых приложениях",
|
||||
"overwrite_changes": "не удалось загрузить ваши закреплённые приложения из QDN. Перезаписать изменения?",
|
||||
"overwrite_qdn": "перезаписать в QDN",
|
||||
"publish_qdn": "опубликовать настройки в QDN (в зашифрованном виде)?",
|
||||
"qdn": "использовать сохранение в QDN",
|
||||
"register_name": "вам нужно зарегистрированное имя Qortal, чтобы сохранять закреплённые приложения в QDN.",
|
||||
"reset_pinned": "не нравятся текущие локальные изменения? Сбросить до стандартных приложений?",
|
||||
"reset_qdn": "не нравятся текущие локальные изменения? Сбросить до сохранённых в QDN приложений?",
|
||||
"revert_default": "сбросить по умолчанию",
|
||||
"revert_qdn": "восстановить из QDN",
|
||||
"save_qdn": "сохранить в QDN",
|
||||
"save": "сохранить",
|
||||
"settings": "вы используете метод экспорта/импорта для сохранения настроек.",
|
||||
"unsaved_changes": "у вас есть несохранённые изменения в закреплённых приложениях. Сохраните их в QDN."
|
||||
},
|
||||
"settings": "настройки",
|
||||
"supply": "предложение",
|
||||
"theme": {
|
||||
"dark": "тёмная тема",
|
||||
"light": "светлая тема"
|
||||
},
|
||||
"time": {
|
||||
"day_one": "{{count}} день",
|
||||
"day_other": "{{count}} дней",
|
||||
"hour_one": "{{count}} час",
|
||||
"hour_other": "{{count}} часов",
|
||||
"minute_one": "{{count}} минута",
|
||||
"minute_other": "{{count}} минут"
|
||||
},
|
||||
"title": "заголовок",
|
||||
"tutorial": "учебник",
|
||||
"user_lookup": "поиск пользователя",
|
||||
"wallet": {
|
||||
"wallet": "кошелёк",
|
||||
"wallet_other": "кошельки"
|
||||
},
|
||||
"welcome": "добро пожаловать"
|
||||
}
|
105
src/i18n/locales/ru/group.json
Normal file
105
src/i18n/locales/ru/group.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"action": {
|
||||
"ban": "заблокировать участника группы",
|
||||
"cancel_ban": "отменить блокировку",
|
||||
"copy_private_key": "скопировать приватный ключ",
|
||||
"create_group": "создать группу",
|
||||
"disable_push_notifications": "отключить все push-уведомления",
|
||||
"enable_dev_mode": "включить режим разработчика",
|
||||
"export_password": "экспортировать пароль",
|
||||
"export_private_key": "экспортировать приватный ключ",
|
||||
"find_group": "найти группу",
|
||||
"join_group": "вступить в группу",
|
||||
"kick_member": "исключить участника из группы",
|
||||
"invite_member": "пригласить участника",
|
||||
"leave_group": "выйти из группы",
|
||||
"load_members": "загрузить участников с именами",
|
||||
"make_admin": "сделать админом",
|
||||
"manage_members": "управлять участниками",
|
||||
"refetch_page": "перезагрузить страницу",
|
||||
"remove_admin": "удалить из админов",
|
||||
"return_to_thread": "вернуться к темам"
|
||||
},
|
||||
"advanced_options": "расширенные настройки",
|
||||
"approval_threshold": "порог одобрения группы (число / процент админов, необходимых для подтверждения транзакции)",
|
||||
"ban_list": "список заблокированных",
|
||||
"block_delay": {
|
||||
"minimum": "минимальная задержка блоков для одобрения транзакций в группе",
|
||||
"maximum": "максимальная задержка блоков для одобрения транзакций в группе"
|
||||
},
|
||||
"group": {
|
||||
"closed": "закрытая (приватная) — требуется разрешение на вступление",
|
||||
"description": "описание группы",
|
||||
"id": "ID группы",
|
||||
"invites": "приглашения группы",
|
||||
"management": "управление группой",
|
||||
"member_number": "количество участников",
|
||||
"name": "название группы",
|
||||
"open": "открытая (публичная)",
|
||||
"type": "тип группы"
|
||||
},
|
||||
"invitation_expiry": "время истечения приглашения",
|
||||
"invitees_list": "список приглашённых",
|
||||
"join_link": "ссылка для вступления в группу",
|
||||
"join_requests": "запросы на вступление",
|
||||
"last_message": "последнее сообщение",
|
||||
"latest_mails": "последние Q-Mail'ы",
|
||||
"message": {
|
||||
"generic": {
|
||||
"already_in_group": "вы уже в этой группе!",
|
||||
"closed_group": "эта группа закрыта, дождитесь подтверждения от администратора",
|
||||
"descrypt_wallet": "дешифровка кошелька...",
|
||||
"encryption_key": "первая общая ключевая фраза группы создаётся. Подождите несколько минут для получения через сеть. Проверка каждые 2 минуты...",
|
||||
"group_invited_you": "{{group}} пригласила вас",
|
||||
"loading_members": "загрузка списка участников с именами... пожалуйста, подождите.",
|
||||
"no_display": "нечего отображать",
|
||||
"no_selection": "группа не выбрана",
|
||||
"not_part_group": "вы не состоите в зашифрованной группе. Дождитесь повторного шифрования ключей администратором.",
|
||||
"only_encrypted": "отображаются только незашифрованные сообщения.",
|
||||
"private_key_copied": "приватный ключ скопирован",
|
||||
"provide_message": "введите первое сообщение для темы",
|
||||
"secure_place": "храните приватный ключ в безопасном месте. Не передавайте его никому!",
|
||||
"setting_group": "настройка группы... пожалуйста, подождите."
|
||||
},
|
||||
"error": {
|
||||
"access_name": "невозможно отправить сообщение без доступа к вашему имени",
|
||||
"descrypt_wallet": "ошибка при дешифровке кошелька {{ :errorMessage }}",
|
||||
"description_required": "введите описание",
|
||||
"group_info": "не удалось получить информацию о группе",
|
||||
"group_secret_key": "не удалось получить секретный ключ группы",
|
||||
"name_required": "введите имя",
|
||||
"notify_admins": "попробуйте связаться с администратором из списка ниже:",
|
||||
"thread_id": "не удалось найти ID темы"
|
||||
},
|
||||
"success": {
|
||||
"group_ban": "участник успешно заблокирован. Распространение изменений может занять несколько минут",
|
||||
"group_creation": "группа успешно создана. Распространение изменений может занять несколько минут",
|
||||
"group_creation_name": "создана группа {{group_name}}: ожидает подтверждения",
|
||||
"group_creation_label": "группа {{name}} создана: успех!",
|
||||
"group_invite": "{{value}} успешно приглашён. Распространение изменений может занять несколько минут",
|
||||
"group_join": "запрос на вступление успешно отправлен. Распространение изменений может занять несколько минут",
|
||||
"group_join_name": "вступление в группу {{group_name}}: ожидает подтверждения",
|
||||
"group_join_label": "вступление в группу {{name}}: успех!",
|
||||
"group_join_request": "запрос на вступление в группу {{group_name}}: ожидает подтверждения",
|
||||
"group_join_outcome": "вступление в группу {{group_name}}: успех!",
|
||||
"group_kick": "участник успешно удалён из группы. Распространение изменений может занять несколько минут",
|
||||
"group_leave": "запрос на выход из группы успешно отправлен. Распространение изменений может занять несколько минут",
|
||||
"group_leave_name": "выход из группы {{group_name}}: ожидает подтверждения",
|
||||
"group_leave_label": "выход из группы {{name}}: успех!",
|
||||
"group_member_admin": "пользователь успешно назначен администратором. Распространение изменений может занять несколько минут",
|
||||
"group_remove_member": "администратор успешно удалён. Распространение изменений может занять несколько минут",
|
||||
"invitation_cancellation": "приглашение успешно отменено. Распространение изменений может занять несколько минут",
|
||||
"invitation_request": "запрос на вступление принят: ожидает подтверждения",
|
||||
"loading_threads": "загрузка тем... пожалуйста, подождите.",
|
||||
"post_creation": "сообщение успешно создано. Публикация может занять некоторое время",
|
||||
"thread_creation": "тема успешно создана. Публикация может занять некоторое время",
|
||||
"unbanned_user": "пользователь успешно разблокирован. Распространение изменений может занять несколько минут",
|
||||
"user_joined": "пользователь успешно присоединился!"
|
||||
}
|
||||
},
|
||||
"question": {
|
||||
"perform_transaction": "вы хотите выполнить транзакцию типа {{action}}?",
|
||||
"provide_thread": "укажите заголовок темы"
|
||||
},
|
||||
"thread_posts": "новые сообщения в теме"
|
||||
}
|
21
src/i18n/locales/ru/tutorial.json
Normal file
21
src/i18n/locales/ru/tutorial.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"1_getting_started": "1. Начало работы",
|
||||
"2_overview": "2. Обзор",
|
||||
"3_groups": "3. Группы Qortal",
|
||||
"4_obtain_qort": "4. Получение QORT",
|
||||
"account_creation": "создание аккаунта",
|
||||
"important_info": "важная информация!",
|
||||
"apps": {
|
||||
"dashboard": "1. Панель приложений",
|
||||
"navigation": "2. Навигация по приложениям"
|
||||
},
|
||||
"initial": {
|
||||
"6_qort": "иметь не менее 6 QORT в кошельке",
|
||||
"explore": "исследовать",
|
||||
"general_chat": "общий чат",
|
||||
"getting_started": "начало работы",
|
||||
"register_name": "зарегистрировать имя",
|
||||
"see_apps": "посмотреть приложения",
|
||||
"trade_qort": "обмен QORT"
|
||||
}
|
||||
}
|
@@ -5,7 +5,7 @@ import './messaging/messagesToBackground';
|
||||
import { MessageQueueProvider } from './MessageQueueContext.tsx';
|
||||
import { ThemeProvider } from './components/Theme/ThemeContext.tsx';
|
||||
import { CssBaseline } from '@mui/material';
|
||||
import '../i18n';
|
||||
import './i18n/i18n.js';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<>
|
||||
|
@@ -61,6 +61,9 @@ import {
|
||||
buyNameRequest,
|
||||
sellNameRequest,
|
||||
cancelSellNameRequest,
|
||||
signForeignFees,
|
||||
multiPaymentWithPrivateData,
|
||||
transferAssetRequest,
|
||||
} from './qortalRequests/get';
|
||||
import { getData, storeData } from './utils/chromeStorage';
|
||||
import { executeEvent } from './utils/events';
|
||||
@@ -752,7 +755,7 @@ function setupMessageListenerQortalRequest() {
|
||||
|
||||
case 'UPDATE_FOREIGN_FEE': {
|
||||
try {
|
||||
const res = await updateForeignFee(request.payload);
|
||||
const res = await updateForeignFee(request.payload, isFromExtension);
|
||||
event.source.postMessage(
|
||||
{
|
||||
requestId: request.requestId,
|
||||
@@ -804,7 +807,10 @@ function setupMessageListenerQortalRequest() {
|
||||
|
||||
case 'SET_CURRENT_FOREIGN_SERVER': {
|
||||
try {
|
||||
const res = await setCurrentForeignServer(request.payload);
|
||||
const res = await setCurrentForeignServer(
|
||||
request.payload,
|
||||
isFromExtension
|
||||
);
|
||||
event.source.postMessage(
|
||||
{
|
||||
requestId: request.requestId,
|
||||
@@ -830,7 +836,7 @@ function setupMessageListenerQortalRequest() {
|
||||
|
||||
case 'ADD_FOREIGN_SERVER': {
|
||||
try {
|
||||
const res = await addForeignServer(request.payload);
|
||||
const res = await addForeignServer(request.payload, isFromExtension);
|
||||
event.source.postMessage(
|
||||
{
|
||||
requestId: request.requestId,
|
||||
@@ -856,7 +862,10 @@ function setupMessageListenerQortalRequest() {
|
||||
|
||||
case 'REMOVE_FOREIGN_SERVER': {
|
||||
try {
|
||||
const res = await removeForeignServer(request.payload);
|
||||
const res = await removeForeignServer(
|
||||
request.payload,
|
||||
isFromExtension
|
||||
);
|
||||
event.source.postMessage(
|
||||
{
|
||||
requestId: request.requestId,
|
||||
@@ -1755,6 +1764,63 @@ function setupMessageListenerQortalRequest() {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'MULTI_ASSET_PAYMENT_WITH_PRIVATE_DATA': {
|
||||
try {
|
||||
const res = await multiPaymentWithPrivateData(
|
||||
request.payload,
|
||||
isFromExtension
|
||||
);
|
||||
event.source.postMessage(
|
||||
{
|
||||
requestId: request.requestId,
|
||||
action: request.action,
|
||||
payload: res,
|
||||
type: 'backgroundMessageResponse',
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
} catch (error) {
|
||||
event.source.postMessage(
|
||||
{
|
||||
requestId: request.requestId,
|
||||
action: request.action,
|
||||
error: error?.message,
|
||||
type: 'backgroundMessageResponse',
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'TRANSFER_ASSET': {
|
||||
try {
|
||||
const res = await transferAssetRequest(
|
||||
request.payload,
|
||||
isFromExtension
|
||||
);
|
||||
event.source.postMessage(
|
||||
{
|
||||
requestId: request.requestId,
|
||||
action: request.action,
|
||||
payload: res,
|
||||
type: 'backgroundMessageResponse',
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
} catch (error) {
|
||||
event.source.postMessage(
|
||||
{
|
||||
requestId: request.requestId,
|
||||
action: request.action,
|
||||
error: error?.message,
|
||||
type: 'backgroundMessageResponse',
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'BUY_NAME': {
|
||||
try {
|
||||
const res = await buyNameRequest(request.payload, isFromExtension);
|
||||
@@ -1833,6 +1899,32 @@ function setupMessageListenerQortalRequest() {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'SIGN_FOREIGN_FEES': {
|
||||
try {
|
||||
const res = await signForeignFees(request.payload, isFromExtension);
|
||||
event.source.postMessage(
|
||||
{
|
||||
requestId: request.requestId,
|
||||
action: request.action,
|
||||
payload: res,
|
||||
type: 'backgroundMessageResponse',
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
} catch (error) {
|
||||
event.source.postMessage(
|
||||
{
|
||||
requestId: request.requestId,
|
||||
action: request.action,
|
||||
error: error.message,
|
||||
type: 'backgroundMessageResponse',
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@@ -32,6 +32,11 @@ import {
|
||||
cancelSellName,
|
||||
buyName,
|
||||
getBaseApi,
|
||||
getAssetBalanceInfo,
|
||||
getNameOrAddress,
|
||||
getAssetInfo,
|
||||
getPublicKey,
|
||||
transferAsset,
|
||||
} from '../background';
|
||||
import {
|
||||
getNameInfo,
|
||||
@@ -50,6 +55,7 @@ import { QORT_DECIMALS } from '../constants/constants';
|
||||
import Base58 from '../deps/Base58';
|
||||
import ed2curve from '../deps/ed2curve';
|
||||
import nacl from '../deps/nacl-fast';
|
||||
|
||||
import {
|
||||
base64ToUint8Array,
|
||||
createSymmetricKeyAndNonce,
|
||||
@@ -78,6 +84,10 @@ import { fileToBase64 } from '../utils/fileReading';
|
||||
import { mimeToExtensionMap } from '../utils/memeTypes';
|
||||
import { RequestQueueWithPromise } from '../utils/queue/queue';
|
||||
import utils from '../utils/utils';
|
||||
import ShortUniqueId from 'short-unique-id';
|
||||
import { isValidBase64WithDecode } from '../utils/decode';
|
||||
|
||||
const uid = new ShortUniqueId({ length: 6 });
|
||||
|
||||
export const requestQueueGetAtAddresses = new RequestQueueWithPromise(10);
|
||||
|
||||
@@ -1293,10 +1303,7 @@ export const publishMultipleQDNResources = async (
|
||||
html: `
|
||||
<div style="max-height: 30vh; overflow-y: auto;">
|
||||
<style>
|
||||
body {
|
||||
background-color: #121212;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
|
||||
.resource-container {
|
||||
display: flex;
|
||||
@@ -1305,7 +1312,7 @@ export const publishMultipleQDNResources = async (
|
||||
padding: 16px;
|
||||
margin: 8px 0;
|
||||
border-radius: 8px;
|
||||
background-color: #1e1e1e;
|
||||
background-color: var(--background-default);
|
||||
}
|
||||
|
||||
.resource-detail {
|
||||
@@ -1314,7 +1321,7 @@ export const publishMultipleQDNResources = async (
|
||||
|
||||
.resource-detail span {
|
||||
font-weight: bold;
|
||||
color: #bb86fc;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
@@ -2649,7 +2656,12 @@ export const getForeignFee = async (data) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const updateForeignFee = async (data) => {
|
||||
function calculateRateFromFee(totalFee, sizeInBytes) {
|
||||
const fee = (totalFee / sizeInBytes) * 1000;
|
||||
return fee.toFixed(0);
|
||||
}
|
||||
|
||||
export const updateForeignFee = async (data, isFromExtension) => {
|
||||
const isGateway = await isRunningGateway();
|
||||
if (isGateway) {
|
||||
throw new Error('This action cannot be done through a public node');
|
||||
@@ -2670,33 +2682,52 @@ export const updateForeignFee = async (data) => {
|
||||
}
|
||||
|
||||
const { coin, type, value } = data;
|
||||
const url = `/crosschain/${coin.toLowerCase()}/update${type}`;
|
||||
|
||||
try {
|
||||
const endpoint = await createEndpoint(url);
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ value }),
|
||||
});
|
||||
const text3 =
|
||||
type === 'feerequired' ? `${value} sats` : `${value} sats per kb`;
|
||||
const text4 =
|
||||
type === 'feerequired'
|
||||
? `*The ${value} sats fee is derived from ${calculateRateFromFee(value, 300)} sats per kb, for a transaction that is approximately 300 bytes in size.`
|
||||
: '';
|
||||
const resPermission = await getUserPermission(
|
||||
{
|
||||
text1: `Do you give this application permission to update foreign fees on your node?`,
|
||||
text2: `type: ${type === 'feerequired' ? 'unlocking' : 'locking'}`,
|
||||
text3: `value: ${text3}`,
|
||||
text4,
|
||||
highlightedText: `Coin: ${coin}`,
|
||||
},
|
||||
isFromExtension
|
||||
);
|
||||
|
||||
if (!response.ok) throw new Error('Failed to update foreign fee');
|
||||
let res;
|
||||
try {
|
||||
res = await response.clone().json();
|
||||
} catch (e) {
|
||||
res = await response.text();
|
||||
}
|
||||
if (res?.error && res?.message) {
|
||||
throw new Error(res.message);
|
||||
}
|
||||
return res; // Return full response here
|
||||
} catch (error) {
|
||||
throw new Error(error?.message || 'Error in update foreign fee');
|
||||
const { accepted } = resPermission;
|
||||
if (!accepted) {
|
||||
throw new Error('User declined request');
|
||||
}
|
||||
const url = `/crosschain/${coin.toLowerCase()}/update${type}`;
|
||||
const valueStringified = JSON.stringify(+value);
|
||||
|
||||
const endpoint = await createEndpoint(url);
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: valueStringified,
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to update foreign fee');
|
||||
let res;
|
||||
try {
|
||||
res = await response.clone().json();
|
||||
} catch (e) {
|
||||
res = await response.text();
|
||||
}
|
||||
if (res?.error && res?.message) {
|
||||
throw new Error(res.message);
|
||||
}
|
||||
return res; // Return full response here
|
||||
};
|
||||
|
||||
export const getServerConnectionHistory = async (data) => {
|
||||
@@ -2749,7 +2780,7 @@ export const getServerConnectionHistory = async (data) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const setCurrentForeignServer = async (data) => {
|
||||
export const setCurrentForeignServer = async (data, isFromExtension) => {
|
||||
const isGateway = await isRunningGateway();
|
||||
if (isGateway) {
|
||||
throw new Error('This action cannot be done through a public node');
|
||||
@@ -2771,6 +2802,21 @@ export const setCurrentForeignServer = async (data) => {
|
||||
}
|
||||
|
||||
const { coin, host, port, type } = data;
|
||||
|
||||
const resPermission = await getUserPermission(
|
||||
{
|
||||
text1: `Do you give this application permission to set the current server?`,
|
||||
text2: `type: ${type}`,
|
||||
text3: `host: ${host}`,
|
||||
highlightedText: `Coin: ${coin}`,
|
||||
},
|
||||
isFromExtension
|
||||
);
|
||||
|
||||
const { accepted } = resPermission;
|
||||
if (!accepted) {
|
||||
throw new Error('User declined request');
|
||||
}
|
||||
const body = {
|
||||
hostName: host,
|
||||
port: port,
|
||||
@@ -2779,37 +2825,33 @@ export const setCurrentForeignServer = async (data) => {
|
||||
|
||||
const url = `/crosschain/${coin.toLowerCase()}/setcurrentserver`;
|
||||
|
||||
const endpoint = await createEndpoint(url); // Assuming createEndpoint is available
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to set current server');
|
||||
|
||||
let res;
|
||||
try {
|
||||
const endpoint = await createEndpoint(url); // Assuming createEndpoint is available
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to set current server');
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await response.clone().json();
|
||||
} catch (e) {
|
||||
res = await response.text();
|
||||
}
|
||||
|
||||
if (res?.error && res?.message) {
|
||||
throw new Error(res.message);
|
||||
}
|
||||
|
||||
return res; // Return the full response
|
||||
} catch (error) {
|
||||
throw new Error(error?.message || 'Error in set current server');
|
||||
res = await response.clone().json();
|
||||
} catch (e) {
|
||||
res = await response.text();
|
||||
}
|
||||
|
||||
if (res?.error && res?.message) {
|
||||
throw new Error(res.message);
|
||||
}
|
||||
|
||||
return res; // Return the full response
|
||||
};
|
||||
|
||||
export const addForeignServer = async (data) => {
|
||||
export const addForeignServer = async (data, isFromExtension) => {
|
||||
const isGateway = await isRunningGateway();
|
||||
if (isGateway) {
|
||||
throw new Error('This action cannot be done through a public node');
|
||||
@@ -2831,6 +2873,21 @@ export const addForeignServer = async (data) => {
|
||||
}
|
||||
|
||||
const { coin, host, port, type } = data;
|
||||
|
||||
const resPermission = await getUserPermission(
|
||||
{
|
||||
text1: `Do you give this application permission to add a server?`,
|
||||
text2: `type: ${type}`,
|
||||
text3: `host: ${host}`,
|
||||
highlightedText: `Coin: ${coin}`,
|
||||
},
|
||||
isFromExtension
|
||||
);
|
||||
|
||||
const { accepted } = resPermission;
|
||||
if (!accepted) {
|
||||
throw new Error('User declined request');
|
||||
}
|
||||
const body = {
|
||||
hostName: host,
|
||||
port: port,
|
||||
@@ -2839,37 +2896,33 @@ export const addForeignServer = async (data) => {
|
||||
|
||||
const url = `/crosschain/${coin.toLowerCase()}/addserver`;
|
||||
|
||||
const endpoint = await createEndpoint(url); // Assuming createEndpoint is available
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to add server');
|
||||
|
||||
let res;
|
||||
try {
|
||||
const endpoint = await createEndpoint(url); // Assuming createEndpoint is available
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to add server');
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await response.clone().json();
|
||||
} catch (e) {
|
||||
res = await response.text();
|
||||
}
|
||||
|
||||
if (res?.error && res?.message) {
|
||||
throw new Error(res.message);
|
||||
}
|
||||
|
||||
return res; // Return the full response
|
||||
} catch (error) {
|
||||
throw new Error(error.message || 'Error in adding server');
|
||||
res = await response.clone().json();
|
||||
} catch (e) {
|
||||
res = await response.text();
|
||||
}
|
||||
|
||||
if (res?.error && res?.message) {
|
||||
throw new Error(res.message);
|
||||
}
|
||||
|
||||
return res; // Return the full response
|
||||
};
|
||||
|
||||
export const removeForeignServer = async (data) => {
|
||||
export const removeForeignServer = async (data, isFromExtension) => {
|
||||
const isGateway = await isRunningGateway();
|
||||
if (isGateway) {
|
||||
throw new Error('This action cannot be done through a public node');
|
||||
@@ -2891,6 +2944,21 @@ export const removeForeignServer = async (data) => {
|
||||
}
|
||||
|
||||
const { coin, host, port, type } = data;
|
||||
|
||||
const resPermission = await getUserPermission(
|
||||
{
|
||||
text1: `Do you give this application permission to remove a server?`,
|
||||
text2: `type: ${type}`,
|
||||
text3: `host: ${host}`,
|
||||
highlightedText: `Coin: ${coin}`,
|
||||
},
|
||||
isFromExtension
|
||||
);
|
||||
|
||||
const { accepted } = resPermission;
|
||||
if (!accepted) {
|
||||
throw new Error('User declined request');
|
||||
}
|
||||
const body = {
|
||||
hostName: host,
|
||||
port: port,
|
||||
@@ -2899,34 +2967,30 @@ export const removeForeignServer = async (data) => {
|
||||
|
||||
const url = `/crosschain/${coin.toLowerCase()}/removeserver`;
|
||||
|
||||
const endpoint = await createEndpoint(url); // Assuming createEndpoint is available
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to remove server');
|
||||
|
||||
let res;
|
||||
try {
|
||||
const endpoint = await createEndpoint(url); // Assuming createEndpoint is available
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to remove server');
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await response.clone().json();
|
||||
} catch (e) {
|
||||
res = await response.text();
|
||||
}
|
||||
|
||||
if (res?.error && res?.message) {
|
||||
throw new Error(res.message);
|
||||
}
|
||||
|
||||
return res; // Return the full response
|
||||
} catch (error) {
|
||||
throw new Error(error?.message || 'Error in removing server');
|
||||
res = await response.clone().json();
|
||||
} catch (e) {
|
||||
res = await response.text();
|
||||
}
|
||||
|
||||
if (res?.error && res?.message) {
|
||||
throw new Error(res.message);
|
||||
}
|
||||
|
||||
return res; // Return the full response
|
||||
};
|
||||
|
||||
export const getDaySummary = async () => {
|
||||
@@ -3484,6 +3548,35 @@ export const sendCoin = async (data, isFromExtension) => {
|
||||
}
|
||||
};
|
||||
|
||||
function calculateFeeFromRate(feePerKb, sizeInBytes) {
|
||||
return (feePerKb / 1000) * sizeInBytes;
|
||||
}
|
||||
|
||||
const getBuyingFees = async (foreignBlockchain) => {
|
||||
const ticker = sellerForeignFee[foreignBlockchain].ticker;
|
||||
if (!ticker) throw new Error('invalid foreign blockchain');
|
||||
const unlockFee = await getForeignFee({
|
||||
coin: ticker,
|
||||
type: 'feerequired',
|
||||
});
|
||||
const lockFee = await getForeignFee({
|
||||
coin: ticker,
|
||||
type: 'feekb',
|
||||
});
|
||||
return {
|
||||
ticker: ticker,
|
||||
lock: {
|
||||
sats: lockFee,
|
||||
fee: lockFee / QORT_DECIMALS,
|
||||
},
|
||||
unlock: {
|
||||
sats: unlockFee,
|
||||
fee: unlockFee / QORT_DECIMALS,
|
||||
feePerKb: +calculateRateFromFee(+unlockFee, 300) / QORT_DECIMALS,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const createBuyOrder = async (data, isFromExtension) => {
|
||||
const requiredFields = ['crosschainAtInfo', 'foreignBlockchain'];
|
||||
const missingFields: string[] = [];
|
||||
@@ -3519,6 +3612,7 @@ export const createBuyOrder = async (data, isFromExtension) => {
|
||||
|
||||
const crosschainAtInfo = await Promise.all(atPromises);
|
||||
try {
|
||||
const buyingFees = await getBuyingFees(foreignBlockchain);
|
||||
const resPermission = await getUserPermission(
|
||||
{
|
||||
text1:
|
||||
@@ -3532,10 +3626,45 @@ export const createBuyOrder = async (data, isFromExtension) => {
|
||||
return latest + +cur?.expectedForeignAmount;
|
||||
}, 0)
|
||||
)}
|
||||
${` ${crosschainAtInfo?.[0]?.foreignBlockchain}`}`,
|
||||
${` ${buyingFees.ticker}`}`,
|
||||
highlightedText: `Is using public node: ${isGateway}`,
|
||||
fee: '',
|
||||
foreignFee: `${sellerForeignFee[foreignBlockchain].value} ${sellerForeignFee[foreignBlockchain].ticker}`,
|
||||
html: `
|
||||
<div style="max-height: 30vh; overflow-y: auto; font-family: sans-serif;">
|
||||
<style>
|
||||
.fee-container {
|
||||
background-color: var(--background-default);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid #444;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.fee-label {
|
||||
font-weight: bold;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.fee-description {
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="fee-container">
|
||||
<div class="fee-label">Total Unlocking Fee:</div>
|
||||
<div>${(+buyingFees?.unlock?.fee * atAddresses?.length)?.toFixed(8)} ${buyingFees.ticker}</div>
|
||||
<div class="fee-description">
|
||||
This fee is an estimate based on ${atAddresses?.length} ${atAddresses?.length > 1 ? 'orders' : 'order'}, assuming a 300-byte size at a rate of ${buyingFees?.unlock?.feePerKb?.toFixed(8)} ${buyingFees.ticker} per KB.
|
||||
</div>
|
||||
|
||||
<div class="fee-label">Total Locking Fee:</div>
|
||||
<div>${+buyingFees?.lock.fee.toFixed(8)} ${buyingFees.ticker} per kb</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
},
|
||||
isFromExtension
|
||||
);
|
||||
@@ -4924,3 +5053,459 @@ export const buyNameRequest = async (data, isFromExtension) => {
|
||||
throw new Error('User declined request');
|
||||
}
|
||||
};
|
||||
|
||||
export const signForeignFees = async (data, isFromExtension) => {
|
||||
const resPermission = await getUserPermission(
|
||||
{
|
||||
text1: `Do you give this application permission to sign the required fees for all your trade offers?`,
|
||||
},
|
||||
isFromExtension
|
||||
);
|
||||
const { accepted } = resPermission;
|
||||
if (accepted) {
|
||||
const wallet = await getSaveWallet();
|
||||
const address = wallet.address0;
|
||||
const resKeyPair = await getKeyPair();
|
||||
const parsedData = resKeyPair;
|
||||
const uint8PrivateKey = Base58.decode(parsedData.privateKey);
|
||||
const uint8PublicKey = Base58.decode(parsedData.publicKey);
|
||||
const keyPair = {
|
||||
privateKey: uint8PrivateKey,
|
||||
publicKey: uint8PublicKey,
|
||||
};
|
||||
|
||||
const unsignedFeesUrl = await createEndpoint(
|
||||
`/crosschain/unsignedfees/${address}`
|
||||
);
|
||||
|
||||
const unsignedFeesResponse = await fetch(unsignedFeesUrl);
|
||||
|
||||
const unsignedFees = await unsignedFeesResponse.json();
|
||||
|
||||
const signedFees = [];
|
||||
|
||||
unsignedFees.forEach((unsignedFee) => {
|
||||
const unsignedDataDecoded = Base58.decode(unsignedFee.data);
|
||||
|
||||
const signature = nacl.sign.detached(
|
||||
unsignedDataDecoded,
|
||||
keyPair.privateKey
|
||||
);
|
||||
|
||||
const signedFee = {
|
||||
timestamp: unsignedFee.timestamp,
|
||||
data: `${Base58.encode(signature)}`,
|
||||
atAddress: unsignedFee.atAddress,
|
||||
fee: unsignedFee.fee,
|
||||
};
|
||||
|
||||
signedFees.push(signedFee);
|
||||
});
|
||||
|
||||
const signedFeesUrl = await createEndpoint(`/crosschain/signedfees`);
|
||||
|
||||
await fetch(signedFeesUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: `${JSON.stringify(signedFees)}`,
|
||||
});
|
||||
|
||||
return true;
|
||||
} else {
|
||||
throw new Error('User declined request');
|
||||
}
|
||||
};
|
||||
export const multiPaymentWithPrivateData = async (data, isFromExtension) => {
|
||||
const requiredFields = ['payments', 'assetId'];
|
||||
requiredFields.forEach((field) => {
|
||||
if (data[field] === undefined || data[field] === null) {
|
||||
throw new Error(`Missing required field: ${field}`);
|
||||
}
|
||||
});
|
||||
const resKeyPair = await getKeyPair();
|
||||
const parsedData = resKeyPair;
|
||||
const privateKey = parsedData.privateKey;
|
||||
const userPublicKey = parsedData.publicKey;
|
||||
const { fee: paymentFee } = await getFee('TRANSFER_ASSET');
|
||||
const { fee: arbitraryFee } = await getFee('ARBITRARY');
|
||||
|
||||
let name = null;
|
||||
const payments = data.payments;
|
||||
const assetId = data.assetId;
|
||||
const pendingTransactions = [];
|
||||
const pendingAdditionalArbitraryTxs = [];
|
||||
const additionalArbitraryTxsWithoutPayment =
|
||||
data?.additionalArbitraryTxsWithoutPayment || [];
|
||||
let totalAmount = 0;
|
||||
let fee = 0;
|
||||
for (const payment of payments) {
|
||||
const paymentRefId = uid.rnd();
|
||||
const requiredFieldsPayment = ['recipient', 'amount'];
|
||||
|
||||
for (const field of requiredFieldsPayment) {
|
||||
if (!payment[field]) {
|
||||
throw new Error(`Missing required field: ${field}`);
|
||||
}
|
||||
}
|
||||
|
||||
const confirmReceiver = await getNameOrAddress(payment.recipient);
|
||||
if (confirmReceiver.error) {
|
||||
throw new Error('Invalid receiver address or name');
|
||||
}
|
||||
const receiverPublicKey = await getPublicKey(confirmReceiver);
|
||||
|
||||
const amount = +payment.amount.toFixed(8);
|
||||
|
||||
pendingTransactions.push({
|
||||
type: 'PAYMENT',
|
||||
recipientAddress: confirmReceiver,
|
||||
amount: amount,
|
||||
paymentRefId,
|
||||
});
|
||||
|
||||
fee = fee + +paymentFee;
|
||||
totalAmount = totalAmount + amount;
|
||||
|
||||
if (payment.arbitraryTxs && payment.arbitraryTxs.length > 0) {
|
||||
for (const arbitraryTx of payment.arbitraryTxs) {
|
||||
const requiredFieldsArbitraryTx = ['service', 'identifier', 'base64'];
|
||||
|
||||
for (const field of requiredFieldsArbitraryTx) {
|
||||
if (!arbitraryTx[field]) {
|
||||
throw new Error(`Missing required field: ${field}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
const getName = await getNameInfo();
|
||||
if (!getName) throw new Error('Name needed to publish');
|
||||
name = getName;
|
||||
}
|
||||
|
||||
const isValid = isValidBase64WithDecode(arbitraryTx.base64);
|
||||
if (!isValid) throw new Error('Invalid base64 data');
|
||||
if (!arbitraryTx?.service?.includes('_PRIVATE'))
|
||||
throw new Error('Please use a PRIVATE service');
|
||||
const additionalPublicKeys = arbitraryTx?.additionalPublicKeys || [];
|
||||
pendingTransactions.push({
|
||||
type: 'ARBITRARY',
|
||||
identifier: arbitraryTx.identifier,
|
||||
service: arbitraryTx.service,
|
||||
base64: arbitraryTx.base64,
|
||||
description: arbitraryTx?.description || '',
|
||||
paymentRefId,
|
||||
publicKeys: [receiverPublicKey, ...additionalPublicKeys],
|
||||
});
|
||||
|
||||
fee = fee + +arbitraryFee;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
additionalArbitraryTxsWithoutPayment &&
|
||||
additionalArbitraryTxsWithoutPayment.length > 0
|
||||
) {
|
||||
for (const arbitraryTx of additionalArbitraryTxsWithoutPayment) {
|
||||
const requiredFieldsArbitraryTx = ['service', 'identifier', 'base64'];
|
||||
|
||||
for (const field of requiredFieldsArbitraryTx) {
|
||||
if (!arbitraryTx[field]) {
|
||||
throw new Error(`Missing required field: ${field}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
const getName = await getNameInfo();
|
||||
if (!getName) throw new Error('Name needed to publish');
|
||||
name = getName;
|
||||
}
|
||||
|
||||
const isValid = isValidBase64WithDecode(arbitraryTx.base64);
|
||||
if (!isValid) throw new Error('Invalid base64 data');
|
||||
if (!arbitraryTx?.service?.includes('_PRIVATE'))
|
||||
throw new Error('Please use a PRIVATE service');
|
||||
const additionalPublicKeys = arbitraryTx?.additionalPublicKeys || [];
|
||||
pendingAdditionalArbitraryTxs.push({
|
||||
type: 'ARBITRARY',
|
||||
identifier: arbitraryTx.identifier,
|
||||
service: arbitraryTx.service,
|
||||
base64: arbitraryTx.base64,
|
||||
description: arbitraryTx?.description || '',
|
||||
publicKeys: additionalPublicKeys,
|
||||
});
|
||||
|
||||
fee = fee + +arbitraryFee;
|
||||
}
|
||||
}
|
||||
|
||||
if (!name) throw new Error('A name is needed to publish');
|
||||
const balance = await getBalanceInfo();
|
||||
|
||||
if (+balance < fee) throw new Error('Your QORT balance is insufficient');
|
||||
const assetBalance = await getAssetBalanceInfo(assetId);
|
||||
const assetInfo = await getAssetInfo(assetId);
|
||||
if (assetBalance < totalAmount)
|
||||
throw new Error('Your asset balance is insufficient');
|
||||
|
||||
const resPermission = await getUserPermission(
|
||||
{
|
||||
text1:
|
||||
'Do you give this application permission to make the following payments and publishes?',
|
||||
text2: `Asset used in payments: ${assetInfo.name}`,
|
||||
html: `
|
||||
<div style="max-height: 30vh; overflow-y: auto;">
|
||||
<style>
|
||||
|
||||
.resource-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid;
|
||||
padding: 16px;
|
||||
margin: 8px 0;
|
||||
border-radius: 8px;
|
||||
background-color: var(--background-default);
|
||||
}
|
||||
|
||||
.resource-detail {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.resource-detail span {
|
||||
font-weight: bold;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
.resource-container {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.resource-detail {
|
||||
flex: 1 1 45%;
|
||||
margin-bottom: 0;
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
${pendingTransactions
|
||||
.filter((item) => item.type === 'PAYMENT')
|
||||
.map(
|
||||
(payment) => `
|
||||
<div class="resource-container">
|
||||
<div class="resource-detail"><span>Recipient:</span> ${
|
||||
payment.recipientAddress
|
||||
}</div>
|
||||
<div class="resource-detail"><span>Amount:</span> ${payment.amount}</div>
|
||||
</div>`
|
||||
)
|
||||
.join('')}
|
||||
${[...pendingTransactions, ...pendingAdditionalArbitraryTxs]
|
||||
.filter((item) => item.type === 'ARBITRARY')
|
||||
.map(
|
||||
(arbitraryTx) => `
|
||||
<div class="resource-container">
|
||||
<div class="resource-detail"><span>Service:</span> ${
|
||||
arbitraryTx.service
|
||||
}</div>
|
||||
<div class="resource-detail"><span>Name:</span> ${name}</div>
|
||||
<div class="resource-detail"><span>Identifier:</span> ${
|
||||
arbitraryTx.identifier
|
||||
}</div>
|
||||
</div>`
|
||||
)
|
||||
.join('')}
|
||||
</div>
|
||||
|
||||
`,
|
||||
highlightedText: `Total Amount: ${totalAmount}`,
|
||||
fee: fee,
|
||||
},
|
||||
isFromExtension
|
||||
);
|
||||
const { accepted, checkbox1 = false } = resPermission;
|
||||
if (!accepted) {
|
||||
throw new Error('User declined request');
|
||||
}
|
||||
|
||||
// const failedTxs = []
|
||||
const paymentsDone = {};
|
||||
|
||||
const transactionsDone = [];
|
||||
|
||||
for (const transaction of pendingTransactions) {
|
||||
const type = transaction.type;
|
||||
|
||||
if (type === 'PAYMENT') {
|
||||
const makePayment = await retryTransaction(
|
||||
transferAsset,
|
||||
[
|
||||
{
|
||||
amount: transaction.amount,
|
||||
assetId,
|
||||
recipient: transaction.recipientAddress,
|
||||
},
|
||||
],
|
||||
true
|
||||
);
|
||||
if (makePayment) {
|
||||
transactionsDone.push(makePayment?.signature);
|
||||
if (transaction.paymentRefId) {
|
||||
paymentsDone[transaction.paymentRefId] = makePayment;
|
||||
}
|
||||
}
|
||||
} else if (type === 'ARBITRARY' && paymentsDone[transaction.paymentRefId]) {
|
||||
const objectToEncrypt = {
|
||||
data: transaction.base64,
|
||||
payment: paymentsDone[transaction.paymentRefId],
|
||||
};
|
||||
|
||||
const toBase64 = await retryTransaction(
|
||||
objectToBase64,
|
||||
[objectToEncrypt],
|
||||
true
|
||||
);
|
||||
|
||||
if (!toBase64) continue; // Skip if encryption fails
|
||||
|
||||
const encryptDataResponse = await retryTransaction(
|
||||
encryptDataGroup,
|
||||
[
|
||||
{
|
||||
data64: toBase64,
|
||||
publicKeys: transaction.publicKeys,
|
||||
privateKey,
|
||||
userPublicKey,
|
||||
},
|
||||
],
|
||||
true
|
||||
);
|
||||
|
||||
if (!encryptDataResponse) continue; // Skip if encryption fails
|
||||
|
||||
const resPublish = await retryTransaction(
|
||||
publishData,
|
||||
[
|
||||
{
|
||||
registeredName: encodeURIComponent(name),
|
||||
file: encryptDataResponse,
|
||||
service: transaction.service,
|
||||
identifier: encodeURIComponent(transaction.identifier),
|
||||
uploadType: 'file',
|
||||
description: transaction?.description,
|
||||
isBase64: true,
|
||||
apiVersion: 2,
|
||||
withFee: true,
|
||||
},
|
||||
],
|
||||
true
|
||||
);
|
||||
|
||||
if (resPublish?.signature) {
|
||||
transactionsDone.push(resPublish?.signature);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const transaction of pendingAdditionalArbitraryTxs) {
|
||||
const objectToEncrypt = {
|
||||
data: transaction.base64,
|
||||
};
|
||||
|
||||
const toBase64 = await retryTransaction(
|
||||
objectToBase64,
|
||||
[objectToEncrypt],
|
||||
true
|
||||
);
|
||||
|
||||
if (!toBase64) continue; // Skip if encryption fails
|
||||
|
||||
const encryptDataResponse = await retryTransaction(
|
||||
encryptDataGroup,
|
||||
[
|
||||
{
|
||||
data64: toBase64,
|
||||
publicKeys: transaction.publicKeys,
|
||||
privateKey,
|
||||
userPublicKey,
|
||||
},
|
||||
],
|
||||
true
|
||||
);
|
||||
|
||||
if (!encryptDataResponse) continue; // Skip if encryption fails
|
||||
|
||||
const resPublish = await retryTransaction(
|
||||
publishData,
|
||||
[
|
||||
{
|
||||
registeredName: encodeURIComponent(name),
|
||||
file: encryptDataResponse,
|
||||
service: transaction.service,
|
||||
identifier: encodeURIComponent(transaction.identifier),
|
||||
uploadType: 'file',
|
||||
description: transaction?.description,
|
||||
isBase64: true,
|
||||
apiVersion: 2,
|
||||
withFee: true,
|
||||
},
|
||||
],
|
||||
true
|
||||
);
|
||||
|
||||
if (resPublish?.signature) {
|
||||
transactionsDone.push(resPublish?.signature);
|
||||
}
|
||||
}
|
||||
|
||||
return transactionsDone;
|
||||
};
|
||||
|
||||
export const transferAssetRequest = async (data, isFromExtension) => {
|
||||
const requiredFields = ['amount', 'assetId', 'recipient'];
|
||||
requiredFields.forEach((field) => {
|
||||
if (data[field] === undefined || data[field] === null) {
|
||||
throw new Error(`Missing required field: ${field}`);
|
||||
}
|
||||
});
|
||||
const amount = data.amount;
|
||||
const assetId = data.assetId;
|
||||
const recipient = data.recipient;
|
||||
|
||||
const { fee } = await getFee('TRANSFER_ASSET');
|
||||
const balance = await getBalanceInfo();
|
||||
|
||||
if (+balance < +fee) throw new Error('Your QORT balance is insufficient');
|
||||
const assetBalance = await getAssetBalanceInfo(assetId);
|
||||
if (assetBalance < amount)
|
||||
throw new Error('Your asset balance is insufficient');
|
||||
const confirmReceiver = await getNameOrAddress(recipient);
|
||||
if (confirmReceiver.error) {
|
||||
throw new Error('Invalid receiver address or name');
|
||||
}
|
||||
const assetInfo = await getAssetInfo(assetId);
|
||||
const resPermission = await getUserPermission(
|
||||
{
|
||||
text1: `Do you give this application permission to transfer the following asset?`,
|
||||
text2: `Asset: ${assetInfo?.name}`,
|
||||
highlightedText: `Amount: ${amount}`,
|
||||
fee: fee,
|
||||
},
|
||||
isFromExtension
|
||||
);
|
||||
|
||||
const { accepted } = resPermission;
|
||||
if (!accepted) {
|
||||
throw new Error('User declined request');
|
||||
}
|
||||
const res = await transferAsset({
|
||||
amount,
|
||||
recipient: confirmReceiver,
|
||||
assetId,
|
||||
});
|
||||
return res;
|
||||
};
|
||||
|
@@ -8,15 +8,14 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tooltip .bottom {
|
||||
.tooltip .core-panel {
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--black);
|
||||
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.5);
|
||||
box-sizing: border-box;
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
max-width: 250px;
|
||||
min-width: 225px;
|
||||
width: max-content;
|
||||
opacity: 0;
|
||||
padding: 10px 10px;
|
||||
position: absolute;
|
||||
@@ -27,23 +26,23 @@
|
||||
z-index: 99999999;
|
||||
}
|
||||
|
||||
.tooltip[data-theme='light'] .bottom {
|
||||
.tooltip[data-theme='light'] .core-panel {
|
||||
background-color: #f1f1f1;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.tooltip[data-theme='dark'] .bottom {
|
||||
.tooltip[data-theme='dark'] .core-panel {
|
||||
background-color: var(--bg-2);
|
||||
color: var(--black);
|
||||
}
|
||||
|
||||
.tooltip:hover .bottom {
|
||||
.tooltip:hover .core-panel {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.tooltip .bottom i {
|
||||
.tooltip .core-panel i {
|
||||
bottom: 100%;
|
||||
height: 12px;
|
||||
left: 50%;
|
||||
|
@@ -56,6 +56,7 @@ const commonThemeOptions = {
|
||||
xl: 1536,
|
||||
},
|
||||
},
|
||||
|
||||
components: {
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
@@ -72,6 +73,7 @@ const commonThemeOptions = {
|
||||
disableRipple: true,
|
||||
},
|
||||
},
|
||||
|
||||
MuiModal: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
|
@@ -48,6 +48,7 @@ export const darkThemeOptions: ThemeOptions = {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: (theme) => ({
|
||||
':root': {
|
||||
@@ -55,14 +56,22 @@ export const darkThemeOptions: ThemeOptions = {
|
||||
'--bg-primary': 'rgba(31, 32, 35, 1)',
|
||||
'--bg-2': 'rgb(39, 40, 44)',
|
||||
'--primary-main': theme.palette.primary.main,
|
||||
'--text-primary': theme.palette.text.primary,
|
||||
'--text-secondary': theme.palette.text.secondary,
|
||||
'--background-default': theme.palette.background.default,
|
||||
'--background-paper': theme.palette.background.paper,
|
||||
'--background-surface': theme.palette.background.surface,
|
||||
},
|
||||
|
||||
'*, *::before, *::after': {
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
|
||||
html: {
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
},
|
||||
|
||||
body: {
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
@@ -95,6 +104,7 @@ export const darkThemeOptions: ThemeOptions = {
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
MuiIcon: {
|
||||
defaultProps: {
|
||||
style: {
|
||||
@@ -103,6 +113,7 @@ export const darkThemeOptions: ThemeOptions = {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
MuiDialog: {
|
||||
styleOverrides: {
|
||||
paper: {
|
||||
@@ -110,6 +121,7 @@ export const darkThemeOptions: ThemeOptions = {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
MuiPopover: {
|
||||
styleOverrides: {
|
||||
paper: {
|
||||
|
@@ -32,6 +32,7 @@ export const lightThemeOptions: ThemeOptions = {
|
||||
unread: 'rgb(66, 151, 226)',
|
||||
},
|
||||
},
|
||||
|
||||
components: {
|
||||
MuiCard: {
|
||||
styleOverrides: {
|
||||
@@ -48,6 +49,7 @@ export const lightThemeOptions: ThemeOptions = {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: (theme) => ({
|
||||
':root': {
|
||||
@@ -55,6 +57,11 @@ export const lightThemeOptions: ThemeOptions = {
|
||||
'--bg-primary': 'rgba(31, 32, 35, 1)',
|
||||
'--bg-2': 'rgba(39, 40, 44, 1)',
|
||||
'--primary-main': theme.palette.primary.main,
|
||||
'--text-primary': theme.palette.text.primary,
|
||||
'--text-secondary': theme.palette.text.secondary,
|
||||
'--background-default': theme.palette.background.default,
|
||||
'--background-paper': theme.palette.background.paper,
|
||||
'--background-surface': theme.palette.background.surface,
|
||||
},
|
||||
|
||||
'*, *::before, *::after': {
|
||||
@@ -108,6 +115,7 @@ export const lightThemeOptions: ThemeOptions = {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
MuiDialog: {
|
||||
styleOverrides: {
|
||||
paper: {
|
||||
@@ -115,6 +123,7 @@ export const lightThemeOptions: ThemeOptions = {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
MuiPopover: {
|
||||
styleOverrides: {
|
||||
paper: {
|
||||
|
35
src/transactions/TransferAssetTransaction.ts
Normal file
35
src/transactions/TransferAssetTransaction.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import { QORT_DECIMALS } from '../constants/constants'
|
||||
import TransactionBase from './TransactionBase'
|
||||
|
||||
export default class TransferAssetTransaction extends TransactionBase {
|
||||
constructor() {
|
||||
super()
|
||||
this.type = 12
|
||||
}
|
||||
|
||||
set recipient(recipient) {
|
||||
this._recipient = recipient instanceof Uint8Array ? recipient : this.constructor.Base58.decode(recipient)
|
||||
}
|
||||
|
||||
set amount(amount) {
|
||||
this._amount = Math.round(amount * QORT_DECIMALS)
|
||||
this._amountBytes = this.constructor.utils.int64ToBytes(this._amount)
|
||||
}
|
||||
|
||||
set assetId(assetId) {
|
||||
this._assetId = this.constructor.utils.int64ToBytes(assetId)
|
||||
}
|
||||
|
||||
get params() {
|
||||
const params = super.params
|
||||
params.push(
|
||||
this._recipient,
|
||||
this._assetId,
|
||||
this._amountBytes,
|
||||
this._feeBytes
|
||||
)
|
||||
return params
|
||||
}
|
||||
}
|
@@ -24,6 +24,7 @@ import UpdateGroupTransaction from './UpdateGroupTransaction.js'
|
||||
import SellNameTransacion from './SellNameTransacion.js'
|
||||
import CancelSellNameTransacion from './CancelSellNameTransacion.js'
|
||||
import BuyNameTransacion from './BuyNameTransacion.js'
|
||||
import TransferAssetTransaction from './TransferAssetTransaction.js'
|
||||
|
||||
|
||||
export const transactionTypes = {
|
||||
@@ -35,6 +36,7 @@ export const transactionTypes = {
|
||||
7: BuyNameTransacion,
|
||||
8: CreatePollTransaction,
|
||||
9: VoteOnPollTransaction,
|
||||
12: TransferAssetTransaction,
|
||||
16: DeployAtTransaction,
|
||||
18: ChatTransaction,
|
||||
181: GroupChatTransaction,
|
||||
|
@@ -13,4 +13,19 @@ export function decodeIfEncoded(input) {
|
||||
|
||||
// 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;
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user