diff --git a/src/background/background.ts b/src/background/background.ts index 2057914..3c688ff 100644 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -440,7 +440,7 @@ const handleNotificationDirect = async (directs) => { let isFocused; const wallet = await getSaveWallet(); const address = wallet.address0; - let isDisableNotifications = + const isDisableNotifications = (await getUserSettings({ key: 'disable-push-notifications' })) || false; const dataDirects = directs.filter((direct) => direct?.sender !== address); try { @@ -1281,7 +1281,6 @@ export async function addUserSettings({ keyValue }) { getData(`${address}-userSettings`) .then((storedData) => { storedData = storedData || {}; // Initialize if no data found - storedData[key] = value; // Update the key-value pair within stored data // Save updated structure back to localStorage @@ -1734,7 +1733,7 @@ export async function decryptSingleFunc({ secretKeyObject, skipDecodeBase64, }) { - let holdMessages = []; + const holdMessages = []; for (const message of messages) { try { @@ -1744,9 +1743,11 @@ export async function decryptSingleFunc({ skipDecodeBase64, }); - const decryptToUnit8Array = base64ToUint8Array(res); - const responseData = uint8ArrayToObject(decryptToUnit8Array); - holdMessages.push({ ...message, decryptedData: responseData }); + if (res) { + const decryptToUnit8Array = base64ToUint8Array(res); + const responseData = uint8ArrayToObject(decryptToUnit8Array); + holdMessages.push({ ...message, decryptedData: responseData }); + } } catch (error) { console.error(error); } @@ -1758,7 +1759,7 @@ export async function decryptSingleForPublishes({ secretKeyObject, skipDecodeBase64, }) { - let holdMessages = []; + const holdMessages = []; for (const message of messages) { try { @@ -2888,6 +2889,7 @@ export async function getTimestampEnterChat() { return {}; } } + export async function getTimestampMention() { const wallet = await getSaveWallet(); const address = wallet.address0; @@ -2900,6 +2902,7 @@ export async function getTimestampMention() { return {}; } } + export async function getTimestampGroupAnnouncement() { const wallet = await getSaveWallet(); const address = wallet.address0; @@ -2996,6 +2999,7 @@ async function getGroupData() { return {}; } } + export async function getGroupDataSingle(groupId) { const wallet = await getSaveWallet(); const address = wallet.address0; @@ -3266,6 +3270,7 @@ function setupMessageListener() { break; case 'updateThreadActivity': updateThreadActivityCase(request, event); + break; case 'decryptGroupEncryption': decryptGroupEncryptionCase(request, event); break; @@ -3387,7 +3392,7 @@ const checkGroupList = async () => { .filter( (item) => item?.name !== 'extension-proxy' && - item?.address !== 'QSMMGSgysEuqDCuLw3S4cHrQkBrh3vP3VH' + item?.address !== 'QSMMGSgysEuqDCuLw3S4cHrQkBrh3vP3VH' // TODO put address in a specific file ) .sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); @@ -3411,7 +3416,7 @@ export const checkNewMessages = async () => { myName = userData.name; } - let newAnnouncements = []; + const newAnnouncements = []; const activeData = (await getStoredData('active-groups-directs')) || { groups: [], directs: [], @@ -3441,6 +3446,7 @@ export const checkNewMessages = async () => { const latestMessage = responseData.filter( (pub) => pub?.name !== myName )[0]; + if (!latestMessage) { return; // continue to the next group } @@ -3463,7 +3469,8 @@ export const checkNewMessages = async () => { } }) ); - let isDisableNotifications = + + const isDisableNotifications = (await getUserSettings({ key: 'disable-push-notifications' })) || false; if ( @@ -3611,8 +3618,8 @@ export const checkThreads = async (bringBack) => { if (userData?.name) { myName = userData.name; } - let newAnnouncements = []; - let dataToBringBack = []; + const newAnnouncements = []; + const dataToBringBack = []; const threadActivity = await getThreadActivity(); if (!threadActivity) return null; @@ -3627,7 +3634,6 @@ export const checkThreads = async (bringBack) => { for (const thread of selectedThreads) { try { const identifier = `thmsg-${thread?.threadId}`; - const name = thread?.qortalName; const endpoint = await getArbitraryEndpoint(); const url = await createEndpoint( `${endpoint}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=1&includemetadata=false&offset=${0}&reverse=true&prefix=true` @@ -3643,7 +3649,6 @@ export const checkThreads = async (bringBack) => { const latestMessage = responseData.filter( (pub) => pub?.name !== myName )[0]; - // const latestMessage = responseData[0] if (!latestMessage) { continue; @@ -3717,7 +3722,7 @@ export const checkThreads = async (bringBack) => { '_type=thread-post' + `_data=${JSON.stringify(newAnnouncements[0])}` ); - let isDisableNotifications = + const isDisableNotifications = (await getUserSettings({ key: 'disable-push-notifications' })) || false; if (!isDisableNotifications) { // Check user settings to see if notifications are disabled diff --git a/src/components/Chat/ChatList.tsx b/src/components/Chat/ChatList.tsx index 6000fab..f5cb3f6 100644 --- a/src/components/Chat/ChatList.tsx +++ b/src/components/Chat/ChatList.tsx @@ -7,6 +7,15 @@ import { ChatOptions } from './ChatOptions'; import ErrorBoundary from '../../common/ErrorBoundary'; import { useTranslation } from 'react-i18next'; +type ReactionItem = { + sender: string; + senderName?: string; +}; + +export type ReactionsMap = { + [reactionType: string]: ReactionItem[]; +}; + export const ChatList = ({ initialMessages, myAddress, @@ -236,7 +245,7 @@ export const ChatList = ({ let message = messages[index] || null; // Safeguard against undefined let replyIndex = -1; let reply = null; - let reactions = null; + let reactions: ReactionsMap | null = null; let isUpdating = false; try { @@ -444,13 +453,13 @@ export const ChatList = ({ {enableMentions && (hasSecretKey || isPrivate === false) && ( )} diff --git a/src/components/Chat/ChatOptions.tsx b/src/components/Chat/ChatOptions.tsx index 2d2c352..ef1a4b8 100644 --- a/src/components/Chat/ChatOptions.tsx +++ b/src/components/Chat/ChatOptions.tsx @@ -25,7 +25,6 @@ import { AppsSearchRight, } from '../Apps/Apps-styles'; import IconClearInput from '../../assets/svgs/ClearInput.svg'; -import { CellMeasurerCache } from 'react-virtualized'; import { getBaseApiReact } from '../../App'; import { MessageDisplay } from './MessageDisplay'; import { useVirtualizer } from '@tanstack/react-virtual'; @@ -36,6 +35,7 @@ import { generateHTML } from '@tiptap/react'; import ErrorBoundary from '../../common/ErrorBoundary'; import { useTranslation } from 'react-i18next'; import { isHtmlString } from '../../utils/chat'; +import TextStyle from '@tiptap/extension-text-style'; const extractTextFromHTML = (htmlString = '') => { return convert(htmlString, { @@ -43,11 +43,6 @@ const extractTextFromHTML = (htmlString = '') => { })?.toLowerCase(); }; -const cache = new CellMeasurerCache({ - fixedWidth: true, - defaultHeight: 50, -}); - export const ChatOptions = ({ messages: untransformedMessages, goToMessage, @@ -86,6 +81,7 @@ export const ChatOptions = ({ Underline, Highlight, Mention, + TextStyle, ]); return { ...item, diff --git a/src/components/Chat/MessageItem.tsx b/src/components/Chat/MessageItem.tsx index a50d312..6580ae8 100644 --- a/src/components/Chat/MessageItem.tsx +++ b/src/components/Chat/MessageItem.tsx @@ -54,6 +54,7 @@ import { messageHasImage, } from '../../utils/chat'; import { useTranslation } from 'react-i18next'; +import { ReactionsMap } from './ChatList'; const getBadgeImg = (level) => { switch (level?.toString()) { @@ -99,558 +100,580 @@ const UserBadge = memo(({ userInfo }) => { ); }); -export const MessageItem = memo( - ({ - message, - onSeen, - isLast, - isTemp, - myAddress, - onReply, - isShowingAsReply, - reply, - replyIndex, - scrollToItem, - handleReaction, - reactions, - isUpdating, - lastSignature, - onEdit, - isPrivate, - }) => { - const { getIndividualUserInfo } = useContext(QORTAL_APP_CONTEXT); - const [anchorEl, setAnchorEl] = useState(null); - const [selectedReaction, setSelectedReaction] = useState(null); - const [userInfo, setUserInfo] = useState(null); +type MessageItemProps = { + handleReaction: (reaction: string, messageId: string) => void; + isLast: boolean; + isPrivate: boolean; + isShowingAsReply?: boolean; + isTemp: boolean; + isUpdating: boolean; + lastSignature: string; + message: string; + myAddress: string; + onEdit: (messageId: string) => void; + onReply: (messageId: string) => void; + onSeen: () => void; + reactions: ReactionsMap | null; + reply: string | null; + replyIndex: number; + scrollToItem: (index: number) => void; +}; - useEffect(() => { - const getInfo = async () => { - if (!message?.sender) return; - try { - const res = await getIndividualUserInfo(message?.sender); - if (!res) return null; - setUserInfo(res); - } catch (error) { - // - } - }; +export const MessageItemComponent = ({ + handleReaction, + isLast, + isPrivate, + isShowingAsReply, + isTemp, + isUpdating, + lastSignature, + message, + myAddress, + onEdit, + onReply, + onSeen, + reactions, + reply, + replyIndex, + scrollToItem, +}: MessageItemProps) => { + const { getIndividualUserInfo } = useContext(QORTAL_APP_CONTEXT); + const [anchorEl, setAnchorEl] = useState(null); + const [selectedReaction, setSelectedReaction] = useState(null); + const [userInfo, setUserInfo] = useState(null); - getInfo(); - }, [message?.sender, getIndividualUserInfo]); - - const htmlText = useMemo(() => { - if (message?.messageText) { - const isHtml = isHtmlString(message?.messageText); - if (isHtml) return message?.messageText; - return generateHTML(message?.messageText, [ - StarterKit, - Underline, - Highlight, - Mention, - TextStyle, - ]); + useEffect(() => { + const getInfo = async () => { + if (!message?.sender) return; + try { + const res = await getIndividualUserInfo(message?.sender); + if (!res) return null; + setUserInfo(res); + } catch (error) { + // } - }, [message?.editTimestamp]); + }; - const htmlReply = useMemo(() => { - if (reply?.messageText) { - const isHtml = isHtmlString(reply?.messageText); - if (isHtml) return reply?.messageText; - return generateHTML(reply?.messageText, [ - StarterKit, - Underline, - Highlight, - Mention, - TextStyle, - ]); - } - }, [reply?.editTimestamp]); + getInfo(); + }, [message?.sender, getIndividualUserInfo]); - const userAvatarUrl = useMemo(() => { - return message?.senderName - ? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${ - message?.senderName - }/qortal_avatar?async=true` - : ''; - }, []); + const htmlText = useMemo(() => { + if (message?.messageText) { + const isHtml = isHtmlString(message?.messageText); + if (isHtml) return message?.messageText; + return generateHTML(message?.messageText, [ + StarterKit, + Underline, + Highlight, + Mention, + TextStyle, + ]); + } + }, [message?.editTimestamp]); - const onSeenFunc = useCallback(() => { - onSeen(message.id); - }, [message?.id]); + const htmlReply = useMemo(() => { + if (reply?.messageText) { + const isHtml = isHtmlString(reply?.messageText); + if (isHtml) return reply?.messageText; + return generateHTML(reply?.messageText, [ + StarterKit, + Underline, + Highlight, + Mention, + TextStyle, + ]); + } + }, [reply?.editTimestamp]); - const theme = useTheme(); - const { t } = useTranslation([ - 'auth', - 'core', - 'group', - 'question', - 'tutorial', - ]); + const userAvatarUrl = useMemo(() => { + return message?.senderName + ? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${ + message?.senderName + }/qortal_avatar?async=true` + : ''; + }, []); - const hasNoMessage = - (!message.decryptedData?.data?.message || - message.decryptedData?.data?.message === '

') && - (message?.images || [])?.length === 0 && - (!message?.messageText || message?.messageText === '

') && - (!message?.text || message?.text === '

'); + const onSeenFunc = useCallback(() => { + onSeen(message.id); + }, [message?.id]); - return ( - <> - {message?.divide && ( -
- {t('core:message.generic.unread_messages', { - postProcess: 'capitalizeFirstChar', - })} -
- )} + const theme = useTheme(); + const { t } = useTranslation([ + 'auth', + 'core', + 'group', + 'question', + 'tutorial', + ]); -

') && + (message?.images || [])?.length === 0 && + (!message?.messageText || message?.messageText === '

') && + (!message?.text || message?.text === '

'); + + return ( + <> + {message?.divide && ( +
+ {t('core:message.generic.unread_messages', { + postProcess: 'capitalizeFirstChar', + })} +
+ )} + + +
-
+ ) : ( + + + + {message?.senderName?.charAt(0)} + + + + + )} + + - {isShowingAsReply ? ( - - ) : ( + + + + {message?.senderName || message?.sender} + + + - - { + onEdit(message); + }} + > + + + )} + + {!isShowingAsReply && ( + { + onReply(message); }} - alt={message?.senderName} - src={userAvatarUrl} > - {message?.senderName?.charAt(0)} - - - + + + )} + + {!isShowingAsReply && handleReaction && ( + { + if ( + reactions && + reactions[val] && + reactions[val]?.find( + (item) => item?.sender === myAddress + ) + ) { + handleReaction(val, message, false); + } else { + handleReaction(val, message, true); + } + }} + /> + )} + + + {reply && ( + <> + + + { + scrollToItem(replyIndex); + }} + > + + + + + {t('core:message.generic.replied_to', { + person: reply?.senderName || reply?.senderAddress, + postProcess: 'capitalizeFirstChar', + })} + + + {reply?.messageText && ( + + )} + + {reply?.decryptedData?.type === 'notification' ? ( + + ) : ( + + )} + + + + )} + + {htmlText && !hasNoMessage && ( + + )} + + {message?.decryptedData?.type === 'notification' ? ( + + ) : hasNoMessage ? null : ( + + )} + {hasNoMessage && ( + + + + {t('core:message.generic.no_message', { + postProcess: 'capitalizeFirstChar', + })} + + + )} + {message?.images && messageHasImage(message) && ( + )} - - - {message?.senderName || message?.sender} - - - - - {message?.sender === myAddress && - (!message?.isNotEncrypted || isPrivate === false) && ( + {reactions && + Object.keys(reactions).map((reaction) => { + const numberOfReactions = reactions[reaction]?.length; + if (numberOfReactions === 0) return null; + return ( { - onEdit(message); + key={reaction} + sx={{ + background: theme.palette.background.surface, + borderRadius: '7px', + height: '30px', + minWidth: '45px', + }} + onClick={(event) => { + event.stopPropagation(); // Prevent event bubbling + setAnchorEl(event.currentTarget); + setSelectedReaction(reaction); }} > - +
+ {reaction} +
{' '} + {numberOfReactions > 1 && ( + + {numberOfReactions} + + )}
- )} + ); + })} +
- {!isShowingAsReply && ( - { - onReply(message); + {selectedReaction && ( + { + setAnchorEl(null); + setSelectedReaction(null); + }} + anchorOrigin={{ + vertical: 'top', + horizontal: 'center', + }} + transformOrigin={{ + vertical: 'bottom', + horizontal: 'center', + }} + slotProps={{ + paper: { + style: { + backgroundColor: theme.palette.background.default, + color: theme.palette.text.primary, + }, + }, + }} + > + + + {t('core:message.generic.people_reaction', { + reaction: selectedReaction, + postProcess: 'capitalizeFirstChar', + })} + + + - - - )} + {reactions[selectedReaction]?.map((reactionItem) => ( + + + + ))} + - {!isShowingAsReply && handleReaction && ( - { +
- - )} - - {htmlText && !hasNoMessage && ( - - )} - - {message?.decryptedData?.type === 'notification' ? ( - - ) : hasNoMessage ? null : ( - - )} - {hasNoMessage && ( - - - - {t('core:message.generic.no_message', { - postProcess: 'capitalizeFirstChar', - })} - - - )} - {message?.images && messageHasImage(message) && ( - + )} - - {reactions && - Object.keys(reactions).map((reaction) => { - const numberOfReactions = reactions[reaction]?.length; - if (numberOfReactions === 0) return null; - return ( - { - event.stopPropagation(); // Prevent event bubbling - setAnchorEl(event.currentTarget); - setSelectedReaction(reaction); - }} - > -
- {reaction} -
{' '} - {numberOfReactions > 1 && ( - - {numberOfReactions} - - )} -
- ); - })} -
- - {selectedReaction && ( - { - setAnchorEl(null); - setSelectedReaction(null); + {message?.isNotEncrypted && isPrivate && ( + - - - {t('core:message.generic.people_reaction', { - reaction: selectedReaction, - postProcess: 'capitalizeFirstChar', - })} - - - - {reactions[selectedReaction]?.map((reactionItem) => ( - - - - ))} - - - - - + /> )} - - {message?.isNotEncrypted && isPrivate && ( - - )} - - {isUpdating ? ( - - {message?.status === 'failed-permanent' - ? t('core:message.error.update_failed', { - postProcess: 'capitalizeFirstChar', - }) - : t('core:message.generic.updating', { - postProcess: 'capitalizeFirstChar', - })} - - ) : isTemp ? ( - - {message?.status === 'failed-permanent' - ? t('core:message.error.send_failed', { - postProcess: 'capitalizeFirstChar', - }) - : t('core:message.generic.sending', { - postProcess: 'capitalizeFirstChar', - })} - - ) : ( - <> - {message?.isEdit && ( - - {t('core:message.generic.edited', { - postProcess: 'capitalizeFirstChar', - })} - - )} - + {isUpdating ? ( + + {message?.status === 'failed-permanent' + ? t('core:message.error.update_failed', { + postProcess: 'capitalizeFirstChar', + }) + : t('core:message.generic.updating', { + postProcess: 'capitalizeFirstChar', + })} + + ) : isTemp ? ( + + {message?.status === 'failed-permanent' + ? t('core:message.error.send_failed', { + postProcess: 'capitalizeFirstChar', + }) + : t('core:message.generic.sending', { + postProcess: 'capitalizeFirstChar', + })} + + ) : ( + <> + {message?.isEdit && ( - {formatTimestamp(message.timestamp)} + {t('core:message.generic.edited', { + postProcess: 'capitalizeFirstChar', + })} - - )} - + )} + + + {formatTimestamp(message.timestamp)} + + + )}
-
- - - ); - } -); + +
+
+ + ); +}; + +const MemoizedMessageItem = memo(MessageItemComponent); +MemoizedMessageItem.displayName = 'MessageItem'; // It ensures React DevTools shows MessageItem as the name (instead of "Anonymous" or "Memo") + +export const MessageItem = MemoizedMessageItem; export const ReplyPreview = ({ message, isEdit = false }) => { const theme = useTheme(); diff --git a/src/components/Chat/TipTap.tsx b/src/components/Chat/TipTap.tsx index 0baef0a..4c69504 100644 --- a/src/components/Chat/TipTap.tsx +++ b/src/components/Chat/TipTap.tsx @@ -1,5 +1,5 @@ import { memo, useCallback, useEffect, useMemo, useRef } from 'react'; -import { EditorProvider, useCurrentEditor } from '@tiptap/react'; +import { Editor, EditorProvider, useCurrentEditor } from '@tiptap/react'; import StarterKit from '@tiptap/starter-kit'; import { Color } from '@tiptap/extension-color'; import ListItem from '@tiptap/extension-list-item'; @@ -34,16 +34,6 @@ import { fileToBase64 } from '../../utils/fileReading/index.js'; import { useTranslation } from 'react-i18next'; import i18n from 'i18next'; -function textMatcher(doc, from) { - const textBeforeCursor = doc.textBetween(0, from, ' ', ' '); - const match = textBeforeCursor.match(/@[\w]*$/); // Match '@' followed by valid characters - if (!match) return null; - - const start = from - match[0].length; - const query = match[0]; - return { start, query }; -} - const MenuBar = memo( ({ setEditorRef, @@ -361,8 +351,8 @@ const MenuBar = memo( ); const extensions = [ + TextStyle, Color.configure({ types: [TextStyle.name, ListItem.name] }), - TextStyle.configure({ types: [ListItem.name] }), StarterKit.configure({ bulletList: { keepMarks: true, @@ -383,11 +373,26 @@ const extensions = [ const content = ``; -export default ({ +type TiptapProps = { + setEditorRef: (editorInstance: Editor | null) => void; + onEnter: () => void | Promise; + disableEnter?: boolean; + isChat?: boolean; + maxHeightOffset?: number; + overrideMobile?: boolean; + customEditorHeight?: number | null; + setIsFocusedParent: React.Dispatch>; + isFocusedParent: boolean; + membersWithNames: unknown[]; + enableMentions?: boolean; + insertImage: (image: any) => void; +}; + +const Tiptap = ({ setEditorRef, onEnter, - disableEnter, - isChat, + disableEnter = false, + isChat = false, maxHeightOffset, setIsFocusedParent, isFocusedParent, @@ -396,7 +401,7 @@ export default ({ membersWithNames, enableMentions, insertImage, -}) => { +}: TiptapProps) => { const theme = useTheme(); const [isDisabledEditorEnter, setIsDisabledEditorEnter] = useAtom( isDisabledEditorEnterAtom @@ -623,3 +628,5 @@ export default ({ ); }; + +export default Tiptap; diff --git a/src/components/Embeds/Embed.tsx b/src/components/Embeds/Embed.tsx index 0038830..7aa2e39 100644 --- a/src/components/Embeds/Embed.tsx +++ b/src/components/Embeds/Embed.tsx @@ -59,7 +59,7 @@ export const Embed = ({ embedLink }) => { const [openSnack, setOpenSnack] = useState(false); const [infoSnack, setInfoSnack] = useState(null); const [external, setExternal] = useState(null); - const [imageUrl, setImageUrl] = useState(''); + const [imageUrl, setImageUrl] = useState(null); const [parsedData, setParsedData] = useState(null); const setBlobs = useSetAtom(blobControllerAtom); const [selectedGroupId] = useAtom(selectedGroupIdAtom); diff --git a/src/components/Embeds/ImageEmbed.tsx b/src/components/Embeds/ImageEmbed.tsx index 53bedc5..7402039 100644 --- a/src/components/Embeds/ImageEmbed.tsx +++ b/src/components/Embeds/ImageEmbed.tsx @@ -209,9 +209,8 @@ export const ImageCard = ({ ); }; -export function ImageViewer({ src, alt = '' }) { +export function ImageViewer({ src = null, alt = '' }) { const [isFullscreen, setIsFullscreen] = useState(false); - const handleOpenFullscreen = () => setIsFullscreen(true); const handleCloseFullscreen = () => setIsFullscreen(false); const theme = useTheme(); diff --git a/src/components/Group/Group.tsx b/src/components/Group/Group.tsx index f4091f2..11bb649 100644 --- a/src/components/Group/Group.tsx +++ b/src/components/Group/Group.tsx @@ -276,15 +276,13 @@ export async function getNameInfo(address: string) { } export const getGroupAdmins = async (groupNumber: number) => { - // const validApi = await findUsableApi(); - const response = await fetch( `${getBaseApiReact()}/groups/members/${groupNumber}?limit=0&onlyAdmins=true` ); const groupData = await response.json(); - let members: any = []; - let membersAddresses = []; - let both = []; + const members: any = []; + const membersAddresses = []; + const both = []; const getMemNames = groupData?.members?.map(async (member) => { if (member?.member) { @@ -600,10 +598,11 @@ export const Group = ({ }, [myAddress]); const getGroupOwner = async (groupId) => { + if (groupId == '0') return; // general group has id=0 try { const url = `${getBaseApiReact()}/groups/${groupId}`; const response = await fetch(url); - let data = await response.json(); + const data = await response.json(); const name = await getNameInfo(data?.owner); if (name) { @@ -742,7 +741,7 @@ export const Group = ({ data = await res.text(); } - const decryptedKey: any = await decryptResource(data); + const decryptedKey: any = await decryptResource(data, null); const dataint8Array = base64ToUint8Array(decryptedKey.data); const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); @@ -877,6 +876,7 @@ export const Group = ({ }; const getOwnerNameForGroup = async (owner: string, groupId: string) => { + if (groupId == '0') return; // general group has id=0 try { if (!owner) return; if (groupsOwnerNamesRef.current[groupId]) return; @@ -899,7 +899,7 @@ export const Group = ({ 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 data = await response.json(); const transformToObject = data.reduce((result, item) => { result[item.groupId] = item; return result; diff --git a/src/components/Wallets.tsx b/src/components/Wallets.tsx index 75a36ea..aef22b3 100644 --- a/src/components/Wallets.tsx +++ b/src/components/Wallets.tsx @@ -244,6 +244,7 @@ export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => { )} )} + {wallets?.length > 0 && ( { postProcess: 'capitalizeFirstChar', })} + { if (!seedValue || !seedName || !password) return; onOk({ seedValue, seedName, password }); }} - autoFocus + variant="contained" > {t('core:action.add', { postProcess: 'capitalizeFirstChar', })} + { - let combinedPublicKeys = [...publicKeys, userPublicKey]; + const combinedPublicKeys = [...publicKeys, userPublicKey]; const decodedPrivateKey = Base58.decode(privateKey); const publicKeysDuplicateFree = [...new Set(combinedPublicKeys)]; const Uint8ArrayData = base64ToUint8Array(data64); @@ -114,7 +114,7 @@ export const encryptDataGroup = ({ const keyNonce = new Uint8Array(24); crypto.getRandomValues(keyNonce); // Encrypt the symmetric key for each recipient. - let encryptedKeys = []; + const encryptedKeys = []; publicKeysDuplicateFree.forEach((recipientPublicKey) => { const publicKeyUnit8Array = Base58.decode(recipientPublicKey); const convertedPrivateKey = ed2curve.convertSecretKey(decodedPrivateKey); @@ -153,7 +153,7 @@ export const encryptDataGroup = ({ encryptedKeysSize += key.length; }); combinedDataSize += encryptedKeysSize; - let combinedData = new Uint8Array(combinedDataSize); + const combinedData = new Uint8Array(combinedDataSize); combinedData.set(strUint8Array); combinedData.set(nonce, strUint8Array.length); combinedData.set(keyNonce, strUint8Array.length + nonce.length); @@ -244,9 +244,6 @@ export const encryptSingle = async ({ encryptedData = nacl.secretbox(Uint8ArrayData, nonce, messageKey); encryptedDataBase64 = uint8ArrayToBase64(encryptedData); - // Convert the nonce to base64 - const nonceBase64 = uint8ArrayToBase64(nonce); - // Concatenate the highest key, type number, nonce, and encrypted data (new format) const highestKeyStr = highestKey.toString().padStart(10, '0'); // Fixed length of 10 digits @@ -281,7 +278,7 @@ export const encryptSingle = async ({ }; export const decodeBase64ForUIChatMessages = (messages) => { - let msgs = []; + const msgs = []; for (const msg of messages) { try { const decoded = atob(msg?.data); @@ -306,107 +303,114 @@ export const decryptSingle = async ({ // First, decode the base64-encoded input (if skipDecodeBase64 is not set) const decodedData = skipDecodeBase64 ? data64 : atob(data64); - // Then, decode it again for the specific format (if double encoding is used) - const decodeForNumber = atob(decodedData); + if (secretKeyObject) { + // Then, decode it again for the specific format (if double encoding is used) + const decodeForNumber = atob(decodedData); - // Extract the key (assuming it's always the first 10 characters) - const keyStr = decodeForNumber.slice(0, 10); + // Extract the key (assuming it's always the first 10 characters) + const keyStr = decodeForNumber.slice(0, 10); - // Convert the key string back to a number - const highestKey = parseInt(keyStr, 10); + // Convert the key string back to a number + const highestKey = parseInt(keyStr, 10); - // Check if we have a valid secret key for the extracted highestKey - if (!secretKeyObject[highestKey]) { - throw new Error( - i18n.t('auth:message.error.find_secret_key', { - postProcess: 'capitalizeFirstChar', - }) - ); - } - - const secretKeyEntry = secretKeyObject[highestKey]; - - let typeNumberStr, nonceBase64, encryptedDataBase64; - - // Determine if typeNumber exists by checking if the next 3 characters after keyStr are digits - const possibleTypeNumberStr = decodeForNumber.slice(10, 13); - const hasTypeNumber = /^\d{3}$/.test(possibleTypeNumberStr); // Check if next 3 characters are digits - - if (secretKeyEntry.nonce) { - // Old format: nonce is present in the secretKeyObject, so no type number exists - nonceBase64 = secretKeyEntry.nonce; - 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( - i18n.t('question:message.error.decryption_failed', { - postProcess: 'capitalizeFirstChar', - }) - ); - } - - // 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) - encryptedDataBase64 = decodeForNumber.slice(45); // The remaining part is the encrypted data - } else { - // Old format without type number (nonce is embedded in the message, first 32 characters after keyStr) - nonceBase64 = decodeForNumber.slice(10, 42); // First 32 characters for the nonce - encryptedDataBase64 = decodeForNumber.slice(42); // The remaining part is the encrypted data + // Check if we have a valid secret key for the extracted highestKey + if (!secretKeyObject[highestKey]) { + throw new Error( + i18n.t('auth:message.error.find_secret_key', { + postProcess: 'capitalizeFirstChar', + }) + ); } - } - // Convert Base64 strings to Uint8Array - const Uint8ArrayData = base64ToUint8Array(encryptedDataBase64); - const nonce = base64ToUint8Array(nonceBase64); - const messageKey = base64ToUint8Array(secretKeyEntry.messageKey); + const secretKeyEntry = secretKeyObject[highestKey]; - if (!(Uint8ArrayData instanceof Uint8Array)) { - throw new Error( - i18n.t('auth:message.error.invalid_uint8', { - postProcess: 'capitalizeFirstChar', - }) + let nonceBase64, encryptedDataBase64; + + // Determine if typeNumber exists by checking if the next 3 characters after keyStr are digits + const possibleTypeNumberStr = decodeForNumber.slice(10, 13); + const hasTypeNumber = /^\d{3}$/.test(possibleTypeNumberStr); // Check if next 3 characters are digits + + if (secretKeyEntry.nonce) { + // Old format: nonce is present in the secretKeyObject, so no type number exists + nonceBase64 = secretKeyEntry.nonce; + 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( + i18n.t('question:message.error.decryption_failed', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + + // Convert the decrypted Uint8Array back to a Base64 string + return uint8ArrayToBase64(decryptedBytes); + } + // New format: Extract type number and nonce + nonceBase64 = decodeForNumber.slice(13, 45); // Extract nonce (next 32 characters after type number) + encryptedDataBase64 = decodeForNumber.slice(45); // The remaining part is the encrypted data + } else { + // Old format without type number (nonce is embedded in the message, first 32 characters after keyStr) + nonceBase64 = decodeForNumber.slice(10, 42); // First 32 characters for the nonce + encryptedDataBase64 = decodeForNumber.slice(42); // The remaining part is the encrypted data + } + } + + // Convert Base64 strings to Uint8Array + const Uint8ArrayData = base64ToUint8Array(encryptedDataBase64); + const nonce = base64ToUint8Array(nonceBase64); + const messageKey = base64ToUint8Array(secretKeyEntry.messageKey); + + if (!(Uint8ArrayData instanceof Uint8Array)) { + throw new Error( + i18n.t('auth:message.error.invalid_uint8', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + + // Decrypt the data using the nonce and messageKey + const decryptedData = nacl.secretbox.open( + Uint8ArrayData, + nonce, + messageKey ); + + // Check if decryption was successful + if (!decryptedData) { + throw new Error( + i18n.t('question:message.error.decryption_failed', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + + // Convert the decrypted Uint8Array back to a Base64 string + return uint8ArrayToBase64(decryptedData); } - // Decrypt the data using the nonce and messageKey - const decryptedData = nacl.secretbox.open(Uint8ArrayData, nonce, messageKey); - - // Check if decryption was successful - if (!decryptedData) { - throw new Error( - i18n.t('question:message.error.decryption_failed', { - postProcess: 'capitalizeFirstChar', - }) - ); - } - - // Convert the decrypted Uint8Array back to a Base64 string - return uint8ArrayToBase64(decryptedData); + return; }; export const decryptGroupEncryptionWithSharingKey = async ({ @@ -424,14 +428,9 @@ export const decryptGroupEncryptionWithSharingKey = async ({ // Extract the shared keyNonce const keyNonceStartPosition = nonceEndPosition; const keyNonceEndPosition = keyNonceStartPosition + 24; // Nonce is 24 bytes - const keyNonce = allCombined.slice( - keyNonceStartPosition, - keyNonceEndPosition - ); // Extract the sender's public key const senderPublicKeyStartPosition = keyNonceEndPosition; const senderPublicKeyEndPosition = senderPublicKeyStartPosition + 32; // Public keys are 32 bytes - // Calculate count first const countStartPosition = allCombined.length - 4; // 4 bytes before the end, since count is stored in Uint32 (4 bytes) const countArray = allCombined.slice( @@ -662,7 +661,6 @@ export function decryptDeprecatedSingle(uint8Array, publicKey, privateKey) { const str = 'qortalEncryptedData'; const strEncoder = new TextEncoder(); const strUint8Array = strEncoder.encode(str); - const strData = combinedData.slice(0, strUint8Array.length); const nonce = combinedData.slice( strUint8Array.length, strUint8Array.length + 24 diff --git a/src/qortal/get.ts b/src/qortal/get.ts index 368bd50..1af583f 100644 --- a/src/qortal/get.ts +++ b/src/qortal/get.ts @@ -552,7 +552,7 @@ export const getUserAccount = async ({ export const encryptData = async (data, sender) => { let data64 = data.data64 || data.base64; - let publicKeys = data.publicKeys || []; + const publicKeys = data.publicKeys || []; if (data?.file || data?.blob) { data64 = await fileToBase64(data?.file || data?.blob); } @@ -587,8 +587,8 @@ export const encryptData = async (data, sender) => { export const encryptQortalGroupData = async (data, sender) => { let data64 = data?.data64 || data?.base64; - let groupId = data?.groupId; - let isAdmins = data?.isAdmins; + const groupId = data?.groupId; + const isAdmins = data?.isAdmins; if (!groupId) { throw new Error( i18n.t('question:message.generic.provide_group_id', { @@ -613,7 +613,7 @@ export const encryptQortalGroupData = async (data, sender) => { groupSecretkeys[groupId] && groupSecretkeys[groupId].secretKeyObject && groupSecretkeys[groupId]?.timestamp && - Date.now() - groupSecretkeys[groupId]?.timestamp < 1200000 + Date.now() - groupSecretkeys[groupId]?.timestamp < 1200000 // TODO magic number ) { secretKeyObject = groupSecretkeys[groupId].secretKeyObject; } @@ -659,7 +659,7 @@ export const encryptQortalGroupData = async (data, sender) => { groupSecretkeys[`admins-${groupId}`] && groupSecretkeys[`admins-${groupId}`].secretKeyObject && groupSecretkeys[`admins-${groupId}`]?.timestamp && - Date.now() - groupSecretkeys[`admins-${groupId}`]?.timestamp < 1200000 + Date.now() - groupSecretkeys[`admins-${groupId}`]?.timestamp < 1200000 // TODO magic number ) { secretKeyObject = groupSecretkeys[`admins-${groupId}`].secretKeyObject; } @@ -717,9 +717,9 @@ export const encryptQortalGroupData = async (data, sender) => { }; export const decryptQortalGroupData = async (data, sender) => { - let data64 = data?.data64 || data?.base64; - let groupId = data?.groupId; - let isAdmins = data?.isAdmins; + const data64 = data?.data64 || data?.base64; + const groupId = data?.groupId; + const isAdmins = data?.isAdmins; if (!groupId) { throw new Error( i18n.t('question:message.generic.provide_group_id', { @@ -742,7 +742,7 @@ export const decryptQortalGroupData = async (data, sender) => { groupSecretkeys[groupId] && groupSecretkeys[groupId].secretKeyObject && groupSecretkeys[groupId]?.timestamp && - Date.now() - groupSecretkeys[groupId]?.timestamp < 1200000 + Date.now() - groupSecretkeys[groupId]?.timestamp < 1200000 // TODO magic number ) { secretKeyObject = groupSecretkeys[groupId].secretKeyObject; } @@ -785,7 +785,7 @@ export const decryptQortalGroupData = async (data, sender) => { groupSecretkeys[`admins-${groupId}`] && groupSecretkeys[`admins-${groupId}`].secretKeyObject && groupSecretkeys[`admins-${groupId}`]?.timestamp && - Date.now() - groupSecretkeys[`admins-${groupId}`]?.timestamp < 1200000 + Date.now() - groupSecretkeys[`admins-${groupId}`]?.timestamp < 1200000 // TODO magic nummber ) { secretKeyObject = groupSecretkeys[`admins-${groupId}`].secretKeyObject; } @@ -843,7 +843,7 @@ export const decryptQortalGroupData = async (data, sender) => { export const encryptDataWithSharingKey = async (data, sender) => { let data64 = data?.data64 || data?.base64; - let publicKeys = data.publicKeys || []; + const publicKeys = data.publicKeys || []; if (data?.file || data?.blob) { data64 = await fileToBase64(data?.file || data?.blob); } @@ -899,6 +899,7 @@ export const decryptDataWithSharingKey = async (data, sender) => { data64EncryptedData: encryptedData, key, }); + const base64ToObject = JSON.parse(atob(decryptedData)); if (!base64ToObject.data) diff --git a/src/qortal/qortal-requests.ts b/src/qortal/qortal-requests.ts index f67b922..ba04c96 100644 --- a/src/qortal/qortal-requests.ts +++ b/src/qortal/qortal-requests.ts @@ -152,7 +152,7 @@ function setupMessageListenerQortalRequest() { appInfo, skipAuth, }); - event.source.postMessage( + event.source!.postMessage( { requestId: request.requestId, action: request.action, @@ -162,7 +162,7 @@ function setupMessageListenerQortalRequest() { event.origin ); } catch (error) { - event.source.postMessage( + event.source!.postMessage( { requestId: request.requestId, action: request.action, @@ -236,7 +236,7 @@ function setupMessageListenerQortalRequest() { request.payload, event.source ); - event.source.postMessage( + event.source!.postMessage( { requestId: request.requestId, action: request.action, @@ -246,7 +246,7 @@ function setupMessageListenerQortalRequest() { event.origin ); } catch (error) { - event.source.postMessage( + event.source!.postMessage( { requestId: request.requestId, action: request.action,