diff --git a/src/App.tsx b/src/App.tsx index b8cd73a..48d425f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -134,7 +134,7 @@ const defaultValues: MyContextInterface = { message: "", }, }; -export let isMobile = false +export let isMobile = true const isMobileDevice = () => { const userAgent = navigator.userAgent || navigator.vendor || window.opera; @@ -1215,7 +1215,9 @@ function App() { }} /> )} - + {authenticatedMode === "ltc" ? ( diff --git a/src/background.ts b/src/background.ts index 8d599fa..9316d69 100644 --- a/src/background.ts +++ b/src/background.ts @@ -27,6 +27,7 @@ import { RequestQueueWithPromise } from "./utils/queue/queue"; import { validateAddress } from "./utils/validateAddress"; import { Sha256 } from "asmcrypto.js"; import { TradeBotRespondMultipleRequest } from "./transactions/TradeBotRespondMultipleRequest"; +import { RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS } from "./constants/resourceTypes"; let lastGroupNotification; export const groupApi = "https://ext-node.qortal.link"; @@ -225,6 +226,27 @@ export function isExtMsg(data) { return isMsgFromExtensionGroup; } +export function isUpdateMsg(data) { + let isUpdateMessage = true; + try { + const decode1 = atob(data); + const decode2 = atob(decode1); + const keyStr = decode2.slice(10, 13); + + // Convert the key string back to a number + const numberKey = parseInt(keyStr, 10); + if (isNaN(numberKey)) { + isUpdateMessage = false; + } else if(numberKey !== RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS){ + isUpdateMessage = false; + } + } catch (error) { + isUpdateMessage = false; + } + + return isUpdateMessage; +} + async function checkWebviewFocus() { return new Promise((resolve) => { // Set a timeout for 1 second @@ -478,7 +500,7 @@ const handleNotification = async (groups) => { if(!isArray(mutedGroups)) mutedGroups = [] let isFocused; - const data = groups.filter((group) => group?.sender !== address && !mutedGroups.includes(group.groupId) && !group?.chatReference); + const data = groups.filter((group) => group?.sender !== address && !mutedGroups.includes(group.groupId) && !isUpdateMsg(group?.data)); try { if(isDisableNotifications) return if (!data || data?.length === 0) return; @@ -3924,9 +3946,9 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { break; } case "encryptSingle": { - const { data, secretKeyObject } = request.payload; + const { data, secretKeyObject, typeNumber } = request.payload; - encryptSingle({ data64: data, secretKeyObject: secretKeyObject }) + encryptSingle({ data64: data, secretKeyObject: secretKeyObject, typeNumber }) .then((res) => { sendResponse(res); }) diff --git a/src/components/Chat/ChatGroup.tsx b/src/components/Chat/ChatGroup.tsx index 7a78ee1..ddc731b 100644 --- a/src/components/Chat/ChatGroup.tsx +++ b/src/components/Chat/ChatGroup.tsx @@ -19,6 +19,7 @@ import { Box, ButtonBase } from '@mui/material' import ShortUniqueId from "short-unique-id"; import { ReplyPreview } from './MessageItem' import { ExitIcon } from '../../assets/Icons/ExitIcon' +import { RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS } from '../../constants/resourceTypes' const uid = new ShortUniqueId({ length: 5 }); @@ -123,6 +124,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, if(hasInitialized.current){ const formatted = response.filter((rawItem)=> !rawItem?.chatReference).map((item: any)=> { + return { ...item, id: item.signature, @@ -376,12 +378,13 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, }, [messages]) - const encryptChatMessage = async (data: string, secretKeyObject: any)=> { + const encryptChatMessage = async (data: string, secretKeyObject: any, reactiontypeNumber?: number)=> { try { return new Promise((res, rej)=> { chrome?.runtime?.sendMessage({ action: "encryptSingle", payload: { data, - secretKeyObject + secretKeyObject, + typeNumber: reactiontypeNumber }}, (response) => { if (!response?.error) { @@ -535,8 +538,8 @@ const clearEditorContent = () => { ...(otherData || {}) } const message64: any = await objectToBase64(objectMessage) - - const encryptSingle = await encryptChatMessage(message64, secretKeyObject) + const reactiontypeNumber = RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS + const encryptSingle = await encryptChatMessage(message64, secretKeyObject, reactiontypeNumber) // const res = await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle}) const sendMessageFunc = async () => { diff --git a/src/components/Chat/ChatList.tsx b/src/components/Chat/ChatList.tsx index 9704073..3876ab7 100644 --- a/src/components/Chat/ChatList.tsx +++ b/src/components/Chat/ChatList.tsx @@ -155,6 +155,10 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR const handleAtBottomStateChange = (atBottom) => { isAtBottomRef.current = atBottom; + if(atBottom){ + handleMessageSeen(); + setShowScrollButton(false) + } }; return ( @@ -165,7 +169,6 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR itemContent={rowRenderer} atBottomThreshold={50} followOutput="smooth" - onScroll={handleScroll} atBottomStateChange={handleAtBottomStateChange} // Detect bottom status increaseViewportBy={3000} diff --git a/src/components/Group/Group.tsx b/src/components/Group/Group.tsx index 0c9080e..faa7c55 100644 --- a/src/components/Group/Group.tsx +++ b/src/components/Group/Group.tsx @@ -76,7 +76,7 @@ import { WebSocketActive } from "./WebsocketActive"; import { flushSync } from "react-dom"; import { useMessageQueue } from "../../MessageQueueContext"; import { DrawerComponent } from "../Drawer/Drawer"; -import { isExtMsg } from "../../background"; +import { isExtMsg, isUpdateMsg } from "../../background"; import { ContextMenu } from "../ContextMenu"; import { MobileFooter } from "../Mobile/MobileFooter"; import Header from "../Mobile/MobileHeader"; @@ -420,6 +420,7 @@ export const Group = ({ const [mobileViewModeKeepOpen, setMobileViewModeKeepOpen] = useState(""); const [desktopSideView, setDesktopSideView] = useState('groups') const isFocusedRef = useRef(true); + const timestampEnterDataRef = useRef({}); const selectedGroupRef = useRef(null); const selectedDirectRef = useRef(null); const groupSectionRef = useRef(null); @@ -429,9 +430,11 @@ export const Group = ({ const settimeoutForRefetchSecretKey = useRef(null); const { clearStatesMessageQueueProvider } = useMessageQueue(); const initiatedGetMembers = useRef(false); - // useEffect(()=> { - // setFullHeight() - // }, []) + const [groupChatTimestamps, setGroupChatTimestamps] = React.useState({}); + + useEffect(()=> { + timestampEnterDataRef.current = timestampEnterData + }, [timestampEnterData]) useEffect(() => { isFocusedRef.current = isFocused; @@ -616,12 +619,14 @@ export const Group = ({ const groupChatHasUnread = useMemo(() => { let hasUnread = false; groups.forEach((group) => { + console.log('isUpdateMsg(group?.data)', isUpdateMsg(group?.data)) + if ( group?.data && isExtMsg(group?.data) && group?.sender !== myAddress && - group?.timestamp && - ((!timestampEnterData[group?.groupId] && !group?.chatReference && + group?.timestamp && (!isUpdateMsg(group?.data) || groupChatTimestamps[group?.groupId]) && + ((!timestampEnterData[group?.groupId] && Date.now() - group?.timestamp < timeDifferenceForNotificationChats) || timestampEnterData[group?.groupId] < group?.timestamp) ) { @@ -918,13 +923,52 @@ export const Group = ({ } catch (error) {} }; + const getCountNewMesg = async (groupId, after)=> { + try { + const response = await fetch( + `${getBaseApiReact()}/chat/messages?after=${after}&txGroupId=${groupId}&haschatreference=false&encoding=BASE64&limit=1` + ); + const data = await response.json(); + if(data && data[0]) return data[0].timestamp + } catch (error) { + + } + } + + const getLatestRegularChat = async (groups)=> { + try { + + 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)){ + const hasMoreRecentMsg = await getCountNewMesg(group.groupId, timestampEnterDataRef.current[group?.groupId] || Date.now() - 24 * 60 * 60 * 1000) + if(hasMoreRecentMsg){ + groupData[group.groupId] = hasMoreRecentMsg + } + } else { + return null + } + }) + + await Promise.all(getGroupData) + setGroupChatTimestamps(groupData) + } catch (error) { + + } + } + + + useEffect(() => { // Listen for messages from the background script chrome?.runtime?.onMessage.addListener((message, sender, sendResponse) => { if (message.action === "SET_GROUPS") { // Update the component state with the received 'sendqort' state setGroups(message.payload); - + getLatestRegularChat(message.payload) setMemberGroups(message.payload); if (selectedGroupRef.current && groupSectionRef.current === "chat") { @@ -1102,13 +1146,13 @@ export const Group = ({ if (!findGroup) return false; if (!findGroup?.data || !isExtMsg(findGroup?.data)) return false; return ( - findGroup?.timestamp && !findGroup?.chatReference && + findGroup?.timestamp && (!isUpdateMsg(findGroup?.data) || groupChatTimestamps[findGroup?.groupId]) && ((!timestampEnterData[selectedGroup?.groupId] && Date.now() - findGroup?.timestamp < timeDifferenceForNotificationChats) || timestampEnterData?.[selectedGroup?.groupId] < findGroup?.timestamp) ); - }, [timestampEnterData, selectedGroup]); + }, [timestampEnterData, selectedGroup, groupChatTimestamps]); const isUnread = useMemo(() => { if (!selectedGroup) return false; @@ -2101,7 +2145,7 @@ export const Group = ({ /> )} {group?.data && - isExtMsg(group?.data) && !group?.chatReference && + isExtMsg(group?.data) && (!isUpdateMsg(group?.data) || groupChatTimestamps[group?.groupId]) && group?.sender !== myAddress && group?.timestamp && ((!timestampEnterData[group?.groupId] && diff --git a/src/components/Group/GroupMenu.tsx b/src/components/Group/GroupMenu.tsx index 0e32c8a..5aff12e 100644 --- a/src/components/Group/GroupMenu.tsx +++ b/src/components/Group/GroupMenu.tsx @@ -128,7 +128,7 @@ export const GroupMenu = ({ setGroupSection, groupSection, setOpenManageMembers, minWidth: '24px !important' }}> - + - + - + { {showPicker && (
e.preventDefault()}> { +export const encryptSingle = async ({ data64, secretKeyObject, typeNumber = 1 }: 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]; @@ -152,47 +152,50 @@ export const encryptSingle = async ({ data64, secretKeyObject }: any) => { let nonce, encryptedData, encryptedDataBase64, finalEncryptedData; + // Convert type number to a fixed length of 3 digits + const typeNumberStr = typeNumber.toString().padStart(3, '0'); + if (highestKeyObject.nonce) { // Old format: Use the nonce from secretKeyObject nonce = base64ToUint8Array(highestKeyObject.nonce); - + // Encrypt the data with the existing nonce and message key encryptedData = nacl.secretbox(Uint8ArrayData, nonce, messageKey); encryptedDataBase64 = uint8ArrayToBase64(encryptedData); - - // Concatenate the highest key with the encrypted data (old format) + + // Concatenate the highest key, type number, and encrypted data (old format) const highestKeyStr = highestKey.toString().padStart(10, '0'); // Fixed length of 10 digits finalEncryptedData = btoa(highestKeyStr + encryptedDataBase64); } else { // New format: Generate a random nonce and embed it in the message - nonce = new Uint8Array(24); // 24 bytes for the nonce + nonce = new Uint8Array(24); // 24 bytes for the nonce crypto.getRandomValues(nonce); - // Encrypt the data with the new nonce and message key encryptedData = nacl.secretbox(Uint8ArrayData, nonce, messageKey); encryptedDataBase64 = uint8ArrayToBase64(encryptedData); - + // Convert the nonce to base64 const nonceBase64 = uint8ArrayToBase64(nonce); - // Concatenate the highest key, nonce, and encrypted data (new format) + // 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 + nonceBase64 + encryptedDataBase64); + finalEncryptedData = btoa(highestKeyStr + typeNumberStr + nonceBase64 + encryptedDataBase64); } return finalEncryptedData; }; + -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); - + // Then, decode it again for the specific format (if double encoding is used) const decodeForNumber = atob(decodedData); - // Extract the key (assuming it's 10 characters long) + // 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 @@ -204,19 +207,28 @@ export const decryptSingle = async ({ data64, secretKeyObject, skipDecodeBase64 } const secretKeyEntry = secretKeyObject[highestKey]; - - let nonceBase64, encryptedDataBase64; + 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 + // 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 { - // New format: nonce is included in the message (first 32 characters) - nonceBase64 = decodeForNumber.slice(10, 42); // First 32 characters for the nonce - encryptedDataBase64 = decodeForNumber.slice(42); // The remaining part is the encrypted data - - + if (hasTypeNumber) { + // 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 + } } // Convert Base64 strings to Uint8Array @@ -241,6 +253,8 @@ export const decryptSingle = async ({ data64, secretKeyObject, skipDecodeBase64 }; + + export function decryptGroupData(data64EncryptedData: string, privateKey: string) {