diff --git a/electron/src/index.ts b/electron/src/index.ts index 338f553..9b729b8 100644 --- a/electron/src/index.ts +++ b/electron/src/index.ts @@ -37,6 +37,14 @@ if (electronIsDev) { setupReloadWatcher(myCapacitorApp); } +const checkForUpdates = async () => { + try { + await autoUpdater.checkForUpdatesAndNotify(); + } catch (error) { + console.error("Error checking for updates:", error); + } +}; + // Run Application (async () => { // Wait for electron app to be ready. @@ -46,10 +54,13 @@ if (electronIsDev) { // Initialize our app, build windows, and load content. await myCapacitorApp.init(); // Check for updates if we are in a packaged app. - autoUpdater.checkForUpdatesAndNotify(); - setInterval(() => { - autoUpdater.checkForUpdatesAndNotify(); - }, 6 * 60 * 60 * 1000); // 24 hours in milliseconds + + checkForUpdates(); + + // Set up periodic update checks + + setInterval(checkForUpdates, 24 * 60 * 60 * 1000); // 24 hours + })(); // Handle when all of our windows are close (platforms have their own expectations). diff --git a/package-lock.json b/package-lock.json index b950a34..adb5729 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "@tiptap/extension-color": "^2.5.9", "@tiptap/extension-highlight": "^2.6.6", "@tiptap/extension-image": "^2.6.6", + "@tiptap/extension-mention": "^2.9.1", "@tiptap/extension-placeholder": "^2.6.2", "@tiptap/extension-text-style": "^2.5.9", "@tiptap/extension-underline": "^2.6.6", @@ -53,6 +54,7 @@ "electron-builder": "^25.1.8", "emoji-picker-react": "^4.12.0", "file-saver": "^2.0.5", + "html-to-text": "^9.0.5", "jssha": "3.3.1", "lodash": "^4.17.21", "mime": "^4.0.4", @@ -76,6 +78,7 @@ "short-unique-id": "^5.2.0", "slate": "^0.103.0", "slate-react": "^0.109.0", + "tippy.js": "^6.3.7", "tiptap-extension-resize-image": "^1.1.8" }, "devDependencies": { @@ -3007,9 +3010,9 @@ } }, "node_modules/@remirror/core-constants": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-2.0.2.tgz", - "integrity": "sha512-dyHY+sMF0ihPus3O27ODd4+agdHMEmuRdyiZJ2CCWjPV5UFmn17ZbElvk6WOGVE4rdCJKZQCrPV2BcikOMLUGQ==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz", + "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==" }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.13.0", @@ -3180,6 +3183,18 @@ "win32" ] }, + "node_modules/@selderee/plugin-htmlparser2": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", + "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", + "dependencies": { + "domhandler": "^5.0.3", + "selderee": "^0.11.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -3515,15 +3530,15 @@ } }, "node_modules/@tiptap/core": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.6.6.tgz", - "integrity": "sha512-VO5qTsjt6rwworkuo0s5AqYMfDA0ZwiTiH6FHKFSu2G/6sS7HKcc/LjPq+5Legzps4QYdBDl3W28wGsGuS1GdQ==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.9.1.tgz", + "integrity": "sha512-tifnLL/ARzQ6/FGEJjVwj9UT3v+pENdWHdk9x6F3X0mB1y0SeCjV21wpFLYESzwNdBPAj8NMp8Behv7dBnhIfw==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/pm": "^2.6.6" + "@tiptap/pm": "^2.7.0" } }, "node_modules/@tiptap/extension-blockquote": { @@ -3768,6 +3783,20 @@ "@tiptap/core": "^2.5.9" } }, + "node_modules/@tiptap/extension-mention": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-mention/-/extension-mention-2.9.1.tgz", + "integrity": "sha512-2IzunpivdNtDNdtAXwRiQbNhTm87zrbkhz1cCE+2y9pWiX1QLXyx0HQq/DIAjxp6v7y4sIh+5UTUTFlH7vD9wQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0", + "@tiptap/suggestion": "^2.7.0" + } + }, "node_modules/@tiptap/extension-ordered-list": { "version": "2.5.9", "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.5.9.tgz", @@ -3854,13 +3883,13 @@ } }, "node_modules/@tiptap/pm": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.6.6.tgz", - "integrity": "sha512-56FGLPn3fwwUlIbLs+BO21bYfyqP9fKyZQbQyY0zWwA/AG2kOwoXaRn7FOVbjP6CylyWpFJnpRRmgn694QKHEg==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.9.1.tgz", + "integrity": "sha512-mvV86fr7kEuDYEApQ2uMPCKL2uagUE0BsXiyyz3KOkY1zifyVm1fzdkscb24Qy1GmLzWAIIihA+3UHNRgYdOlQ==", "dependencies": { "prosemirror-changeset": "^2.2.1", "prosemirror-collab": "^1.3.1", - "prosemirror-commands": "^1.5.2", + "prosemirror-commands": "^1.6.0", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", @@ -3868,14 +3897,14 @@ "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.0", "prosemirror-menu": "^1.2.4", - "prosemirror-model": "^1.22.2", + "prosemirror-model": "^1.22.3", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.4.0", - "prosemirror-trailing-node": "^2.0.9", - "prosemirror-transform": "^1.9.0", - "prosemirror-view": "^1.33.9" + "prosemirror-trailing-node": "^3.0.0", + "prosemirror-transform": "^1.10.0", + "prosemirror-view": "^1.34.3" }, "funding": { "type": "github", @@ -3933,6 +3962,20 @@ "url": "https://github.com/sponsors/ueberdosis" } }, + "node_modules/@tiptap/suggestion": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-2.9.1.tgz", + "integrity": "sha512-MMxwpbtocxUsbmc8qtFY1AQYNTW5i/M4aNSv9zsKKRISaS5hMD7XVrw2eod0x0yEqZU3izLiPDZPmgr8glF+jQ==", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -6588,6 +6631,14 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -6855,11 +6906,62 @@ "csstype": "^3.0.2" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, "node_modules/dompurify": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz", "integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==" }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dotenv": { "version": "16.4.5", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", @@ -8710,6 +8812,39 @@ "node": ">=18" } }, + "node_modules/html-to-text": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", + "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==", + "dependencies": { + "@selderee/plugin-htmlparser2": "^0.11.0", + "deepmerge": "^4.3.1", + "dom-serializer": "^2.0.0", + "htmlparser2": "^8.0.2", + "selderee": "^0.11.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -9653,6 +9788,14 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/leac": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", + "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -13346,6 +13489,18 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parseley": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", + "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", + "dependencies": { + "leac": "^0.6.0", + "peberminta": "^0.9.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -13434,6 +13589,14 @@ "url": "https://github.com/sponsors/jet2jet" } }, + "node_modules/peberminta": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", + "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -13810,11 +13973,11 @@ } }, "node_modules/prosemirror-trailing-node": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-2.0.9.tgz", - "integrity": "sha512-YvyIn3/UaLFlFKrlJB6cObvUhmwFNZVhy1Q8OpW/avoTbD/Y7H5EcjK4AZFKhmuS6/N6WkGgt7gWtBWDnmFvHg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz", + "integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==", "dependencies": { - "@remirror/core-constants": "^2.0.2", + "@remirror/core-constants": "3.0.0", "escape-string-regexp": "^4.0.0" }, "peerDependencies": { @@ -13835,17 +13998,17 @@ } }, "node_modules/prosemirror-transform": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.9.0.tgz", - "integrity": "sha512-5UXkr1LIRx3jmpXXNKDhv8OyAOeLTGuXNwdVfg8x27uASna/wQkr9p6fD3eupGOi4PLJfbezxTyi/7fSJypXHg==", + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.2.tgz", + "integrity": "sha512-2iUq0wv2iRoJO/zj5mv8uDUriOHWzXRnOTVgCzSXnktS/2iQRa3UUQwVlkBlYZFtygw6Nh1+X4mGqoYBINn5KQ==", "dependencies": { "prosemirror-model": "^1.21.0" } }, "node_modules/prosemirror-view": { - "version": "1.33.9", - "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.33.9.tgz", - "integrity": "sha512-xV1A0Vz9cIcEnwmMhKKFAOkfIp8XmJRnaZoPqNXrPS7EK5n11Ov8V76KhR0RsfQd/SIzmWY+bg+M44A2Lx/Nnw==", + "version": "1.36.0", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.36.0.tgz", + "integrity": "sha512-U0GQd5yFvV5qUtT41X1zCQfbw14vkbbKwLlQXhdylEmgpYVHkefXYcC4HHwWOfZa3x6Y8wxDLUBv7dxN5XQ3nA==", "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", @@ -14863,6 +15026,17 @@ "compute-scroll-into-view": "^3.0.2" } }, + "node_modules/selderee": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", + "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", + "dependencies": { + "parseley": "^0.12.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/semver": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", diff --git a/package.json b/package.json index 0abcf79..4fac9b7 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@tiptap/extension-color": "^2.5.9", "@tiptap/extension-highlight": "^2.6.6", "@tiptap/extension-image": "^2.6.6", + "@tiptap/extension-mention": "^2.9.1", "@tiptap/extension-placeholder": "^2.6.2", "@tiptap/extension-text-style": "^2.5.9", "@tiptap/extension-underline": "^2.6.6", @@ -57,6 +58,7 @@ "electron-builder": "^25.1.8", "emoji-picker-react": "^4.12.0", "file-saver": "^2.0.5", + "html-to-text": "^9.0.5", "jssha": "3.3.1", "lodash": "^4.17.21", "mime": "^4.0.4", @@ -80,6 +82,7 @@ "short-unique-id": "^5.2.0", "slate": "^0.103.0", "slate-react": "^0.109.0", + "tippy.js": "^6.3.7", "tiptap-extension-resize-image": "^1.1.8" }, "devDependencies": { diff --git a/src/App.tsx b/src/App.tsx index b084c62..5d731ea 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -575,6 +575,7 @@ function App() { }; }; + const getBalanceFunc = () => { setQortBalanceLoading(true); window @@ -2173,6 +2174,7 @@ function App() { onClick={() => { setRawWallet(null); setExtstate("not-authenticated"); + logoutFunc() }} src={Return} /> diff --git a/src/MessageQueueContext.tsx b/src/MessageQueueContext.tsx index b1b569f..3d46e50 100644 --- a/src/MessageQueueContext.tsx +++ b/src/MessageQueueContext.tsx @@ -6,15 +6,15 @@ const MessageQueueContext = createContext(null); export const useMessageQueue = () => useContext(MessageQueueContext); const uid = new ShortUniqueId({ length: 8 }); -let messageQueue = []; // Global message queue export const MessageQueueProvider = ({ children }) => { + const messageQueueRef = useRef([]); const [queueChats, setQueueChats] = useState({}); // Stores chats and status for display const isProcessingRef = useRef(false); // To track if the queue is being processed const maxRetries = 2; const clearStatesMessageQueueProvider = useCallback(() => { setQueueChats({}); - messageQueue = []; + messageQueueRef.current = []; isProcessingRef.current = false; }, []); @@ -36,9 +36,9 @@ export const MessageQueueProvider = ({ children }) => { [groupDirectId]: [...(prev[groupDirectId] || []), chatData] })); - // Add the message to the global messageQueue - messageQueue = [ - ...messageQueue, + // Add the message to the global messageQueueRef.current + messageQueueRef.current = [ + ...messageQueueRef.current, { func: sendMessageFunc, identifier: tempId, groupDirectId, specialId: messageObj?.message?.specialId } ]; @@ -51,10 +51,10 @@ export const MessageQueueProvider = ({ children }) => { processQueue(newMessages, groupDirectId); }; - // Function to process the messageQueue and handle new messages + // Function to process the messageQueueRef.current and handle new messages const processQueue = useCallback(async (newMessages = [], groupDirectId) => { // Filter out any message in the queue that matches the specialId from newMessages - messageQueue = messageQueue.filter((msg) => { + messageQueueRef.current = messageQueueRef.current.filter((msg) => { return !newMessages.some(newMsg => newMsg?.specialId === msg?.specialId); }); @@ -81,12 +81,12 @@ export const MessageQueueProvider = ({ children }) => { }); // If currently processing or the queue is empty, return - if (isProcessingRef.current || messageQueue.length === 0) return; + if (isProcessingRef.current || messageQueueRef.current.length === 0) return; isProcessingRef.current = true; // Lock the queue for processing - while (messageQueue.length > 0) { - const currentMessage = messageQueue[0]; // Get the first message in the queue + while (messageQueueRef.current.length > 0) { + const currentMessage = messageQueueRef.current[0]; // Get the first message in the queue const { groupDirectId, identifier } = currentMessage; // Update the chat status to 'sending' @@ -104,10 +104,10 @@ export const MessageQueueProvider = ({ children }) => { }); try { - // Execute the function stored in the messageQueue + // Execute the function stored in the messageQueueRef.current await currentMessage.func(); - // Remove the message from the messageQueue after successful sending - messageQueue = messageQueue.slice(1); // Slice here remains for successful messages + // Remove the message from the messageQueueRef.current after successful sending + messageQueueRef.current.shift(); // Slice here remains for successful messages // Remove the message from queueChats after success // setQueueChats((prev) => { @@ -136,8 +136,8 @@ export const MessageQueueProvider = ({ children }) => { // Max retries reached, set status to 'failed-permanent' updatedChats[groupDirectId][chatIndex].status = 'failed-permanent'; - // Remove the message from the messageQueue after max retries - messageQueue = messageQueue.slice(1); // Slice for failed messages after max retries + // Remove the message from the messageQueueRef.current after max retries + messageQueueRef.current.shift();// Slice for failed messages after max retries // // Remove the message from queueChats after failure // updatedChats[groupDirectId] = updatedChats[groupDirectId].filter( @@ -147,7 +147,7 @@ export const MessageQueueProvider = ({ children }) => { } return updatedChats; }); - } + } // Delay between processing each message to avoid overlap await new Promise((res) => setTimeout(res, 5000)); diff --git a/src/background-cases.ts b/src/background-cases.ts index e64135c..2500434 100644 --- a/src/background-cases.ts +++ b/src/background-cases.ts @@ -3,6 +3,7 @@ import { addEnteredQmailTimestamp, addTimestampEnterChat, addTimestampGroupAnnouncement, + addTimestampMention, addUserSettings, banFromGroup, cancelBan, @@ -29,6 +30,7 @@ import { getTempPublish, getTimestampEnterChat, getTimestampGroupAnnouncement, + getTimestampMention, getUserInfo, getUserSettings, handleActiveGroupDataFromSocket, @@ -1217,6 +1219,59 @@ export async function getTimestampEnterChatCase(request, event) { } } +export async function getTimestampMentionCase(request, event) { + try { + const response = await getTimestampMention(); + + event.source.postMessage( + { + requestId: request.requestId, + action: "getTimestampMention", + payload: response, + type: "backgroundMessageResponse", + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: "getTimestampMention", + error: error?.message, + type: "backgroundMessageResponse", + }, + event.origin + ); + } +} + +export async function addTimestampMentionCase(request, event) { + try { + const { groupId, timestamp } = request.payload; + const response = await addTimestampMention({ groupId, timestamp }); + + event.source.postMessage( + { + requestId: request.requestId, + action: "addTimestampMention", + payload: response, + type: "backgroundMessageResponse", + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: "addTimestampMention", + error: error?.message, + type: "backgroundMessageResponse", + }, + event.origin + ); + } +} + export async function getGroupNotificationTimestampCase(request, event) { try { const response = await getTimestampGroupAnnouncement(); diff --git a/src/background-old.ts b/src/background-old.ts index 414a382..930fa2c 100644 --- a/src/background-old.ts +++ b/src/background-old.ts @@ -2827,7 +2827,6 @@ async function getChatHeadsDirect() { chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { if (request) { - console.log('REQUEST MESSAGE', request) switch (request.action) { case "version": diff --git a/src/background.ts b/src/background.ts index 392ff82..bbea94e 100644 --- a/src/background.ts +++ b/src/background.ts @@ -35,6 +35,7 @@ import { addEnteredQmailTimestampCase, addGroupNotificationTimestampCase, addTimestampEnterChatCase, + addTimestampMentionCase, addUserSettingsCase, balanceCase, banFromGroupCase, @@ -59,6 +60,7 @@ import { getTempPublishCase, getThreadActivityCase, getTimestampEnterChatCase, + getTimestampMentionCase, getUserSettingsCase, getWalletInfoCase, handleActiveGroupDataFromSocketCase, @@ -2543,6 +2545,18 @@ export async function getTimestampEnterChat() { return {}; } } +export async function getTimestampMention() { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const key = `enter-mention-timestamp-${address}`; + const res = await getData(key).catch(() => null); + if (res) { + const parsedData = res; + return parsedData; + } else { + return {}; + } +} export async function getTimestampGroupAnnouncement() { const wallet = await getSaveWallet(); const address = wallet.address0; @@ -2666,6 +2680,21 @@ export async function addTimestampEnterChat({ groupId, timestamp }) { }); } +export async function addTimestampMention({ groupId, timestamp }) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const data = await getTimestampMention(); + data[groupId] = timestamp; + return await new Promise((resolve, reject) => { + storeData(`enter-mention-timestamp-${address}`, data) + .then(() => resolve(true)) + .catch((error) => { + reject(new Error(error.message || "Error saving data")); + }); + }); +} + + export async function notifyAdminRegenerateSecretKey({ groupName, adminAddress, @@ -2841,6 +2870,12 @@ function setupMessageListener() { case "getTimestampEnterChat": getTimestampEnterChatCase(request, event); break; + case "addTimestampMention": + addTimestampMentionCase(request, event); + break; + case "getTimestampMention": + getTimestampMentionCase(request, event); + break; case "getGroupNotificationTimestamp": getGroupNotificationTimestampCase(request, event); break; diff --git a/src/components/Chat/ChatGroup.tsx b/src/components/Chat/ChatGroup.tsx index 92c327c..d6c583b 100644 --- a/src/components/Chat/ChatGroup.tsx +++ b/src/components/Chat/ChatGroup.tsx @@ -24,7 +24,7 @@ import { isExtMsg } from '../../background' const uid = new ShortUniqueId({ length: 5 }); -export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, myAddress, handleNewEncryptionNotification, hide, handleSecretKeyCreationInProgress, triedToFetchSecretKey, myName, balance}) => { +export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, myAddress, handleNewEncryptionNotification, hide, handleSecretKeyCreationInProgress, triedToFetchSecretKey, myName, balance, getTimestampEnterChatParent}) => { const [messages, setMessages] = useState([]) const [chatReferences, setChatReferences] = useState({}) const [isSending, setIsSending] = useState(false) @@ -43,6 +43,61 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, const editorRef = useRef(null); const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue(); const [, forceUpdate] = useReducer((x) => x + 1, 0); + const lastReadTimestamp = useRef(null) + + + + const getTimestampEnterChat = async () => { + try { + return new Promise((res, rej) => { + window.sendMessage("getTimestampEnterChat") + .then((response) => { + if (!response?.error) { + if(response && selectedGroup && response[selectedGroup]){ + lastReadTimestamp.current = response[selectedGroup] + window.sendMessage("addTimestampEnterChat", { + timestamp: Date.now(), + groupId: selectedGroup + }).catch((error) => { + console.error("Failed to add timestamp:", error.message || "An error occurred"); + }); + + + setTimeout(() => { + getTimestampEnterChatParent(); + }, 200); + } + + res(response); + return; + } + rej(response.error); + }) + .catch((error) => { + rej(error.message || "An error occurred"); + }); + + }); + } catch (error) {} + }; + + useEffect(()=> { + getTimestampEnterChat() + }, []) + + + + const members = useMemo(() => { + const uniqueMembers = new Set(); + + messages.forEach((message) => { + if (message?.senderName) { + uniqueMembers.add(message?.senderName); + } + }); + + return Array.from(uniqueMembers); + }, [messages]); const triggerRerender = () => { forceUpdate(); // Trigger re-render by updating the state @@ -145,16 +200,20 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, res(combineUIAndExtensionMsgs); if (isInitiated) { + const formatted = combineUIAndExtensionMsgs .filter((rawItem) => !rawItem?.chatReference) - .map((item) => ({ - ...item, - id: item.signature, - text: item?.decryptedData?.message || "", - repliedTo: item?.repliedTo || item?.decryptedData?.repliedTo, - unread: item?.sender === myAddress ? false : !!item?.chatReference ? false : true, - isNotEncrypted: !!item?.messageText, - })); + .map((item) => { + + return { + ...item, + id: item.signature, + text: item?.decryptedData?.message || "", + repliedTo: item?.repliedTo || item?.decryptedData?.repliedTo, + unread: item?.sender === myAddress ? false : !!item?.chatReference ? false : true, + isNotEncrypted: !!item?.messageText, + } + }); setMessages((prev) => [...prev, ...formatted]); setChatReferences((prev) => { @@ -211,16 +270,25 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, return organizedChatReferences; }); } else { + let firstUnreadFound = false; const formatted = combineUIAndExtensionMsgs .filter((rawItem) => !rawItem?.chatReference) - .map((item) => ({ - ...item, - id: item.signature, - text: item?.decryptedData?.message || "", - repliedTo: item?.repliedTo || item?.decryptedData?.repliedTo, - isNotEncrypted: !!item?.messageText, - unread: false, - })); + .map((item) => { + const divide = lastReadTimestamp.current && !firstUnreadFound && item.timestamp > lastReadTimestamp.current && myAddress !== item?.sender; + + if(divide){ + firstUnreadFound = true + } + return { + ...item, + id: item.signature, + text: item?.decryptedData?.message || "", + repliedTo: item?.repliedTo || item?.decryptedData?.repliedTo, + isNotEncrypted: !!item?.messageText, + unread: false, + divide + } + }); setMessages(formatted); setChatReferences((prev) => { @@ -620,9 +688,9 @@ const clearEditorContent = () => { position: hide ? 'absolute' : 'relative', left: hide && '-100000px', }}> - - - + + +
{ )} - +
{ +export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onReply, handleReaction, chatReferences, tempChatReferences, members, myName, selectedGroup, enableMentions }) => { const parentRef = useRef(); const [messages, setMessages] = useState(initialMessages); const [showScrollButton, setShowScrollButton] = useState(false); + const [showScrollDownButton, setShowScrollDownButton] = useState(false); + const hasLoadedInitialRef = useRef(false); const isAtBottomRef = useRef(true); @@ -32,7 +36,7 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR setMessages(totalMessages); setTimeout(() => { - const hasUnreadMessages = totalMessages.some((msg) => msg.unread && !msg?.chatReference); + const hasUnreadMessages = totalMessages.some((msg) => msg.unread && !msg?.chatReference && !msg?.isTemp); if (parentRef.current) { const { scrollTop, scrollHeight, clientHeight } = parentRef.current; const atBottom = scrollTop + clientHeight >= scrollHeight - 10; // Adjust threshold as needed @@ -43,21 +47,30 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR } } if (!hasLoadedInitialRef.current) { - scrollToBottom(totalMessages); + const findDivideIndex = totalMessages.findIndex((item)=> !!item?.divide) + const divideIndex = findDivideIndex !== -1 ? findDivideIndex : undefined + scrollToBottom(totalMessages, divideIndex); hasLoadedInitialRef.current = true; } }, 500); }, [initialMessages, tempMessages]); - const scrollToBottom = (initialMsgs) => { + const scrollToBottom = (initialMsgs, divideIndex) => { const index = initialMsgs ? initialMsgs.length - 1 : messages.length - 1; if (rowVirtualizer) { + if(divideIndex){ + rowVirtualizer.scrollToIndex(divideIndex, { align: 'start' }) + } else { rowVirtualizer.scrollToIndex(index, { align: 'end' }) + + } } handleMessageSeen() }; + + const handleMessageSeen = useCallback(() => { setMessages((prevMessages) => prevMessages.map((msg) => ({ @@ -98,19 +111,51 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR getScrollElement: () => parentRef.current, estimateSize: () => 80, // Provide an estimated height of items, adjust this as needed overscan: 10, // Number of items to render outside the visible area to improve smoothness + observeElementOffset: (instance, cb) => { + const offsetCheck = () => { + const { scrollHeight, scrollTop, clientHeight } = instance.scrollElement; + const atBottom = scrollHeight - scrollTop - clientHeight <= 300; + if(showScrollButton){ + setShowScrollDownButton(false) + } else + if(atBottom){ + setShowScrollDownButton(false) + + } else { + setShowScrollDownButton(true) + + } + cb(scrollTop); // Pass scroll offset to callback + // setShowScrollToBottom(!atBottom); + }; + + // Initial check and continuous monitoring + offsetCheck(); + instance.scrollElement.addEventListener('scroll', offsetCheck); + return () => instance.scrollElement.removeEventListener('scroll', offsetCheck); + }, }); + const goToMessage = useCallback((idx)=> { + rowVirtualizer.scrollToIndex(idx) + }, []) return ( +
rowVirtualizer.scrollToIndex(idx)} + scrollToItem={goToMessage} handleReaction={handleReaction} reactions={reactions} isUpdating={isUpdating} @@ -214,18 +261,44 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR bottom: 20, position: 'absolute', right: 20, - backgroundColor: '#ff5a5f', + backgroundColor: 'var(--unread)', color: 'white', padding: '10px 20px', borderRadius: '20px', cursor: 'pointer', zIndex: 10, + border: 'none', + outline: 'none' }} > Scroll to Unread Messages )} + {showScrollDownButton && ( + + )}
- + {enableMentions && ( + + + )} + ); }; diff --git a/src/components/Chat/ChatOptions.tsx b/src/components/Chat/ChatOptions.tsx new file mode 100644 index 0000000..4315152 --- /dev/null +++ b/src/components/Chat/ChatOptions.tsx @@ -0,0 +1,693 @@ +import { + Avatar, + Box, + ButtonBase, + InputBase, + MenuItem, + Select, + Typography, +} from "@mui/material"; +import React, { useEffect, useMemo, useRef, useState } from "react"; +import SearchIcon from "@mui/icons-material/Search"; +import { Spacer } from "../../common/Spacer"; +import AlternateEmailIcon from "@mui/icons-material/AlternateEmail"; +import CloseIcon from "@mui/icons-material/Close"; +import { + AppsSearchContainer, + AppsSearchLeft, + AppsSearchRight, +} from "../Apps/Apps-styles"; +import IconSearch from "../../assets/svgs/Search.svg"; +import IconClearInput from "../../assets/svgs/ClearInput.svg"; +import { + AutoSizer, + CellMeasurer, + CellMeasurerCache, + List, +} from "react-virtualized"; +import { getBaseApiReact } from "../../App"; +import { MessageDisplay } from "./MessageDisplay"; +import { useVirtualizer } from "@tanstack/react-virtual"; +import { formatTimestamp } from "../../utils/time"; +import { ContextMenuMentions } from "../ContextMenuMentions"; +import { convert } from 'html-to-text'; + +const extractTextFromHTML = (htmlString = '') => { + return convert(htmlString, { + wordwrap: false, // Disable word wrapping + })?.toLowerCase(); +}; +const cache = new CellMeasurerCache({ + fixedWidth: true, + defaultHeight: 50, +}); + +export const ChatOptions = ({ messages, goToMessage, members, myName, selectedGroup }) => { + const [mode, setMode] = useState("default"); + const [searchValue, setSearchValue] = useState(""); + const [selectedMember, setSelectedMember] = useState(0); + + const parentRef = useRef(); + const parentRefMentions = useRef(); + const [lastMentionTimestamp, setLastMentionTimestamp] = useState(null) + const [debouncedValue, setDebouncedValue] = useState(""); // Debounced value + + const getTimestampMention = async () => { + try { + return new Promise((res, rej) => { + window.sendMessage("getTimestampMention") + .then((response) => { + if (!response?.error) { + if(response && selectedGroup && response[selectedGroup]){ + setLastMentionTimestamp(response[selectedGroup]) + + + + + + } + + res(response); + return; + } + rej(response.error); + }) + .catch((error) => { + rej(error.message || "An error occurred"); + }); + + }); + } catch (error) {} + }; + + useEffect(()=> { + if(mode === 'mentions' && selectedGroup){ + window.sendMessage("addTimestampMention", { + timestamp: Date.now(), + groupId: selectedGroup + }).then((res)=> { + getTimestampMention() + }).catch((error) => { + console.error("Failed to add timestamp:", error.message || "An error occurred"); + }); + } + }, [mode, selectedGroup]) + + useEffect(()=> { + getTimestampMention() + }, []) + + // Debounce logic + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(searchValue); + }, 350); + + // Cleanup timeout if searchValue changes before the timeout completes + return () => { + clearTimeout(handler); + }; + }, [searchValue]); // Runs effect when searchValue changes + + const searchedList = useMemo(() => { + if (!debouncedValue?.trim()) { + if (selectedMember) { + return messages + .filter((message) => message?.senderName === selectedMember) + ?.sort((a, b) => b?.timestamp - a?.timestamp); + } + return []; + } + if (selectedMember) { + return messages + .filter( + (message) => + message?.senderName === selectedMember && + extractTextFromHTML(message?.decryptedData?.message)?.includes( + debouncedValue.toLowerCase() + ) + ) + ?.sort((a, b) => b?.timestamp - a?.timestamp); + } + return messages + .filter((message) => + extractTextFromHTML(message?.decryptedData?.message)?.includes(debouncedValue.toLowerCase()) + ) + ?.sort((a, b) => b?.timestamp - a?.timestamp); + }, [debouncedValue, messages, selectedMember]); + + const mentionList = useMemo(() => { + if(!messages || messages.length === 0 || !myName) return [] + + return messages + .filter((message) => + extractTextFromHTML(message?.decryptedData?.message)?.includes(`@${myName}`) + ) + ?.sort((a, b) => b?.timestamp - a?.timestamp); + }, [messages, myName]); + + const rowVirtualizer = useVirtualizer({ + count: searchedList.length, + getItemKey: React.useCallback( + (index) => searchedList[index].signature, + [searchedList] + ), + getScrollElement: () => parentRef.current, + estimateSize: () => 80, // Provide an estimated height of items, adjust this as needed + overscan: 10, // Number of items to render outside the visible area to improve smoothness + }); + + const rowVirtualizerMentions = useVirtualizer({ + count: mentionList.length, + getItemKey: React.useCallback( + (index) => mentionList[index].signature, + [mentionList] + ), + getScrollElement: () => parentRefMentions.current, + estimateSize: () => 80, // Provide an estimated height of items, adjust this as needed + overscan: 10, // Number of items to render outside the visible area to improve smoothness + }); + + + + if (mode === "mentions") { + return ( + + + { + setMode("default"); + }} + sx={{ + cursor: "pointer", + color: "white", + }} + /> + + + + + + {mentionList?.length === 0 && ( + + No results + + )} + +
+
+
+
+ {rowVirtualizerMentions.getVirtualItems().map((virtualRow) => { + const index = virtualRow.index; + let message = mentionList[index]; + return ( +
+ + + + + {message?.senderName?.charAt(0)} + + + {message?.senderName} + + + + + {formatTimestamp(message.timestamp)} + { + const findMsgIndex = messages.findIndex( + (item) => + item?.signature === message?.signature + ); + if (findMsgIndex !== -1) { + goToMessage(findMsgIndex); + } + }} + > +

" + } + /> +
+
+
+ ); + })} +
+
+
+
+
+
+
+ ); + } + + if (mode === "search") { + return ( + + + { + setMode("default"); + }} + sx={{ + cursor: "pointer", + color: "white", + }} + /> + + + + + + setSearchValue(e.target.value)} + sx={{ ml: 1, flex: 1 }} + placeholder="Search chat text" + inputProps={{ + "aria-label": "Search for apps", + fontSize: "16px", + fontWeight: 400, + }} + /> + + + {searchValue && ( + { + setSearchValue(""); + }} + > + + + )} + + + + + {!!selectedMember && ( + { + setSelectedMember(0); + }} + sx={{ + cursor: "pointer", + color: "white", + }} + /> + )} + + + + {debouncedValue && searchedList?.length === 0 && ( + + No results + + )} + +
+
+
+
+ {rowVirtualizer.getVirtualItems().map((virtualRow) => { + const index = virtualRow.index; + let message = searchedList[index]; + return ( +
+ + + + + {message?.senderName?.charAt(0)} + + + {message?.senderName} + + + + + {formatTimestamp(message.timestamp)} + { + const findMsgIndex = messages.findIndex( + (item) => + item?.signature === message?.signature + ); + if (findMsgIndex !== -1) { + goToMessage(findMsgIndex); + } + }} + > +

" + } + /> +
+
+
+ ); + })} +
+
+
+
+
+
+
+ ); + } + return ( + + + { + setMode("search") + }}> + + + + { + setMode("mentions") + setSearchValue('') + setSelectedMember(0) + }}> + 0 && (!lastMentionTimestamp || lastMentionTimestamp < mentionList[0]?.timestamp) ? 'var(--unread)' : 'white' + }} /> + + + + + ); +}; diff --git a/src/components/Chat/MentionList.tsx b/src/components/Chat/MentionList.tsx new file mode 100644 index 0000000..85f6890 --- /dev/null +++ b/src/components/Chat/MentionList.tsx @@ -0,0 +1,69 @@ +import React, { + forwardRef, useEffect, useImperativeHandle, + useState, + } from 'react' + + export default forwardRef((props, ref) => { + const [selectedIndex, setSelectedIndex] = useState(0) + + const selectItem = index => { + const item = props.items[index] + + if (item) { + props.command(item) + } + } + + const upHandler = () => { + setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length) + } + + const downHandler = () => { + setSelectedIndex((selectedIndex + 1) % props.items.length) + } + + const enterHandler = () => { + selectItem(selectedIndex) + } + + useEffect(() => setSelectedIndex(0), [props.items]) + + useImperativeHandle(ref, () => ({ + onKeyDown: ({ event }) => { + if (event.key === 'ArrowUp') { + upHandler() + return true + } + + if (event.key === 'ArrowDown') { + downHandler() + return true + } + + if (event.key === 'Enter') { + enterHandler() + return true + } + + return false + }, + })) + + return ( +
+ {props.items.length + ? props.items.map((item, index) => ( + + )) + :
No result
+ } +
+ ) + }) + \ No newline at end of file diff --git a/src/components/Chat/MessageItem.tsx b/src/components/Chat/MessageItem.tsx index 03e6c3c..55786ed 100644 --- a/src/components/Chat/MessageItem.tsx +++ b/src/components/Chat/MessageItem.tsx @@ -47,6 +47,12 @@ export const MessageItem = ({ return ( + <> + {message?.divide && ( +
+ Unread messages below +
+ )}
-
{reaction}
{numberOfReactions > 1 && ( +
{reaction}
{numberOfReactions > 1 && ( {' '} {numberOfReactions} @@ -307,6 +315,7 @@ export const MessageItem = ({ > */} {/* {!message.unread && Seen} */}
+ ); }; @@ -355,5 +364,6 @@ export const ReplyPreview = ({message})=> { )} + ) } \ No newline at end of file diff --git a/src/components/Chat/TipTap.tsx b/src/components/Chat/TipTap.tsx index e90e5eb..234d66e 100644 --- a/src/components/Chat/TipTap.tsx +++ b/src/components/Chat/TipTap.tsx @@ -1,5 +1,5 @@ -import React, { useEffect, useRef, useState } from "react"; -import { EditorProvider, useCurrentEditor } from "@tiptap/react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { EditorProvider, useCurrentEditor, useEditor } from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; import { Color } from "@tiptap/extension-color"; import ListItem from "@tiptap/extension-list-item"; @@ -22,10 +22,28 @@ import RedoIcon from "@mui/icons-material/Redo"; import FormatHeadingIcon from "@mui/icons-material/FormatSize"; import DeveloperModeIcon from "@mui/icons-material/DeveloperMode"; import Compressor from "compressorjs"; - +import Mention from '@tiptap/extension-mention'; import ImageResize from "tiptap-extension-resize-image"; // Import the ResizeImage extension import { isMobile } from "../../App"; +import tippy from "tippy.js"; +import "tippy.js/dist/tippy.css"; +import Popover from '@mui/material/Popover'; +import List from '@mui/material/List'; +import ListItemMui from '@mui/material/ListItem'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemText from '@mui/material/ListItemText'; +import { ReactRenderer } from '@tiptap/react' +import MentionList from './MentionList.jsx' +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 = ({ setEditorRef, isChat }) => { const { editor } = useCurrentEditor(); const fileInputRef = useRef(null); @@ -279,8 +297,10 @@ export default ({ isFocusedParent, overrideMobile, customEditorHeight, + membersWithNames, + enableMentions }) => { - const [isFocused, setIsFocused] = useState(false); + const extensionsFiltered = isChat ? extensions.filter((item) => item?.name !== "image") : extensions; @@ -290,6 +310,32 @@ export default ({ setEditorRef(editorInstance); }; + // const users = [ + // { id: 1, label: 'Alice' }, + // { id: 2, label: 'Bob' }, + // { id: 3, label: 'Charlie' }, + // ]; + + + + const users = useMemo(()=> { + return (membersWithNames || [])?.map((item)=> { + return { + id: item, + label: item + } + }) + }, [membersWithNames]) + + + + + + const usersRef = useRef([]); + useEffect(() => { + usersRef.current = users; // Keep users up-to-date + }, [users]); + const handleFocus = () => { if (!isMobile) return; setIsFocusedParent(true); @@ -302,14 +348,89 @@ export default ({ } }; + const additionalExtensions = useMemo(()=> { + if(!enableMentions) return [] + return [ + Mention.configure({ + HTMLAttributes: { + class: 'mention', + }, + suggestion: { + items: ({ query }) => { + if (!query) return usersRef?.current; + return usersRef?.current?.filter((user) => + user.label.toLowerCase().includes(query.toLowerCase()) + ); + }, + render: () => { + let popup; // Reference to the Tippy.js instance + let component; + + return { + onStart: props => { + component = new ReactRenderer(MentionList, { + props, + editor: props.editor, + }) + + if (!props.clientRect) { + return + } + + popup = tippy('body', { + getReferenceClientRect: props.clientRect, + appendTo: () => document.body, + content: component.element, + showOnCreate: true, + interactive: true, + trigger: 'manual', + placement: 'bottom-start', + }) + }, + + onUpdate(props) { + component.updateProps(props) + + if (!props.clientRect) { + return + } + + popup[0].setProps({ + getReferenceClientRect: props.clientRect, + }) + }, + + onKeyDown(props) { + if (props.event.key === 'Escape') { + popup[0].hide() + + return true + } + + return component.ref?.onKeyDown(props) + }, + + onExit() { + popup[0].destroy() + component.destroy() + }, + } + }, + }, + }) + ] + }, [enableMentions]) + return ( +
) } - extensions={extensionsFiltered} + extensions={[...extensionsFiltered, ...additionalExtensions + ]} content={content} onCreate={({ editor }) => { editor.on("focus", handleFocus); // Listen for focus event @@ -348,5 +469,7 @@ export default ({ }, }} /> +
+ ); }; diff --git a/src/components/Chat/styles.css b/src/components/Chat/styles.css index 7c65eb0..fc229fc 100644 --- a/src/components/Chat/styles.css +++ b/src/components/Chat/styles.css @@ -123,3 +123,50 @@ .isReply p { font-size: 12px !important; } + +.tiptap .mention { + box-decoration-break: clone; + color: lightblue; + padding: 0.1rem 0.3rem; +} + + +.unread-divider { + width: 90%; + color: white; + border-bottom: 1px solid white; + display: flex; + justify-content: center; + border-radius: 2px; +} + +.mention-item { + cursor: pointer; +} + +.dropdown-menu { + display: flex; + flex-direction: column; + gap: 0.1rem; + padding: 0.4rem; + position: relative; + max-height: 200px; + overflow: auto; + + button { + align-items: center; + background-color: transparent; + display: flex; + gap: 0.25rem; + text-align: left; + font-size: 16px; + width: 100%; + border: none; + color: white; + cursor: pointer; + &:hover, + &:hover.is-selected { + background-color: gray; + } + } +} \ No newline at end of file diff --git a/src/components/ContextMenuMentions.tsx b/src/components/ContextMenuMentions.tsx new file mode 100644 index 0000000..8042c1a --- /dev/null +++ b/src/components/ContextMenuMentions.tsx @@ -0,0 +1,130 @@ +import React, { useState, useRef, useMemo, useEffect } from "react"; +import { + ListItemIcon, + Menu, + MenuItem, + Typography, + styled, +} from "@mui/material"; + +import { executeEvent } from "../utils/events"; + +const CustomStyledMenu = styled(Menu)(({ theme }) => ({ + "& .MuiPaper-root": { + backgroundColor: "#f9f9f9", + borderRadius: "12px", + padding: theme.spacing(1), + boxShadow: "0 5px 15px rgba(0, 0, 0, 0.2)", + }, + "& .MuiMenuItem-root": { + fontSize: "14px", // Smaller font size for the menu item text + color: "#444", + transition: "0.3s background-color", + "&:hover": { + backgroundColor: "#f0f0f0", // Explicit hover state + }, + }, +})); + +export const ContextMenuMentions = ({ + children, + groupId, + getTimestampMention +}) => { + const [menuPosition, setMenuPosition] = useState(null); + const longPressTimeout = useRef(null); + const preventClick = useRef(false); // Flag to prevent click after long-press or right-click + + + + // Handle right-click (context menu) for desktop + const handleContextMenu = (event) => { + event.preventDefault(); + event.stopPropagation(); // Prevent parent click + + // Set flag to prevent any click event after right-click + preventClick.current = true; + + setMenuPosition({ + mouseX: event.clientX, + mouseY: event.clientY, + }); + }; + + // Handle long-press for mobile + const handleTouchStart = (event) => { + longPressTimeout.current = setTimeout(() => { + preventClick.current = true; // Prevent the next click after long-press + event.stopPropagation(); // Prevent parent click + setMenuPosition({ + mouseX: event.touches[0].clientX, + mouseY: event.touches[0].clientY, + }); + }, 500); // Long press duration + }; + + const handleTouchEnd = (event) => { + clearTimeout(longPressTimeout.current); + + if (preventClick.current) { + event.preventDefault(); + event.stopPropagation(); // Prevent synthetic click after long-press + preventClick.current = false; // Reset the flag + } + }; + + + const handleClose = (e) => { + e.preventDefault(); + e.stopPropagation(); + setMenuPosition(null); + }; + + const addTimestamp = ()=> { + window.sendMessage("addTimestampMention", { + timestamp: Date.now(), + groupId + }).then((res)=> { + getTimestampMention() + }).catch((error) => { + console.error("Failed to add timestamp:", error.message || "An error occurred"); + }); + } + + return ( +
+ {children} + + { + e.stopPropagation(); + }} + > + { + handleClose(e); + addTimestamp() + }} + > + + Unmark + + + +
+ ); +}; diff --git a/src/components/Group/Group.tsx b/src/components/Group/Group.tsx index 3d4a7fd..1879f48 100644 --- a/src/components/Group/Group.tsx +++ b/src/components/Group/Group.tsx @@ -445,6 +445,7 @@ export const Group = ({ const [appsModeDev, setAppsModeDev] = useState('home') const [isOpenSideViewDirects, setIsOpenSideViewDirects] = useState(false) const [isOpenSideViewGroups, setIsOpenSideViewGroups] = useState(false) + const toggleSideViewDirects = ()=> { if(isOpenSideViewGroups){ setIsOpenSideViewGroups(false) @@ -2013,17 +2014,17 @@ export const Group = ({ // getTimestampEnterChat(); }, 200); - window.sendMessage("addTimestampEnterChat", { - timestamp: Date.now(), - groupId: group.groupId, - }).catch((error) => { - console.error("Failed to add timestamp:", error.message || "An error occurred"); - }); + // window.sendMessage("addTimestampEnterChat", { + // timestamp: Date.now(), + // groupId: group.groupId, + // }).catch((error) => { + // console.error("Failed to add timestamp:", error.message || "An error occurred"); + // }); - setTimeout(() => { - getTimestampEnterChat(); - }, 200); + // setTimeout(() => { + // getTimestampEnterChat(); + // }, 200); }} @@ -2485,6 +2486,7 @@ export const Group = ({ triedToFetchSecretKey={triedToFetchSecretKey} myName={userInfo?.name} balance={balance} + getTimestampEnterChatParent={getTimestampEnterChat} /> )} {firstSecretKeyInCreation && diff --git a/src/components/Group/ListOfGroupPromotions.tsx b/src/components/Group/ListOfGroupPromotions.tsx index c306326..9cdf8a8 100644 --- a/src/components/Group/ListOfGroupPromotions.tsx +++ b/src/components/Group/ListOfGroupPromotions.tsx @@ -162,7 +162,7 @@ export const ListOfGroupPromotions = () => { }); await Promise.all(getPromos); - const groupWithInfo = await getGroupNames(data); + const groupWithInfo = await getGroupNames(data.sort((a, b) => b.created - a.created)); setPromotions(groupWithInfo); } catch (error) { console.error(error); @@ -559,10 +559,11 @@ export const ListOfGroupPromotions = () => { }} >