mirror of
https://github.com/Qortal/Qortal-Hub.git
synced 2025-04-24 03:47:53 +00:00
added search, mentions, keep place , scroll dn
This commit is contained in:
parent
8562b0c5ce
commit
54bb22e24e
@ -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).
|
||||
|
224
package-lock.json
generated
224
package-lock.json
generated
@ -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",
|
||||
|
@ -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": {
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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));
|
||||
|
@ -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();
|
||||
|
@ -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":
|
||||
|
@ -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<any>(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;
|
||||
|
@ -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',
|
||||
}}>
|
||||
|
||||
<ChatList onReply={onReply} chatId={selectedGroup} initialMessages={messages} myAddress={myAddress} tempMessages={tempMessages} handleReaction={handleReaction} chatReferences={chatReferences} tempChatReferences={tempChatReferences}/>
|
||||
|
||||
|
||||
<ChatList enableMentions onReply={onReply} chatId={selectedGroup} initialMessages={messages} myAddress={myAddress} tempMessages={tempMessages} handleReaction={handleReaction} chatReferences={chatReferences} tempChatReferences={tempChatReferences} members={members} myName={myName} selectedGroup={selectedGroup} />
|
||||
|
||||
|
||||
<div style={{
|
||||
// position: 'fixed',
|
||||
@ -669,7 +737,7 @@ const clearEditorContent = () => {
|
||||
)}
|
||||
|
||||
|
||||
<Tiptap setEditorRef={setEditorRef} onEnter={sendMessage} isChat disableEnter={isMobile ? true : false} isFocusedParent={isFocusedParent} setIsFocusedParent={setIsFocusedParent} />
|
||||
<Tiptap enableMentions setEditorRef={setEditorRef} onEnter={sendMessage} isChat disableEnter={isMobile ? true : false} isFocusedParent={isFocusedParent} setIsFocusedParent={setIsFocusedParent} membersWithNames={members} />
|
||||
</div>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
|
@ -3,11 +3,15 @@ import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { MessageItem } from './MessageItem';
|
||||
import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events';
|
||||
import { useInView } from 'react-intersection-observer'
|
||||
import { Box } from '@mui/material';
|
||||
import { ChatOptions } from './ChatOptions';
|
||||
|
||||
export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onReply, handleReaction, chatReferences, tempChatReferences }) => {
|
||||
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 (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}}>
|
||||
<div style={{
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
flexDirection: 'column',
|
||||
width: '100%'
|
||||
}}>
|
||||
|
||||
<div
|
||||
@ -181,8 +226,10 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
|
||||
width: '100%', // Control width (90% of the parent)
|
||||
padding: '10px 0',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
overscrollBehavior: 'none',
|
||||
flexDirection: 'column',
|
||||
gap: '5px'
|
||||
}}
|
||||
>
|
||||
<MessageItem
|
||||
@ -195,7 +242,7 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
|
||||
onReply={onReply}
|
||||
reply={reply}
|
||||
replyIndex={replyIndex}
|
||||
scrollToItem={(idx) => 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
|
||||
</button>
|
||||
)}
|
||||
{showScrollDownButton && (
|
||||
<button
|
||||
onClick={() => scrollToBottom()}
|
||||
style={{
|
||||
bottom: 20,
|
||||
position: 'absolute',
|
||||
right: 20,
|
||||
backgroundColor: 'var(--Mail-Background)',
|
||||
color: 'white',
|
||||
padding: '10px 20px',
|
||||
borderRadius: '20px',
|
||||
cursor: 'pointer',
|
||||
zIndex: 10,
|
||||
border: 'none',
|
||||
outline: 'none'
|
||||
}}
|
||||
>
|
||||
Scroll to bottom
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{enableMentions && (
|
||||
<ChatOptions messages={messages} goToMessage={goToMessage} members={members} myName={myName} selectedGroup={selectedGroup}/>
|
||||
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
693
src/components/Chat/ChatOptions.tsx
Normal file
693
src/components/Chat/ChatOptions.tsx
Normal file
@ -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 (
|
||||
<Box
|
||||
sx={{
|
||||
width: "300px",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
// alignItems: 'center',
|
||||
backgroundColor: "#1F2023",
|
||||
borderBottomLeftRadius: "20px",
|
||||
borderTopLeftRadius: "20px",
|
||||
overflow: "auto",
|
||||
flexShrink: 0,
|
||||
flexGrow: 0,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
padding: "10px",
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<CloseIcon
|
||||
onClick={() => {
|
||||
setMode("default");
|
||||
}}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
color: "white",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
|
||||
|
||||
|
||||
{mentionList?.length === 0 && (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "11px",
|
||||
fontWeight: 400,
|
||||
color: "rgba(255, 255, 255, 0.2)",
|
||||
}}
|
||||
>
|
||||
No results
|
||||
</Typography>
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={parentRefMentions}
|
||||
className="List"
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
overflow: "auto",
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
height: "0px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: rowVirtualizerMentions.getTotalSize(),
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{rowVirtualizerMentions.getVirtualItems().map((virtualRow) => {
|
||||
const index = virtualRow.index;
|
||||
let message = mentionList[index];
|
||||
return (
|
||||
<div
|
||||
data-index={virtualRow.index} //needed for dynamic row height measurement
|
||||
ref={rowVirtualizerMentions.measureElement} //measure dynamic row height
|
||||
key={message.signature}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: "50%", // Move to the center horizontally
|
||||
transform: `translateY(${virtualRow.start}px) translateX(-50%)`, // Adjust for centering
|
||||
width: "100%", // Control width (90% of the parent)
|
||||
padding: "10px 0",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
overscrollBehavior: "none",
|
||||
flexDirection: "column",
|
||||
gap: "5px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
padding: "0px 20px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "15px",
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
sx={{
|
||||
backgroundColor: "#27282c",
|
||||
color: "white",
|
||||
height: "25px",
|
||||
width: "25px",
|
||||
}}
|
||||
alt={message?.senderName}
|
||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
message?.senderName
|
||||
}/qortal_avatar?async=true`}
|
||||
>
|
||||
{message?.senderName?.charAt(0)}
|
||||
</Avatar>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWight: 600,
|
||||
fontFamily: "Inter",
|
||||
color: "cadetBlue",
|
||||
}}
|
||||
>
|
||||
{message?.senderName}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Spacer height="5px" />
|
||||
<Typography sx={{
|
||||
fontSize: '12px'
|
||||
}}>{formatTimestamp(message.timestamp)}</Typography>
|
||||
<Box
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
const findMsgIndex = messages.findIndex(
|
||||
(item) =>
|
||||
item?.signature === message?.signature
|
||||
);
|
||||
if (findMsgIndex !== -1) {
|
||||
goToMessage(findMsgIndex);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MessageDisplay
|
||||
htmlContent={
|
||||
message?.decryptedData?.message || "<p></p>"
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (mode === "search") {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: "300px",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
// alignItems: 'center',
|
||||
backgroundColor: "#1F2023",
|
||||
borderBottomLeftRadius: "20px",
|
||||
borderTopLeftRadius: "20px",
|
||||
overflow: "auto",
|
||||
flexShrink: 0,
|
||||
flexGrow: 0,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
padding: "10px",
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<CloseIcon
|
||||
onClick={() => {
|
||||
setMode("default");
|
||||
}}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
color: "white",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<AppsSearchContainer>
|
||||
<AppsSearchLeft>
|
||||
<img src={IconSearch} />
|
||||
<InputBase
|
||||
value={searchValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
sx={{ ml: 1, flex: 1 }}
|
||||
placeholder="Search chat text"
|
||||
inputProps={{
|
||||
"aria-label": "Search for apps",
|
||||
fontSize: "16px",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
/>
|
||||
</AppsSearchLeft>
|
||||
<AppsSearchRight>
|
||||
{searchValue && (
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setSearchValue("");
|
||||
}}
|
||||
>
|
||||
<img src={IconClearInput} />
|
||||
</ButtonBase>
|
||||
)}
|
||||
</AppsSearchRight>
|
||||
</AppsSearchContainer>
|
||||
<Box
|
||||
sx={{
|
||||
padding: "10px",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Select
|
||||
size="small"
|
||||
labelId="demo-simple-select-label"
|
||||
id="demo-simple-select"
|
||||
value={selectedMember}
|
||||
label="By member"
|
||||
onChange={(e) => setSelectedMember(e.target.value)}
|
||||
>
|
||||
<MenuItem value={0}>
|
||||
<em>By member</em>
|
||||
</MenuItem>
|
||||
{members?.map((member) => {
|
||||
return (
|
||||
<MenuItem key={member} value={member}>
|
||||
{member}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
{!!selectedMember && (
|
||||
<CloseIcon
|
||||
onClick={() => {
|
||||
setSelectedMember(0);
|
||||
}}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
color: "white",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
|
||||
{debouncedValue && searchedList?.length === 0 && (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "11px",
|
||||
fontWeight: 400,
|
||||
color: "rgba(255, 255, 255, 0.2)",
|
||||
}}
|
||||
>
|
||||
No results
|
||||
</Typography>
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={parentRef}
|
||||
className="List"
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
overflow: "auto",
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
height: "0px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: rowVirtualizer.getTotalSize(),
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const index = virtualRow.index;
|
||||
let message = searchedList[index];
|
||||
return (
|
||||
<div
|
||||
data-index={virtualRow.index} //needed for dynamic row height measurement
|
||||
ref={rowVirtualizer.measureElement} //measure dynamic row height
|
||||
key={message.signature}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: "50%", // Move to the center horizontally
|
||||
transform: `translateY(${virtualRow.start}px) translateX(-50%)`, // Adjust for centering
|
||||
width: "100%", // Control width (90% of the parent)
|
||||
padding: "10px 0",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
overscrollBehavior: "none",
|
||||
flexDirection: "column",
|
||||
gap: "5px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
padding: "0px 20px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "15px",
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
sx={{
|
||||
backgroundColor: "#27282c",
|
||||
color: "white",
|
||||
height: "25px",
|
||||
width: "25px",
|
||||
}}
|
||||
alt={message?.senderName}
|
||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
message?.senderName
|
||||
}/qortal_avatar?async=true`}
|
||||
>
|
||||
{message?.senderName?.charAt(0)}
|
||||
</Avatar>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWight: 600,
|
||||
fontFamily: "Inter",
|
||||
color: "cadetBlue",
|
||||
}}
|
||||
>
|
||||
{message?.senderName}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Spacer height="5px" />
|
||||
<Typography sx={{
|
||||
fontSize: '12px'
|
||||
}}>{formatTimestamp(message.timestamp)}</Typography>
|
||||
<Box
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
const findMsgIndex = messages.findIndex(
|
||||
(item) =>
|
||||
item?.signature === message?.signature
|
||||
);
|
||||
if (findMsgIndex !== -1) {
|
||||
goToMessage(findMsgIndex);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MessageDisplay
|
||||
htmlContent={
|
||||
message?.decryptedData?.message || "<p></p>"
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: "50px",
|
||||
height: "100%",
|
||||
gap: "20px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
padding: "10px",
|
||||
gap: "20px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#1F2023",
|
||||
borderBottomLeftRadius: "20px",
|
||||
borderTopLeftRadius: "20px",
|
||||
minHeight: "200px",
|
||||
}}
|
||||
>
|
||||
<ButtonBase onClick={() => {
|
||||
setMode("search")
|
||||
}}>
|
||||
<SearchIcon />
|
||||
</ButtonBase>
|
||||
<ContextMenuMentions getTimestampMention={getTimestampMention} groupId={selectedGroup}>
|
||||
<ButtonBase onClick={() => {
|
||||
setMode("mentions")
|
||||
setSearchValue('')
|
||||
setSelectedMember(0)
|
||||
}}>
|
||||
<AlternateEmailIcon sx={{
|
||||
color: mentionList?.length > 0 && (!lastMentionTimestamp || lastMentionTimestamp < mentionList[0]?.timestamp) ? 'var(--unread)' : 'white'
|
||||
}} />
|
||||
</ButtonBase>
|
||||
</ContextMenuMentions>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
69
src/components/Chat/MentionList.tsx
Normal file
69
src/components/Chat/MentionList.tsx
Normal file
@ -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 (
|
||||
<div className="dropdown-menu">
|
||||
{props.items.length
|
||||
? props.items.map((item, index) => (
|
||||
<button
|
||||
className={index === selectedIndex ? 'is-selected' : ''}
|
||||
key={item.id || index}
|
||||
onClick={() => selectItem(index)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))
|
||||
: <div className="item">No result</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
@ -47,6 +47,12 @@ export const MessageItem = ({
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{message?.divide && (
|
||||
<div className="unread-divider" id="unread-divider-id">
|
||||
Unread messages below
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
ref={lastSignature === message?.signature ? ref : null}
|
||||
style={{
|
||||
@ -239,7 +245,9 @@ export const MessageItem = ({
|
||||
handleReaction(reaction, message, true)
|
||||
}
|
||||
}}>
|
||||
<div>{reaction}</div> {numberOfReactions > 1 && (
|
||||
<div style={{
|
||||
fontSize: '16px'
|
||||
}}>{reaction}</div> {numberOfReactions > 1 && (
|
||||
<Typography sx={{
|
||||
marginLeft: '4px'
|
||||
}}>{' '} {numberOfReactions}</Typography>
|
||||
@ -307,6 +315,7 @@ export const MessageItem = ({
|
||||
></Message> */}
|
||||
{/* {!message.unread && <span style={{ color: 'green' }}> Seen</span>} */}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -355,5 +364,6 @@ export const ReplyPreview = ({message})=> {
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
)
|
||||
}
|
@ -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 (
|
||||
<div>
|
||||
<EditorProvider
|
||||
slotBefore={
|
||||
(isFocusedParent || !isMobile || overrideMobile) && (
|
||||
<MenuBar setEditorRef={setEditorRefFunc} isChat={isChat} />
|
||||
)
|
||||
}
|
||||
extensions={extensionsFiltered}
|
||||
extensions={[...extensionsFiltered, ...additionalExtensions
|
||||
]}
|
||||
content={content}
|
||||
onCreate={({ editor }) => {
|
||||
editor.on("focus", handleFocus); // Listen for focus event
|
||||
@ -348,5 +469,7 @@ export default ({
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
130
src/components/ContextMenuMentions.tsx
Normal file
130
src/components/ContextMenuMentions.tsx
Normal file
@ -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 (
|
||||
<div
|
||||
onContextMenu={handleContextMenu} // For desktop right-click
|
||||
onTouchStart={handleTouchStart} // For mobile long-press start
|
||||
onTouchEnd={handleTouchEnd} // For mobile long-press end
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
>
|
||||
{children}
|
||||
|
||||
<CustomStyledMenu
|
||||
disableAutoFocusItem
|
||||
open={!!menuPosition}
|
||||
onClose={handleClose}
|
||||
anchorReference="anchorPosition"
|
||||
anchorPosition={
|
||||
menuPosition
|
||||
? { top: menuPosition.mouseY, left: menuPosition.mouseX }
|
||||
: undefined
|
||||
}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
handleClose(e);
|
||||
addTimestamp()
|
||||
}}
|
||||
>
|
||||
<Typography variant="inherit" sx={{ fontSize: "14px" }}>
|
||||
Unmark
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
</CustomStyledMenu>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -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 &&
|
||||
|
@ -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 = () => {
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
// variant="contained"
|
||||
onClick={(event) => handlePopoverOpen(event, promotion?.groupId)}
|
||||
sx={{
|
||||
fontSize: "12px",
|
||||
color: 'white'
|
||||
}}
|
||||
>
|
||||
Join Group: {` ${promotion?.groupName}`}
|
||||
@ -698,7 +699,7 @@ export const ListOfGroupPromotions = () => {
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">
|
||||
{"Publish Group Promotion"}
|
||||
{"Promote your group to non-members"}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
|
@ -48,10 +48,11 @@ export const ThingsToDoInitial = ({ myAddress, name, hasGroups, balance, userInf
|
||||
if (name) setChecked2(true);
|
||||
}, [name]);
|
||||
|
||||
|
||||
const isLoaded = React.useMemo(()=> {
|
||||
if(balance !== null && userInfo !== null) return true
|
||||
if(userInfo !== null) return true
|
||||
return false
|
||||
}, [balance, userInfo])
|
||||
}, [ userInfo])
|
||||
|
||||
const hasDoneNameAndBalanceAndIsLoaded = React.useMemo(()=> {
|
||||
if(isLoaded && checked1 && checked2) return true
|
||||
|
@ -40,6 +40,7 @@
|
||||
|
||||
body {
|
||||
margin: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
|
@ -323,7 +323,6 @@ export const getUserAccount = async ({isFromExtension, appInfo}) => {
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('per error', error)
|
||||
throw new Error("Unable to fetch user account");
|
||||
}
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user