fixed notifications for reactions

This commit is contained in:
PhilReact 2024-09-27 15:38:01 +03:00
parent c577b91dfe
commit 53c23bf5ea
9 changed files with 134 additions and 45 deletions

View File

@ -134,7 +134,7 @@ const defaultValues: MyContextInterface = {
message: "", message: "",
}, },
}; };
export let isMobile = false export let isMobile = true
const isMobileDevice = () => { const isMobileDevice = () => {
const userAgent = navigator.userAgent || navigator.vendor || window.opera; const userAgent = navigator.userAgent || navigator.vendor || window.opera;
@ -1215,7 +1215,9 @@ function App() {
}} /></Box> }} /></Box>
)} )}
<AuthenticatedContainerInnerLeft> <AuthenticatedContainerInnerLeft sx={{
overflowY: isMobile && 'auto'
}}>
<Spacer height="48px" /> <Spacer height="48px" />
{authenticatedMode === "ltc" ? ( {authenticatedMode === "ltc" ? (

View File

@ -27,6 +27,7 @@ import { RequestQueueWithPromise } from "./utils/queue/queue";
import { validateAddress } from "./utils/validateAddress"; import { validateAddress } from "./utils/validateAddress";
import { Sha256 } from "asmcrypto.js"; import { Sha256 } from "asmcrypto.js";
import { TradeBotRespondMultipleRequest } from "./transactions/TradeBotRespondMultipleRequest"; import { TradeBotRespondMultipleRequest } from "./transactions/TradeBotRespondMultipleRequest";
import { RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS } from "./constants/resourceTypes";
let lastGroupNotification; let lastGroupNotification;
export const groupApi = "https://ext-node.qortal.link"; export const groupApi = "https://ext-node.qortal.link";
@ -225,6 +226,27 @@ export function isExtMsg(data) {
return isMsgFromExtensionGroup; 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() { async function checkWebviewFocus() {
return new Promise((resolve) => { return new Promise((resolve) => {
// Set a timeout for 1 second // Set a timeout for 1 second
@ -478,7 +500,7 @@ const handleNotification = async (groups) => {
if(!isArray(mutedGroups)) mutedGroups = [] if(!isArray(mutedGroups)) mutedGroups = []
let isFocused; 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 { try {
if(isDisableNotifications) return if(isDisableNotifications) return
if (!data || data?.length === 0) return; if (!data || data?.length === 0) return;
@ -3924,9 +3946,9 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => {
break; break;
} }
case "encryptSingle": { 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) => { .then((res) => {
sendResponse(res); sendResponse(res);
}) })

View File

@ -19,6 +19,7 @@ import { Box, ButtonBase } from '@mui/material'
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from "short-unique-id";
import { ReplyPreview } from './MessageItem' import { ReplyPreview } from './MessageItem'
import { ExitIcon } from '../../assets/Icons/ExitIcon' import { ExitIcon } from '../../assets/Icons/ExitIcon'
import { RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS } from '../../constants/resourceTypes'
const uid = new ShortUniqueId({ length: 5 }); const uid = new ShortUniqueId({ length: 5 });
@ -123,6 +124,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
if(hasInitialized.current){ if(hasInitialized.current){
const formatted = response.filter((rawItem)=> !rawItem?.chatReference).map((item: any)=> { const formatted = response.filter((rawItem)=> !rawItem?.chatReference).map((item: any)=> {
return { return {
...item, ...item,
id: item.signature, id: item.signature,
@ -376,12 +378,13 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
}, [messages]) }, [messages])
const encryptChatMessage = async (data: string, secretKeyObject: any)=> { const encryptChatMessage = async (data: string, secretKeyObject: any, reactiontypeNumber?: number)=> {
try { try {
return new Promise((res, rej)=> { return new Promise((res, rej)=> {
chrome?.runtime?.sendMessage({ action: "encryptSingle", payload: { chrome?.runtime?.sendMessage({ action: "encryptSingle", payload: {
data, data,
secretKeyObject secretKeyObject,
typeNumber: reactiontypeNumber
}}, (response) => { }}, (response) => {
if (!response?.error) { if (!response?.error) {
@ -535,8 +538,8 @@ const clearEditorContent = () => {
...(otherData || {}) ...(otherData || {})
} }
const message64: any = await objectToBase64(objectMessage) const message64: any = await objectToBase64(objectMessage)
const reactiontypeNumber = RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS
const encryptSingle = await encryptChatMessage(message64, secretKeyObject) const encryptSingle = await encryptChatMessage(message64, secretKeyObject, reactiontypeNumber)
// const res = await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle}) // const res = await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle})
const sendMessageFunc = async () => { const sendMessageFunc = async () => {

View File

@ -155,6 +155,10 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
const handleAtBottomStateChange = (atBottom) => { const handleAtBottomStateChange = (atBottom) => {
isAtBottomRef.current = atBottom; isAtBottomRef.current = atBottom;
if(atBottom){
handleMessageSeen();
setShowScrollButton(false)
}
}; };
return ( return (
@ -165,7 +169,6 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
itemContent={rowRenderer} itemContent={rowRenderer}
atBottomThreshold={50} atBottomThreshold={50}
followOutput="smooth" followOutput="smooth"
onScroll={handleScroll}
atBottomStateChange={handleAtBottomStateChange} // Detect bottom status atBottomStateChange={handleAtBottomStateChange} // Detect bottom status
increaseViewportBy={3000} increaseViewportBy={3000}

View File

@ -76,7 +76,7 @@ import { WebSocketActive } from "./WebsocketActive";
import { flushSync } from "react-dom"; import { flushSync } from "react-dom";
import { useMessageQueue } from "../../MessageQueueContext"; import { useMessageQueue } from "../../MessageQueueContext";
import { DrawerComponent } from "../Drawer/Drawer"; import { DrawerComponent } from "../Drawer/Drawer";
import { isExtMsg } from "../../background"; import { isExtMsg, isUpdateMsg } from "../../background";
import { ContextMenu } from "../ContextMenu"; import { ContextMenu } from "../ContextMenu";
import { MobileFooter } from "../Mobile/MobileFooter"; import { MobileFooter } from "../Mobile/MobileFooter";
import Header from "../Mobile/MobileHeader"; import Header from "../Mobile/MobileHeader";
@ -420,6 +420,7 @@ export const Group = ({
const [mobileViewModeKeepOpen, setMobileViewModeKeepOpen] = useState(""); const [mobileViewModeKeepOpen, setMobileViewModeKeepOpen] = useState("");
const [desktopSideView, setDesktopSideView] = useState('groups') const [desktopSideView, setDesktopSideView] = useState('groups')
const isFocusedRef = useRef(true); const isFocusedRef = useRef(true);
const timestampEnterDataRef = useRef({});
const selectedGroupRef = useRef(null); const selectedGroupRef = useRef(null);
const selectedDirectRef = useRef(null); const selectedDirectRef = useRef(null);
const groupSectionRef = useRef(null); const groupSectionRef = useRef(null);
@ -429,9 +430,11 @@ export const Group = ({
const settimeoutForRefetchSecretKey = useRef(null); const settimeoutForRefetchSecretKey = useRef(null);
const { clearStatesMessageQueueProvider } = useMessageQueue(); const { clearStatesMessageQueueProvider } = useMessageQueue();
const initiatedGetMembers = useRef(false); const initiatedGetMembers = useRef(false);
// useEffect(()=> { const [groupChatTimestamps, setGroupChatTimestamps] = React.useState({});
// setFullHeight()
// }, []) useEffect(()=> {
timestampEnterDataRef.current = timestampEnterData
}, [timestampEnterData])
useEffect(() => { useEffect(() => {
isFocusedRef.current = isFocused; isFocusedRef.current = isFocused;
@ -616,12 +619,14 @@ export const Group = ({
const groupChatHasUnread = useMemo(() => { const groupChatHasUnread = useMemo(() => {
let hasUnread = false; let hasUnread = false;
groups.forEach((group) => { groups.forEach((group) => {
console.log('isUpdateMsg(group?.data)', isUpdateMsg(group?.data))
if ( if (
group?.data && group?.data &&
isExtMsg(group?.data) && isExtMsg(group?.data) &&
group?.sender !== myAddress && group?.sender !== myAddress &&
group?.timestamp && group?.timestamp && (!isUpdateMsg(group?.data) || groupChatTimestamps[group?.groupId]) &&
((!timestampEnterData[group?.groupId] && !group?.chatReference && ((!timestampEnterData[group?.groupId] &&
Date.now() - group?.timestamp < timeDifferenceForNotificationChats) || Date.now() - group?.timestamp < timeDifferenceForNotificationChats) ||
timestampEnterData[group?.groupId] < group?.timestamp) timestampEnterData[group?.groupId] < group?.timestamp)
) { ) {
@ -918,13 +923,52 @@ export const Group = ({
} catch (error) {} } 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(() => { useEffect(() => {
// Listen for messages from the background script // Listen for messages from the background script
chrome?.runtime?.onMessage.addListener((message, sender, sendResponse) => { chrome?.runtime?.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === "SET_GROUPS") { if (message.action === "SET_GROUPS") {
// Update the component state with the received 'sendqort' state // Update the component state with the received 'sendqort' state
setGroups(message.payload); setGroups(message.payload);
getLatestRegularChat(message.payload)
setMemberGroups(message.payload); setMemberGroups(message.payload);
if (selectedGroupRef.current && groupSectionRef.current === "chat") { if (selectedGroupRef.current && groupSectionRef.current === "chat") {
@ -1102,13 +1146,13 @@ export const Group = ({
if (!findGroup) return false; if (!findGroup) return false;
if (!findGroup?.data || !isExtMsg(findGroup?.data)) return false; if (!findGroup?.data || !isExtMsg(findGroup?.data)) return false;
return ( return (
findGroup?.timestamp && !findGroup?.chatReference && findGroup?.timestamp && (!isUpdateMsg(findGroup?.data) || groupChatTimestamps[findGroup?.groupId]) &&
((!timestampEnterData[selectedGroup?.groupId] && ((!timestampEnterData[selectedGroup?.groupId] &&
Date.now() - findGroup?.timestamp < Date.now() - findGroup?.timestamp <
timeDifferenceForNotificationChats) || timeDifferenceForNotificationChats) ||
timestampEnterData?.[selectedGroup?.groupId] < findGroup?.timestamp) timestampEnterData?.[selectedGroup?.groupId] < findGroup?.timestamp)
); );
}, [timestampEnterData, selectedGroup]); }, [timestampEnterData, selectedGroup, groupChatTimestamps]);
const isUnread = useMemo(() => { const isUnread = useMemo(() => {
if (!selectedGroup) return false; if (!selectedGroup) return false;
@ -2101,7 +2145,7 @@ export const Group = ({
/> />
)} )}
{group?.data && {group?.data &&
isExtMsg(group?.data) && !group?.chatReference && isExtMsg(group?.data) && (!isUpdateMsg(group?.data) || groupChatTimestamps[group?.groupId]) &&
group?.sender !== myAddress && group?.sender !== myAddress &&
group?.timestamp && group?.timestamp &&
((!timestampEnterData[group?.groupId] && ((!timestampEnterData[group?.groupId] &&

View File

@ -128,7 +128,7 @@ export const GroupMenu = ({ setGroupSection, groupSection, setOpenManageMembers,
minWidth: '24px !important' minWidth: '24px !important'
}}> }}>
<ChatIcon sx={{ color: hasUnreadChat ? 'var(--unread)' : "#fff" }} /> <ChatIcon color={hasUnreadChat ? 'var(--unread)' : "#fff"} />
</ListItemIcon> </ListItemIcon>
<ListItemText sx={{ <ListItemText sx={{
"& .MuiTypography-root": { "& .MuiTypography-root": {
@ -147,7 +147,7 @@ export const GroupMenu = ({ setGroupSection, groupSection, setOpenManageMembers,
minWidth: '24px !important' minWidth: '24px !important'
}}> }}>
<NotificationIcon2 sx={{ color: hasUnreadAnnouncements ? 'var(--unread)' : "#fff" }} /> <NotificationIcon2 color={hasUnreadAnnouncements ? 'var(--unread)' : "#fff" } />
</ListItemIcon> </ListItemIcon>
<ListItemText sx={{ <ListItemText sx={{
"& .MuiTypography-root": { "& .MuiTypography-root": {
@ -165,7 +165,7 @@ export const GroupMenu = ({ setGroupSection, groupSection, setOpenManageMembers,
<ListItemIcon sx={{ <ListItemIcon sx={{
minWidth: '24px !important' minWidth: '24px !important'
}}> }}>
<ThreadsIcon sx={{ color: "#fff" }} /> <ThreadsIcon color={"#fff"} />
</ListItemIcon> </ListItemIcon>
<ListItemText sx={{ <ListItemText sx={{

View File

@ -67,8 +67,8 @@ export const ReactionPicker = ({ onReaction }) => {
{showPicker && ( {showPicker && (
<div className="emoji-picker" ref={pickerRef} onClick={(e) => e.preventDefault()}> <div className="emoji-picker" ref={pickerRef} onClick={(e) => e.preventDefault()}>
<Picker <Picker
height={isMobile ? 300 : 450} height={isMobile ? 350 : 450}
width={isMobile ? 250 : 350 } width={isMobile ? 300 : 350 }
reactionsDefaultOpen={true} reactionsDefaultOpen={true}
onReactionClick={handleReaction} onReactionClick={handleReaction}
onEmojiClick={handlePicker} onEmojiClick={handlePicker}

View File

@ -0,0 +1 @@
export const RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS = 102

View File

@ -137,7 +137,7 @@ export const encryptDataGroup = ({ data64, publicKeys, privateKey, userPublicKey
} }
} }
export const encryptSingle = async ({ data64, secretKeyObject }: any) => { export const encryptSingle = async ({ data64, secretKeyObject, typeNumber = 1 }: any) => {
// Find the highest key in the secretKeyObject // Find the highest key in the secretKeyObject
const highestKey = Math.max(...Object.keys(secretKeyObject).filter(item => !isNaN(+item)).map(Number)); const highestKey = Math.max(...Object.keys(secretKeyObject).filter(item => !isNaN(+item)).map(Number));
const highestKeyObject = secretKeyObject[highestKey]; const highestKeyObject = secretKeyObject[highestKey];
@ -152,6 +152,9 @@ export const encryptSingle = async ({ data64, secretKeyObject }: any) => {
let nonce, encryptedData, encryptedDataBase64, finalEncryptedData; 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) { if (highestKeyObject.nonce) {
// Old format: Use the nonce from secretKeyObject // Old format: Use the nonce from secretKeyObject
nonce = base64ToUint8Array(highestKeyObject.nonce); nonce = base64ToUint8Array(highestKeyObject.nonce);
@ -160,15 +163,14 @@ export const encryptSingle = async ({ data64, secretKeyObject }: any) => {
encryptedData = nacl.secretbox(Uint8ArrayData, nonce, messageKey); encryptedData = nacl.secretbox(Uint8ArrayData, nonce, messageKey);
encryptedDataBase64 = uint8ArrayToBase64(encryptedData); 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 const highestKeyStr = highestKey.toString().padStart(10, '0'); // Fixed length of 10 digits
finalEncryptedData = btoa(highestKeyStr + encryptedDataBase64); finalEncryptedData = btoa(highestKeyStr + encryptedDataBase64);
} else { } else {
// New format: Generate a random nonce and embed it in the message // 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); crypto.getRandomValues(nonce);
// Encrypt the data with the new nonce and message key // Encrypt the data with the new nonce and message key
encryptedData = nacl.secretbox(Uint8ArrayData, nonce, messageKey); encryptedData = nacl.secretbox(Uint8ArrayData, nonce, messageKey);
encryptedDataBase64 = uint8ArrayToBase64(encryptedData); encryptedDataBase64 = uint8ArrayToBase64(encryptedData);
@ -176,23 +178,24 @@ export const encryptSingle = async ({ data64, secretKeyObject }: any) => {
// Convert the nonce to base64 // Convert the nonce to base64
const nonceBase64 = uint8ArrayToBase64(nonce); 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 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; 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) // First, decode the base64-encoded input (if skipDecodeBase64 is not set)
const decodedData = skipDecodeBase64 ? data64 : atob(data64); const decodedData = skipDecodeBase64 ? data64 : atob(data64);
// Then, decode it again for the specific format (if double encoding is used) // Then, decode it again for the specific format (if double encoding is used)
const decodeForNumber = atob(decodedData); 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); const keyStr = decodeForNumber.slice(0, 10);
// Convert the key string back to a number // Convert the key string back to a number
@ -205,18 +208,27 @@ export const decryptSingle = async ({ data64, secretKeyObject, skipDecodeBase64
const secretKeyEntry = secretKeyObject[highestKey]; 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) { 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; nonceBase64 = secretKeyEntry.nonce;
encryptedDataBase64 = decodeForNumber.slice(10); // The remaining part is the encrypted data encryptedDataBase64 = decodeForNumber.slice(10); // The remaining part is the encrypted data
} else { } else {
// New format: nonce is included in the message (first 32 characters) if (hasTypeNumber) {
nonceBase64 = decodeForNumber.slice(10, 42); // First 32 characters for the nonce // New format: Extract type number and nonce
encryptedDataBase64 = decodeForNumber.slice(42); // The remaining part is the encrypted data 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 // Convert Base64 strings to Uint8Array
@ -243,6 +255,8 @@ export const decryptSingle = async ({ data64, secretKeyObject, skipDecodeBase64
export function decryptGroupData(data64EncryptedData: string, privateKey: string) { export function decryptGroupData(data64EncryptedData: string, privateKey: string) {
const allCombined = base64ToUint8Array(data64EncryptedData) const allCombined = base64ToUint8Array(data64EncryptedData)