public group chats

This commit is contained in:
PhilReact 2024-12-16 03:39:41 +02:00
parent c5e29de0e9
commit f481dee813
17 changed files with 638 additions and 293 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -126,7 +126,7 @@
}
.tiptap .mention {
.tiptap [data-type="mention"] {
box-decoration-break: clone;
color: lightblue;
padding: 0.1rem 0.3rem;

View File

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

View File

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

View File

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