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 a54ac70..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 && (
- {
+
-
-
- {reply && (
- <>
-
-
- {
- scrollToItem(replyIndex);
- }}
- >
-
-
-
-
- {t('core:message.generic.replied_to', {
- person: reply?.senderName || reply?.senderAddress,
- postProcess: 'capitalizeFirstChar',
- })}
-
-
- {reply?.messageText && (
-
- )}
-
- {reply?.decryptedData?.type === 'notification' ? (
-
- ) : (
-
- )}
-
+ {reactions[selectedReaction]?.find(
+ (item) => item?.sender === myAddress
+ )
+ ? t('core:action.remove_reaction', {
+ postProcess: 'capitalizeFirstChar',
+ })
+ : t('core:action.add_reaction', {
+ postProcess: 'capitalizeFirstChar',
+ })}
+
- >
- )}
-
- {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 487fe25..9438166 100644
--- a/src/components/Embeds/ImageEmbed.tsx
+++ b/src/components/Embeds/ImageEmbed.tsx
@@ -208,9 +208,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 607280e..8d67c47 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/Group/ListOfJoinRequests.tsx b/src/components/Group/ListOfJoinRequests.tsx
index 2a65cfe..60bdb0a 100644
--- a/src/components/Group/ListOfJoinRequests.tsx
+++ b/src/components/Group/ListOfJoinRequests.tsx
@@ -124,7 +124,7 @@ export const ListOfJoinRequests = ({
setIsLoadingAccept(false);
setInfoSnack({
type: 'success',
- message: t('group:message.success,group_join', {
+ message: t('group:message.success.group_join', {
postProcess: 'capitalizeFirstChar',
}),
});
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,