mirror of
https://github.com/Qortal/qortal-mobile.git
synced 2025-03-14 20:02:33 +00:00
public group chats
This commit is contained in:
parent
c5e29de0e9
commit
f481dee813
@ -100,6 +100,47 @@ transition: all 0.2s;
|
||||
|
||||
`
|
||||
|
||||
interface CustomButtonProps {
|
||||
bgColor?: string;
|
||||
color?: string;
|
||||
}
|
||||
export const CustomButtonAccept = styled(Box)<CustomButtonProps>(
|
||||
({ bgColor, color }) => ({
|
||||
boxSizing: "border-box",
|
||||
padding: "15px 20px",
|
||||
gap: "10px",
|
||||
border: "0.5px solid rgba(255, 255, 255, 0.5)",
|
||||
filter: "drop-shadow(1px 4px 10.5px rgba(0,0,0,0.3))",
|
||||
borderRadius: 5,
|
||||
display: "inline-flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: "fit-content",
|
||||
transition: "all 0.2s",
|
||||
minWidth: 160,
|
||||
cursor: "pointer",
|
||||
fontWeight: 600,
|
||||
fontFamily: "Inter",
|
||||
textAlign: "center",
|
||||
opacity: 0.7,
|
||||
// Use the passed-in props or fallback defaults
|
||||
backgroundColor: bgColor || "transparent",
|
||||
color: color || "white",
|
||||
|
||||
"&:hover": {
|
||||
opacity: 1,
|
||||
backgroundColor: bgColor
|
||||
? bgColor
|
||||
: "rgba(41, 41, 43, 1)", // fallback hover bg
|
||||
color: color || "white",
|
||||
svg: {
|
||||
path: {
|
||||
fill: color || "white",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
export const CustomButton = styled(Box)`
|
||||
|
||||
/* Authenticate */
|
||||
|
150
src/App.tsx
150
src/App.tsx
@ -62,6 +62,7 @@ import {
|
||||
AuthenticatedContainerInnerLeft,
|
||||
AuthenticatedContainerInnerRight,
|
||||
CustomButton,
|
||||
CustomButtonAccept,
|
||||
CustomInput,
|
||||
CustomLabel,
|
||||
TextItalic,
|
||||
@ -436,6 +437,8 @@ function App() {
|
||||
const [isOpenSendQortSuccess, setIsOpenSendQortSuccess] = useState(false);
|
||||
const [rootHeight, setRootHeight] = useState("100%");
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||
const [showSeed, setShowSeed] = useState(false)
|
||||
const [creationStep, setCreationStep] = useState(1)
|
||||
const qortalRequestCheckbox1Ref = useRef(null);
|
||||
useRetrieveDataLocalStorage();
|
||||
useQortalGetSaveSettings(userInfo?.name);
|
||||
@ -1095,6 +1098,8 @@ function App() {
|
||||
setExtstate("authenticated");
|
||||
setIsOpenSendQort(false);
|
||||
setIsOpenSendQortSuccess(false);
|
||||
setShowSeed(false)
|
||||
setCreationStep(1)
|
||||
};
|
||||
|
||||
const resetAllStates = () => {
|
||||
@ -1124,6 +1129,8 @@ function App() {
|
||||
setTxList([]);
|
||||
setMemberGroups([]);
|
||||
resetAllRecoil();
|
||||
setShowSeed(false)
|
||||
setCreationStep(1)
|
||||
};
|
||||
|
||||
function roundUpToDecimals(number, decimals = 8) {
|
||||
@ -2534,7 +2541,15 @@ await showInfo({
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
if(creationStep === 2){
|
||||
setCreationStep(1)
|
||||
return
|
||||
}
|
||||
setExtstate("not-authenticated");
|
||||
setShowSeed(false)
|
||||
setCreationStep(1)
|
||||
setWalletToBeDownloadedPasswordConfirm('')
|
||||
setWalletToBeDownloadedPassword('')
|
||||
}}
|
||||
src={Return}
|
||||
/>
|
||||
@ -2567,32 +2582,110 @@ await showInfo({
|
||||
padding: '10px'
|
||||
}}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
display: creationStep === 1 ? 'flex' : 'none',
|
||||
|
||||
flexDirection: 'column',
|
||||
maxWidth: '400px',
|
||||
alignItems: 'center',
|
||||
gap: '10px'
|
||||
width: '350px',
|
||||
maxWidth: '95%',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<Typography sx={{
|
||||
fontSize: '14px'
|
||||
}}>Your seedphrase</Typography>
|
||||
}}>
|
||||
A ‘ <span onClick={()=> {
|
||||
setShowSeed(true)
|
||||
}} style={{
|
||||
fontSize: '14px',
|
||||
color: 'steelblue',
|
||||
cursor: 'pointer'
|
||||
}}>SEEDPHRASE</span> ’ has been randomly generated in the background.
|
||||
|
||||
|
||||
</Typography>
|
||||
<Typography sx={{
|
||||
fontSize: '12px'
|
||||
}}>Only shown once! Please copy and keep safe!</Typography>
|
||||
fontSize: '14px',
|
||||
marginTop: '5px'
|
||||
}}>
|
||||
If you wish to VIEW THE SEEDPHRASE, click the word 'SEEDPHRASE' in this text. Seedphrases are used to generate the private key for your Qortal account. For security by default, seedphrases are NOT displayed unless specifically chosen.
|
||||
</Typography>
|
||||
<Typography sx={{
|
||||
fontSize: '16px',
|
||||
marginTop: '15px',
|
||||
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
Create your Qortal account by clicking <span style={{
|
||||
fontWeight: 'bold'
|
||||
}}>NEXT</span> below.
|
||||
|
||||
</Typography>
|
||||
<Spacer height="17px" />
|
||||
<CustomButton onClick={()=> {
|
||||
setCreationStep(2)
|
||||
}}>
|
||||
Next
|
||||
</CustomButton>
|
||||
</Box>
|
||||
<div style={{
|
||||
display: 'none'
|
||||
}}>
|
||||
|
||||
<random-sentence-generator
|
||||
ref={generatorRef}
|
||||
template="adverb verb noun adjective noun adverb verb noun adjective noun adjective verbed adjective noun"
|
||||
|
||||
></random-sentence-generator>
|
||||
</div>
|
||||
<Dialog
|
||||
open={showSeed}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<DialogContent>
|
||||
<Box sx={{
|
||||
flexDirection: 'column',
|
||||
maxWidth: '400px',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
display: showSeed ? 'flex' : 'none'
|
||||
}}>
|
||||
<Typography sx={{
|
||||
fontSize: '14px'
|
||||
}}>Your seedphrase</Typography>
|
||||
|
||||
<Box sx={{
|
||||
textAlign: 'center',
|
||||
width: '100%',
|
||||
backgroundColor: '#1f2023',
|
||||
borderRadius: '5px',
|
||||
padding: '10px',
|
||||
}}>
|
||||
{generatorRef.current?.parsedString}
|
||||
</Box>
|
||||
</Box>
|
||||
<CustomButton sx={{
|
||||
|
||||
<CustomButton sx={{
|
||||
padding: '7px',
|
||||
fontSize: '12px'
|
||||
}} onClick={exportSeedphrase}>
|
||||
Export Seedphrase
|
||||
</CustomButton>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
|
||||
<Button variant="contained" onClick={()=> setShowSeed(false)}>
|
||||
close
|
||||
</Button>
|
||||
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
<Box sx={{
|
||||
display: creationStep === 2 ? 'flex' : 'none',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
|
||||
}}>
|
||||
<Spacer height="14px" />
|
||||
<CustomLabel htmlFor="standard-adornment-password">
|
||||
Wallet Password
|
||||
@ -2622,6 +2715,7 @@ await showInfo({
|
||||
<CustomButton onClick={createAccountFunc}>
|
||||
Create Account
|
||||
</CustomButton>
|
||||
</Box>
|
||||
<ErrorText>{walletToBeDownloadedError}</ErrorText>
|
||||
</>
|
||||
)}
|
||||
@ -2776,11 +2870,29 @@ await showInfo({
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="contained" onClick={onCancel}>
|
||||
Disagree
|
||||
<Button sx={{
|
||||
backgroundColor: 'var(--green)',
|
||||
color: 'black',
|
||||
opacity: 0.7,
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--green)',
|
||||
color: 'black',
|
||||
opacity: 1
|
||||
},
|
||||
}} variant="contained" onClick={onOk} autoFocus>
|
||||
accept
|
||||
</Button>
|
||||
<Button variant="contained" onClick={onOk} autoFocus>
|
||||
Agree
|
||||
<Button sx={{
|
||||
backgroundColor: 'var(--unread)',
|
||||
color: 'black',
|
||||
opacity: 0.7,
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--unread)',
|
||||
color: 'black',
|
||||
opacity: 1
|
||||
},
|
||||
}} variant="contained" onClick={onCancel}>
|
||||
decline
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
@ -3051,22 +3163,26 @@ await showInfo({
|
||||
gap: "14px",
|
||||
}}
|
||||
>
|
||||
<CustomButton
|
||||
<CustomButtonAccept
|
||||
color="black"
|
||||
bgColor="var(--green)"
|
||||
sx={{
|
||||
minWidth: "102px",
|
||||
}}
|
||||
onClick={() => onOkQortalRequestExtension("accepted")}
|
||||
>
|
||||
accept
|
||||
</CustomButton>
|
||||
<CustomButton
|
||||
</CustomButtonAccept>
|
||||
<CustomButtonAccept
|
||||
color="black"
|
||||
bgColor="var(--unread)"
|
||||
sx={{
|
||||
minWidth: "102px",
|
||||
}}
|
||||
onClick={() => onCancelQortalRequestExtension()}
|
||||
>
|
||||
decline
|
||||
</CustomButton>
|
||||
</CustomButtonAccept>
|
||||
</Box>
|
||||
<ErrorText>{sendPaymentError}</ErrorText>
|
||||
</Box>
|
||||
|
@ -92,21 +92,6 @@ export const MessageQueueProvider = ({ children }) => {
|
||||
|
||||
// Remove the message from the queue after successful sending
|
||||
messageQueueRef.current.shift();
|
||||
// Remove the message from queueChats
|
||||
setQueueChats((prev) => {
|
||||
const updatedChats = { ...prev };
|
||||
if (updatedChats[groupDirectId]) {
|
||||
updatedChats[groupDirectId] = updatedChats[groupDirectId].filter(
|
||||
(item) => item.identifier !== identifier
|
||||
);
|
||||
|
||||
// If no more chats for this group, delete the groupDirectId entry
|
||||
if (updatedChats[groupDirectId].length === 0) {
|
||||
delete updatedChats[groupDirectId];
|
||||
}
|
||||
}
|
||||
return updatedChats;
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Message sending failed', error);
|
||||
@ -142,15 +127,25 @@ export const MessageQueueProvider = ({ children }) => {
|
||||
|
||||
// Method to process with new messages and groupDirectId
|
||||
const processWithNewMessages = (newMessages, groupDirectId) => {
|
||||
let updatedNewMessages = newMessages
|
||||
if (newMessages.length > 0) {
|
||||
messageQueueRef.current = messageQueueRef.current.filter((msg) => {
|
||||
return !newMessages.some(newMsg => newMsg?.specialId === msg?.specialId);
|
||||
});
|
||||
|
||||
// Remove corresponding entries in queueChats for the provided groupDirectId
|
||||
setQueueChats((prev) => {
|
||||
const updatedChats = { ...prev };
|
||||
if (updatedChats[groupDirectId]) {
|
||||
|
||||
updatedNewMessages = newMessages?.map((msg)=> {
|
||||
const findTempMsg = updatedChats[groupDirectId]?.find((msg2)=> msg2?.message?.specialId === msg?.specialId)
|
||||
if(findTempMsg){
|
||||
return {
|
||||
...msg,
|
||||
tempSignature: findTempMsg?.signature
|
||||
}
|
||||
}
|
||||
return msg
|
||||
})
|
||||
|
||||
|
||||
updatedChats[groupDirectId] = updatedChats[groupDirectId].filter((chat) => {
|
||||
return !newMessages.some(newMsg => newMsg?.specialId === chat?.message?.specialId);
|
||||
});
|
||||
@ -167,8 +162,23 @@ export const MessageQueueProvider = ({ children }) => {
|
||||
}
|
||||
return updatedChats;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if(!messageQueueRef.current.find((msg) => msg?.groupDirectId === groupDirectId)){
|
||||
setQueueChats((prev) => {
|
||||
const updatedChats = { ...prev };
|
||||
if (updatedChats[groupDirectId]) {
|
||||
delete updatedChats[groupDirectId]
|
||||
}
|
||||
|
||||
return updatedChats
|
||||
}
|
||||
)
|
||||
}
|
||||
}, 300);
|
||||
|
||||
return updatedNewMessages
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -608,8 +608,7 @@ const handleNotification = async (groups) => {
|
||||
const data = groups.filter(
|
||||
(group) =>
|
||||
group?.sender !== address &&
|
||||
!mutedGroups.includes(group.groupId) &&
|
||||
!isUpdateMsg(group?.data)
|
||||
!mutedGroups.includes(group.groupId)
|
||||
);
|
||||
const dataWithUpdates = groups.filter(
|
||||
(group) => group?.sender !== address && !mutedGroups.includes(group.groupId)
|
||||
@ -657,8 +656,7 @@ const handleNotification = async (groups) => {
|
||||
Date.now() - lastGroupNotification >= 120000
|
||||
) {
|
||||
if (
|
||||
!newestLatestTimestamp?.data ||
|
||||
!isExtMsg(newestLatestTimestamp?.data)
|
||||
!newestLatestTimestamp?.data
|
||||
) return;
|
||||
|
||||
const notificationId = generateId()
|
||||
|
@ -22,7 +22,7 @@ import { useRecoilState, useSetRecoilState } from "recoil";
|
||||
import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from "../../atoms/global";
|
||||
import { saveToLocalStorage } from "./AppsNavBar";
|
||||
|
||||
export const AppInfoSnippet = ({ app, myName, isFromCategory }) => {
|
||||
export const AppInfoSnippet = ({ app, myName, isFromCategory, parentStyles = {} }) => {
|
||||
|
||||
const isInstalled = app?.status?.status === 'READY'
|
||||
const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(sortablePinnedAppsAtom);
|
||||
@ -30,7 +30,9 @@ export const AppInfoSnippet = ({ app, myName, isFromCategory }) => {
|
||||
const isSelectedAppPinned = !!sortablePinnedApps?.find((item)=> item?.name === app?.name && item?.service === app?.service)
|
||||
const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom);
|
||||
return (
|
||||
<AppInfoSnippetContainer>
|
||||
<AppInfoSnippetContainer sx={{
|
||||
...parentStyles
|
||||
}}>
|
||||
<AppInfoSnippetLeft>
|
||||
<ButtonBase
|
||||
sx={{
|
||||
|
@ -86,6 +86,8 @@ export const AppsCategory = ({ availableQapps, myName, category, isShow }) =>
|
||||
|
||||
|
||||
const categoryList = useMemo(() => {
|
||||
if(category?.id === 'all') return availableQapps
|
||||
|
||||
return availableQapps.filter(
|
||||
(app) =>
|
||||
app?.metadata?.category === category?.id
|
||||
@ -99,7 +101,11 @@ export const AppsCategory = ({ availableQapps, myName, category, isShow }) =>
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(searchValue);
|
||||
}, 350);
|
||||
|
||||
setTimeout(() => {
|
||||
virtuosoRef.current.scrollToIndex({
|
||||
index: 0
|
||||
});
|
||||
}, 500);
|
||||
// Cleanup timeout if searchValue changes before the timeout completes
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
@ -111,14 +117,14 @@ export const AppsCategory = ({ availableQapps, myName, category, isShow }) =>
|
||||
const searchedList = useMemo(() => {
|
||||
if (!debouncedValue) return categoryList
|
||||
return categoryList.filter((app) =>
|
||||
app.name.toLowerCase().includes(debouncedValue.toLowerCase())
|
||||
app.name.toLowerCase().includes(debouncedValue.toLowerCase()) || (app?.metadata?.title && app?.metadata?.title?.toLowerCase().includes(debouncedValue.toLowerCase()))
|
||||
);
|
||||
}, [debouncedValue, categoryList]);
|
||||
|
||||
const rowRenderer = (index) => {
|
||||
|
||||
let app = searchedList[index];
|
||||
return <AppInfoSnippet key={`${app?.service}-${app?.name}`} app={app} myName={myName} isFromCategory={true} />;
|
||||
return <AppInfoSnippet key={`${app?.service}-${app?.name}`} app={app} myName={myName} isFromCategory={true} />;
|
||||
};
|
||||
|
||||
|
||||
|
@ -17,7 +17,7 @@ import {
|
||||
PublishQAppCTARight,
|
||||
PublishQAppDotsBG,
|
||||
} from "./Apps-styles";
|
||||
import { Avatar, Box, ButtonBase, InputBase, styled } from "@mui/material";
|
||||
import { Avatar, Box, ButtonBase, InputBase, Typography, styled } from "@mui/material";
|
||||
import { Add } from "@mui/icons-material";
|
||||
import { MyContext, getBaseApiReact } from "../../App";
|
||||
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
|
||||
@ -101,7 +101,11 @@ export const AppsLibrary = ({ availableQapps, setMode, myName, hasPublishApp, i
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(searchValue);
|
||||
}, 350);
|
||||
|
||||
setTimeout(() => {
|
||||
virtuosoRef.current.scrollToIndex({
|
||||
index: 0
|
||||
});
|
||||
}, 500);
|
||||
// Cleanup timeout if searchValue changes before the timeout completes
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
@ -113,7 +117,7 @@ export const AppsLibrary = ({ availableQapps, setMode, myName, hasPublishApp, i
|
||||
const searchedList = useMemo(() => {
|
||||
if (!debouncedValue) return [];
|
||||
return availableQapps.filter((app) =>
|
||||
app.name.toLowerCase().includes(debouncedValue.toLowerCase())
|
||||
app.name.toLowerCase().includes(debouncedValue.toLowerCase()) || (app?.metadata?.title && app?.metadata?.title?.toLowerCase().includes(debouncedValue.toLowerCase()))
|
||||
);
|
||||
}, [debouncedValue]);
|
||||
|
||||
@ -214,6 +218,10 @@ export const AppsLibrary = ({ availableQapps, setMode, myName, hasPublishApp, i
|
||||
/>
|
||||
</StyledVirtuosoContainer>
|
||||
</AppsWidthLimiter>
|
||||
) : searchedList?.length === 0 && debouncedValue ? (
|
||||
<AppsWidthLimiter>
|
||||
<Typography>No results</Typography>
|
||||
</AppsWidthLimiter>
|
||||
) : (
|
||||
<>
|
||||
<AppsWidthLimiter>
|
||||
@ -313,6 +321,33 @@ export const AppsLibrary = ({ availableQapps, setMode, myName, hasPublishApp, i
|
||||
// Hide scrollbar for IE and older Edge
|
||||
"-ms-overflow-style": "none",
|
||||
}}>
|
||||
<ButtonBase
|
||||
|
||||
onClick={() => {
|
||||
executeEvent("selectedCategory", {
|
||||
data: {
|
||||
id: 'all',
|
||||
name: 'All'
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '110px',
|
||||
width: '110px',
|
||||
background: 'linear-gradient(163.47deg, #4BBCFE 27.55%, #1386C9 86.56%)',
|
||||
color: '#1D1D1E',
|
||||
fontWeight: 700,
|
||||
fontSize: '16px',
|
||||
flexShrink: 0,
|
||||
borderRadius: '11px'
|
||||
}}>
|
||||
All
|
||||
</Box>
|
||||
</ButtonBase>
|
||||
{categories?.map((category)=> {
|
||||
return (
|
||||
<ButtonBase key={category?.id} onClick={()=> {
|
||||
|
@ -116,9 +116,9 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi
|
||||
data: encryptedMessages,
|
||||
involvingAddress: selectedDirect?.address,
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response?.error) {
|
||||
processWithNewMessages(response, selectedDirect?.address);
|
||||
.then((decryptResponse) => {
|
||||
if (!decryptResponse?.error) {
|
||||
const response = processWithNewMessages(decryptResponse, selectedDirect?.address);
|
||||
res(response);
|
||||
|
||||
if (isInitiated) {
|
||||
@ -366,7 +366,7 @@ useEffect(() => {
|
||||
const htmlContent = editorRef?.current.getHTML();
|
||||
const stringified = JSON.stringify(htmlContent);
|
||||
const size = new Blob([stringified]).size;
|
||||
setMessageSize(size + 100);
|
||||
setMessageSize(size + 200);
|
||||
};
|
||||
|
||||
// Add a listener for the editorRef?.current's content updates
|
||||
@ -381,7 +381,7 @@ useEffect(() => {
|
||||
|
||||
const sendMessage = async ()=> {
|
||||
try {
|
||||
|
||||
if(messageSize > 4000) return
|
||||
|
||||
if(+balance < 4) throw new Error('You need at least 4 QORT to send a message')
|
||||
if(isSending) return
|
||||
|
@ -31,7 +31,7 @@ import { throttle } from 'lodash'
|
||||
|
||||
const uid = new ShortUniqueId({ length: 5 });
|
||||
|
||||
export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, myAddress, handleNewEncryptionNotification, hide, handleSecretKeyCreationInProgress, triedToFetchSecretKey, myName, balance, getTimestampEnterChatParent}) => {
|
||||
export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, myAddress, handleNewEncryptionNotification, hide, handleSecretKeyCreationInProgress, triedToFetchSecretKey, myName, balance, getTimestampEnterChatParent, isPrivate}) => {
|
||||
const [messages, setMessages] = useState([])
|
||||
const [chatReferences, setChatReferences] = useState({})
|
||||
const [isSending, setIsSending] = useState(false)
|
||||
@ -191,7 +191,6 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
||||
try {
|
||||
if(!secretKeyRef.current){
|
||||
checkForFirstSecretKeyNotification(encryptedMessages)
|
||||
return
|
||||
}
|
||||
return new Promise((res, rej)=> {
|
||||
window.sendMessage("decryptSingle", {
|
||||
@ -203,9 +202,9 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
||||
const filterUIMessages = encryptedMessages.filter((item) => !isExtMsg(item.data));
|
||||
const decodedUIMessages = decodeBase64ForUIChatMessages(filterUIMessages);
|
||||
|
||||
const combineUIAndExtensionMsgs = [...decodedUIMessages, ...response];
|
||||
processWithNewMessages(
|
||||
combineUIAndExtensionMsgs.map((item) => ({
|
||||
const combineUIAndExtensionMsgsBefore = [...decodedUIMessages, ...response];
|
||||
const combineUIAndExtensionMsgs = processWithNewMessages(
|
||||
combineUIAndExtensionMsgsBefore.map((item) => ({
|
||||
...item,
|
||||
...(item?.decryptedData || {}),
|
||||
})),
|
||||
@ -233,7 +232,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
||||
setChatReferences((prev) => {
|
||||
const organizedChatReferences = { ...prev };
|
||||
combineUIAndExtensionMsgs
|
||||
.filter((rawItem) => rawItem && rawItem.chatReference && (rawItem.decryptedData?.type === "reaction" || rawItem.decryptedData?.type === "edit"))
|
||||
.filter((rawItem) => rawItem && rawItem.chatReference && (rawItem.decryptedData?.type === "reaction" || rawItem.decryptedData?.type === "edit" || rawItem?.type === "edit" || rawItem?.type === "reaction"))
|
||||
.forEach((item) => {
|
||||
try {
|
||||
if(item.decryptedData?.type === "edit"){
|
||||
@ -241,11 +240,16 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
||||
...(organizedChatReferences[item.chatReference] || {}),
|
||||
edit: item.decryptedData,
|
||||
};
|
||||
} else if(item?.type === "edit"){
|
||||
organizedChatReferences[item.chatReference] = {
|
||||
...(organizedChatReferences[item.chatReference] || {}),
|
||||
edit: item,
|
||||
};
|
||||
} else {
|
||||
const content = item.decryptedData?.content;
|
||||
const content = item?.content || item.decryptedData?.content;
|
||||
const sender = item.sender;
|
||||
const newTimestamp = item.timestamp;
|
||||
const contentState = item.decryptedData?.contentState;
|
||||
const contentState = item?.contentState || item.decryptedData?.contentState;
|
||||
|
||||
if (!content || typeof content !== "string" || !sender || typeof sender !== "string" || !newTimestamp) {
|
||||
console.warn("Invalid content, sender, or timestamp in reaction data", item);
|
||||
@ -316,7 +320,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
||||
const organizedChatReferences = { ...prev };
|
||||
|
||||
combineUIAndExtensionMsgs
|
||||
.filter((rawItem) => rawItem && rawItem.chatReference && (rawItem.decryptedData?.type === "reaction" || rawItem.decryptedData?.type === "edit"))
|
||||
.filter((rawItem) => rawItem && rawItem.chatReference && (rawItem.decryptedData?.type === "reaction" || rawItem.decryptedData?.type === "edit" || rawItem?.type === "edit" || rawItem?.type === "reaction"))
|
||||
.forEach((item) => {
|
||||
try {
|
||||
if(item.decryptedData?.type === "edit"){
|
||||
@ -324,11 +328,16 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
||||
...(organizedChatReferences[item.chatReference] || {}),
|
||||
edit: item.decryptedData,
|
||||
};
|
||||
} else if(item?.type === "edit"){
|
||||
organizedChatReferences[item.chatReference] = {
|
||||
...(organizedChatReferences[item.chatReference] || {}),
|
||||
edit: item,
|
||||
};
|
||||
} else {
|
||||
const content = item.decryptedData?.content;
|
||||
const content = item?.content || item.decryptedData?.content;
|
||||
const sender = item.sender;
|
||||
const newTimestamp = item.timestamp;
|
||||
const contentState = item.decryptedData?.contentState;
|
||||
const contentState = item?.contentState || item.decryptedData?.contentState;
|
||||
|
||||
if (!content || typeof content !== "string" || !sender || typeof sender !== "string" || !newTimestamp) {
|
||||
console.warn("Invalid content, sender, or timestamp in reaction data", item);
|
||||
@ -463,10 +472,11 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
||||
setIsLoading(true)
|
||||
initWebsocketMessageGroup()
|
||||
}
|
||||
}, [triedToFetchSecretKey, secretKey])
|
||||
}, [triedToFetchSecretKey, secretKey, isPrivate])
|
||||
|
||||
useEffect(()=> {
|
||||
if(!secretKey || hasInitializedWebsocket.current) return
|
||||
if(isPrivate === null) return
|
||||
if(isPrivate === false || !secretKey || hasInitializedWebsocket.current) return
|
||||
forceCloseWebSocket()
|
||||
setMessages([])
|
||||
setIsLoading(true)
|
||||
@ -476,17 +486,32 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
||||
}, 6000);
|
||||
initWebsocketMessageGroup()
|
||||
hasInitializedWebsocket.current = true
|
||||
}, [secretKey])
|
||||
}, [secretKey, isPrivate])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorRef?.current) return;
|
||||
|
||||
handleUpdateRef.current = throttle(() => {
|
||||
const htmlContent = editorRef.current.getHTML();
|
||||
const size = new TextEncoder().encode(htmlContent).length;
|
||||
setMessageSize(size + 100);
|
||||
handleUpdateRef.current = throttle(async () => {
|
||||
try {
|
||||
if(isPrivate){
|
||||
const htmlContent = editorRef.current.getHTML();
|
||||
const message64 = await objectToBase64(JSON.stringify(htmlContent))
|
||||
const secretKeyObject = await getSecretKey(false, true)
|
||||
const encryptSingle = await encryptChatMessage(message64, secretKeyObject)
|
||||
setMessageSize((encryptSingle?.length || 0) + 200);
|
||||
} else {
|
||||
const htmlContent = editorRef.current.getJSON();
|
||||
const message = JSON.stringify(htmlContent)
|
||||
const size = new Blob([message]).size
|
||||
setMessageSize(size + 300);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
// calc size error
|
||||
}
|
||||
}, 1200);
|
||||
|
||||
|
||||
const currentEditor = editorRef.current;
|
||||
|
||||
@ -495,7 +520,9 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
||||
return () => {
|
||||
currentEditor.off("update", handleUpdateRef.current);
|
||||
};
|
||||
}, [editorRef, setMessageSize]);
|
||||
}, [editorRef, setMessageSize, isFocusedParent, isPrivate]);
|
||||
|
||||
|
||||
|
||||
|
||||
useEffect(()=> {
|
||||
@ -587,6 +614,8 @@ const clearEditorContent = () => {
|
||||
|
||||
const sendMessage = async ()=> {
|
||||
try {
|
||||
if(messageSize > 4000) return
|
||||
if(isPrivate === null) throw new Error('Unable to determine if group is private')
|
||||
if(isSending) return
|
||||
if(+balance < 4) throw new Error('You need at least 4 QORT to send a message')
|
||||
pauseAllQueues()
|
||||
@ -594,8 +623,10 @@ const sendMessage = async ()=> {
|
||||
const htmlContent = editorRef.current.getHTML();
|
||||
|
||||
if(!htmlContent?.trim() || htmlContent?.trim() === '<p></p>') return
|
||||
|
||||
|
||||
setIsSending(true)
|
||||
const message = htmlContent
|
||||
const message = isPrivate === false ? editorRef.current.getJSON() : htmlContent
|
||||
const secretKeyObject = await getSecretKey(false, true)
|
||||
|
||||
let repliedTo = replyMessage?.signature
|
||||
@ -605,19 +636,24 @@ const sendMessage = async ()=> {
|
||||
}
|
||||
let chatReference = onEditMessage?.signature
|
||||
|
||||
const publicData = isPrivate ? {} : {
|
||||
isEdited : chatReference ? true : false,
|
||||
}
|
||||
const otherData = {
|
||||
repliedTo,
|
||||
...(onEditMessage?.decryptedData || {}),
|
||||
type: chatReference ? 'edit' : '',
|
||||
specialId: uid.rnd(),
|
||||
...publicData
|
||||
}
|
||||
const objectMessage = {
|
||||
...(otherData || {}),
|
||||
message
|
||||
[isPrivate ? 'message' : 'messageText']: message,
|
||||
version: 3
|
||||
}
|
||||
const message64: any = await objectToBase64(objectMessage)
|
||||
|
||||
const encryptSingle = await encryptChatMessage(message64, secretKeyObject)
|
||||
const encryptSingle = isPrivate === false ? JSON.stringify(objectMessage) : await encryptChatMessage(message64, secretKeyObject)
|
||||
// const res = await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle})
|
||||
|
||||
const sendMessageFunc = async () => {
|
||||
@ -627,7 +663,7 @@ const sendMessage = async ()=> {
|
||||
// Add the function to the queue
|
||||
const messageObj = {
|
||||
message: {
|
||||
text: message,
|
||||
text: htmlContent,
|
||||
timestamp: Date.now(),
|
||||
senderName: myName,
|
||||
sender: myAddress,
|
||||
@ -687,10 +723,8 @@ const sendMessage = async ()=> {
|
||||
setReplyMessage(null)
|
||||
setIsFocusedParent(true);
|
||||
setTimeout(() => {
|
||||
editorRef.current.chain().focus().setContent(message?.text).run();
|
||||
|
||||
}, 250);
|
||||
|
||||
editorRef.current.chain().focus().setContent(message?.messageText || message?.text).run();
|
||||
}, 250);
|
||||
}, [])
|
||||
|
||||
const handleReaction = useCallback(async (reaction, chatMessage, reactionState = true)=> {
|
||||
@ -718,7 +752,7 @@ const sendMessage = async ()=> {
|
||||
}
|
||||
const message64: any = await objectToBase64(objectMessage)
|
||||
const reactiontypeNumber = RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS
|
||||
const encryptSingle = await encryptChatMessage(message64, secretKeyObject, reactiontypeNumber)
|
||||
const encryptSingle = isPrivate === false ? JSON.stringify(objectMessage) : await encryptChatMessage(message64, secretKeyObject, reactiontypeNumber)
|
||||
// const res = await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle})
|
||||
|
||||
const sendMessageFunc = async () => {
|
||||
@ -770,7 +804,7 @@ const sendMessage = async ()=> {
|
||||
left: hide && '-100000px',
|
||||
}}>
|
||||
|
||||
<ChatList enableMentions onReply={onReply} onEdit={onEdit} chatId={selectedGroup} initialMessages={messages} myAddress={myAddress} tempMessages={tempMessages} handleReaction={handleReaction} chatReferences={chatReferences} tempChatReferences={tempChatReferences} members={members} myName={myName} selectedGroup={selectedGroup}/>
|
||||
<ChatList isPrivate={isPrivate} enableMentions onReply={onReply} onEdit={onEdit} chatId={selectedGroup} initialMessages={messages} myAddress={myAddress} tempMessages={tempMessages} handleReaction={handleReaction} chatReferences={chatReferences} tempChatReferences={tempChatReferences} members={members} myName={myName} selectedGroup={selectedGroup}/>
|
||||
|
||||
|
||||
<div style={{
|
||||
@ -836,7 +870,7 @@ const sendMessage = async ()=> {
|
||||
<div style={{
|
||||
display: isFocusedParent ? 'none' : 'block'
|
||||
}}>
|
||||
<ChatOptions openQManager={openQManager}
|
||||
<ChatOptions isPrivate={isPrivate} openQManager={openQManager}
|
||||
messages={messages} goToMessage={()=> {}} members={members} myName={myName} selectedGroup={selectedGroup}/>
|
||||
</div>
|
||||
</Box>
|
||||
@ -878,7 +912,6 @@ const sendMessage = async ()=> {
|
||||
{isFocusedParent && (
|
||||
<CustomButton
|
||||
onClick={()=> {
|
||||
if(messageSize > 4000) return
|
||||
if(isSending) return
|
||||
sendMessage()
|
||||
}}
|
||||
|
@ -6,7 +6,7 @@ import { useInView } from 'react-intersection-observer'
|
||||
import { Typography } from '@mui/material';
|
||||
import ErrorBoundary from '../../common/ErrorBoundary';
|
||||
|
||||
export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onReply, handleReaction, chatReferences, tempChatReferences, onEdit
|
||||
export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onReply, handleReaction, chatReferences, tempChatReferences, isPrivate, onEdit
|
||||
}) => {
|
||||
const parentRef = useRef();
|
||||
const [messages, setMessages] = useState(initialMessages);
|
||||
@ -20,7 +20,7 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
|
||||
// Initialize the virtualizer
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: messages.length,
|
||||
getItemKey: (index) => messages[index].signature,
|
||||
getItemKey: (index) => messages[index]?.tempSignature || messages[index].signature,
|
||||
getScrollElement: () => parentRef?.current,
|
||||
estimateSize: useCallback(() => 80, []), // Provide an estimated height of items, adjust this as needed
|
||||
overscan: 10, // Number of items to render outside the visible area to improve smoothness
|
||||
@ -264,7 +264,10 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
|
||||
message.text = chatReferences[message.signature]?.edit?.message;
|
||||
message.isEdit = true
|
||||
}
|
||||
|
||||
if (chatReferences[message.signature]?.edit?.messageText && message?.messageText) {
|
||||
message.messageText = chatReferences[message.signature]?.edit?.messageText;
|
||||
message.isEdit = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -348,6 +351,7 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
|
||||
handleReaction={handleReaction}
|
||||
reactions={reactions}
|
||||
isUpdating={isUpdating}
|
||||
isPrivate={isPrivate}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
@ -33,6 +33,12 @@ import { ContextMenuMentions } from "../ContextMenuMentions";
|
||||
import { convert } from 'html-to-text';
|
||||
import { executeEvent } from "../../utils/events";
|
||||
import InsertLinkIcon from '@mui/icons-material/InsertLink';
|
||||
import Highlight from "@tiptap/extension-highlight";
|
||||
import Mention from "@tiptap/extension-mention";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import Underline from "@tiptap/extension-underline";
|
||||
import { generateHTML } from "@tiptap/react";
|
||||
import ErrorBoundary from "../../common/ErrorBoundary";
|
||||
|
||||
const extractTextFromHTML = (htmlString = '') => {
|
||||
return convert(htmlString, {
|
||||
@ -44,7 +50,7 @@ const cache = new CellMeasurerCache({
|
||||
defaultHeight: 50,
|
||||
});
|
||||
|
||||
export const ChatOptions = ({ messages, goToMessage, members, myName, selectedGroup, openQManager }) => {
|
||||
export const ChatOptions = ({ messages : untransformedMessages, goToMessage, members, myName, selectedGroup, openQManager, isPrivate }) => {
|
||||
const [mode, setMode] = useState("default");
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const [selectedMember, setSelectedMember] = useState(0);
|
||||
@ -53,6 +59,27 @@ export const ChatOptions = ({ messages, goToMessage, members, myName, selectedGr
|
||||
const parentRefMentions = useRef();
|
||||
const [lastMentionTimestamp, setLastMentionTimestamp] = useState(null)
|
||||
const [debouncedValue, setDebouncedValue] = useState(""); // Debounced value
|
||||
const messages = useMemo(()=> {
|
||||
return untransformedMessages?.map((item)=> {
|
||||
if(item?.messageText){
|
||||
let transformedMessage = item?.messageText
|
||||
try {
|
||||
transformedMessage = generateHTML(item?.messageText, [
|
||||
StarterKit,
|
||||
Underline,
|
||||
Highlight,
|
||||
Mention
|
||||
])
|
||||
return {
|
||||
...item,
|
||||
messageText: transformedMessage
|
||||
}
|
||||
} catch (error) {
|
||||
// error
|
||||
}
|
||||
} else return item
|
||||
})
|
||||
}, [untransformedMessages])
|
||||
|
||||
const getTimestampMention = async () => {
|
||||
try {
|
||||
@ -125,7 +152,7 @@ export const ChatOptions = ({ messages, goToMessage, members, myName, selectedGr
|
||||
.filter(
|
||||
(message) =>
|
||||
message?.senderName === selectedMember &&
|
||||
extractTextFromHTML(message?.decryptedData?.message)?.includes(
|
||||
extractTextFromHTML(isPrivate ? message?.messageText : message?.decryptedData?.message)?.includes(
|
||||
debouncedValue.toLowerCase()
|
||||
)
|
||||
)
|
||||
@ -133,20 +160,27 @@ export const ChatOptions = ({ messages, goToMessage, members, myName, selectedGr
|
||||
}
|
||||
return messages
|
||||
.filter((message) =>
|
||||
extractTextFromHTML(message?.decryptedData?.message)?.includes(debouncedValue.toLowerCase())
|
||||
extractTextFromHTML(isPrivate === false ? message?.messageText : message?.decryptedData?.message)?.includes(debouncedValue.toLowerCase())
|
||||
)
|
||||
?.sort((a, b) => b?.timestamp - a?.timestamp);
|
||||
}, [debouncedValue, messages, selectedMember]);
|
||||
}, [debouncedValue, messages, selectedMember, isPrivate]);
|
||||
|
||||
const mentionList = useMemo(() => {
|
||||
if(!messages || messages.length === 0 || !myName) return []
|
||||
|
||||
if(isPrivate === false){
|
||||
return messages
|
||||
.filter((message) =>
|
||||
extractTextFromHTML(message?.messageText)?.includes(`@${myName}`)
|
||||
)
|
||||
?.sort((a, b) => b?.timestamp - a?.timestamp);
|
||||
|
||||
}
|
||||
return messages
|
||||
.filter((message) =>
|
||||
extractTextFromHTML(message?.decryptedData?.message)?.includes(`@${myName}`)
|
||||
)
|
||||
?.sort((a, b) => b?.timestamp - a?.timestamp);
|
||||
}, [messages, myName]);
|
||||
}, [messages, myName, isPrivate]);
|
||||
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: searchedList.length,
|
||||
@ -297,86 +331,7 @@ export const ChatOptions = ({ messages, goToMessage, members, myName, selectedGr
|
||||
gap: "5px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
padding: "0px 20px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "15px",
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
sx={{
|
||||
backgroundColor: "#27282c",
|
||||
color: "white",
|
||||
height: "25px",
|
||||
width: "25px",
|
||||
}}
|
||||
alt={message?.senderName}
|
||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
message?.senderName
|
||||
}/qortal_avatar?async=true`}
|
||||
>
|
||||
{message?.senderName?.charAt(0)}
|
||||
</Avatar>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWight: 600,
|
||||
fontFamily: "Inter",
|
||||
color: "cadetBlue",
|
||||
}}
|
||||
>
|
||||
{message?.senderName}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Spacer height="5px" />
|
||||
<Typography sx={{
|
||||
fontSize: '12px'
|
||||
}}>{formatTimestamp(message.timestamp)}</Typography>
|
||||
<Box
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
const findMsgIndex = messages.findIndex(
|
||||
(item) =>
|
||||
item?.signature === message?.signature
|
||||
);
|
||||
if (findMsgIndex !== -1) {
|
||||
if(isMobile){
|
||||
setMode("default");
|
||||
executeEvent('goToMessage', {index: findMsgIndex})
|
||||
|
||||
} else {
|
||||
goToMessage(findMsgIndex);
|
||||
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MessageDisplay
|
||||
htmlContent={
|
||||
message?.decryptedData?.message || "<p></p>"
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<ShowMessage messages={messages} goToMessage={goToMessage} message={message} setMode={setMode} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@ -580,86 +535,15 @@ export const ChatOptions = ({ messages, goToMessage, members, myName, selectedGr
|
||||
gap: "5px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
padding: "0px 20px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "15px",
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
sx={{
|
||||
backgroundColor: "#27282c",
|
||||
color: "white",
|
||||
height: "25px",
|
||||
width: "25px",
|
||||
}}
|
||||
alt={message?.senderName}
|
||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
message?.senderName
|
||||
}/qortal_avatar?async=true`}
|
||||
>
|
||||
{message?.senderName?.charAt(0)}
|
||||
</Avatar>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWight: 600,
|
||||
fontFamily: "Inter",
|
||||
color: "cadetBlue",
|
||||
}}
|
||||
>
|
||||
{message?.senderName}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Spacer height="5px" />
|
||||
<Typography sx={{
|
||||
fontSize: '12px'
|
||||
}}>{formatTimestamp(message.timestamp)}</Typography>
|
||||
<Box
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
const findMsgIndex = messages.findIndex(
|
||||
(item) =>
|
||||
item?.signature === message?.signature
|
||||
);
|
||||
if (findMsgIndex !== -1) {
|
||||
if(isMobile){
|
||||
setMode("default");
|
||||
executeEvent('goToMessage', {index: findMsgIndex})
|
||||
|
||||
} else {
|
||||
goToMessage(findMsgIndex);
|
||||
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MessageDisplay
|
||||
htmlContent={
|
||||
message?.decryptedData?.message || "<p></p>"
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<ErrorBoundary
|
||||
fallback={
|
||||
<Typography>
|
||||
Error loading content: Invalid Data
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<ShowMessage message={message} goToMessage={goToMessage} messages={messages} setMode={setMode} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@ -727,3 +611,96 @@ export const ChatOptions = ({ messages, goToMessage, members, myName, selectedGr
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const ShowMessage = ({message, goToMessage, messages, setMode})=> {
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
padding: "0px 20px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "15px",
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
sx={{
|
||||
backgroundColor: "#27282c",
|
||||
color: "white",
|
||||
height: "25px",
|
||||
width: "25px",
|
||||
}}
|
||||
alt={message?.senderName}
|
||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
message?.senderName
|
||||
}/qortal_avatar?async=true`}
|
||||
>
|
||||
{message?.senderName?.charAt(0)}
|
||||
</Avatar>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWight: 600,
|
||||
fontFamily: "Inter",
|
||||
color: "cadetBlue",
|
||||
}}
|
||||
>
|
||||
{message?.senderName}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Spacer height="5px" />
|
||||
<Typography sx={{
|
||||
fontSize: '12px'
|
||||
}}>{formatTimestamp(message.timestamp)}</Typography>
|
||||
<Box
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
const findMsgIndex = messages.findIndex(
|
||||
(item) =>
|
||||
item?.signature === message?.signature
|
||||
);
|
||||
if (findMsgIndex !== -1) {
|
||||
if(isMobile){
|
||||
setMode("default");
|
||||
executeEvent('goToMessage', {index: findMsgIndex})
|
||||
|
||||
} else {
|
||||
goToMessage(findMsgIndex);
|
||||
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{message?.messageText && (
|
||||
<MessageDisplay
|
||||
htmlContent={message?.messageText}
|
||||
/>
|
||||
)}
|
||||
{message?.decryptedData?.message && (
|
||||
<MessageDisplay
|
||||
htmlContent={
|
||||
message?.decryptedData?.message || "<p></p>"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
@ -108,7 +108,7 @@ export const MessageDisplay = ({ htmlContent, isReply }) => {
|
||||
};
|
||||
|
||||
|
||||
const embedLink = htmlContent.match(/qortal:\/\/use-embed\/[^\s<>]+/);
|
||||
const embedLink = htmlContent?.match(/qortal:\/\/use-embed\/[^\s<>]+/);
|
||||
|
||||
let embedData = null;
|
||||
|
||||
|
@ -17,6 +17,7 @@ import { Spacer } from "../../common/Spacer";
|
||||
import { ReactionPicker } from "../ReactionPicker";
|
||||
import KeyOffIcon from '@mui/icons-material/KeyOff';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import Mention from "@tiptap/extension-mention";
|
||||
|
||||
export const MessageItem = ({
|
||||
message,
|
||||
@ -33,7 +34,8 @@ export const MessageItem = ({
|
||||
reactions,
|
||||
isUpdating,
|
||||
lastSignature,
|
||||
onEdit
|
||||
onEdit,
|
||||
isPrivate
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [selectedReaction, setSelectedReaction] = useState(null);
|
||||
@ -133,7 +135,7 @@ export const MessageItem = ({
|
||||
gap: '10px',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
{message?.sender === myAddress && !message?.isNotEncrypted && (
|
||||
{message?.sender === myAddress && (!message?.isNotEncrypted || isPrivate === false) && (
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
onEdit(message);
|
||||
@ -202,6 +204,7 @@ export const MessageItem = ({
|
||||
StarterKit,
|
||||
Underline,
|
||||
Highlight,
|
||||
Mention
|
||||
])}
|
||||
/>
|
||||
)}
|
||||
@ -220,6 +223,7 @@ export const MessageItem = ({
|
||||
StarterKit,
|
||||
Underline,
|
||||
Highlight,
|
||||
Mention
|
||||
])}
|
||||
/>
|
||||
)}
|
||||
@ -336,7 +340,7 @@ export const MessageItem = ({
|
||||
alignItems: 'center',
|
||||
gap: '15px'
|
||||
}}>
|
||||
{message?.isNotEncrypted && (
|
||||
{message?.isNotEncrypted && isPrivate && (
|
||||
<KeyOffIcon sx={{
|
||||
color: 'white',
|
||||
marginLeft: '10px'
|
||||
@ -451,6 +455,7 @@ export const ReplyPreview = ({message, isEdit})=> {
|
||||
StarterKit,
|
||||
Underline,
|
||||
Highlight,
|
||||
Mention
|
||||
])}
|
||||
/>
|
||||
)}
|
||||
|
@ -126,7 +126,7 @@
|
||||
}
|
||||
|
||||
|
||||
.tiptap .mention {
|
||||
.tiptap [data-type="mention"] {
|
||||
box-decoration-break: clone;
|
||||
color: lightblue;
|
||||
padding: 0.1rem 0.3rem;
|
||||
|
@ -34,7 +34,8 @@ import RefreshIcon from "@mui/icons-material/Refresh";
|
||||
import AnnouncementsIcon from "@mui/icons-material/Notifications";
|
||||
import GroupIcon from "@mui/icons-material/Group";
|
||||
import PersonIcon from "@mui/icons-material/Person";
|
||||
|
||||
import LockIcon from '@mui/icons-material/Lock';
|
||||
import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred';
|
||||
import {
|
||||
AuthenticatedContainerInnerRight,
|
||||
CustomButton,
|
||||
@ -119,6 +120,19 @@ import { sortArrayByTimestampAndGroupName } from "../../utils/time";
|
||||
// }
|
||||
// });
|
||||
|
||||
function areKeysEqual(array1, array2) {
|
||||
// If lengths differ, the arrays cannot be equal
|
||||
if (array1?.length !== array2?.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sort both arrays and compare their elements
|
||||
const sortedArray1 = [...array1].sort();
|
||||
const sortedArray2 = [...array2].sort();
|
||||
|
||||
return sortedArray1.every((key, index) => key === sortedArray2[index]);
|
||||
}
|
||||
|
||||
|
||||
export const getPublishesFromAdmins = async (admins: string[], groupId) => {
|
||||
// const validApi = await findUsableApi();
|
||||
@ -476,6 +490,16 @@ export const Group = ({
|
||||
const [appsMode, setAppsMode] = useState('home')
|
||||
const [isOpenSideViewDirects, setIsOpenSideViewDirects] = useState(false)
|
||||
const [isOpenSideViewGroups, setIsOpenSideViewGroups] = useState(false)
|
||||
|
||||
const [groupsProperties, setGroupsProperties] = useState({})
|
||||
|
||||
const isPrivate = useMemo(()=> {
|
||||
if(!selectedGroup?.groupId || !groupsProperties[selectedGroup?.groupId]) return null
|
||||
if(groupsProperties[selectedGroup?.groupId]?.isOpen === true) return false
|
||||
if(groupsProperties[selectedGroup?.groupId]?.isOpen === false) return true
|
||||
return null
|
||||
}, [selectedGroup])
|
||||
|
||||
const setSelectedGroupId = useSetRecoilState(selectedGroupIdAtom)
|
||||
const toggleSideViewDirects = ()=> {
|
||||
if(isOpenSideViewGroups){
|
||||
@ -682,9 +706,8 @@ export const Group = ({
|
||||
|
||||
if (
|
||||
group?.data &&
|
||||
isExtMsg(group?.data) &&
|
||||
group?.sender !== myAddress &&
|
||||
group?.timestamp && (!isUpdateMsg(group?.data) || groupChatTimestamps[group?.groupId]) &&
|
||||
group?.timestamp && groupChatTimestamps[group?.groupId] &&
|
||||
((!timestampEnterData[group?.groupId] &&
|
||||
Date.now() - group?.timestamp < timeDifferenceForNotificationChats) ||
|
||||
timestampEnterData[group?.groupId] < group?.timestamp)
|
||||
@ -844,12 +867,19 @@ export const Group = ({
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedGroup) {
|
||||
setTriedToFetchSecretKey(false);
|
||||
getSecretKey(true);
|
||||
if (selectedGroup && isPrivate !== null) {
|
||||
if(isPrivate){
|
||||
setTriedToFetchSecretKey(false);
|
||||
getSecretKey(true);
|
||||
}
|
||||
|
||||
getGroupOwner(selectedGroup?.groupId);
|
||||
}
|
||||
}, [selectedGroup]);
|
||||
if(isPrivate === false){
|
||||
setTriedToFetchSecretKey(true);
|
||||
|
||||
}
|
||||
}, [selectedGroup, isPrivate]);
|
||||
|
||||
|
||||
|
||||
@ -880,9 +910,8 @@ export const Group = ({
|
||||
const groupData = {}
|
||||
|
||||
const getGroupData = groups.map(async(group)=> {
|
||||
const isUpdate = isUpdateMsg(group?.data)
|
||||
if(!group.groupId || !group?.timestamp) return null
|
||||
if(isUpdate && (!groupData[group.groupId] || groupData[group.groupId] < group.timestamp)){
|
||||
if((!groupData[group.groupId] || groupData[group.groupId] < group.timestamp)){
|
||||
const hasMoreRecentMsg = await getCountNewMesg(group.groupId, timestampEnterDataRef.current[group?.groupId] || Date.now() - 24 * 60 * 60 * 1000)
|
||||
if(hasMoreRecentMsg){
|
||||
groupData[group.groupId] = hasMoreRecentMsg
|
||||
@ -899,6 +928,31 @@ export const Group = ({
|
||||
}
|
||||
}
|
||||
|
||||
const getGroupsProperties = useCallback(async(address)=> {
|
||||
try {
|
||||
const url = `${getBaseApiReact()}/groups/member/${address}`;
|
||||
const response = await fetch(url);
|
||||
if(!response.ok) throw new Error('Cannot get group properties')
|
||||
let data = await response.json();
|
||||
const transformToObject = data.reduce((result, item) => {
|
||||
|
||||
result[item.groupId] = item
|
||||
return result;
|
||||
}, {});
|
||||
setGroupsProperties(transformToObject)
|
||||
} catch (error) {
|
||||
// error
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
useEffect(()=> {
|
||||
if(!myAddress) return
|
||||
if(areKeysEqual(groups?.map((grp)=> grp?.groupId), Object.keys(groupsProperties))){
|
||||
} else {
|
||||
getGroupsProperties(myAddress)
|
||||
}
|
||||
}, [groups, myAddress])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
@ -1089,9 +1143,9 @@ export const Group = ({
|
||||
.filter((group) => group?.sender !== myAddress)
|
||||
.find((gr) => gr?.groupId === selectedGroup?.groupId);
|
||||
if (!findGroup) return false;
|
||||
if (!findGroup?.data || !isExtMsg(findGroup?.data)) return false;
|
||||
if (!findGroup?.data) return false;
|
||||
return (
|
||||
findGroup?.timestamp && (!isUpdateMsg(findGroup?.data) || groupChatTimestamps[findGroup?.groupId]) &&
|
||||
findGroup?.timestamp && groupChatTimestamps[findGroup?.groupId] &&
|
||||
((!timestampEnterData[selectedGroup?.groupId] &&
|
||||
Date.now() - findGroup?.timestamp <
|
||||
timeDifferenceForNotificationChats) ||
|
||||
@ -1930,16 +1984,37 @@ export const Group = ({
|
||||
}}
|
||||
>
|
||||
<ListItemAvatar>
|
||||
<Avatar
|
||||
sx={{
|
||||
{groupsProperties[group?.groupId]?.isOpen === false ? (
|
||||
<Box sx={{
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
borderRadius: '50%',
|
||||
background: "#232428",
|
||||
color: "white",
|
||||
}}
|
||||
alt={group?.groupName}
|
||||
// src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${groupOwner?.name}/qortal_group_avatar_${group.groupId}?async=true`}
|
||||
>
|
||||
{group.groupName?.charAt(0)}
|
||||
</Avatar>
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<LockIcon sx={{
|
||||
color: 'var(--green)'
|
||||
}} />
|
||||
</Box>
|
||||
): (
|
||||
<Box sx={{
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
borderRadius: '50%',
|
||||
background: "#232428",
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<NoEncryptionGmailerrorredIcon sx={{
|
||||
color: 'var(--unread)'
|
||||
}} />
|
||||
</Box>
|
||||
|
||||
)}
|
||||
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={group.groupName}
|
||||
@ -1974,8 +2049,8 @@ export const Group = ({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{group?.data &&
|
||||
isExtMsg(group?.data) && (!isUpdateMsg(group?.data) || groupChatTimestamps[group?.groupId]) &&
|
||||
{group?.data &&
|
||||
groupChatTimestamps[group?.groupId] &&
|
||||
group?.sender !== myAddress &&
|
||||
group?.timestamp &&
|
||||
((!timestampEnterData[group?.groupId] &&
|
||||
@ -2054,6 +2129,7 @@ export const Group = ({
|
||||
|
||||
{isMobile && (
|
||||
<Header
|
||||
isPrivate={isPrivate}
|
||||
setMobileViewModeKeepOpen={setMobileViewModeKeepOpen}
|
||||
isThin={
|
||||
mobileViewMode === "groups" ||
|
||||
@ -2310,6 +2386,7 @@ export const Group = ({
|
||||
>
|
||||
{triedToFetchSecretKey && (
|
||||
<ChatGroup
|
||||
isPrivate={isPrivate}
|
||||
myAddress={myAddress}
|
||||
selectedGroup={selectedGroup?.groupId}
|
||||
getSecretKey={getSecretKey}
|
||||
@ -2318,7 +2395,7 @@ export const Group = ({
|
||||
handleNewEncryptionNotification={
|
||||
setNewEncryptionNotification
|
||||
}
|
||||
hide={groupSection !== "chat" || !secretKey || selectedDirect || newChat}
|
||||
hide={groupSection !== "chat" || (!secretKey && isPrivate) || selectedDirect || newChat}
|
||||
|
||||
handleSecretKeyCreationInProgress={
|
||||
handleSecretKeyCreationInProgress
|
||||
@ -2330,7 +2407,7 @@ export const Group = ({
|
||||
|
||||
/>
|
||||
)}
|
||||
{firstSecretKeyInCreation &&
|
||||
{isPrivate && firstSecretKeyInCreation &&
|
||||
triedToFetchSecretKey &&
|
||||
!secretKeyPublishDate && (
|
||||
<div
|
||||
@ -2351,7 +2428,7 @@ export const Group = ({
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
{!admins.includes(myAddress) &&
|
||||
{isPrivate && !admins.includes(myAddress) &&
|
||||
!secretKey &&
|
||||
triedToFetchSecretKey ? (
|
||||
<>
|
||||
@ -2404,7 +2481,7 @@ export const Group = ({
|
||||
) : null}
|
||||
</>
|
||||
) : admins.includes(myAddress) &&
|
||||
!secretKey &&
|
||||
(!secretKey && isPrivate) &&
|
||||
triedToFetchSecretKey ? null : !triedToFetchSecretKey ? null : (
|
||||
<>
|
||||
<GroupAnnouncements
|
||||
@ -2445,7 +2522,7 @@ export const Group = ({
|
||||
zIndex: 100,
|
||||
}}
|
||||
>
|
||||
{admins.includes(myAddress) &&
|
||||
{isPrivate && admins.includes(myAddress) &&
|
||||
shouldReEncrypt &&
|
||||
triedToFetchSecretKey &&
|
||||
!firstSecretKeyInCreation &&
|
||||
|
@ -143,7 +143,7 @@ export const encryptDataGroup = ({ data64, publicKeys, privateKey, userPublicKey
|
||||
}
|
||||
}
|
||||
|
||||
export const encryptSingle = async ({ data64, secretKeyObject, typeNumber = 1 }: any) => {
|
||||
export const encryptSingle = async ({ data64, secretKeyObject, typeNumber = 2 }: any) => {
|
||||
// Find the highest key in the secretKeyObject
|
||||
const highestKey = Math.max(...Object.keys(secretKeyObject).filter(item => !isNaN(+item)).map(Number));
|
||||
const highestKeyObject = secretKeyObject[highestKey];
|
||||
@ -186,13 +186,29 @@ export const encryptSingle = async ({ data64, secretKeyObject, typeNumber = 1 }:
|
||||
|
||||
// Concatenate the highest key, type number, nonce, and encrypted data (new format)
|
||||
const highestKeyStr = highestKey.toString().padStart(10, '0'); // Fixed length of 10 digits
|
||||
finalEncryptedData = btoa(highestKeyStr + typeNumberStr + nonceBase64 + encryptedDataBase64);
|
||||
|
||||
const highestKeyBytes = new TextEncoder().encode(highestKeyStr.padStart(10, '0'));
|
||||
const typeNumberBytes = new TextEncoder().encode(typeNumberStr.padStart(3, '0'));
|
||||
|
||||
// Step 3: Concatenate all binary
|
||||
const combinedBinary = new Uint8Array(
|
||||
highestKeyBytes.length + typeNumberBytes.length + nonce.length + encryptedData.length
|
||||
);
|
||||
// finalEncryptedData = btoa(highestKeyStr) + btoa(typeNumberStr) + nonceBase64 + encryptedDataBase64;
|
||||
combinedBinary.set(highestKeyBytes, 0);
|
||||
combinedBinary.set(typeNumberBytes, highestKeyBytes.length);
|
||||
combinedBinary.set(nonce, highestKeyBytes.length + typeNumberBytes.length);
|
||||
combinedBinary.set(encryptedData, highestKeyBytes.length + typeNumberBytes.length + nonce.length);
|
||||
|
||||
// Step 4: Base64 encode once
|
||||
finalEncryptedData = uint8ArrayToBase64(combinedBinary);
|
||||
}
|
||||
|
||||
return finalEncryptedData;
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const decodeBase64ForUIChatMessages = (messages)=> {
|
||||
|
||||
let msgs = []
|
||||
@ -200,12 +216,12 @@ export const decodeBase64ForUIChatMessages = (messages)=> {
|
||||
try {
|
||||
const decoded = atob(msg?.data);
|
||||
const parseDecoded =JSON.parse(decodeURIComponent(escape(decoded)))
|
||||
if(parseDecoded?.messageText){
|
||||
|
||||
msgs.push({
|
||||
...msg,
|
||||
...parseDecoded
|
||||
})
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
@ -215,7 +231,7 @@ export const decodeBase64ForUIChatMessages = (messages)=> {
|
||||
|
||||
|
||||
|
||||
export const decryptSingle = async ({ data64, secretKeyObject, skipDecodeBase64 }: any) => {
|
||||
export const decryptSingle = async ({ data64, secretKeyObject, skipDecodeBase64 }: any) => {
|
||||
// First, decode the base64-encoded input (if skipDecodeBase64 is not set)
|
||||
const decodedData = skipDecodeBase64 ? data64 : atob(data64);
|
||||
|
||||
@ -247,6 +263,28 @@ export const decodeBase64ForUIChatMessages = (messages)=> {
|
||||
encryptedDataBase64 = decodeForNumber.slice(10); // The remaining part is the encrypted data
|
||||
} else {
|
||||
if (hasTypeNumber) {
|
||||
// const typeNumberStr = new TextDecoder().decode(typeNumberBytes);
|
||||
if(decodeForNumber.slice(10, 13) !== '001'){
|
||||
const decodedBinary = base64ToUint8Array(decodedData);
|
||||
const highestKeyBytes = decodedBinary.slice(0, 10); // if ASCII digits only
|
||||
const highestKeyStr = new TextDecoder().decode(highestKeyBytes);
|
||||
|
||||
const nonce = decodedBinary.slice(13, 13 + 24);
|
||||
const encryptedData = decodedBinary.slice(13 + 24);
|
||||
const highestKey = parseInt(highestKeyStr, 10);
|
||||
|
||||
const messageKey = base64ToUint8Array(secretKeyObject[+highestKey].messageKey);
|
||||
const decryptedBytes = nacl.secretbox.open(encryptedData, nonce, messageKey);
|
||||
|
||||
// Check if decryption was successful
|
||||
if (!decryptedBytes) {
|
||||
throw new Error("Decryption failed");
|
||||
}
|
||||
|
||||
// Convert the decrypted Uint8Array back to a Base64 string
|
||||
return uint8ArrayToBase64(decryptedBytes);
|
||||
|
||||
}
|
||||
// New format: Extract type number and nonce
|
||||
typeNumberStr = possibleTypeNumberStr; // Extract type number
|
||||
nonceBase64 = decodeForNumber.slice(13, 45); // Extract nonce (next 32 characters after type number)
|
||||
|
@ -7,6 +7,9 @@ import { mimeToExtensionMap } from '../memeTypes';
|
||||
import PhraseWallet from './phrase-wallet';
|
||||
import * as WORDLISTS from './wordlists';
|
||||
import { Filesystem, Directory, Encoding } from '@capacitor/filesystem';
|
||||
import ShortUniqueId from "short-unique-id";
|
||||
const uid = new ShortUniqueId({ length: 8 });
|
||||
|
||||
export function generateRandomSentence(template = 'adverb verb noun adjective noun adverb verb noun adjective noun adjective verbed adjective noun', maxWordLength = 0, capitalize = true) {
|
||||
const partsOfSpeechMap = {
|
||||
'noun': 'nouns',
|
||||
@ -88,7 +91,7 @@ export const createAccount = async(generatedSeedPhrase)=> {
|
||||
export const saveFileToDisk = async (data: any, qortAddress: string) => {
|
||||
|
||||
const dataString = JSON.stringify(data);
|
||||
const fileName = `qortal_backup_${qortAddress}.json`;
|
||||
const fileName = `qortal_backup_${qortAddress}_${uid.rnd()}.json`;
|
||||
|
||||
// Write the file to the Filesystem
|
||||
await Filesystem.writeFile({
|
||||
@ -102,7 +105,7 @@ export const createAccount = async(generatedSeedPhrase)=> {
|
||||
|
||||
export const saveSeedPhraseToDisk = async (data) => {
|
||||
|
||||
const fileName = "qortal_seedphrase.txt"
|
||||
const fileName = `qortal_seedphrase_${uid.rnd()}.txt`
|
||||
|
||||
await Filesystem.writeFile({
|
||||
path: fileName,
|
||||
|
Loading…
x
Reference in New Issue
Block a user