Merge branch 'develop' into chat-image

This commit is contained in:
2025-05-10 23:50:59 +03:00
102 changed files with 3335 additions and 1288 deletions

View File

@@ -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(() => {

View File

@@ -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>
)}

View File

@@ -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',
}}

View File

@@ -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(() => {

View File

@@ -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) {

View File

@@ -63,6 +63,7 @@ export const DownloadWallet = ({
wallet,
qortAddress: rawWallet.address0,
});
return {
wallet,
qortAddress: rawWallet.address0,

View File

@@ -18,7 +18,7 @@ export const AnnouncementList = ({
loadMore,
myName,
}) => {
const listRef = useRef();
const listRef = useRef(null);
const [messages, setMessages] = useState(initialMessages);
useEffect(() => {

View File

@@ -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);

View File

@@ -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(() => {

View File

@@ -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({

View File

@@ -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>
);

View File

@@ -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';

View File

@@ -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');

View File

@@ -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);

View File

@@ -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',

View File

@@ -1,4 +1,3 @@
import React from 'react';
import { JoinGroup } from './JoinGroup';
export const GlobalActions = () => {

View File

@@ -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

View File

@@ -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>

View File

@@ -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,
},

View File

@@ -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={{

View File

@@ -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 ? (

View File

@@ -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]);

View File

@@ -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,

View File

@@ -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={{

View File

@@ -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={{

View File

@@ -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({

View File

@@ -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

View File

@@ -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>

View File

@@ -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

View File

@@ -18,6 +18,7 @@ import { getNameInfo } from './Group';
import { getFee } from '../../background';
import { LoadingButton } from '@mui/lab';
import { getBaseApiReact } from '../../App';
import { useTranslation } from 'react-i18next';
export const getMemberInvites = async (groupNumber) => {
const response = await fetch(
@@ -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>

View File

@@ -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',

View File

@@ -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>

View File

@@ -1,4 +1,4 @@
import * as React from 'react';
import { useEffect, useState } from 'react';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
@@ -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) => {

View File

@@ -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>
);
};

View File

@@ -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>
)}

View File

@@ -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>
</>
);
};

View File

@@ -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',

View File

@@ -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>

View File

@@ -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) {

View File

@@ -164,6 +164,7 @@ export const useBlockedAddresses = () => {
});
});
}
if (address) {
await new Promise((res, rej) => {
window

View File

@@ -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 {

View File

@@ -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" />

View File

@@ -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 }}>

View File

@@ -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>
);
};

View File

@@ -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>
)
}
);
};

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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

View File

@@ -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() ||

View File

@@ -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);
}
};

View File

@@ -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);
}