mirror of
https://github.com/Qortal/chrome-extension.git
synced 2025-02-11 17:55:49 +00:00
batch of updates 1
This commit is contained in:
parent
f464a5b049
commit
d0719a30af
291
package-lock.json
generated
291
package-lock.json
generated
@ -23,6 +23,7 @@
|
|||||||
"@tiptap/extension-color": "^2.5.9",
|
"@tiptap/extension-color": "^2.5.9",
|
||||||
"@tiptap/extension-highlight": "^2.6.6",
|
"@tiptap/extension-highlight": "^2.6.6",
|
||||||
"@tiptap/extension-image": "^2.6.6",
|
"@tiptap/extension-image": "^2.6.6",
|
||||||
|
"@tiptap/extension-mention": "^2.10.4",
|
||||||
"@tiptap/extension-placeholder": "^2.6.2",
|
"@tiptap/extension-placeholder": "^2.6.2",
|
||||||
"@tiptap/extension-text-style": "^2.5.9",
|
"@tiptap/extension-text-style": "^2.5.9",
|
||||||
"@tiptap/extension-underline": "^2.6.6",
|
"@tiptap/extension-underline": "^2.6.6",
|
||||||
@ -37,6 +38,7 @@
|
|||||||
"dompurify": "^3.1.6",
|
"dompurify": "^3.1.6",
|
||||||
"emoji-picker-react": "^4.12.0",
|
"emoji-picker-react": "^4.12.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
|
"html-to-text": "^9.0.5",
|
||||||
"jssha": "3.3.1",
|
"jssha": "3.3.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mime": "^4.0.4",
|
"mime": "^4.0.4",
|
||||||
@ -60,7 +62,8 @@
|
|||||||
"short-unique-id": "^5.2.0",
|
"short-unique-id": "^5.2.0",
|
||||||
"slate": "^0.103.0",
|
"slate": "^0.103.0",
|
||||||
"slate-react": "^0.109.0",
|
"slate-react": "^0.109.0",
|
||||||
"tiptap-extension-resize-image": "^1.1.8"
|
"tiptap-extension-resize-image": "^1.1.8",
|
||||||
|
"ts-key-enum": "^2.0.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/dom": "^10.3.0",
|
"@testing-library/dom": "^10.3.0",
|
||||||
@ -1721,9 +1724,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@remirror/core-constants": {
|
"node_modules/@remirror/core-constants": {
|
||||||
"version": "2.0.2",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
|
||||||
"integrity": "sha512-dyHY+sMF0ihPus3O27ODd4+agdHMEmuRdyiZJ2CCWjPV5UFmn17ZbElvk6WOGVE4rdCJKZQCrPV2BcikOMLUGQ=="
|
"integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg=="
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.13.0",
|
"version": "4.13.0",
|
||||||
@ -1894,6 +1897,18 @@
|
|||||||
"win32"
|
"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": {
|
"node_modules/@sinclair/typebox": {
|
||||||
"version": "0.27.8",
|
"version": "0.27.8",
|
||||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
|
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
|
||||||
@ -2207,15 +2222,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/core": {
|
"node_modules/@tiptap/core": {
|
||||||
"version": "2.6.6",
|
"version": "2.10.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.6.6.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.10.4.tgz",
|
||||||
"integrity": "sha512-VO5qTsjt6rwworkuo0s5AqYMfDA0ZwiTiH6FHKFSu2G/6sS7HKcc/LjPq+5Legzps4QYdBDl3W28wGsGuS1GdQ==",
|
"integrity": "sha512-fExFRTRgb6MSpg2VvR5qO2dPTQAZWuUoU4UsBCurIVcPWcyVv4FG1YzgMyoLDKy44rebFtwUGJbfU9NzX7Q/bA==",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/ueberdosis"
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tiptap/pm": "^2.6.6"
|
"@tiptap/pm": "^2.7.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/extension-blockquote": {
|
"node_modules/@tiptap/extension-blockquote": {
|
||||||
@ -2460,6 +2475,20 @@
|
|||||||
"@tiptap/core": "^2.5.9"
|
"@tiptap/core": "^2.5.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tiptap/extension-mention": {
|
||||||
|
"version": "2.10.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-mention/-/extension-mention-2.10.4.tgz",
|
||||||
|
"integrity": "sha512-pVouKWxSVQSy4zn6HrljPIP1AG826gkm/w18Asi8QnZvR0AMqGLh9q7qd9Kc0j8NKoCzlzK8hECGlYPEaBldow==",
|
||||||
|
"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": {
|
"node_modules/@tiptap/extension-ordered-list": {
|
||||||
"version": "2.5.9",
|
"version": "2.5.9",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.5.9.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.5.9.tgz",
|
||||||
@ -2546,28 +2575,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/pm": {
|
"node_modules/@tiptap/pm": {
|
||||||
"version": "2.6.6",
|
"version": "2.10.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.6.6.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.10.4.tgz",
|
||||||
"integrity": "sha512-56FGLPn3fwwUlIbLs+BO21bYfyqP9fKyZQbQyY0zWwA/AG2kOwoXaRn7FOVbjP6CylyWpFJnpRRmgn694QKHEg==",
|
"integrity": "sha512-pZ4NEkRtYoDLe0spARvXZ1N3hNv/5u6vfPdPtEbmNpoOSjSNqDC1kVM+qJY0iaCYpxbxcv7cxn3kBumcFLQpJQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prosemirror-changeset": "^2.2.1",
|
"prosemirror-changeset": "^2.2.1",
|
||||||
"prosemirror-collab": "^1.3.1",
|
"prosemirror-collab": "^1.3.1",
|
||||||
"prosemirror-commands": "^1.5.2",
|
"prosemirror-commands": "^1.6.2",
|
||||||
"prosemirror-dropcursor": "^1.8.1",
|
"prosemirror-dropcursor": "^1.8.1",
|
||||||
"prosemirror-gapcursor": "^1.3.2",
|
"prosemirror-gapcursor": "^1.3.2",
|
||||||
"prosemirror-history": "^1.4.1",
|
"prosemirror-history": "^1.4.1",
|
||||||
"prosemirror-inputrules": "^1.4.0",
|
"prosemirror-inputrules": "^1.4.0",
|
||||||
"prosemirror-keymap": "^1.2.2",
|
"prosemirror-keymap": "^1.2.2",
|
||||||
"prosemirror-markdown": "^1.13.0",
|
"prosemirror-markdown": "^1.13.1",
|
||||||
"prosemirror-menu": "^1.2.4",
|
"prosemirror-menu": "^1.2.4",
|
||||||
"prosemirror-model": "^1.22.2",
|
"prosemirror-model": "^1.23.0",
|
||||||
"prosemirror-schema-basic": "^1.2.3",
|
"prosemirror-schema-basic": "^1.2.3",
|
||||||
"prosemirror-schema-list": "^1.4.1",
|
"prosemirror-schema-list": "^1.4.1",
|
||||||
"prosemirror-state": "^1.4.3",
|
"prosemirror-state": "^1.4.3",
|
||||||
"prosemirror-tables": "^1.4.0",
|
"prosemirror-tables": "^1.6.1",
|
||||||
"prosemirror-trailing-node": "^2.0.9",
|
"prosemirror-trailing-node": "^3.0.0",
|
||||||
"prosemirror-transform": "^1.9.0",
|
"prosemirror-transform": "^1.10.2",
|
||||||
"prosemirror-view": "^1.33.9"
|
"prosemirror-view": "^1.37.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@ -2625,6 +2654,20 @@
|
|||||||
"url": "https://github.com/sponsors/ueberdosis"
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tiptap/suggestion": {
|
||||||
|
"version": "2.10.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-2.10.4.tgz",
|
||||||
|
"integrity": "sha512-7Bzcn1REA7OmVRxiMF2kVK9EhosXotdLAGaEvSbn4zQtHCJG0tREuYvPy53LGzVuPkBDR6Pf6sp1QbGvSne/8g==",
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^2.7.0",
|
||||||
|
"@tiptap/pm": "^2.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/aria-query": {
|
"node_modules/@types/aria-query": {
|
||||||
"version": "5.0.4",
|
"version": "5.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||||
@ -2720,12 +2763,31 @@
|
|||||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/linkify-it": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="
|
||||||
|
},
|
||||||
"node_modules/@types/lodash": {
|
"node_modules/@types/lodash": {
|
||||||
"version": "4.17.7",
|
"version": "4.17.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz",
|
||||||
"integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==",
|
"integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/markdown-it": {
|
||||||
|
"version": "14.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
|
||||||
|
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/linkify-it": "^5",
|
||||||
|
"@types/mdurl": "^2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/mdurl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="
|
||||||
|
},
|
||||||
"node_modules/@types/parse-json": {
|
"node_modules/@types/parse-json": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
|
||||||
@ -4084,6 +4146,14 @@
|
|||||||
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
||||||
"dev": true
|
"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/define-data-property": {
|
"node_modules/define-data-property": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||||
@ -4198,11 +4268,62 @@
|
|||||||
"csstype": "^3.0.2"
|
"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": {
|
"node_modules/dompurify": {
|
||||||
"version": "3.1.6",
|
"version": "3.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz",
|
||||||
"integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ=="
|
"integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/domutils": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-xWXmuRnN9OMP6ptPd2+H0cCbcYBULa5YDTbMm/2lvkWvNA3O4wcW+GvzooqBuNM8yy6pl3VIAeJTUUWUbfI5Fw==",
|
||||||
|
"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/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.4.710",
|
"version": "1.4.710",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.710.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.710.tgz",
|
||||||
@ -5189,6 +5310,39 @@
|
|||||||
"node": ">=18"
|
"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-proxy-agent": {
|
"node_modules/http-proxy-agent": {
|
||||||
"version": "7.0.2",
|
"version": "7.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||||
@ -5840,6 +5994,14 @@
|
|||||||
"json-buffer": "3.0.1"
|
"json-buffer": "3.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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": {
|
"node_modules/levn": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||||
@ -8795,6 +8957,18 @@
|
|||||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
"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": {
|
"node_modules/path-exists": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
@ -8852,6 +9026,14 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||||
@ -9010,13 +9192,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prosemirror-commands": {
|
"node_modules/prosemirror-commands": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.6.2.tgz",
|
||||||
"integrity": "sha512-xn1U/g36OqXn2tn5nGmvnnimAj/g1pUx2ypJJIe8WkVX83WyJVC5LTARaxZa2AtQRwntu9Jc5zXs9gL9svp/mg==",
|
"integrity": "sha512-0nDHH++qcf/BuPLYvmqZTUUsPJUCPBUXt0J1ErTcDIS369CTp773itzLGIgIXG4LJXOlwYCr44+Mh4ii6MP1QA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prosemirror-model": "^1.0.0",
|
"prosemirror-model": "^1.0.0",
|
||||||
"prosemirror-state": "^1.0.0",
|
"prosemirror-state": "^1.0.0",
|
||||||
"prosemirror-transform": "^1.0.0"
|
"prosemirror-transform": "^1.10.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prosemirror-dropcursor": {
|
"node_modules/prosemirror-dropcursor": {
|
||||||
@ -9070,10 +9252,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prosemirror-markdown": {
|
"node_modules/prosemirror-markdown": {
|
||||||
"version": "1.13.0",
|
"version": "1.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.1.tgz",
|
||||||
"integrity": "sha512-UziddX3ZYSYibgx8042hfGKmukq5Aljp2qoBiJRejD/8MH70siQNz5RB1TrdTPheqLMy4aCe4GYNF10/3lQS5g==",
|
"integrity": "sha512-Sl+oMfMtAjWtlcZoj/5L/Q39MpEnVZ840Xo330WJWUvgyhNmLBLN7MsHn07s53nG/KImevWHSE6fEj4q/GihHw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/markdown-it": "^14.0.0",
|
||||||
"markdown-it": "^14.0.0",
|
"markdown-it": "^14.0.0",
|
||||||
"prosemirror-model": "^1.20.0"
|
"prosemirror-model": "^1.20.0"
|
||||||
}
|
}
|
||||||
@ -9090,9 +9273,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prosemirror-model": {
|
"node_modules/prosemirror-model": {
|
||||||
"version": "1.22.3",
|
"version": "1.24.1",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.22.3.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.24.1.tgz",
|
||||||
"integrity": "sha512-V4XCysitErI+i0rKFILGt/xClnFJaohe/wrrlT2NSZ+zk8ggQfDH4x2wNK7Gm0Hp4CIoWizvXFP7L9KMaCuI0Q==",
|
"integrity": "sha512-YM053N+vTThzlWJ/AtPtF1j0ebO36nvbmDy4U7qA2XQB8JVaQp1FmB9Jhrps8s+z+uxhhVTny4m20ptUvhk0Mg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"orderedmap": "^2.0.0"
|
"orderedmap": "^2.0.0"
|
||||||
}
|
}
|
||||||
@ -9126,23 +9309,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prosemirror-tables": {
|
"node_modules/prosemirror-tables": {
|
||||||
"version": "1.4.0",
|
"version": "1.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.6.2.tgz",
|
||||||
"integrity": "sha512-fxryZZkQG12fSCNuZDrYx6Xvo2rLYZTbKLRd8rglOPgNJGMKIS8uvTt6gGC38m7UCu/ENnXIP9pEz5uDaPc+cA==",
|
"integrity": "sha512-97dKocVLrEVTQjZ4GBLdrrMw7Gv3no8H8yMwf5IRM9OoHrzbWpcH5jJxYgNQIRCtdIqwDctT1HdMHrGTiwp1dQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prosemirror-keymap": "^1.1.2",
|
"prosemirror-keymap": "^1.2.2",
|
||||||
"prosemirror-model": "^1.8.1",
|
"prosemirror-model": "^1.24.1",
|
||||||
"prosemirror-state": "^1.3.1",
|
"prosemirror-state": "^1.4.3",
|
||||||
"prosemirror-transform": "^1.2.1",
|
"prosemirror-transform": "^1.10.2",
|
||||||
"prosemirror-view": "^1.13.3"
|
"prosemirror-view": "^1.37.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prosemirror-trailing-node": {
|
"node_modules/prosemirror-trailing-node": {
|
||||||
"version": "2.0.9",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-2.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz",
|
||||||
"integrity": "sha512-YvyIn3/UaLFlFKrlJB6cObvUhmwFNZVhy1Q8OpW/avoTbD/Y7H5EcjK4AZFKhmuS6/N6WkGgt7gWtBWDnmFvHg==",
|
"integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@remirror/core-constants": "^2.0.2",
|
"@remirror/core-constants": "3.0.0",
|
||||||
"escape-string-regexp": "^4.0.0"
|
"escape-string-regexp": "^4.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@ -9163,17 +9346,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prosemirror-transform": {
|
"node_modules/prosemirror-transform": {
|
||||||
"version": "1.9.0",
|
"version": "1.10.2",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.2.tgz",
|
||||||
"integrity": "sha512-5UXkr1LIRx3jmpXXNKDhv8OyAOeLTGuXNwdVfg8x27uASna/wQkr9p6fD3eupGOi4PLJfbezxTyi/7fSJypXHg==",
|
"integrity": "sha512-2iUq0wv2iRoJO/zj5mv8uDUriOHWzXRnOTVgCzSXnktS/2iQRa3UUQwVlkBlYZFtygw6Nh1+X4mGqoYBINn5KQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prosemirror-model": "^1.21.0"
|
"prosemirror-model": "^1.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prosemirror-view": {
|
"node_modules/prosemirror-view": {
|
||||||
"version": "1.33.9",
|
"version": "1.37.1",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.33.9.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.37.1.tgz",
|
||||||
"integrity": "sha512-xV1A0Vz9cIcEnwmMhKKFAOkfIp8XmJRnaZoPqNXrPS7EK5n11Ov8V76KhR0RsfQd/SIzmWY+bg+M44A2Lx/Nnw==",
|
"integrity": "sha512-MEAnjOdXU1InxEmhjgmEzQAikaS6lF3hD64MveTPpjOGNTl87iRLA1HupC/DEV6YuK7m4Q9DHFNTjwIVtqz5NA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prosemirror-model": "^1.20.0",
|
"prosemirror-model": "^1.20.0",
|
||||||
"prosemirror-state": "^1.0.0",
|
"prosemirror-state": "^1.0.0",
|
||||||
@ -9942,6 +10125,17 @@
|
|||||||
"compute-scroll-into-view": "^3.0.2"
|
"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": {
|
"node_modules/semver": {
|
||||||
"version": "7.6.0",
|
"version": "7.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
|
||||||
@ -10456,6 +10650,11 @@
|
|||||||
"typescript": ">=4.2.0"
|
"typescript": ">=4.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ts-key-enum": {
|
||||||
|
"version": "2.0.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-key-enum/-/ts-key-enum-2.0.13.tgz",
|
||||||
|
"integrity": "sha512-zixs6j8+NhzazLUQ1SiFrlo1EFWG/DbqLuUGcWWZ5zhwjRT7kbi1hBlofxdqel+h28zrby2It5TrOyKp04kvqw=="
|
||||||
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.6.2",
|
"version": "2.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||||
|
@ -27,6 +27,7 @@
|
|||||||
"@tiptap/extension-color": "^2.5.9",
|
"@tiptap/extension-color": "^2.5.9",
|
||||||
"@tiptap/extension-highlight": "^2.6.6",
|
"@tiptap/extension-highlight": "^2.6.6",
|
||||||
"@tiptap/extension-image": "^2.6.6",
|
"@tiptap/extension-image": "^2.6.6",
|
||||||
|
"@tiptap/extension-mention": "^2.10.4",
|
||||||
"@tiptap/extension-placeholder": "^2.6.2",
|
"@tiptap/extension-placeholder": "^2.6.2",
|
||||||
"@tiptap/extension-text-style": "^2.5.9",
|
"@tiptap/extension-text-style": "^2.5.9",
|
||||||
"@tiptap/extension-underline": "^2.6.6",
|
"@tiptap/extension-underline": "^2.6.6",
|
||||||
@ -41,6 +42,7 @@
|
|||||||
"dompurify": "^3.1.6",
|
"dompurify": "^3.1.6",
|
||||||
"emoji-picker-react": "^4.12.0",
|
"emoji-picker-react": "^4.12.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
|
"html-to-text": "^9.0.5",
|
||||||
"jssha": "3.3.1",
|
"jssha": "3.3.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mime": "^4.0.4",
|
"mime": "^4.0.4",
|
||||||
@ -64,7 +66,8 @@
|
|||||||
"short-unique-id": "^5.2.0",
|
"short-unique-id": "^5.2.0",
|
||||||
"slate": "^0.103.0",
|
"slate": "^0.103.0",
|
||||||
"slate-react": "^0.109.0",
|
"slate-react": "^0.109.0",
|
||||||
"tiptap-extension-resize-image": "^1.1.8"
|
"tiptap-extension-resize-image": "^1.1.8",
|
||||||
|
"ts-key-enum": "^2.0.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/dom": "^10.3.0",
|
"@testing-library/dom": "^10.3.0",
|
||||||
|
75
src/App.tsx
75
src/App.tsx
@ -103,9 +103,13 @@ import { MainAvatar } from "./components/MainAvatar";
|
|||||||
import { useRetrieveDataLocalStorage } from "./useRetrieveDataLocalStorage";
|
import { useRetrieveDataLocalStorage } from "./useRetrieveDataLocalStorage";
|
||||||
import { useQortalGetSaveSettings } from "./useQortalGetSaveSettings";
|
import { useQortalGetSaveSettings } from "./useQortalGetSaveSettings";
|
||||||
import { useRecoilState, useResetRecoilState } from "recoil";
|
import { useRecoilState, useResetRecoilState } from "recoil";
|
||||||
import { canSaveSettingToQdnAtom, fullScreenAtom, hasSettingsChangedAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from "./atoms/global";
|
import { canSaveSettingToQdnAtom, fullScreenAtom, hasSettingsChangedAtom, isUsingImportExportSettingsAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from "./atoms/global";
|
||||||
import { useAppFullScreen } from "./useAppFullscreen";
|
import { useAppFullScreen } from "./useAppFullscreen";
|
||||||
import { NotAuthenticated } from "./ExtStates/NotAuthenticated";
|
import { NotAuthenticated } from "./ExtStates/NotAuthenticated";
|
||||||
|
import { useFetchResources } from "./common/useFetchResources";
|
||||||
|
import { Tutorials } from "./components/Tutorials/Tutorials";
|
||||||
|
import { useHandleTutorials } from "./components/Tutorials/useHandleTutorials";
|
||||||
|
import { CoreSyncStatus } from "./components/CoreSyncStatus";
|
||||||
|
|
||||||
type extStates =
|
type extStates =
|
||||||
| "not-authenticated"
|
| "not-authenticated"
|
||||||
@ -217,8 +221,12 @@ export const resumeAllQueues = () => {
|
|||||||
payload: {},
|
payload: {},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
const defaultValuesGlobal = {
|
||||||
|
openTutorialModal: null,
|
||||||
|
setOpenTutorialModal: ()=> {}
|
||||||
|
}
|
||||||
export const MyContext = createContext<MyContextInterface>(defaultValues);
|
export const MyContext = createContext<MyContextInterface>(defaultValues);
|
||||||
|
export const GlobalContext = createContext<MyContextInterface>(defaultValuesGlobal);
|
||||||
|
|
||||||
export let globalApiKey: string | null = null;
|
export let globalApiKey: string | null = null;
|
||||||
|
|
||||||
@ -308,6 +316,7 @@ function App() {
|
|||||||
const isFocusedRef = useRef<boolean>(true);
|
const isFocusedRef = useRef<boolean>(true);
|
||||||
const { isShow, onCancel, onOk, show, message } = useModal();
|
const { isShow, onCancel, onOk, show, message } = useModal();
|
||||||
const { isShow: isShowUnsavedChanges, onCancel: onCancelUnsavedChanges, onOk: onOkUnsavedChanges, show: showUnsavedChanges, message: messageUnsavedChanges } = useModal();
|
const { isShow: isShowUnsavedChanges, onCancel: onCancelUnsavedChanges, onOk: onOkUnsavedChanges, show: showUnsavedChanges, message: messageUnsavedChanges } = useModal();
|
||||||
|
const {downloadResource} = useFetchResources()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
onCancel: onCancelQortalRequest,
|
onCancel: onCancelQortalRequest,
|
||||||
@ -339,10 +348,11 @@ function App() {
|
|||||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
const qortalRequestCheckbox1Ref = useRef(null);
|
const qortalRequestCheckbox1Ref = useRef(null);
|
||||||
useRetrieveDataLocalStorage()
|
useRetrieveDataLocalStorage()
|
||||||
useQortalGetSaveSettings(userInfo?.name)
|
useQortalGetSaveSettings(userInfo?.name, extState === "authenticated")
|
||||||
const [fullScreen, setFullScreen] = useRecoilState(fullScreenAtom);
|
const [fullScreen, setFullScreen] = useRecoilState(fullScreenAtom);
|
||||||
|
const resetAtomIsUsingImportExportSettingsAtom = useResetRecoilState(isUsingImportExportSettingsAtom)
|
||||||
const { toggleFullScreen } = useAppFullScreen(setFullScreen);
|
const { toggleFullScreen } = useAppFullScreen(setFullScreen);
|
||||||
|
const {showTutorial, openTutorialModal, shownTutorialsInitiated, setOpenTutorialModal} = useHandleTutorials()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Attach a global event listener for double-click
|
// Attach a global event listener for double-click
|
||||||
@ -371,6 +381,7 @@ function App() {
|
|||||||
resetAtomSettingsQDNLastUpdatedAtom();
|
resetAtomSettingsQDNLastUpdatedAtom();
|
||||||
resetAtomSettingsLocalLastUpdatedAtom();
|
resetAtomSettingsLocalLastUpdatedAtom();
|
||||||
resetAtomOldPinnedAppsAtom();
|
resetAtomOldPinnedAppsAtom();
|
||||||
|
resetAtomIsUsingImportExportSettingsAtom()
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isMobile) return;
|
if (!isMobile) return;
|
||||||
@ -1472,18 +1483,20 @@ function App() {
|
|||||||
Get QORT at Q-Trade
|
Get QORT at Q-Trade
|
||||||
</TextP>
|
</TextP>
|
||||||
</AuthenticatedContainerInnerLeft>
|
</AuthenticatedContainerInnerLeft>
|
||||||
<AuthenticatedContainerInnerRight>
|
<AuthenticatedContainerInnerRight sx={{
|
||||||
|
height: "100%",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Spacer height="20px" />
|
<Spacer height="20px" />
|
||||||
<img
|
|
||||||
onClick={() => {
|
|
||||||
setExtstate("download-wallet");
|
|
||||||
setIsOpenDrawerProfile(false);
|
|
||||||
}}
|
|
||||||
src={Download}
|
|
||||||
style={{
|
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<>
|
<>
|
||||||
<Spacer height="20px" />
|
<Spacer height="20px" />
|
||||||
@ -1539,6 +1552,29 @@ function App() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<Spacer height="20px" />
|
||||||
|
<CoreSyncStatus />
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
onClick={() => {
|
||||||
|
setExtstate("download-wallet");
|
||||||
|
setIsOpenDrawerProfile(false);
|
||||||
|
}}
|
||||||
|
src={Download}
|
||||||
|
style={{
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Spacer height="40px" />
|
||||||
|
</Box>
|
||||||
</AuthenticatedContainerInnerRight>
|
</AuthenticatedContainerInnerRight>
|
||||||
</AuthenticatedContainer>
|
</AuthenticatedContainer>
|
||||||
);
|
);
|
||||||
@ -1554,6 +1590,13 @@ function App() {
|
|||||||
backgroundRepeat: desktopViewMode === 'apps' && 'no-repeat',
|
backgroundRepeat: desktopViewMode === 'apps' && 'no-repeat',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<GlobalContext.Provider value={{
|
||||||
|
showTutorial,
|
||||||
|
openTutorialModal,
|
||||||
|
setOpenTutorialModal,
|
||||||
|
downloadResource
|
||||||
|
}}>
|
||||||
|
<Tutorials />
|
||||||
|
|
||||||
{extState === "not-authenticated" && (
|
{extState === "not-authenticated" && (
|
||||||
<NotAuthenticated getRootProps={getRootProps} getInputProps={getInputProps} setExtstate={setExtstate} apiKey={apiKey} globalApiKey={globalApiKey} setApiKey={setApiKey} handleSetGlobalApikey={handleSetGlobalApikey}/>
|
<NotAuthenticated getRootProps={getRootProps} getInputProps={getInputProps} setExtstate={setExtstate} apiKey={apiKey} globalApiKey={globalApiKey} setApiKey={setApiKey} handleSetGlobalApikey={handleSetGlobalApikey}/>
|
||||||
@ -1574,6 +1617,7 @@ function App() {
|
|||||||
show,
|
show,
|
||||||
message,
|
message,
|
||||||
rootHeight,
|
rootHeight,
|
||||||
|
downloadResource
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
@ -2920,6 +2964,7 @@ function App() {
|
|||||||
>
|
>
|
||||||
{renderProfile()}
|
{renderProfile()}
|
||||||
</DrawerComponent>
|
</DrawerComponent>
|
||||||
|
</GlobalContext.Provider>
|
||||||
</AppContainer>
|
</AppContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -6,18 +6,20 @@ const MessageQueueContext = createContext(null);
|
|||||||
export const useMessageQueue = () => useContext(MessageQueueContext);
|
export const useMessageQueue = () => useContext(MessageQueueContext);
|
||||||
|
|
||||||
const uid = new ShortUniqueId({ length: 8 });
|
const uid = new ShortUniqueId({ length: 8 });
|
||||||
let messageQueue = []; // Global message queue
|
|
||||||
|
|
||||||
export const MessageQueueProvider = ({ children }) => {
|
export const MessageQueueProvider = ({ children }) => {
|
||||||
|
const messageQueueRef = useRef([]);
|
||||||
const [queueChats, setQueueChats] = useState({}); // Stores chats and status for display
|
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 maxRetries = 3;
|
|
||||||
const clearStatesMessageQueueProvider = useCallback(() => {
|
const clearStatesMessageQueueProvider = useCallback(() => {
|
||||||
setQueueChats({});
|
setQueueChats({});
|
||||||
messageQueue = [];
|
messageQueueRef.current = [];
|
||||||
isProcessingRef.current = false;
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Promise-based lock to prevent concurrent executions
|
||||||
|
const processingPromiseRef = useRef(Promise.resolve());
|
||||||
|
|
||||||
// Function to add a message to the queue
|
// Function to add a message to the queue
|
||||||
const addToQueue = useCallback((sendMessageFunc, messageObj, type, groupDirectId) => {
|
const addToQueue = useCallback((sendMessageFunc, messageObj, type, groupDirectId) => {
|
||||||
const tempId = uid.rnd();
|
const tempId = uid.rnd();
|
||||||
@ -25,6 +27,7 @@ export const MessageQueueProvider = ({ children }) => {
|
|||||||
...messageObj,
|
...messageObj,
|
||||||
type,
|
type,
|
||||||
groupDirectId,
|
groupDirectId,
|
||||||
|
signature: uid.rnd(),
|
||||||
identifier: tempId,
|
identifier: tempId,
|
||||||
retries: 0, // Retry count for display purposes
|
retries: 0, // Retry count for display purposes
|
||||||
status: 'pending' // Initial status is 'pending'
|
status: 'pending' // Initial status is 'pending'
|
||||||
@ -36,58 +39,36 @@ export const MessageQueueProvider = ({ children }) => {
|
|||||||
[groupDirectId]: [...(prev[groupDirectId] || []), chatData]
|
[groupDirectId]: [...(prev[groupDirectId] || []), chatData]
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Add the message to the global messageQueue
|
// Add the message to the global messageQueueRef
|
||||||
messageQueue = [
|
messageQueueRef.current.push({
|
||||||
...messageQueue,
|
func: sendMessageFunc,
|
||||||
{ func: sendMessageFunc, identifier: tempId, groupDirectId, specialId: messageObj?.message?.specialId }
|
identifier: tempId,
|
||||||
];
|
groupDirectId,
|
||||||
|
specialId: messageObj?.message?.specialId
|
||||||
|
});
|
||||||
|
|
||||||
// Start processing the queue if not already processing
|
// Start processing the queue
|
||||||
processQueue([], groupDirectId);
|
processQueue([], groupDirectId);
|
||||||
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Method to process with new messages and groupDirectId
|
// Function to process the message queue
|
||||||
const processWithNewMessages = (newMessages, groupDirectId) => {
|
const processQueue = useCallback((newMessages = [], groupDirectId) => {
|
||||||
processQueue(newMessages, groupDirectId);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function to process the messageQueue and handle new messages
|
processingPromiseRef.current = processingPromiseRef.current
|
||||||
const processQueue = useCallback(async (newMessages = [], groupDirectId) => {
|
.then(() => processQueueInternal(newMessages, groupDirectId))
|
||||||
// Filter out any message in the queue that matches the specialId from newMessages
|
.catch((err) => console.error('Error in processQueue:', err));
|
||||||
messageQueue = messageQueue.filter((msg) => {
|
}, []);
|
||||||
return !newMessages.some(newMsg => newMsg?.specialId === msg?.specialId);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove any corresponding entries in queueChats for the provided groupDirectId
|
// Internal function to handle queue processing
|
||||||
setQueueChats((prev) => {
|
const processQueueInternal = async (newMessages, groupDirectId) => {
|
||||||
const updatedChats = { ...prev };
|
// Remove any messages from the queue that match the specialId from newMessages
|
||||||
if (updatedChats[groupDirectId]) {
|
|
||||||
// Remove any message in queueChats that has a matching specialId
|
|
||||||
updatedChats[groupDirectId] = updatedChats[groupDirectId].filter((chat) => {
|
|
||||||
|
|
||||||
return !newMessages.some(newMsg => newMsg?.specialId === chat?.message?.specialId);
|
// If the queue is empty, no need to process
|
||||||
});
|
if (messageQueueRef.current.length === 0) return;
|
||||||
|
|
||||||
updatedChats[groupDirectId] = updatedChats[groupDirectId].filter((chat) => {
|
// Process messages sequentially
|
||||||
return chat?.status !== 'failed-permanent'
|
while (messageQueueRef.current.length > 0) {
|
||||||
});
|
const currentMessage = messageQueueRef.current[0]; // Get the first message in the queue
|
||||||
|
|
||||||
// If no more chats for this group, delete the groupDirectId entry
|
|
||||||
if (updatedChats[groupDirectId].length === 0) {
|
|
||||||
delete updatedChats[groupDirectId];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return updatedChats;
|
|
||||||
});
|
|
||||||
|
|
||||||
// If currently processing or the queue is empty, return
|
|
||||||
if (isProcessingRef.current || messageQueue.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
|
|
||||||
const { groupDirectId, identifier } = currentMessage;
|
const { groupDirectId, identifier } = currentMessage;
|
||||||
|
|
||||||
// Update the chat status to 'sending'
|
// Update the chat status to 'sending'
|
||||||
@ -105,20 +86,13 @@ export const MessageQueueProvider = ({ children }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Execute the function stored in the messageQueue
|
// Execute the function stored in the messageQueueRef
|
||||||
|
|
||||||
await currentMessage.func();
|
await currentMessage.func();
|
||||||
|
|
||||||
// Remove the message from the messageQueue after successful sending
|
// Remove the message from the queue after successful sending
|
||||||
messageQueue = messageQueue.slice(1); // Slice here remains for successful messages
|
messageQueueRef.current.shift();
|
||||||
|
|
||||||
// Remove the message from queueChats after success
|
|
||||||
// setQueueChats((prev) => {
|
|
||||||
// const updatedChats = { ...prev };
|
|
||||||
// updatedChats[groupDirectId] = updatedChats[groupDirectId].filter(
|
|
||||||
// (item) => item.identifier !== identifier
|
|
||||||
// );
|
|
||||||
// return updatedChats;
|
|
||||||
// });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Message sending failed', error);
|
console.error('Message sending failed', error);
|
||||||
|
|
||||||
@ -138,26 +112,74 @@ export const MessageQueueProvider = ({ children }) => {
|
|||||||
// Max retries reached, set status to 'failed-permanent'
|
// Max retries reached, set status to 'failed-permanent'
|
||||||
updatedChats[groupDirectId][chatIndex].status = 'failed-permanent';
|
updatedChats[groupDirectId][chatIndex].status = 'failed-permanent';
|
||||||
|
|
||||||
// Remove the message from the messageQueue after max retries
|
// Remove the message from the queue after max retries
|
||||||
messageQueue = messageQueue.slice(1); // Slice for failed messages after max retries
|
messageQueueRef.current.shift();
|
||||||
|
|
||||||
// Remove the message from queueChats after failure
|
|
||||||
// updatedChats[groupDirectId] = updatedChats[groupDirectId].filter(
|
|
||||||
// (item) => item.identifier !== identifier
|
|
||||||
// );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return updatedChats;
|
return updatedChats;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delay between processing each message to avoid overlap
|
// Optional delay between processing messages
|
||||||
await new Promise((res) => setTimeout(res, 5000));
|
// await new Promise((res) => setTimeout(res, 5000));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Method to process with new messages and groupDirectId
|
||||||
|
const processWithNewMessages = (newMessages, groupDirectId) => {
|
||||||
|
let updatedNewMessages = newMessages
|
||||||
|
if (newMessages.length > 0) {
|
||||||
|
// Remove corresponding entries in queueChats for the provided groupDirectId
|
||||||
|
setQueueChats((prev) => {
|
||||||
|
const updatedChats = { ...prev };
|
||||||
|
if (updatedChats[groupDirectId]) {
|
||||||
|
|
||||||
|
updatedNewMessages = newMessages?.map((msg)=> {
|
||||||
|
const findTempMsg = updatedChats[groupDirectId]?.find((msg2)=> msg2?.message?.specialId === msg?.specialId)
|
||||||
|
if(findTempMsg){
|
||||||
|
return {
|
||||||
|
...msg,
|
||||||
|
tempSignature: findTempMsg?.signature
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return msg
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
updatedChats[groupDirectId] = updatedChats[groupDirectId].filter((chat) => {
|
||||||
|
return !newMessages.some(newMsg => newMsg?.specialId === chat?.message?.specialId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove messages with status 'failed-permanent'
|
||||||
|
updatedChats[groupDirectId] = updatedChats[groupDirectId].filter((chat) => {
|
||||||
|
return chat?.status !== 'failed-permanent';
|
||||||
|
});
|
||||||
|
|
||||||
|
// If no more chats for this group, delete the groupDirectId entry
|
||||||
|
if (updatedChats[groupDirectId].length === 0) {
|
||||||
|
delete updatedChats[groupDirectId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updatedChats;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
if(!messageQueueRef.current.find((msg) => msg?.groupDirectId === groupDirectId)){
|
||||||
|
setQueueChats((prev) => {
|
||||||
|
const updatedChats = { ...prev };
|
||||||
|
if (updatedChats[groupDirectId]) {
|
||||||
|
delete updatedChats[groupDirectId]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset the processing lock once all messages are processed
|
return updatedChats
|
||||||
isProcessingRef.current = false;
|
}
|
||||||
}, []);
|
)
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
return updatedNewMessages
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessageQueueContext.Provider value={{ addToQueue, queueChats, clearStatesMessageQueueProvider, processWithNewMessages }}>
|
<MessageQueueContext.Provider value={{ addToQueue, queueChats, clearStatesMessageQueueProvider, processWithNewMessages }}>
|
||||||
|
BIN
src/assets/syncStatus/synced.png
Normal file
BIN
src/assets/syncStatus/synced.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
BIN
src/assets/syncStatus/synced_minting.png
Normal file
BIN
src/assets/syncStatus/synced_minting.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
BIN
src/assets/syncStatus/syncing.png
Normal file
BIN
src/assets/syncStatus/syncing.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
@ -1,4 +1,4 @@
|
|||||||
import { atom } from 'recoil';
|
import { atom, selectorFamily } from 'recoil';
|
||||||
|
|
||||||
|
|
||||||
export const sortablePinnedAppsAtom = atom({
|
export const sortablePinnedAppsAtom = atom({
|
||||||
@ -89,3 +89,39 @@ export const promotionsAtom = atom({
|
|||||||
key: 'promotionsAtom',
|
key: 'promotionsAtom',
|
||||||
default: [],
|
default: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const resourceDownloadControllerAtom = atom({
|
||||||
|
key: 'resourceDownloadControllerAtom',
|
||||||
|
default: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const resourceKeySelector = selectorFamily({
|
||||||
|
key: 'resourceKeySelector',
|
||||||
|
get: (key) => ({ get }) => {
|
||||||
|
const resources = get(resourceDownloadControllerAtom);
|
||||||
|
return resources[key] || null; // Return the value for the key or null if not found
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const blobControllerAtom = atom({
|
||||||
|
key: 'blobControllerAtom',
|
||||||
|
default: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const blobKeySelector = selectorFamily({
|
||||||
|
key: 'blobKeySelector',
|
||||||
|
get: (key) => ({ get }) => {
|
||||||
|
const blobs = get(blobControllerAtom);
|
||||||
|
return blobs[key] || null; // Return the value for the key or null if not found
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const selectedGroupIdAtom = atom({
|
||||||
|
key: 'selectedGroupIdAtom',
|
||||||
|
default: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const isUsingImportExportSettingsAtom = atom({
|
||||||
|
key: 'isUsingImportExportSettingsAtom',
|
||||||
|
default: null,
|
||||||
|
});
|
@ -44,6 +44,9 @@ export function getProtocol(url) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export let groupSecretkeys = {}
|
||||||
|
|
||||||
|
|
||||||
export const gateways = ['ext-node.qortal.link']
|
export const gateways = ['ext-node.qortal.link']
|
||||||
|
|
||||||
|
|
||||||
@ -154,7 +157,7 @@ const getCustomNodesFromStorage = async () => {
|
|||||||
// return `/arbitrary/resources/searchsimple`;
|
// return `/arbitrary/resources/searchsimple`;
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
const getArbitraryEndpoint = async () => {
|
export const getArbitraryEndpoint = async () => {
|
||||||
const apiKey = await getApiKeyFromStorage(); // Retrieve apiKey asynchronously
|
const apiKey = await getApiKeyFromStorage(); // Retrieve apiKey asynchronously
|
||||||
if (apiKey) {
|
if (apiKey) {
|
||||||
return `/arbitrary/resources/searchsimple`;
|
return `/arbitrary/resources/searchsimple`;
|
||||||
@ -3006,6 +3009,41 @@ async function addTimestampGroupAnnouncement({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getTimestampMention() {
|
||||||
|
const wallet = await getSaveWallet();
|
||||||
|
const address = wallet.address0;
|
||||||
|
const key = `enter-mention-timestamp-${address}`;
|
||||||
|
const res = await chrome.storage.local.get([key]);
|
||||||
|
if (res?.[key]) {
|
||||||
|
const parsedData = JSON.parse(res[key]);
|
||||||
|
return parsedData;
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function addTimestampMention({ groupId, timestamp }) {
|
||||||
|
const wallet = await getSaveWallet();
|
||||||
|
const address = wallet.address0;
|
||||||
|
const data = await getTimestampMention();
|
||||||
|
data[groupId] = timestamp;
|
||||||
|
const dataString = JSON.stringify(data);
|
||||||
|
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
chrome.storage.local.set(
|
||||||
|
{ [`enter-mention-timestamp-${address}`]: dataString },
|
||||||
|
() => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
reject(new Error(chrome.runtime.lastError.message));
|
||||||
|
} else {
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function getGroupData() {
|
async function getGroupData() {
|
||||||
const wallet = await getSaveWallet();
|
const wallet = await getSaveWallet();
|
||||||
const address = wallet.address0;
|
const address = wallet.address0;
|
||||||
@ -3459,6 +3497,29 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
case "addTimestampMention": {
|
||||||
|
const { groupId, timestamp } = request.payload;
|
||||||
|
addTimestampMention({ groupId, timestamp })
|
||||||
|
.then((res) => {
|
||||||
|
sendResponse(res);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({ error: error.message });
|
||||||
|
console.error(error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "getTimestampMention": {
|
||||||
|
getTimestampMention()
|
||||||
|
.then((res) => {
|
||||||
|
sendResponse(res);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({ error: error.message });
|
||||||
|
console.error(error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
case "makeAdmin":
|
case "makeAdmin":
|
||||||
{
|
{
|
||||||
const { groupId, qortalAddress } = request.payload;
|
const { groupId, qortalAddress } = request.payload;
|
||||||
@ -4508,7 +4569,7 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => {
|
|||||||
// for announcement notification
|
// for announcement notification
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
}
|
}
|
||||||
|
groupSecretkeys = {}
|
||||||
const wallet = await getSaveWallet();
|
const wallet = await getSaveWallet();
|
||||||
const address = wallet.address0;
|
const address = wallet.address0;
|
||||||
const key1 = `tempPublish-${address}`;
|
const key1 = `tempPublish-${address}`;
|
||||||
|
36
src/common/ErrorBoundary.tsx
Normal file
36
src/common/ErrorBoundary.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React, { ReactNode } from 'react'
|
||||||
|
|
||||||
|
interface ErrorBoundaryProps {
|
||||||
|
children: ReactNode
|
||||||
|
fallback: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorBoundaryState {
|
||||||
|
hasError: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
class ErrorBoundary extends React.Component<
|
||||||
|
ErrorBoundaryProps,
|
||||||
|
ErrorBoundaryState
|
||||||
|
> {
|
||||||
|
state: ErrorBoundaryState = {
|
||||||
|
hasError: false
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(_: Error): ErrorBoundaryState {
|
||||||
|
return { hasError: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
|
||||||
|
// You can log the error and errorInfo here, for example, to an error reporting service.
|
||||||
|
console.error('Error caught in ErrorBoundary:', error, errorInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): React.ReactNode {
|
||||||
|
if (this.state.hasError) return this.props.fallback
|
||||||
|
|
||||||
|
return this.props.children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorBoundary
|
168
src/common/useFetchResources.tsx
Normal file
168
src/common/useFetchResources.tsx
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import React, { useCallback, useRef } from 'react';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
import { resourceDownloadControllerAtom } from '../atoms/global';
|
||||||
|
import { getBaseApiReact } from '../App';
|
||||||
|
|
||||||
|
export const useFetchResources = () => {
|
||||||
|
const [resources, setResources] = useRecoilState(resourceDownloadControllerAtom);
|
||||||
|
|
||||||
|
const downloadResource = useCallback(({ service, name, identifier }, build) => {
|
||||||
|
setResources((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[`${service}-${name}-${identifier}`]: {
|
||||||
|
...(prev[`${service}-${name}-${identifier}`] || {}),
|
||||||
|
service,
|
||||||
|
name,
|
||||||
|
identifier,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
let isCalling = false;
|
||||||
|
let percentLoaded = 0;
|
||||||
|
let timer = 24;
|
||||||
|
let tries = 0;
|
||||||
|
let calledFirstTime = false
|
||||||
|
let intervalId
|
||||||
|
let timeoutId
|
||||||
|
const callFunction = async ()=> {
|
||||||
|
if (isCalling) return;
|
||||||
|
isCalling = true;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let res
|
||||||
|
|
||||||
|
if(!build){
|
||||||
|
const urlFirstTime = `${getBaseApiReact()}/arbitrary/resource/status/${service}/${name}/${identifier}`;
|
||||||
|
const resCall = await fetch(urlFirstTime, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
res = await resCall.json()
|
||||||
|
if(tries > 18 ){
|
||||||
|
if(intervalId){
|
||||||
|
clearInterval(intervalId)
|
||||||
|
}
|
||||||
|
if(timeoutId){
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
}
|
||||||
|
setResources((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[`${service}-${name}-${identifier}`]: {
|
||||||
|
...(prev[`${service}-${name}-${identifier}`] || {}),
|
||||||
|
status: {
|
||||||
|
...res,
|
||||||
|
status: 'FAILED_TO_DOWNLOAD',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tries = tries + 1
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if(build || (calledFirstTime === false && res?.status !== 'READY')){
|
||||||
|
const url = `${getBaseApiReact()}/arbitrary/resource/properties/${service}/${name}/${identifier}?build=true`;
|
||||||
|
const resCall = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
res = await resCall.json();
|
||||||
|
|
||||||
|
}
|
||||||
|
calledFirstTime = true
|
||||||
|
isCalling = false;
|
||||||
|
|
||||||
|
if (res.localChunkCount) {
|
||||||
|
if (res.percentLoaded) {
|
||||||
|
if (res.percentLoaded === percentLoaded && res.percentLoaded !== 100) {
|
||||||
|
timer = timer - 5;
|
||||||
|
} else {
|
||||||
|
timer = 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timer < 0) {
|
||||||
|
timer = 24;
|
||||||
|
isCalling = true;
|
||||||
|
|
||||||
|
// Update Recoil state for refetching
|
||||||
|
setResources((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[`${service}-${name}-${identifier}`]: {
|
||||||
|
...(prev[`${service}-${name}-${identifier}`] || {}),
|
||||||
|
status: {
|
||||||
|
...res,
|
||||||
|
status: 'REFETCHING',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
isCalling = false;
|
||||||
|
downloadResource({ name, service, identifier }, true);
|
||||||
|
}, 25000);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
percentLoaded = res.percentLoaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Recoil state for progress
|
||||||
|
setResources((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[`${service}-${name}-${identifier}`]: {
|
||||||
|
...(prev[`${service}-${name}-${identifier}`] || {}),
|
||||||
|
status: res,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if progress is 100% and clear interval if true
|
||||||
|
if (res?.status === 'READY') {
|
||||||
|
if(intervalId){
|
||||||
|
clearInterval(intervalId);
|
||||||
|
|
||||||
|
}
|
||||||
|
if(timeoutId){
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
}
|
||||||
|
// Update Recoil state for completion
|
||||||
|
setResources((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[`${service}-${name}-${identifier}`]: {
|
||||||
|
...(prev[`${service}-${name}-${identifier}`] || {}),
|
||||||
|
status: res,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if(res?.status === 'DOWNLOADED'){
|
||||||
|
const url = `${getBaseApiReact()}/arbitrary/resource/status/${service}/${name}/${identifier}?build=true`;
|
||||||
|
const resCall = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
res = await resCall.json();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callFunction()
|
||||||
|
intervalId = setInterval(async () => {
|
||||||
|
callFunction()
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during resource fetch:', error);
|
||||||
|
}
|
||||||
|
}, [setResources]);
|
||||||
|
|
||||||
|
return { downloadResource };
|
||||||
|
};
|
@ -3,7 +3,7 @@ import { AppViewer } from './AppViewer';
|
|||||||
import Frame from 'react-frame-component';
|
import Frame from 'react-frame-component';
|
||||||
import { MyContext, isMobile } from '../../App';
|
import { MyContext, isMobile } from '../../App';
|
||||||
|
|
||||||
const AppViewerContainer = React.forwardRef(({ app, isSelected, hide }, ref) => {
|
const AppViewerContainer = React.forwardRef(({ app, isSelected, hide, customHeight }, ref) => {
|
||||||
const { rootHeight } = useContext(MyContext);
|
const { rootHeight } = useContext(MyContext);
|
||||||
|
|
||||||
|
|
||||||
@ -36,7 +36,7 @@ const AppViewerContainer = React.forwardRef(({ app, isSelected, hide }, ref) =>
|
|||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
display: (!isSelected || hide) && 'none',
|
display: (!isSelected || hide) && 'none',
|
||||||
height: !isMobile ? '100vh' : `calc(${rootHeight} - 60px - 45px)`,
|
height: customHeight ? customHeight : !isMobile ? '100vh' : `calc(${rootHeight} - 60px - 45px)`,
|
||||||
border: 'none',
|
border: 'none',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
@ -366,7 +366,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
<Save isDesktop />
|
<Save isDesktop myName={myName} />
|
||||||
{mode !== 'home' && (
|
{mode !== 'home' && (
|
||||||
<AppsNavBarDesktop />
|
<AppsNavBarDesktop />
|
||||||
|
|
||||||
|
@ -33,8 +33,12 @@ import {
|
|||||||
sortablePinnedAppsAtom,
|
sortablePinnedAppsAtom,
|
||||||
} from "../../atoms/global";
|
} from "../../atoms/global";
|
||||||
|
|
||||||
export function saveToLocalStorage(key, subKey, newValue) {
|
export function saveToLocalStorage(key, subKey, newValue, otherRootData = {}, deleteWholeKey) {
|
||||||
try {
|
try {
|
||||||
|
if(deleteWholeKey){
|
||||||
|
localStorage.setItem(key, null);
|
||||||
|
return
|
||||||
|
}
|
||||||
// Fetch existing data
|
// Fetch existing data
|
||||||
const existingData = localStorage.getItem(key);
|
const existingData = localStorage.getItem(key);
|
||||||
let combinedData = {};
|
let combinedData = {};
|
||||||
@ -45,12 +49,14 @@ export function saveToLocalStorage(key, subKey, newValue) {
|
|||||||
// Merge with the new data under the subKey
|
// Merge with the new data under the subKey
|
||||||
combinedData = {
|
combinedData = {
|
||||||
...parsedData,
|
...parsedData,
|
||||||
|
...otherRootData,
|
||||||
timestamp: Date.now(), // Update the root timestamp
|
timestamp: Date.now(), // Update the root timestamp
|
||||||
[subKey]: newValue, // Assuming the data is an array
|
[subKey]: newValue, // Assuming the data is an array
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// If no existing data, just use the new data under the subKey
|
// If no existing data, just use the new data under the subKey
|
||||||
combinedData = {
|
combinedData = {
|
||||||
|
...otherRootData,
|
||||||
timestamp: Date.now(), // Set the initial root timestamp
|
timestamp: Date.now(), // Set the initial root timestamp
|
||||||
[subKey]: newValue,
|
[subKey]: newValue,
|
||||||
};
|
};
|
||||||
@ -63,7 +69,6 @@ export function saveToLocalStorage(key, subKey, newValue) {
|
|||||||
console.error("Error saving to localStorage:", error);
|
console.error("Error saving to localStorage:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppsNavBar = () => {
|
export const AppsNavBar = () => {
|
||||||
const [tabs, setTabs] = useState([]);
|
const [tabs, setTabs] = useState([]);
|
||||||
const [selectedTab, setSelectedTab] = useState(null);
|
const [selectedTab, setSelectedTab] = useState(null);
|
||||||
|
@ -3,6 +3,105 @@ import FileSaver from 'file-saver';
|
|||||||
import { executeEvent } from '../../utils/events';
|
import { executeEvent } from '../../utils/events';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
import { navigationControllerAtom } from '../../atoms/global';
|
import { navigationControllerAtom } from '../../atoms/global';
|
||||||
|
import { extractComponents } from '../Chat/MessageDisplay';
|
||||||
|
|
||||||
|
|
||||||
|
const missingFieldsFunc = (data, requiredFields)=> {
|
||||||
|
const missingFields: string[] = [];
|
||||||
|
requiredFields.forEach((field) => {
|
||||||
|
if (!data[field]) {
|
||||||
|
missingFields.push(field);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (missingFields.length > 0) {
|
||||||
|
const missingFieldsString = missingFields.join(", ");
|
||||||
|
const errorMsg = `Missing fields: ${missingFieldsString}`;
|
||||||
|
throw new Error(errorMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const encode = (value) => encodeURIComponent(value.trim()); // Helper to encode values
|
||||||
|
const buildQueryParams = (data) => {
|
||||||
|
const allowedParams= ["name", "service", "identifier", "mimeType", "fileName", "encryptionType", "key"]
|
||||||
|
return Object.entries(data)
|
||||||
|
.map(([key, value]) => {
|
||||||
|
if (value === undefined || value === null || value === false || !allowedParams.includes(key)) return null; // Skip null, undefined, or false
|
||||||
|
if (typeof value === "boolean") return `${key}=${value}`; // Handle boolean values
|
||||||
|
return `${key}=${encode(value)}`; // Encode other values
|
||||||
|
})
|
||||||
|
.filter(Boolean) // Remove null values
|
||||||
|
.join("&"); // Join with `&`
|
||||||
|
};
|
||||||
|
export const createAndCopyEmbedLink = async (data) => {
|
||||||
|
const requiredFields = [
|
||||||
|
"type",
|
||||||
|
];
|
||||||
|
const missingFields: string[] = [];
|
||||||
|
requiredFields.forEach((field) => {
|
||||||
|
if (!data[field]) {
|
||||||
|
missingFields.push(field);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (missingFields.length > 0) {
|
||||||
|
const missingFieldsString = missingFields.join(", ");
|
||||||
|
const errorMsg = `Missing fields: ${missingFieldsString}`;
|
||||||
|
throw new Error(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
switch (data.type) {
|
||||||
|
case "POLL": {
|
||||||
|
missingFieldsFunc(data, [
|
||||||
|
"type",
|
||||||
|
"name"
|
||||||
|
])
|
||||||
|
|
||||||
|
const queryParams = [
|
||||||
|
`name=${encode(data.name)}`,
|
||||||
|
data.ref ? `ref=${encode(data.ref)}` : null, // Add only if ref exists
|
||||||
|
]
|
||||||
|
.filter(Boolean) // Remove null values
|
||||||
|
.join("&"); // Join with `&`
|
||||||
|
const link = `qortal://use-embed/POLL?${queryParams}`
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(link);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('Failed to copy to clipboard.')
|
||||||
|
}
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
case "IMAGE":
|
||||||
|
case "ATTACHMENT":
|
||||||
|
{
|
||||||
|
missingFieldsFunc(data, [
|
||||||
|
"type",
|
||||||
|
"name",
|
||||||
|
"service",
|
||||||
|
"identifier"
|
||||||
|
])
|
||||||
|
if(data?.encryptionType === 'private' && !data?.key){
|
||||||
|
throw new Error('For an encrypted resource, you must provide the key to create the shared link')
|
||||||
|
}
|
||||||
|
const queryParams = buildQueryParams(data)
|
||||||
|
|
||||||
|
const link = `qortal://use-embed/${data.type}?${queryParams}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(link);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('Failed to copy to clipboard.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error('Invalid type')
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
class Semaphore {
|
class Semaphore {
|
||||||
constructor(count) {
|
constructor(count) {
|
||||||
this.count = count
|
this.count = count
|
||||||
@ -140,7 +239,7 @@ const UIQortalRequests = [
|
|||||||
'GET_TX_ACTIVITY_SUMMARY', 'GET_FOREIGN_FEE', 'UPDATE_FOREIGN_FEE',
|
'GET_TX_ACTIVITY_SUMMARY', 'GET_FOREIGN_FEE', 'UPDATE_FOREIGN_FEE',
|
||||||
'GET_SERVER_CONNECTION_HISTORY', 'SET_CURRENT_FOREIGN_SERVER',
|
'GET_SERVER_CONNECTION_HISTORY', 'SET_CURRENT_FOREIGN_SERVER',
|
||||||
'ADD_FOREIGN_SERVER', 'REMOVE_FOREIGN_SERVER', 'GET_DAY_SUMMARY', 'CREATE_TRADE_BUY_ORDER',
|
'ADD_FOREIGN_SERVER', 'REMOVE_FOREIGN_SERVER', 'GET_DAY_SUMMARY', 'CREATE_TRADE_BUY_ORDER',
|
||||||
'CREATE_TRADE_SELL_ORDER', 'CANCEL_TRADE_SELL_ORDER', 'IS_USING_GATEWAY', 'ADMIN_ACTION'
|
'CREATE_TRADE_SELL_ORDER', 'CANCEL_TRADE_SELL_ORDER', 'IS_USING_GATEWAY', 'ADMIN_ACTION', 'SIGN_TRANSACTION', 'DECRYPT_QORTAL_GROUP_DATA'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
@ -350,6 +449,37 @@ isDOMContentLoaded: false
|
|||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const openNewTab = async (data) => {
|
||||||
|
const requiredFields = [
|
||||||
|
"qortalLink",
|
||||||
|
];
|
||||||
|
const missingFields: string[] = [];
|
||||||
|
requiredFields.forEach((field) => {
|
||||||
|
if (!data[field]) {
|
||||||
|
missingFields.push(field);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (missingFields.length > 0) {
|
||||||
|
const missingFieldsString = missingFields.join(", ");
|
||||||
|
const errorMsg = `Missing fields: ${missingFieldsString}`;
|
||||||
|
throw new Error(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = extractComponents(data.qortalLink);
|
||||||
|
if (res) {
|
||||||
|
const { service, name, identifier, path } = res;
|
||||||
|
if(!service && !name) throw new Error('Invalid qortal link')
|
||||||
|
executeEvent("addTab", { data: { service, name, identifier, path } });
|
||||||
|
executeEvent("open-apps-mode", { });
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid qortal link")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
const resetHistory = useCallback(()=> {
|
const resetHistory = useCallback(()=> {
|
||||||
setHistory({
|
setHistory({
|
||||||
@ -395,7 +525,7 @@ isDOMContentLoaded: false
|
|||||||
} else if (
|
} else if (
|
||||||
event?.data?.action === 'PUBLISH_MULTIPLE_QDN_RESOURCES' ||
|
event?.data?.action === 'PUBLISH_MULTIPLE_QDN_RESOURCES' ||
|
||||||
event?.data?.action === 'PUBLISH_QDN_RESOURCE' ||
|
event?.data?.action === 'PUBLISH_QDN_RESOURCE' ||
|
||||||
event?.data?.action === 'ENCRYPT_DATA' || event?.data?.action === 'SAVE_FILE'
|
event?.data?.action === 'ENCRYPT_DATA' || event?.data?.action === 'SAVE_FILE' || event?.data?.action === 'ENCRYPT_DATA_WITH_SHARING_KEY' || event?.data?.action === 'ENCRYPT_QORTAL_GROUP_DATA'
|
||||||
|
|
||||||
) {
|
) {
|
||||||
let data;
|
let data;
|
||||||
@ -469,6 +599,33 @@ isDOMContentLoaded: false
|
|||||||
name: event?.data?.payload?.name
|
name: event?.data?.payload?.name
|
||||||
} }, '*'
|
} }, '*'
|
||||||
);
|
);
|
||||||
|
} else if(event?.data?.action === 'OPEN_NEW_TAB'){
|
||||||
|
try {
|
||||||
|
await openNewTab(event?.data?.payload)
|
||||||
|
event.ports[0].postMessage({
|
||||||
|
result: true,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
event.ports[0].postMessage({
|
||||||
|
result: null,
|
||||||
|
error: error?.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if(event?.data?.action === 'CREATE_AND_COPY_EMBED_LINK'){
|
||||||
|
try {
|
||||||
|
const link = await createAndCopyEmbedLink(event?.data?.payload)
|
||||||
|
event.ports[0].postMessage({
|
||||||
|
result: link,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
event.ports[0].postMessage({
|
||||||
|
result: null,
|
||||||
|
error: error?.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ const uid = new ShortUniqueId({ length: 5 });
|
|||||||
export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDirect, setNewChat, getTimestampEnterChat, myName, balance, close, setMobileViewModeKeepOpen}) => {
|
export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDirect, setNewChat, getTimestampEnterChat, myName, balance, close, setMobileViewModeKeepOpen}) => {
|
||||||
const { queueChats, addToQueue, processWithNewMessages} = useMessageQueue();
|
const { queueChats, addToQueue, processWithNewMessages} = useMessageQueue();
|
||||||
const [isFocusedParent, setIsFocusedParent] = useState(false);
|
const [isFocusedParent, setIsFocusedParent] = useState(false);
|
||||||
|
const [messageSize, setMessageSize] = useState(0)
|
||||||
|
|
||||||
const [messages, setMessages] = useState([])
|
const [messages, setMessages] = useState([])
|
||||||
const [isSending, setIsSending] = useState(false)
|
const [isSending, setIsSending] = useState(false)
|
||||||
@ -43,6 +44,9 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi
|
|||||||
const timeoutIdRef = useRef(null);
|
const timeoutIdRef = useRef(null);
|
||||||
const groupSocketTimeoutRef = useRef(null);
|
const groupSocketTimeoutRef = useRef(null);
|
||||||
const [replyMessage, setReplyMessage] = useState(null)
|
const [replyMessage, setReplyMessage] = useState(null)
|
||||||
|
const [onEditMessage, setOnEditMessage] = useState(null)
|
||||||
|
const [chatReferences, setChatReferences] = useState({})
|
||||||
|
|
||||||
const setEditorRef = (editorInstance) => {
|
const setEditorRef = (editorInstance) => {
|
||||||
editorRef.current = editorInstance;
|
editorRef.current = editorInstance;
|
||||||
};
|
};
|
||||||
@ -65,10 +69,19 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi
|
|||||||
const tempMessages = useMemo(()=> {
|
const tempMessages = useMemo(()=> {
|
||||||
if(!selectedDirect?.address) return []
|
if(!selectedDirect?.address) return []
|
||||||
if(queueChats[selectedDirect?.address]){
|
if(queueChats[selectedDirect?.address]){
|
||||||
return queueChats[selectedDirect?.address]
|
return queueChats[selectedDirect?.address]?.filter((item)=> !item?.chatReference)
|
||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
}, [selectedDirect?.address, queueChats])
|
}, [selectedDirect?.address, queueChats])
|
||||||
|
|
||||||
|
const tempChatReferences = useMemo(()=> {
|
||||||
|
if(!selectedDirect?.address) return []
|
||||||
|
if(queueChats[selectedDirect?.address]){
|
||||||
|
return queueChats[selectedDirect?.address]?.filter((item)=> !!item?.chatReference)
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}, [selectedDirect?.address, queueChats])
|
||||||
|
|
||||||
useEffect(()=> {
|
useEffect(()=> {
|
||||||
if(selectedDirect?.address){
|
if(selectedDirect?.address){
|
||||||
publicKeyOfRecipientRef.current = selectedDirect?.address
|
publicKeyOfRecipientRef.current = selectedDirect?.address
|
||||||
@ -104,37 +117,63 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi
|
|||||||
chrome?.runtime?.sendMessage({ action: "decryptDirect", payload: {
|
chrome?.runtime?.sendMessage({ action: "decryptDirect", payload: {
|
||||||
data: encryptedMessages,
|
data: encryptedMessages,
|
||||||
involvingAddress: selectedDirect?.address
|
involvingAddress: selectedDirect?.address
|
||||||
}}, (response) => {
|
}}, (decryptResponse) => {
|
||||||
|
|
||||||
if (!response?.error) {
|
if (!decryptResponse?.error) {
|
||||||
|
const response = processWithNewMessages(decryptResponse, selectedDirect?.address);
|
||||||
|
res(response);
|
||||||
|
|
||||||
processWithNewMessages(response, selectedDirect?.address)
|
|
||||||
|
|
||||||
res(response)
|
|
||||||
if (isInitiated) {
|
if (isInitiated) {
|
||||||
|
const formatted = response.filter((rawItem) => !rawItem?.chatReference).map((item) => ({
|
||||||
const formatted = response.map((item: any)=> {
|
|
||||||
return {
|
|
||||||
...item,
|
...item,
|
||||||
id: item.signature,
|
id: item.signature,
|
||||||
text: item.message,
|
text: item.message,
|
||||||
unread: item?.sender === myAddress ? false : true
|
unread: item?.sender === myAddress ? false : true,
|
||||||
|
}));
|
||||||
|
setMessages((prev) => [...prev, ...formatted]);
|
||||||
|
setChatReferences((prev) => {
|
||||||
|
const organizedChatReferences = { ...prev };
|
||||||
|
|
||||||
|
response.filter((rawItem) => !!rawItem?.chatReference && rawItem?.type === 'edit').forEach((item) => {
|
||||||
|
try {
|
||||||
|
organizedChatReferences[item.chatReference] = {
|
||||||
|
...(organizedChatReferences[item.chatReference] || {}),
|
||||||
|
edit: item
|
||||||
|
};
|
||||||
|
} catch(error){
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
setMessages((prev)=> [...prev, ...formatted])
|
return organizedChatReferences
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
const formatted = response.map((item: any)=> {
|
hasInitialized.current = true;
|
||||||
return {
|
const formatted = response.filter((rawItem) => !rawItem?.chatReference)
|
||||||
|
.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
id: item.signature,
|
id: item.signature,
|
||||||
text: item.message,
|
text: item.message,
|
||||||
unread: false
|
unread: false,
|
||||||
}
|
}));
|
||||||
} )
|
setMessages(formatted);
|
||||||
setMessages(formatted)
|
|
||||||
hasInitialized.current = true
|
setChatReferences((prev) => {
|
||||||
|
const organizedChatReferences = { ...prev };
|
||||||
|
|
||||||
|
response.filter((rawItem) => !!rawItem?.chatReference && rawItem?.type === 'edit').forEach((item) => {
|
||||||
|
try {
|
||||||
|
organizedChatReferences[item.chatReference] = {
|
||||||
|
...(organizedChatReferences[item.chatReference] || {}),
|
||||||
|
edit: item
|
||||||
|
};
|
||||||
|
} catch(error){
|
||||||
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
return organizedChatReferences
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
rej(response.error)
|
rej(response.error)
|
||||||
});
|
});
|
||||||
@ -291,6 +330,8 @@ const sendChatDirect = async ({ chatReference = undefined, messageText, otherDat
|
|||||||
}
|
}
|
||||||
const clearEditorContent = () => {
|
const clearEditorContent = () => {
|
||||||
if (editorRef.current) {
|
if (editorRef.current) {
|
||||||
|
setMessageSize(0)
|
||||||
|
|
||||||
editorRef.current.chain().focus().clearContent().run();
|
editorRef.current.chain().focus().clearContent().run();
|
||||||
if(isMobile){
|
if(isMobile){
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -305,9 +346,28 @@ const clearEditorContent = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editorRef?.current) return;
|
||||||
|
const handleUpdate = () => {
|
||||||
|
const htmlContent = editorRef?.current.getHTML();
|
||||||
|
const stringified = JSON.stringify(htmlContent);
|
||||||
|
const size = new Blob([stringified]).size;
|
||||||
|
setMessageSize(size + 200);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add a listener for the editorRef?.current's content updates
|
||||||
|
editorRef?.current.on('update', handleUpdate);
|
||||||
|
|
||||||
|
// Cleanup the listener on unmount
|
||||||
|
return () => {
|
||||||
|
editorRef?.current.off('update', handleUpdate);
|
||||||
|
};
|
||||||
|
}, [editorRef?.current]);
|
||||||
|
|
||||||
|
|
||||||
const sendMessage = async ()=> {
|
const sendMessage = async ()=> {
|
||||||
try {
|
try {
|
||||||
|
if(messageSize > 4000) return
|
||||||
|
|
||||||
|
|
||||||
if(+balance < 4) throw new Error('You need at least 4 QORT to send a message')
|
if(+balance < 4) throw new Error('You need at least 4 QORT to send a message')
|
||||||
@ -330,12 +390,16 @@ const clearEditorContent = () => {
|
|||||||
if (replyMessage?.chatReference) {
|
if (replyMessage?.chatReference) {
|
||||||
repliedTo = replyMessage?.chatReference
|
repliedTo = replyMessage?.chatReference
|
||||||
}
|
}
|
||||||
|
let chatReference = onEditMessage?.signature
|
||||||
|
|
||||||
const otherData = {
|
const otherData = {
|
||||||
|
...(onEditMessage?.decryptedData || {}),
|
||||||
specialId: uid.rnd(),
|
specialId: uid.rnd(),
|
||||||
repliedTo
|
repliedTo: onEditMessage ? onEditMessage?.repliedTo : repliedTo,
|
||||||
|
type: chatReference ? 'edit' : ''
|
||||||
}
|
}
|
||||||
const sendMessageFunc = async () => {
|
const sendMessageFunc = async () => {
|
||||||
await sendChatDirect({ chatReference: undefined, messageText: htmlContent, otherData}, selectedDirect?.address, publicKeyOfRecipient, false)
|
return await sendChatDirect({ chatReference, messageText: htmlContent, otherData}, selectedDirect?.address, publicKeyOfRecipient, false)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -343,13 +407,13 @@ const clearEditorContent = () => {
|
|||||||
// Add the function to the queue
|
// Add the function to the queue
|
||||||
const messageObj = {
|
const messageObj = {
|
||||||
message: {
|
message: {
|
||||||
text: htmlContent,
|
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
senderName: myName,
|
senderName: myName,
|
||||||
sender: myAddress,
|
sender: myAddress,
|
||||||
...(otherData || {})
|
...(otherData || {}),
|
||||||
|
text: htmlContent,
|
||||||
},
|
},
|
||||||
|
chatReference
|
||||||
}
|
}
|
||||||
addToQueue(sendMessageFunc, messageObj, 'chat-direct',
|
addToQueue(sendMessageFunc, messageObj, 'chat-direct',
|
||||||
selectedDirect?.address );
|
selectedDirect?.address );
|
||||||
@ -358,6 +422,8 @@ const clearEditorContent = () => {
|
|||||||
}, 150);
|
}, 150);
|
||||||
clearEditorContent()
|
clearEditorContent()
|
||||||
setReplyMessage(null)
|
setReplyMessage(null)
|
||||||
|
setOnEditMessage(null)
|
||||||
|
|
||||||
}
|
}
|
||||||
// send chat message
|
// send chat message
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -375,10 +441,21 @@ const clearEditorContent = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onReply = useCallback((message)=> {
|
const onReply = useCallback((message)=> {
|
||||||
|
if(onEditMessage){
|
||||||
|
clearEditorContent()
|
||||||
|
}
|
||||||
setReplyMessage(message)
|
setReplyMessage(message)
|
||||||
|
setOnEditMessage(null)
|
||||||
editorRef?.current?.chain().focus()
|
editorRef?.current?.chain().focus()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const onEdit = useCallback((message)=> {
|
||||||
|
setOnEditMessage(message)
|
||||||
|
setReplyMessage(null)
|
||||||
|
editorRef.current.chain().focus().setContent(message?.text).run();
|
||||||
|
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
@ -485,7 +562,7 @@ const clearEditorContent = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ChatList onReply={onReply} chatId={selectedDirect?.address} initialMessages={messages} myAddress={myAddress} tempMessages={tempMessages}/>
|
<ChatList chatReferences={chatReferences} onEdit={onEdit} onReply={onReply} chatId={selectedDirect?.address} initialMessages={messages} myAddress={myAddress} tempMessages={tempMessages} tempChatReferences={tempChatReferences}/>
|
||||||
|
|
||||||
|
|
||||||
<div style={{
|
<div style={{
|
||||||
@ -525,6 +602,30 @@ const clearEditorContent = () => {
|
|||||||
<ButtonBase
|
<ButtonBase
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setReplyMessage(null)
|
setReplyMessage(null)
|
||||||
|
setOnEditMessage(null)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExitIcon />
|
||||||
|
</ButtonBase>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{onEditMessage && (
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '5px',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
width: '100%'
|
||||||
|
}}>
|
||||||
|
<ReplyPreview isEdit message={onEditMessage} />
|
||||||
|
|
||||||
|
<ButtonBase
|
||||||
|
onClick={() => {
|
||||||
|
setReplyMessage(null)
|
||||||
|
setOnEditMessage(null)
|
||||||
|
|
||||||
|
clearEditorContent()
|
||||||
|
|
||||||
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ExitIcon />
|
<ExitIcon />
|
||||||
@ -533,6 +634,20 @@ const clearEditorContent = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Tiptap isFocusedParent={isFocusedParent} setEditorRef={setEditorRef} onEnter={sendMessage} isChat disableEnter={isMobile ? true : false} setIsFocusedParent={setIsFocusedParent}/>
|
<Tiptap isFocusedParent={isFocusedParent} setEditorRef={setEditorRef} onEnter={sendMessage} isChat disableEnter={isMobile ? true : false} setIsFocusedParent={setIsFocusedParent}/>
|
||||||
|
{messageSize > 750 && (
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
position: 'relative',
|
||||||
|
}}>
|
||||||
|
<Typography sx={{
|
||||||
|
fontSize: '12px',
|
||||||
|
color: messageSize > 4000 ? 'var(--danger)' : 'unset'
|
||||||
|
}}>{`Your message size is of ${messageSize} bytes out of a maximum of 4000`}</Typography>
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
@ -15,16 +15,19 @@ import { CustomizedSnackbars } from '../Snackbar/Snackbar'
|
|||||||
import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from '../../constants/codes'
|
import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from '../../constants/codes'
|
||||||
import { useMessageQueue } from '../../MessageQueueContext'
|
import { useMessageQueue } from '../../MessageQueueContext'
|
||||||
import { executeEvent } from '../../utils/events'
|
import { executeEvent } from '../../utils/events'
|
||||||
import { Box, ButtonBase } from '@mui/material'
|
import { Box, ButtonBase, Divider, Typography } from '@mui/material'
|
||||||
import ShortUniqueId from "short-unique-id";
|
import ShortUniqueId from "short-unique-id";
|
||||||
import { ReplyPreview } from './MessageItem'
|
import { ReplyPreview } from './MessageItem'
|
||||||
import { ExitIcon } from '../../assets/Icons/ExitIcon'
|
import { ExitIcon } from '../../assets/Icons/ExitIcon'
|
||||||
import { RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS } from '../../constants/resourceTypes'
|
import { RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS } from '../../constants/resourceTypes'
|
||||||
import { isExtMsg } from '../../background'
|
import { isExtMsg } from '../../background'
|
||||||
|
import { throttle } from 'lodash'
|
||||||
|
import AppViewerContainer from '../Apps/AppViewerContainer'
|
||||||
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
|
|
||||||
const uid = new ShortUniqueId({ length: 5 });
|
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, isPrivate, hideView}) => {
|
||||||
const [messages, setMessages] = useState([])
|
const [messages, setMessages] = useState([])
|
||||||
const [chatReferences, setChatReferences] = useState({})
|
const [chatReferences, setChatReferences] = useState({})
|
||||||
const [isSending, setIsSending] = useState(false)
|
const [isSending, setIsSending] = useState(false)
|
||||||
@ -41,9 +44,76 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
|||||||
const timeoutIdRef = useRef(null); // Timeout ID reference
|
const timeoutIdRef = useRef(null); // Timeout ID reference
|
||||||
const groupSocketTimeoutRef = useRef(null); // Group Socket Timeout reference
|
const groupSocketTimeoutRef = useRef(null); // Group Socket Timeout reference
|
||||||
const editorRef = useRef(null);
|
const editorRef = useRef(null);
|
||||||
|
const [isOpenQManager, setIsOpenQManager] = useState(null)
|
||||||
|
const [onEditMessage, setOnEditMessage] = useState(null)
|
||||||
|
const [messageSize, setMessageSize] = useState(0)
|
||||||
|
|
||||||
const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue();
|
const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue();
|
||||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||||
|
|
||||||
|
const lastReadTimestamp = useRef(null)
|
||||||
|
const handleUpdateRef = useRef(null);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const getTimestampEnterChat = async () => {
|
||||||
|
try {
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
chrome?.runtime?.sendMessage(
|
||||||
|
{
|
||||||
|
action: "getTimestampEnterChat",
|
||||||
|
},
|
||||||
|
(response) => {
|
||||||
|
if (!response?.error && selectedGroup && response[selectedGroup]) {
|
||||||
|
lastReadTimestamp.current = response[selectedGroup]
|
||||||
|
chrome?.runtime?.sendMessage({
|
||||||
|
action: "addTimestampEnterChat",
|
||||||
|
payload: {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
groupId: selectedGroup,
|
||||||
|
},
|
||||||
|
}, (response2)=> {
|
||||||
|
setTimeout(() => {
|
||||||
|
getTimestampEnterChatParent();
|
||||||
|
}, 200);
|
||||||
|
})
|
||||||
|
res(response);
|
||||||
|
}
|
||||||
|
rej(response.error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} catch (error) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(()=> {
|
||||||
|
getTimestampEnterChat()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const openQManager = useCallback(()=> {
|
||||||
|
setIsOpenQManager(true)
|
||||||
|
}, [])
|
||||||
|
const members = useMemo(() => {
|
||||||
|
const uniqueMembers = new Set();
|
||||||
|
|
||||||
|
messages.forEach((message) => {
|
||||||
|
if (message?.senderName) {
|
||||||
|
uniqueMembers.add(message?.senderName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(uniqueMembers);
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
const onEdit = useCallback((message)=> {
|
||||||
|
setOnEditMessage(message)
|
||||||
|
setReplyMessage(null)
|
||||||
|
editorRef.current.chain().focus().setContent(message?.messageText || message?.text).run();
|
||||||
|
|
||||||
|
}, [])
|
||||||
|
|
||||||
const triggerRerender = () => {
|
const triggerRerender = () => {
|
||||||
forceUpdate(); // Trigger re-render by updating the state
|
forceUpdate(); // Trigger re-render by updating the state
|
||||||
};
|
};
|
||||||
@ -122,7 +192,6 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
|||||||
try {
|
try {
|
||||||
if(!secretKeyRef.current){
|
if(!secretKeyRef.current){
|
||||||
checkForFirstSecretKeyNotification(encryptedMessages)
|
checkForFirstSecretKeyNotification(encryptedMessages)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
return new Promise((res, rej)=> {
|
return new Promise((res, rej)=> {
|
||||||
chrome?.runtime?.sendMessage({ action: "decryptSingle", payload: {
|
chrome?.runtime?.sendMessage({ action: "decryptSingle", payload: {
|
||||||
@ -130,163 +199,188 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
|||||||
secretKeyObject: secretKey
|
secretKeyObject: secretKey
|
||||||
}}, (response) => {
|
}}, (response) => {
|
||||||
if (!response?.error) {
|
if (!response?.error) {
|
||||||
const filterUImessages = encryptedMessages.filter((item)=> !isExtMsg(item.data))
|
const filterUIMessages = encryptedMessages.filter((item) => !isExtMsg(item.data));
|
||||||
const decodedUIMessages = decodeBase64ForUIChatMessages(filterUImessages)
|
const decodedUIMessages = decodeBase64ForUIChatMessages(filterUIMessages);
|
||||||
|
|
||||||
const combineUIAndExtensionMsgs = [...decodedUIMessages, ...response]
|
const combineUIAndExtensionMsgsBefore = [...decodedUIMessages, ...response];
|
||||||
processWithNewMessages(combineUIAndExtensionMsgs?.map((item)=> {
|
const combineUIAndExtensionMsgs = processWithNewMessages(
|
||||||
return {
|
combineUIAndExtensionMsgsBefore.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
...(item?.decryptedData || {})
|
...(item?.decryptedData || {}),
|
||||||
}
|
})),
|
||||||
}), selectedGroup)
|
selectedGroup
|
||||||
res(combineUIAndExtensionMsgs)
|
);
|
||||||
|
res(combineUIAndExtensionMsgs);
|
||||||
|
|
||||||
if (isInitiated) {
|
if (isInitiated) {
|
||||||
|
|
||||||
const formatted = combineUIAndExtensionMsgs.filter((rawItem)=> !rawItem?.chatReference).map((item: any)=> {
|
const formatted = combineUIAndExtensionMsgs
|
||||||
|
.filter((rawItem) => !rawItem?.chatReference)
|
||||||
|
.map((item) => {
|
||||||
|
const additionalFields = item?.data === 'NDAwMQ==' ? {
|
||||||
|
text: "<p>First group key created.</p>"
|
||||||
|
} : {}
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
id: item.signature,
|
id: item.signature,
|
||||||
text: item?.decryptedData?.message || "",
|
text: item?.decryptedData?.message || "",
|
||||||
repliedTo: item?.repliedTo || item?.decryptedData?.repliedTo,
|
repliedTo: item?.repliedTo || item?.decryptedData?.repliedTo,
|
||||||
unread: item?.sender === myAddress ? false : !!item?.chatReference ? false : true,
|
unread: item?.sender === myAddress ? false : !!item?.chatReference ? false : true,
|
||||||
isNotEncrypted: !!item?.messageText
|
isNotEncrypted: !!item?.messageText,
|
||||||
|
...additionalFields
|
||||||
}
|
}
|
||||||
} )
|
});
|
||||||
setMessages((prev)=> [...prev, ...formatted])
|
setMessages((prev) => [...prev, ...formatted]);
|
||||||
|
|
||||||
|
|
||||||
setChatReferences((prev) => {
|
setChatReferences((prev) => {
|
||||||
let organizedChatReferences = { ...prev };
|
const organizedChatReferences = { ...prev };
|
||||||
|
|
||||||
combineUIAndExtensionMsgs
|
combineUIAndExtensionMsgs
|
||||||
.filter((rawItem) => rawItem && rawItem.chatReference && rawItem.decryptedData?.type === 'reaction')
|
.filter((rawItem) => rawItem && rawItem.chatReference && (rawItem?.decryptedData?.type === "reaction" || rawItem?.decryptedData?.type === "edit" || rawItem?.type === "edit" || rawItem?.isEdited || rawItem?.type === "reaction"))
|
||||||
.forEach((item) => {
|
.forEach((item) => {
|
||||||
try {
|
try {
|
||||||
const content = item.decryptedData?.content;
|
if(item?.decryptedData?.type === "edit"){
|
||||||
|
organizedChatReferences[item.chatReference] = {
|
||||||
|
...(organizedChatReferences[item.chatReference] || {}),
|
||||||
|
edit: item.decryptedData,
|
||||||
|
};
|
||||||
|
} else if(item?.type === "edit" || item?.isEdited){
|
||||||
|
organizedChatReferences[item.chatReference] = {
|
||||||
|
...(organizedChatReferences[item.chatReference] || {}),
|
||||||
|
edit: item,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const content = item?.content || item.decryptedData?.content;
|
||||||
const sender = item.sender;
|
const sender = item.sender;
|
||||||
const newTimestamp = item.timestamp;
|
const newTimestamp = item.timestamp;
|
||||||
const contentState = item.decryptedData?.contentState;
|
const contentState = item?.contentState || item.decryptedData?.contentState;
|
||||||
|
|
||||||
if (!content || typeof content !== 'string' || !sender || typeof sender !== 'string' || !newTimestamp) {
|
if (!content || typeof content !== "string" || !sender || typeof sender !== "string" || !newTimestamp) {
|
||||||
console.warn("Invalid content, sender, or timestamp in reaction data", item);
|
console.warn("Invalid content, sender, or timestamp in reaction data", item);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize chat reference and reactions if not present
|
|
||||||
organizedChatReferences[item.chatReference] = {
|
organizedChatReferences[item.chatReference] = {
|
||||||
...(organizedChatReferences[item.chatReference] || {}),
|
...(organizedChatReferences[item.chatReference] || {}),
|
||||||
reactions: organizedChatReferences[item.chatReference]?.reactions || {}
|
reactions: organizedChatReferences[item.chatReference]?.reactions || {},
|
||||||
};
|
};
|
||||||
|
|
||||||
organizedChatReferences[item.chatReference].reactions[content] =
|
organizedChatReferences[item.chatReference].reactions[content] =
|
||||||
organizedChatReferences[item.chatReference].reactions[content] || [];
|
organizedChatReferences[item.chatReference].reactions[content] || [];
|
||||||
|
|
||||||
// Remove any existing reactions from the same sender before adding the new one
|
|
||||||
let latestTimestampForSender = null;
|
let latestTimestampForSender = null;
|
||||||
|
|
||||||
// Track the latest reaction timestamp for the same content and sender
|
|
||||||
organizedChatReferences[item.chatReference].reactions[content] =
|
organizedChatReferences[item.chatReference].reactions[content] =
|
||||||
organizedChatReferences[item.chatReference].reactions[content].filter((reaction) => {
|
organizedChatReferences[item.chatReference].reactions[content].filter((reaction) => {
|
||||||
if (reaction.sender === sender) {
|
if (reaction.sender === sender) {
|
||||||
// Track the latest timestamp for this sender
|
|
||||||
latestTimestampForSender = Math.max(latestTimestampForSender || 0, reaction.timestamp);
|
latestTimestampForSender = Math.max(latestTimestampForSender || 0, reaction.timestamp);
|
||||||
}
|
}
|
||||||
return reaction.sender !== sender;
|
return reaction.sender !== sender;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Compare with the latest tracked timestamp for this sender
|
|
||||||
if (latestTimestampForSender && newTimestamp < latestTimestampForSender) {
|
if (latestTimestampForSender && newTimestamp < latestTimestampForSender) {
|
||||||
// Ignore this item if it's older than the latest known reaction
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the new reaction only if contentState is true
|
|
||||||
if (contentState !== false) {
|
if (contentState !== false) {
|
||||||
organizedChatReferences[item.chatReference].reactions[content].push(item);
|
organizedChatReferences[item.chatReference].reactions[content].push(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the reactions for a specific content are empty, clean up the object
|
|
||||||
if (organizedChatReferences[item.chatReference].reactions[content].length === 0) {
|
if (organizedChatReferences[item.chatReference].reactions[content].length === 0) {
|
||||||
delete organizedChatReferences[item.chatReference].reactions[content];
|
delete organizedChatReferences[item.chatReference].reactions[content];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error processing reaction item:", error, item);
|
console.error("Error processing reaction/edit item:", error, item);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return organizedChatReferences;
|
return organizedChatReferences;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
const formatted = combineUIAndExtensionMsgs.filter((rawItem)=> !rawItem?.chatReference).map((item: any)=> {
|
let firstUnreadFound = false;
|
||||||
|
const formatted = combineUIAndExtensionMsgs
|
||||||
|
.filter((rawItem) => !rawItem?.chatReference)
|
||||||
|
.map((item) => {
|
||||||
|
const additionalFields = item?.data === 'NDAwMQ==' ? {
|
||||||
|
text: "<p>First group key created.</p>"
|
||||||
|
} : {}
|
||||||
|
const divide = lastReadTimestamp.current && !firstUnreadFound && item.timestamp > lastReadTimestamp.current && myAddress !== item?.sender;
|
||||||
|
|
||||||
|
if(divide){
|
||||||
|
firstUnreadFound = true
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
id: item.signature,
|
id: item.signature,
|
||||||
text: item?.decryptedData?.message || "",
|
text: item?.decryptedData?.message || "",
|
||||||
repliedTo: item?.repliedTo || item?.decryptedData?.repliedTo,
|
repliedTo: item?.repliedTo || item?.decryptedData?.repliedTo,
|
||||||
isNotEncrypted: !!item?.messageText,
|
isNotEncrypted: !!item?.messageText,
|
||||||
unread: false
|
unread: false,
|
||||||
|
divide,
|
||||||
|
...additionalFields
|
||||||
}
|
}
|
||||||
} )
|
});
|
||||||
setMessages(formatted)
|
setMessages(formatted);
|
||||||
|
|
||||||
setChatReferences((prev) => {
|
setChatReferences((prev) => {
|
||||||
let organizedChatReferences = { ...prev };
|
const organizedChatReferences = { ...prev };
|
||||||
|
|
||||||
combineUIAndExtensionMsgs
|
combineUIAndExtensionMsgs
|
||||||
.filter((rawItem) => rawItem && rawItem.chatReference && rawItem.decryptedData?.type === 'reaction')
|
.filter((rawItem) => rawItem && rawItem.chatReference && (rawItem?.decryptedData?.type === "reaction" || rawItem?.decryptedData?.type === "edit" || rawItem?.type === "edit" || rawItem?.isEdited || rawItem?.type === "reaction"))
|
||||||
.forEach((item) => {
|
.forEach((item) => {
|
||||||
try {
|
try {
|
||||||
const content = item.decryptedData?.content;
|
if(item?.decryptedData?.type === "edit"){
|
||||||
|
organizedChatReferences[item.chatReference] = {
|
||||||
|
...(organizedChatReferences[item.chatReference] || {}),
|
||||||
|
edit: item.decryptedData,
|
||||||
|
};
|
||||||
|
} else if(item?.type === "edit" || item?.isEdited){
|
||||||
|
organizedChatReferences[item.chatReference] = {
|
||||||
|
...(organizedChatReferences[item.chatReference] || {}),
|
||||||
|
edit: item,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const content = item?.content || item.decryptedData?.content;
|
||||||
const sender = item.sender;
|
const sender = item.sender;
|
||||||
const newTimestamp = item.timestamp;
|
const newTimestamp = item.timestamp;
|
||||||
const contentState = item.decryptedData?.contentState;
|
const contentState = item?.contentState || item.decryptedData?.contentState;
|
||||||
|
|
||||||
if (!content || typeof content !== 'string' || !sender || typeof sender !== 'string' || !newTimestamp) {
|
if (!content || typeof content !== "string" || !sender || typeof sender !== "string" || !newTimestamp) {
|
||||||
console.warn("Invalid content, sender, or timestamp in reaction data", item);
|
console.warn("Invalid content, sender, or timestamp in reaction data", item);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize chat reference and reactions if not present
|
|
||||||
organizedChatReferences[item.chatReference] = {
|
organizedChatReferences[item.chatReference] = {
|
||||||
...(organizedChatReferences[item.chatReference] || {}),
|
...(organizedChatReferences[item.chatReference] || {}),
|
||||||
reactions: organizedChatReferences[item.chatReference]?.reactions || {}
|
reactions: organizedChatReferences[item.chatReference]?.reactions || {},
|
||||||
};
|
};
|
||||||
|
|
||||||
organizedChatReferences[item.chatReference].reactions[content] =
|
organizedChatReferences[item.chatReference].reactions[content] =
|
||||||
organizedChatReferences[item.chatReference].reactions[content] || [];
|
organizedChatReferences[item.chatReference].reactions[content] || [];
|
||||||
|
|
||||||
// Remove any existing reactions from the same sender before adding the new one
|
|
||||||
let latestTimestampForSender = null;
|
let latestTimestampForSender = null;
|
||||||
|
|
||||||
// Track the latest reaction timestamp for the same content and sender
|
|
||||||
organizedChatReferences[item.chatReference].reactions[content] =
|
organizedChatReferences[item.chatReference].reactions[content] =
|
||||||
organizedChatReferences[item.chatReference].reactions[content].filter((reaction) => {
|
organizedChatReferences[item.chatReference].reactions[content].filter((reaction) => {
|
||||||
if (reaction.sender === sender) {
|
if (reaction.sender === sender) {
|
||||||
// Track the latest timestamp for this sender
|
|
||||||
latestTimestampForSender = Math.max(latestTimestampForSender || 0, reaction.timestamp);
|
latestTimestampForSender = Math.max(latestTimestampForSender || 0, reaction.timestamp);
|
||||||
}
|
}
|
||||||
return reaction.sender !== sender;
|
return reaction.sender !== sender;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Compare with the latest tracked timestamp for this sender
|
|
||||||
if (latestTimestampForSender && newTimestamp < latestTimestampForSender) {
|
if (latestTimestampForSender && newTimestamp < latestTimestampForSender) {
|
||||||
// Ignore this item if it's older than the latest known reaction
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the new reaction only if contentState is true
|
|
||||||
if (contentState !== false) {
|
if (contentState !== false) {
|
||||||
organizedChatReferences[item.chatReference].reactions[content].push(item);
|
organizedChatReferences[item.chatReference].reactions[content].push(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the reactions for a specific content are empty, clean up the object
|
|
||||||
if (organizedChatReferences[item.chatReference].reactions[content].length === 0) {
|
if (organizedChatReferences[item.chatReference].reactions[content].length === 0) {
|
||||||
delete organizedChatReferences[item.chatReference].reactions[content];
|
delete organizedChatReferences[item.chatReference].reactions[content];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error processing reaction item:", error, item);
|
console.error("Error processing reaction item:", error, item);
|
||||||
}
|
}
|
||||||
@ -294,14 +388,9 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
|||||||
|
|
||||||
return organizedChatReferences;
|
return organizedChatReferences;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rej(response.error)
|
rej(response.error);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -386,10 +475,11 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
|||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
initWebsocketMessageGroup()
|
initWebsocketMessageGroup()
|
||||||
}
|
}
|
||||||
}, [triedToFetchSecretKey, secretKey])
|
}, [triedToFetchSecretKey, secretKey, isPrivate])
|
||||||
|
|
||||||
useEffect(()=> {
|
useEffect(()=> {
|
||||||
if(!secretKey || hasInitializedWebsocket.current) return
|
if(isPrivate === null) return
|
||||||
|
if(isPrivate === false || !secretKey || hasInitializedWebsocket.current) return
|
||||||
forceCloseWebSocket()
|
forceCloseWebSocket()
|
||||||
setMessages([])
|
setMessages([])
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
@ -399,7 +489,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
|||||||
}, 6000);
|
}, 6000);
|
||||||
initWebsocketMessageGroup()
|
initWebsocketMessageGroup()
|
||||||
hasInitializedWebsocket.current = true
|
hasInitializedWebsocket.current = true
|
||||||
}, [secretKey])
|
}, [secretKey, isPrivate])
|
||||||
|
|
||||||
|
|
||||||
useEffect(()=> {
|
useEffect(()=> {
|
||||||
@ -471,6 +561,8 @@ const clearEditorContent = () => {
|
|||||||
|
|
||||||
const sendMessage = async ()=> {
|
const sendMessage = async ()=> {
|
||||||
try {
|
try {
|
||||||
|
if(messageSize > 4000) return
|
||||||
|
if(isPrivate === null) throw new Error('Unable to determine if group is private')
|
||||||
if(isSending) return
|
if(isSending) return
|
||||||
if(+balance < 4) throw new Error('You need at least 4 QORT to send a message')
|
if(+balance < 4) throw new Error('You need at least 4 QORT to send a message')
|
||||||
pauseAllQueues()
|
pauseAllQueues()
|
||||||
@ -478,8 +570,10 @@ const clearEditorContent = () => {
|
|||||||
const htmlContent = editorRef.current.getHTML();
|
const htmlContent = editorRef.current.getHTML();
|
||||||
|
|
||||||
if(!htmlContent?.trim() || htmlContent?.trim() === '<p></p>') return
|
if(!htmlContent?.trim() || htmlContent?.trim() === '<p></p>') return
|
||||||
|
|
||||||
|
|
||||||
setIsSending(true)
|
setIsSending(true)
|
||||||
const message = htmlContent
|
const message = isPrivate === false ? editorRef.current.getJSON() : htmlContent
|
||||||
const secretKeyObject = await getSecretKey(false, true)
|
const secretKeyObject = await getSecretKey(false, true)
|
||||||
|
|
||||||
let repliedTo = replyMessage?.signature
|
let repliedTo = replyMessage?.signature
|
||||||
@ -487,33 +581,42 @@ const clearEditorContent = () => {
|
|||||||
if (replyMessage?.chatReference) {
|
if (replyMessage?.chatReference) {
|
||||||
repliedTo = replyMessage?.chatReference
|
repliedTo = replyMessage?.chatReference
|
||||||
}
|
}
|
||||||
|
let chatReference = onEditMessage?.signature
|
||||||
|
|
||||||
|
const publicData = isPrivate ? {} : {
|
||||||
|
isEdited : chatReference ? true : false,
|
||||||
|
}
|
||||||
const otherData = {
|
const otherData = {
|
||||||
|
repliedTo,
|
||||||
|
...(onEditMessage?.decryptedData || {}),
|
||||||
|
type: chatReference ? 'edit' : '',
|
||||||
specialId: uid.rnd(),
|
specialId: uid.rnd(),
|
||||||
repliedTo
|
...publicData
|
||||||
}
|
}
|
||||||
const objectMessage = {
|
const objectMessage = {
|
||||||
message,
|
...(otherData || {}),
|
||||||
...(otherData || {})
|
[isPrivate ? 'message' : 'messageText']: message,
|
||||||
|
version: 3
|
||||||
}
|
}
|
||||||
const message64: any = await objectToBase64(objectMessage)
|
const message64: any = await objectToBase64(objectMessage)
|
||||||
|
|
||||||
const encryptSingle = await encryptChatMessage(message64, secretKeyObject)
|
const encryptSingle = isPrivate === false ? JSON.stringify(objectMessage) : await encryptChatMessage(message64, secretKeyObject)
|
||||||
// const res = await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle})
|
// const res = await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle})
|
||||||
|
|
||||||
const sendMessageFunc = async () => {
|
const sendMessageFunc = async () => {
|
||||||
await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle})
|
return await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle, chatReference})
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add the function to the queue
|
// Add the function to the queue
|
||||||
const messageObj = {
|
const messageObj = {
|
||||||
message: {
|
message: {
|
||||||
text: message,
|
text: htmlContent,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
senderName: myName,
|
senderName: myName,
|
||||||
sender: myAddress,
|
sender: myAddress,
|
||||||
...(otherData || {})
|
...(otherData || {})
|
||||||
},
|
},
|
||||||
|
chatReference
|
||||||
}
|
}
|
||||||
addToQueue(sendMessageFunc, messageObj, 'chat',
|
addToQueue(sendMessageFunc, messageObj, 'chat',
|
||||||
selectedGroup );
|
selectedGroup );
|
||||||
@ -522,6 +625,7 @@ const clearEditorContent = () => {
|
|||||||
}, 150);
|
}, 150);
|
||||||
clearEditorContent()
|
clearEditorContent()
|
||||||
setReplyMessage(null)
|
setReplyMessage(null)
|
||||||
|
setOnEditMessage(null)
|
||||||
}
|
}
|
||||||
// send chat message
|
// send chat message
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -538,6 +642,38 @@ const clearEditorContent = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editorRef?.current) return;
|
||||||
|
|
||||||
|
handleUpdateRef.current = throttle(async () => {
|
||||||
|
try {
|
||||||
|
if(isPrivate){
|
||||||
|
const htmlContent = editorRef.current.getHTML();
|
||||||
|
const message64 = await objectToBase64(JSON.stringify(htmlContent))
|
||||||
|
const secretKeyObject = await getSecretKey(false, true)
|
||||||
|
const encryptSingle = await encryptChatMessage(message64, secretKeyObject)
|
||||||
|
setMessageSize((encryptSingle?.length || 0) + 200);
|
||||||
|
} else {
|
||||||
|
const htmlContent = editorRef.current.getJSON();
|
||||||
|
const message = JSON.stringify(htmlContent)
|
||||||
|
const size = new Blob([message]).size
|
||||||
|
setMessageSize(size + 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// calc size error
|
||||||
|
}
|
||||||
|
}, 1200);
|
||||||
|
|
||||||
|
const currentEditor = editorRef.current;
|
||||||
|
|
||||||
|
currentEditor.on("update", handleUpdateRef.current);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
currentEditor.off("update", handleUpdateRef.current);
|
||||||
|
};
|
||||||
|
}, [editorRef, setMessageSize, isPrivate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hide) {
|
if (hide) {
|
||||||
setTimeout(() => setIsMoved(true), 500); // Wait for the fade-out to complete before moving
|
setTimeout(() => setIsMoved(true), 500); // Wait for the fade-out to complete before moving
|
||||||
@ -547,7 +683,11 @@ const clearEditorContent = () => {
|
|||||||
}, [hide]);
|
}, [hide]);
|
||||||
|
|
||||||
const onReply = useCallback((message)=> {
|
const onReply = useCallback((message)=> {
|
||||||
|
if(onEditMessage){
|
||||||
|
clearEditorContent()
|
||||||
|
}
|
||||||
setReplyMessage(message)
|
setReplyMessage(message)
|
||||||
|
setOnEditMessage(null)
|
||||||
editorRef?.current?.chain().focus()
|
editorRef?.current?.chain().focus()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -576,11 +716,11 @@ const clearEditorContent = () => {
|
|||||||
}
|
}
|
||||||
const message64: any = await objectToBase64(objectMessage)
|
const message64: any = await objectToBase64(objectMessage)
|
||||||
const reactiontypeNumber = RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS
|
const reactiontypeNumber = RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS
|
||||||
const encryptSingle = await encryptChatMessage(message64, secretKeyObject, reactiontypeNumber)
|
const encryptSingle = isPrivate === false ? JSON.stringify(objectMessage) : await encryptChatMessage(message64, secretKeyObject, reactiontypeNumber)
|
||||||
// const res = await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle})
|
// const res = await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle})
|
||||||
|
|
||||||
const sendMessageFunc = async () => {
|
const sendMessageFunc = async () => {
|
||||||
await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle, chatReference: chatMessage.signature})
|
return await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle, chatReference: chatMessage.signature})
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add the function to the queue
|
// Add the function to the queue
|
||||||
@ -617,6 +757,8 @@ const clearEditorContent = () => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
console.log('isPrivate', isPrivate)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
height: isMobile ? '100%' : '100%',
|
height: isMobile ? '100%' : '100%',
|
||||||
@ -628,17 +770,16 @@ const clearEditorContent = () => {
|
|||||||
left: hide && '-100000px',
|
left: hide && '-100000px',
|
||||||
}}>
|
}}>
|
||||||
|
|
||||||
<ChatList onReply={onReply} chatId={selectedGroup} initialMessages={messages} myAddress={myAddress} tempMessages={tempMessages} handleReaction={handleReaction} chatReferences={chatReferences} tempChatReferences={tempChatReferences}/>
|
<ChatList isPrivate={isPrivate} hasSecretKey={!!secretKey} openQManager={openQManager} enableMentions onReply={onReply} onEdit={onEdit} chatId={selectedGroup} initialMessages={messages} myAddress={myAddress} tempMessages={tempMessages} handleReaction={handleReaction} chatReferences={chatReferences} tempChatReferences={tempChatReferences} members={members} myName={myName} selectedGroup={selectedGroup} />
|
||||||
|
|
||||||
|
|
||||||
|
{(!!secretKey || isPrivate === false) && (
|
||||||
<div style={{
|
<div style={{
|
||||||
// position: 'fixed',
|
// position: 'fixed',
|
||||||
// bottom: '0px',
|
// bottom: '0px',
|
||||||
backgroundColor: "#232428",
|
backgroundColor: "#232428",
|
||||||
minHeight: isMobile ? '0px' : '150px',
|
minHeight: isMobile ? '0px' : '150px',
|
||||||
maxHeight: isMobile ? 'auto' : '400px',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'row',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
@ -649,12 +790,15 @@ const clearEditorContent = () => {
|
|||||||
zIndex: isFocusedParent ? 5 : 'unset',
|
zIndex: isFocusedParent ? 5 : 'unset',
|
||||||
flexShrink: 0
|
flexShrink: 0
|
||||||
}}>
|
}}>
|
||||||
|
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
flexGrow: isMobile && 1,
|
flexGrow: isMobile && 1,
|
||||||
overflow: !isMobile && "auto",
|
overflow: !isMobile && "auto",
|
||||||
flexShrink: 0
|
flexShrink: 0,
|
||||||
|
width: 'calc(100% - 100px)',
|
||||||
|
justifyContent: 'flex-end'
|
||||||
}}>
|
}}>
|
||||||
{replyMessage && (
|
{replyMessage && (
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
@ -668,6 +812,31 @@ const clearEditorContent = () => {
|
|||||||
<ButtonBase
|
<ButtonBase
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setReplyMessage(null)
|
setReplyMessage(null)
|
||||||
|
|
||||||
|
setOnEditMessage(null)
|
||||||
|
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExitIcon />
|
||||||
|
</ButtonBase>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{onEditMessage && (
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '5px',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
width: '100%'
|
||||||
|
}}>
|
||||||
|
<ReplyPreview isEdit message={onEditMessage} />
|
||||||
|
|
||||||
|
<ButtonBase
|
||||||
|
onClick={() => {
|
||||||
|
setReplyMessage(null)
|
||||||
|
setOnEditMessage(null)
|
||||||
|
|
||||||
|
clearEditorContent()
|
||||||
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ExitIcon />
|
<ExitIcon />
|
||||||
@ -676,40 +845,35 @@ 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>
|
{messageSize > 750 && (
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
width: '100&',
|
width: '100%',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
position: 'relative',
|
||||||
|
}}>
|
||||||
|
<Typography sx={{
|
||||||
|
fontSize: '12px',
|
||||||
|
color: messageSize > 4000 ? 'var(--danger)' : 'unset'
|
||||||
|
}}>{`Your message size is of ${messageSize} bytes out of a maximum of 4000`}</Typography>
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
width: '100px',
|
||||||
gap: '10px',
|
gap: '10px',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}>
|
}}>
|
||||||
{isFocusedParent && (
|
|
||||||
<CustomButton
|
<CustomButton
|
||||||
onClick={()=> {
|
onClick={()=> {
|
||||||
if(isSending) return
|
|
||||||
setIsFocusedParent(false)
|
|
||||||
clearEditorContent()
|
|
||||||
// Unfocus the editor
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
marginTop: 'auto',
|
|
||||||
alignSelf: 'center',
|
|
||||||
cursor: isSending ? 'default' : 'pointer',
|
|
||||||
background: 'red',
|
|
||||||
flexShrink: 0,
|
|
||||||
padding: isMobile && '5px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
|
|
||||||
{` Close`}
|
|
||||||
</CustomButton>
|
|
||||||
|
|
||||||
)}
|
|
||||||
<CustomButton
|
|
||||||
onClick={()=> {
|
|
||||||
if(isSending) return
|
if(isSending) return
|
||||||
sendMessage()
|
sendMessage()
|
||||||
}}
|
}}
|
||||||
@ -719,7 +883,9 @@ const clearEditorContent = () => {
|
|||||||
cursor: isSending ? 'default' : 'pointer',
|
cursor: isSending ? 'default' : 'pointer',
|
||||||
background: isSending && 'rgba(0, 0, 0, 0.8)',
|
background: isSending && 'rgba(0, 0, 0, 0.8)',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
padding: isMobile && '5px',
|
padding: '5px',
|
||||||
|
width: '100px',
|
||||||
|
minWidth: 'auto'
|
||||||
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -740,8 +906,57 @@ const clearEditorContent = () => {
|
|||||||
</CustomButton>
|
</CustomButton>
|
||||||
|
|
||||||
</Box>
|
</Box>
|
||||||
{/* <button onClick={sendMessage}>send</button> */}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{isOpenQManager !== null && (
|
||||||
|
<Box sx={{
|
||||||
|
position: 'fixed',
|
||||||
|
height: '600px',
|
||||||
|
|
||||||
|
maxHeight: '100vh',
|
||||||
|
width: '400px',
|
||||||
|
maxWidth: '100vw',
|
||||||
|
backgroundColor: '#27282c',
|
||||||
|
zIndex: 100,
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
overflow: 'hidden',
|
||||||
|
borderTopLeftRadius: '10px',
|
||||||
|
borderTopRightRadius: '10px',
|
||||||
|
display: isOpenQManager === true ? 'block' : 'none',
|
||||||
|
boxShadow: 4,
|
||||||
|
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
height: '40px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '5px',
|
||||||
|
|
||||||
|
justifyContent: 'space-between'
|
||||||
|
}}>
|
||||||
|
<Typography>Q-Manager</Typography>
|
||||||
|
<ButtonBase onClick={()=> {
|
||||||
|
setIsOpenQManager(false)
|
||||||
|
}}><CloseIcon sx={{
|
||||||
|
color: 'white'
|
||||||
|
}} /></ButtonBase>
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
|
<AppViewerContainer customHeight="560px" app={{
|
||||||
|
tabId: '5558588',
|
||||||
|
name: 'Q-Manager',
|
||||||
|
service: 'APP',
|
||||||
|
path: `?groupId=${selectedGroup}`
|
||||||
|
}} isSelected />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
{/* <ChatContainerComp messages={formatMessages} /> */}
|
{/* <ChatContainerComp messages={formatMessages} /> */}
|
||||||
<LoadingSnackbar open={isLoading} info={{
|
<LoadingSnackbar open={isLoading} info={{
|
||||||
message: "Loading chat... please wait."
|
message: "Loading chat... please wait."
|
||||||
|
@ -9,6 +9,9 @@ import { useVirtualizer } from "@tanstack/react-virtual";
|
|||||||
import { MessageItem } from "./MessageItem";
|
import { MessageItem } from "./MessageItem";
|
||||||
import { subscribeToEvent, unsubscribeFromEvent } from "../../utils/events";
|
import { subscribeToEvent, unsubscribeFromEvent } from "../../utils/events";
|
||||||
import { useInView } from "react-intersection-observer";
|
import { useInView } from "react-intersection-observer";
|
||||||
|
import { Box, Typography } from "@mui/material";
|
||||||
|
import { ChatOptions } from "./ChatOptions";
|
||||||
|
import ErrorBoundary from "../../common/ErrorBoundary";
|
||||||
|
|
||||||
export const ChatList = ({
|
export const ChatList = ({
|
||||||
initialMessages,
|
initialMessages,
|
||||||
@ -16,24 +19,73 @@ export const ChatList = ({
|
|||||||
tempMessages,
|
tempMessages,
|
||||||
chatId,
|
chatId,
|
||||||
onReply,
|
onReply,
|
||||||
|
onEdit,
|
||||||
handleReaction,
|
handleReaction,
|
||||||
chatReferences,
|
chatReferences,
|
||||||
tempChatReferences,
|
tempChatReferences,
|
||||||
|
members,
|
||||||
|
myName,
|
||||||
|
selectedGroup,
|
||||||
|
enableMentions,
|
||||||
|
openQManager,
|
||||||
|
hasSecretKey,
|
||||||
|
isPrivate
|
||||||
}) => {
|
}) => {
|
||||||
const parentRef = useRef();
|
const parentRef = useRef();
|
||||||
const [messages, setMessages] = useState(initialMessages);
|
const [messages, setMessages] = useState(initialMessages);
|
||||||
const [showScrollButton, setShowScrollButton] = useState(false);
|
const [showScrollButton, setShowScrollButton] = useState(false);
|
||||||
|
const [showScrollDownButton, setShowScrollDownButton] = useState(false);
|
||||||
const hasLoadedInitialRef = useRef(false);
|
const hasLoadedInitialRef = useRef(false);
|
||||||
const isAtBottomRef = useRef(true);
|
const scrollingIntervalRef = useRef(null);
|
||||||
// const [ref, inView] = useInView({
|
const lastSeenUnreadMessageTimestamp = useRef(null);
|
||||||
// threshold: 0.7
|
console.log('messages', messages)
|
||||||
// })
|
// Initialize the virtualizer
|
||||||
|
const rowVirtualizer = useVirtualizer({
|
||||||
|
count: messages.length,
|
||||||
|
getItemKey: (index) => messages[index]?.tempSignature || messages[index].signature,
|
||||||
|
getScrollElement: () => parentRef?.current,
|
||||||
|
estimateSize: useCallback(() => 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
|
||||||
|
});
|
||||||
|
|
||||||
// useEffect(() => {
|
const isAtBottom = useMemo(()=> {
|
||||||
// if (inView) {
|
if (parentRef.current && rowVirtualizer?.isScrolling !== undefined) {
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = parentRef.current;
|
||||||
|
const atBottom = scrollTop + clientHeight >= scrollHeight - 10; // Adjust threshold as needed
|
||||||
|
return atBottom
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
|
||||||
|
}, [rowVirtualizer?.isScrolling])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!parentRef.current || rowVirtualizer?.isScrolling === undefined) return;
|
||||||
|
if(isAtBottom){
|
||||||
|
if (scrollingIntervalRef.current) {
|
||||||
|
clearTimeout(scrollingIntervalRef.current);
|
||||||
|
}
|
||||||
|
setShowScrollDownButton(false);
|
||||||
|
return;
|
||||||
|
} else
|
||||||
|
if (rowVirtualizer?.isScrolling) {
|
||||||
|
if (scrollingIntervalRef.current) {
|
||||||
|
clearTimeout(scrollingIntervalRef.current);
|
||||||
|
}
|
||||||
|
setShowScrollDownButton(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = parentRef.current;
|
||||||
|
const atBottom = scrollHeight - scrollTop - clientHeight <= 300;
|
||||||
|
if (!atBottom) {
|
||||||
|
scrollingIntervalRef.current = setTimeout(() => {
|
||||||
|
setShowScrollDownButton(true);
|
||||||
|
}, 250);
|
||||||
|
} else {
|
||||||
|
setShowScrollDownButton(false);
|
||||||
|
}
|
||||||
|
}, [rowVirtualizer?.isScrolling, isAtBottom]);
|
||||||
|
|
||||||
// }
|
|
||||||
// }, [inView])
|
|
||||||
// Update message list with unique signatures and tempMessages
|
// Update message list with unique signatures and tempMessages
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let uniqueInitialMessagesMap = new Map();
|
let uniqueInitialMessagesMap = new Map();
|
||||||
@ -56,29 +108,39 @@ export const ChatList = ({
|
|||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const hasUnreadMessages = totalMessages.some(
|
const hasUnreadMessages = totalMessages.some(
|
||||||
(msg) => msg.unread && !msg?.chatReference
|
(msg) => msg.unread && !msg?.chatReference && !msg?.isTemp && (!msg?.chatReference && msg?.timestamp > lastSeenUnreadMessageTimestamp.current || 0)
|
||||||
);
|
);
|
||||||
if (parentRef.current) {
|
if (parentRef.current) {
|
||||||
const { scrollTop, scrollHeight, clientHeight } = parentRef.current;
|
const { scrollTop, scrollHeight, clientHeight } = parentRef.current;
|
||||||
const atBottom = scrollTop + clientHeight >= scrollHeight - 10; // Adjust threshold as needed
|
const atBottom = scrollTop + clientHeight >= scrollHeight - 10; // Adjust threshold as needed
|
||||||
if (!atBottom && hasUnreadMessages) {
|
if (!atBottom && hasUnreadMessages) {
|
||||||
setShowScrollButton(hasUnreadMessages);
|
setShowScrollButton(hasUnreadMessages);
|
||||||
|
setShowScrollDownButton(false);
|
||||||
} else {
|
} else {
|
||||||
handleMessageSeen();
|
handleMessageSeen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!hasLoadedInitialRef.current) {
|
if (!hasLoadedInitialRef.current) {
|
||||||
scrollToBottom(totalMessages);
|
const findDivideIndex = totalMessages.findIndex(
|
||||||
|
(item) => !!item?.divide
|
||||||
|
);
|
||||||
|
const divideIndex =
|
||||||
|
findDivideIndex !== -1 ? findDivideIndex : undefined;
|
||||||
|
scrollToBottom(totalMessages, divideIndex);
|
||||||
hasLoadedInitialRef.current = true;
|
hasLoadedInitialRef.current = true;
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
}, [initialMessages, tempMessages]);
|
}, [initialMessages, tempMessages]);
|
||||||
|
|
||||||
const scrollToBottom = (initialMsgs) => {
|
const scrollToBottom = (initialMsgs, divideIndex) => {
|
||||||
const index = initialMsgs ? initialMsgs.length - 1 : messages.length - 1;
|
const index = initialMsgs ? initialMsgs.length - 1 : messages.length - 1;
|
||||||
if (rowVirtualizer) {
|
if (rowVirtualizer) {
|
||||||
|
if (divideIndex) {
|
||||||
|
rowVirtualizer.scrollToIndex(divideIndex, { align: "start" });
|
||||||
|
} else {
|
||||||
rowVirtualizer.scrollToIndex(index, { align: "end" });
|
rowVirtualizer.scrollToIndex(index, { align: "end" });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
handleMessageSeen();
|
handleMessageSeen();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -90,17 +152,17 @@ export const ChatList = ({
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
setShowScrollButton(false);
|
setShowScrollButton(false);
|
||||||
|
lastSeenUnreadMessageTimestamp.current = Date.now()
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// const scrollToBottom = (initialMsgs) => {
|
|
||||||
// const index = initialMsgs ? initialMsgs.length - 1 : messages.length - 1;
|
|
||||||
// if (parentRef.current) {
|
|
||||||
// parentRef.current.scrollToIndex(index);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
const sentNewMessageGroupFunc = useCallback(() => {
|
const sentNewMessageGroupFunc = useCallback(() => {
|
||||||
|
const { scrollHeight, scrollTop, clientHeight } = parentRef.current;
|
||||||
|
|
||||||
|
// Check if the user is within 200px from the bottom
|
||||||
|
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
||||||
|
if (distanceFromBottom <= 700) {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
|
}
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -116,25 +178,24 @@ export const ChatList = ({
|
|||||||
return messages[lastIndex]?.signature;
|
return messages[lastIndex]?.signature;
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
// Initialize the virtualizer
|
const goToMessage = useCallback((idx) => {
|
||||||
const rowVirtualizer = useVirtualizer({
|
rowVirtualizer.scrollToIndex(idx);
|
||||||
count: messages.length,
|
}, []);
|
||||||
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
|
|
||||||
getItemKey: React.useCallback(
|
|
||||||
(index) => messages[index].signature,
|
|
||||||
[messages]
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: "100%",
|
height: "100%",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
width: "100%",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -160,28 +221,40 @@ export const ChatList = ({
|
|||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
// transform: `translateY(${rowVirtualizer.getVirtualItems()[0]?.start ?? 0}px)`,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
||||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
||||||
const index = virtualRow.index;
|
const index = virtualRow.index;
|
||||||
let message = messages[index];
|
let message = messages[index] || null; // Safeguard against undefined
|
||||||
let replyIndex = messages.findIndex(
|
let replyIndex = -1;
|
||||||
|
let reply = null;
|
||||||
|
let reactions = null;
|
||||||
|
let isUpdating = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Safeguard for message existence
|
||||||
|
if (message) {
|
||||||
|
// Check for repliedTo logic
|
||||||
|
replyIndex = messages.findIndex(
|
||||||
(msg) => msg?.signature === message?.repliedTo
|
(msg) => msg?.signature === message?.repliedTo
|
||||||
);
|
);
|
||||||
let reply;
|
|
||||||
let reactions = null;
|
|
||||||
|
|
||||||
if (message?.repliedTo && replyIndex !== -1) {
|
if (message?.repliedTo && replyIndex !== -1) {
|
||||||
reply = messages[replyIndex];
|
reply = { ...(messages[replyIndex] || {}) };
|
||||||
|
if (chatReferences?.[reply?.signature]?.edit) {
|
||||||
|
reply.decryptedData = chatReferences[reply?.signature]?.edit;
|
||||||
|
reply.text = chatReferences[reply?.signature]?.edit?.message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GroupDirectId logic
|
||||||
if (message?.message && message?.groupDirectId) {
|
if (message?.message && message?.groupDirectId) {
|
||||||
replyIndex = messages.findIndex(
|
replyIndex = messages.findIndex(
|
||||||
(msg) => msg?.signature === message?.message?.repliedTo
|
(msg) => msg?.signature === message?.message?.repliedTo
|
||||||
);
|
);
|
||||||
if (message?.message?.repliedTo && replyIndex !== -1) {
|
if (message?.message?.repliedTo && replyIndex !== -1) {
|
||||||
reply = messages[replyIndex];
|
reply = messages[replyIndex] || null;
|
||||||
}
|
}
|
||||||
message = {
|
message = {
|
||||||
...(message?.message || {}),
|
...(message?.message || {}),
|
||||||
@ -191,26 +264,64 @@ export const ChatList = ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chatReferences && chatReferences[message?.signature]) {
|
// Check for reactions and edits
|
||||||
if (chatReferences[message.signature]?.reactions) {
|
if (chatReferences?.[message.signature]) {
|
||||||
reactions = chatReferences[message.signature]?.reactions;
|
reactions = chatReferences[message.signature]?.reactions || null;
|
||||||
|
|
||||||
|
if (chatReferences[message.signature]?.edit?.message && message?.text) {
|
||||||
|
message.text = chatReferences[message.signature]?.edit?.message;
|
||||||
|
message.isEdit = true
|
||||||
}
|
}
|
||||||
|
if (chatReferences[message.signature]?.edit?.messageText && message?.messageText) {
|
||||||
|
message.messageText = chatReferences[message.signature]?.edit?.messageText;
|
||||||
|
message.isEdit = true
|
||||||
}
|
}
|
||||||
|
|
||||||
let isUpdating = false;
|
}
|
||||||
|
|
||||||
|
// Check if message is updating
|
||||||
if (
|
if (
|
||||||
tempChatReferences &&
|
tempChatReferences?.some(
|
||||||
tempChatReferences?.find(
|
|
||||||
(item) => item?.chatReference === message?.signature
|
(item) => item?.chatReference === message?.signature
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
isUpdating = true;
|
isUpdating = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing message:", error, { index, message });
|
||||||
|
// Gracefully handle the error by providing fallback data
|
||||||
|
message = null;
|
||||||
|
reply = null;
|
||||||
|
reactions = null;
|
||||||
|
}
|
||||||
|
// Render fallback if message is null
|
||||||
|
if (!message) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={virtualRow.index}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: "50%",
|
||||||
|
transform: `translateY(${virtualRow.start}px) translateX(-50%)`,
|
||||||
|
width: "100%",
|
||||||
|
padding: "10px 0",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography>Error loading message.</Typography>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-index={virtualRow.index} //needed for dynamic row height measurement
|
data-index={virtualRow.index} //needed for dynamic row height measurement
|
||||||
ref={(node) => rowVirtualizer.measureElement(node)} //measure dynamic row height
|
ref={rowVirtualizer.measureElement} //measure dynamic row height
|
||||||
key={message.signature}
|
key={message.signature}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
@ -220,9 +331,18 @@ export const ChatList = ({
|
|||||||
width: "100%", // Control width (90% of the parent)
|
width: "100%", // Control width (90% of the parent)
|
||||||
padding: "10px 0",
|
padding: "10px 0",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "center",
|
alignItems: "center",
|
||||||
overscrollBehavior: "none",
|
overscrollBehavior: "none",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "5px",
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
<ErrorBoundary
|
||||||
|
fallback={
|
||||||
|
<Typography>
|
||||||
|
Error loading content: Invalid Data
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<MessageItem
|
<MessageItem
|
||||||
isLast={index === messages.length - 1}
|
isLast={index === messages.length - 1}
|
||||||
@ -232,16 +352,20 @@ export const ChatList = ({
|
|||||||
isTemp={!!message?.isTemp}
|
isTemp={!!message?.isTemp}
|
||||||
myAddress={myAddress}
|
myAddress={myAddress}
|
||||||
onReply={onReply}
|
onReply={onReply}
|
||||||
|
onEdit={onEdit}
|
||||||
reply={reply}
|
reply={reply}
|
||||||
replyIndex={replyIndex}
|
replyIndex={replyIndex}
|
||||||
scrollToItem={(idx) => rowVirtualizer.scrollToIndex(idx)}
|
scrollToItem={goToMessage}
|
||||||
handleReaction={handleReaction}
|
handleReaction={handleReaction}
|
||||||
reactions={reactions}
|
reactions={reactions}
|
||||||
isUpdating={isUpdating}
|
isUpdating={isUpdating}
|
||||||
|
isPrivate={isPrivate}
|
||||||
/>
|
/>
|
||||||
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -249,20 +373,55 @@ export const ChatList = ({
|
|||||||
<button
|
<button
|
||||||
onClick={() => scrollToBottom()}
|
onClick={() => scrollToBottom()}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
|
||||||
bottom: 20,
|
bottom: 20,
|
||||||
|
position: "absolute",
|
||||||
right: 20,
|
right: 20,
|
||||||
backgroundColor: "#ff5a5f",
|
backgroundColor: "var(--unread)",
|
||||||
color: "white",
|
color: "black",
|
||||||
padding: "10px 20px",
|
padding: "10px 20px",
|
||||||
borderRadius: "20px",
|
borderRadius: "20px",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
|
border: "none",
|
||||||
|
outline: "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Scroll to Unread Messages
|
Scroll to Unread Messages
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{showScrollDownButton && !showScrollButton && (
|
||||||
|
<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",
|
||||||
|
fontSize: "16px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Scroll to bottom
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{enableMentions && (hasSecretKey || isPrivate === false) && (
|
||||||
|
<ChatOptions
|
||||||
|
openQManager={openQManager}
|
||||||
|
messages={messages}
|
||||||
|
goToMessage={goToMessage}
|
||||||
|
members={members}
|
||||||
|
myName={myName}
|
||||||
|
selectedGroup={selectedGroup}
|
||||||
|
isPrivate={isPrivate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
718
src/components/Chat/ChatOptions.tsx
Normal file
718
src/components/Chat/ChatOptions.tsx
Normal file
@ -0,0 +1,718 @@
|
|||||||
|
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 InsertLinkIcon from "@mui/icons-material/InsertLink";
|
||||||
|
import Highlight from "@tiptap/extension-highlight";
|
||||||
|
import Mention from "@tiptap/extension-mention";
|
||||||
|
import StarterKit from "@tiptap/starter-kit";
|
||||||
|
import Underline from "@tiptap/extension-underline";
|
||||||
|
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";
|
||||||
|
import { generateHTML } from "@tiptap/react";
|
||||||
|
import ErrorBoundary from "../../common/ErrorBoundary";
|
||||||
|
|
||||||
|
const extractTextFromHTML = (htmlString = "") => {
|
||||||
|
return convert(htmlString, {
|
||||||
|
wordwrap: false, // Disable word wrapping
|
||||||
|
})?.toLowerCase();
|
||||||
|
};
|
||||||
|
const cache = new CellMeasurerCache({
|
||||||
|
fixedWidth: true,
|
||||||
|
defaultHeight: 50,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ChatOptions = ({
|
||||||
|
messages: untransformedMessages,
|
||||||
|
goToMessage,
|
||||||
|
members,
|
||||||
|
myName,
|
||||||
|
selectedGroup,
|
||||||
|
openQManager,
|
||||||
|
isPrivate,
|
||||||
|
}) => {
|
||||||
|
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 messages = useMemo(() => {
|
||||||
|
return untransformedMessages?.map((item) => {
|
||||||
|
if (item?.messageText) {
|
||||||
|
let transformedMessage = item?.messageText;
|
||||||
|
try {
|
||||||
|
transformedMessage = generateHTML(item?.messageText, [
|
||||||
|
StarterKit,
|
||||||
|
Underline,
|
||||||
|
Highlight,
|
||||||
|
Mention,
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
messageText: transformedMessage,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// error
|
||||||
|
}
|
||||||
|
} else return item;
|
||||||
|
});
|
||||||
|
}, [untransformedMessages]);
|
||||||
|
const getTimestampMention = async () => {
|
||||||
|
try {
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
chrome?.runtime?.sendMessage(
|
||||||
|
{
|
||||||
|
action: "getTimestampMention",
|
||||||
|
},
|
||||||
|
(response) => {
|
||||||
|
if (!response?.error && selectedGroup && response[selectedGroup]) {
|
||||||
|
setLastMentionTimestamp(response[selectedGroup]);
|
||||||
|
res(response);
|
||||||
|
}
|
||||||
|
rej(response.error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} catch (error) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mode === "mentions" && selectedGroup) {
|
||||||
|
chrome?.runtime?.sendMessage(
|
||||||
|
{
|
||||||
|
action: "addTimestampMention",
|
||||||
|
payload: {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
groupId: selectedGroup,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(response) => {
|
||||||
|
if (!response?.error) {
|
||||||
|
getTimestampMention();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
}, [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(
|
||||||
|
isPrivate ? message?.messageText : message?.decryptedData?.message
|
||||||
|
)?.includes(debouncedValue.toLowerCase())
|
||||||
|
)
|
||||||
|
?.sort((a, b) => b?.timestamp - a?.timestamp);
|
||||||
|
}
|
||||||
|
return messages
|
||||||
|
.filter((message) =>
|
||||||
|
extractTextFromHTML(
|
||||||
|
isPrivate === false
|
||||||
|
? message?.messageText
|
||||||
|
: message?.decryptedData?.message
|
||||||
|
)?.includes(debouncedValue.toLowerCase())
|
||||||
|
)
|
||||||
|
?.sort((a, b) => b?.timestamp - a?.timestamp);
|
||||||
|
}, [debouncedValue, messages, selectedMember, isPrivate]);
|
||||||
|
|
||||||
|
const mentionList = useMemo(() => {
|
||||||
|
if (!messages || messages.length === 0 || !myName) return [];
|
||||||
|
if (isPrivate === false) {
|
||||||
|
return messages
|
||||||
|
.filter((message) =>
|
||||||
|
extractTextFromHTML(message?.messageText)?.includes(`@${myName}`)
|
||||||
|
)
|
||||||
|
?.sort((a, b) => b?.timestamp - a?.timestamp);
|
||||||
|
}
|
||||||
|
return messages
|
||||||
|
.filter((message) =>
|
||||||
|
extractTextFromHTML(message?.decryptedData?.message)?.includes(
|
||||||
|
`@${myName}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
?.sort((a, b) => b?.timestamp - a?.timestamp);
|
||||||
|
}, [messages, myName, isPrivate]);
|
||||||
|
|
||||||
|
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",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ShowMessage
|
||||||
|
messages={messages}
|
||||||
|
goToMessage={goToMessage}
|
||||||
|
message={message}
|
||||||
|
/>
|
||||||
|
</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",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ErrorBoundary
|
||||||
|
fallback={
|
||||||
|
<Typography>
|
||||||
|
Error loading content: Invalid Data
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ShowMessage
|
||||||
|
message={message}
|
||||||
|
goToMessage={goToMessage}
|
||||||
|
messages={messages}
|
||||||
|
/>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</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>
|
||||||
|
<ButtonBase
|
||||||
|
onClick={() => {
|
||||||
|
setMode("default");
|
||||||
|
setSearchValue("");
|
||||||
|
setSelectedMember(0);
|
||||||
|
openQManager();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InsertLinkIcon
|
||||||
|
sx={{
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ShowMessage = ({ message, goToMessage, messages }) => {
|
||||||
|
return (
|
||||||
|
<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);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{message?.messageText && (
|
||||||
|
<MessageDisplay htmlContent={message?.messageText} />
|
||||||
|
)}
|
||||||
|
{message?.decryptedData?.message && (
|
||||||
|
<MessageDisplay
|
||||||
|
htmlContent={message?.decryptedData?.message || "<p></p>"}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</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>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
@ -1,15 +1,20 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from 'react';
|
||||||
import DOMPurify from "dompurify";
|
import DOMPurify from 'dompurify';
|
||||||
import "./styles.css";
|
import './styles.css';
|
||||||
import { executeEvent } from "../../utils/events";
|
import { executeEvent } from '../../utils/events';
|
||||||
|
import { Embed } from '../Embeds/Embed';
|
||||||
|
|
||||||
const extractComponents = (url) => {
|
export const extractComponents = (url) => {
|
||||||
if (!url || !url.startsWith("qortal://")) {
|
if (!url || !url.startsWith("qortal://")) {
|
||||||
// Check if url exists and starts with "qortal://"
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
url = url.replace(/^(qortal\:\/\/)/, ""); // Safe to use replace now
|
// Skip links starting with "qortal://use-"
|
||||||
|
if (url.startsWith("qortal://use-")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
url = url.replace(/^(qortal\:\/\/)/, "");
|
||||||
if (url.includes("/")) {
|
if (url.includes("/")) {
|
||||||
let parts = url.split("/");
|
let parts = url.split("/");
|
||||||
const service = parts[0].toUpperCase();
|
const service = parts[0].toUpperCase();
|
||||||
@ -26,19 +31,20 @@ const extractComponents = (url) => {
|
|||||||
|
|
||||||
function processText(input) {
|
function processText(input) {
|
||||||
const linkRegex = /(qortal:\/\/\S+)/g;
|
const linkRegex = /(qortal:\/\/\S+)/g;
|
||||||
|
|
||||||
function processNode(node) {
|
function processNode(node) {
|
||||||
if (node.nodeType === Node.TEXT_NODE) {
|
if (node.nodeType === Node.TEXT_NODE) {
|
||||||
const parts = node.textContent.split(linkRegex);
|
const parts = node.textContent.split(linkRegex);
|
||||||
if (parts.length > 0) {
|
if (parts.length > 0) {
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
parts.forEach((part) => {
|
parts.forEach((part) => {
|
||||||
if (part.startsWith("qortal://")) {
|
if (part.startsWith('qortal://')) {
|
||||||
const link = document.createElement("span");
|
const link = document.createElement('span');
|
||||||
link.setAttribute("data-url", part);
|
link.setAttribute('data-url', part);
|
||||||
link.textContent = part;
|
link.textContent = part;
|
||||||
link.style.color = "var(--code-block-text-color)";
|
link.style.color = 'var(--code-block-text-color)';
|
||||||
link.style.textDecoration = "underline";
|
link.style.textDecoration = 'underline';
|
||||||
link.style.cursor = "pointer";
|
link.style.cursor = 'pointer';
|
||||||
fragment.appendChild(link);
|
fragment.appendChild(link);
|
||||||
} else {
|
} else {
|
||||||
fragment.appendChild(document.createTextNode(part));
|
fragment.appendChild(document.createTextNode(part));
|
||||||
@ -51,7 +57,7 @@ function processText(input) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const wrapper = document.createElement("div");
|
const wrapper = document.createElement('div');
|
||||||
wrapper.innerHTML = input;
|
wrapper.innerHTML = input;
|
||||||
processNode(wrapper);
|
processNode(wrapper);
|
||||||
return wrapper.innerHTML;
|
return wrapper.innerHTML;
|
||||||
@ -64,84 +70,33 @@ export const MessageDisplay = ({ htmlContent, isReply }) => {
|
|||||||
let textFormatted = text;
|
let textFormatted = text;
|
||||||
const urlPattern = /(\bhttps?:\/\/[^\s<]+|\bwww\.[^\s<]+)/g;
|
const urlPattern = /(\bhttps?:\/\/[^\s<]+|\bwww\.[^\s<]+)/g;
|
||||||
textFormatted = text.replace(urlPattern, (url) => {
|
textFormatted = text.replace(urlPattern, (url) => {
|
||||||
const href = url.startsWith("http") ? url : `https://${url}`;
|
const href = url.startsWith('http') ? url : `https://${url}`;
|
||||||
return `<a href="${DOMPurify.sanitize(
|
return `<a href="${DOMPurify.sanitize(href)}" class="auto-link">${DOMPurify.sanitize(url)}</a>`;
|
||||||
href
|
|
||||||
)}" class="auto-link">${DOMPurify.sanitize(url)}</a>`;
|
|
||||||
});
|
});
|
||||||
return processText(textFormatted);
|
return processText(textFormatted);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const sanitizedContent = DOMPurify.sanitize(linkify(htmlContent), {
|
const sanitizedContent = DOMPurify.sanitize(linkify(htmlContent), {
|
||||||
ALLOWED_TAGS: [
|
ALLOWED_TAGS: [
|
||||||
"a",
|
'a', 'b', 'i', 'em', 'strong', 'p', 'br', 'div', 'span', 'img',
|
||||||
"b",
|
'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'code', 'pre', 'table', 'thead', 'tbody', 'tr', 'th', 'td'
|
||||||
"i",
|
|
||||||
"em",
|
|
||||||
"strong",
|
|
||||||
"p",
|
|
||||||
"br",
|
|
||||||
"div",
|
|
||||||
"span",
|
|
||||||
"img",
|
|
||||||
"ul",
|
|
||||||
"ol",
|
|
||||||
"li",
|
|
||||||
"h1",
|
|
||||||
"h2",
|
|
||||||
"h3",
|
|
||||||
"h4",
|
|
||||||
"h5",
|
|
||||||
"h6",
|
|
||||||
"blockquote",
|
|
||||||
"code",
|
|
||||||
"pre",
|
|
||||||
"table",
|
|
||||||
"thead",
|
|
||||||
"tbody",
|
|
||||||
"tr",
|
|
||||||
"th",
|
|
||||||
"td",
|
|
||||||
],
|
],
|
||||||
ALLOWED_ATTR: [
|
ALLOWED_ATTR: [
|
||||||
"href",
|
'href', 'target', 'rel', 'class', 'src', 'alt', 'title',
|
||||||
"target",
|
'width', 'height', 'style', 'align', 'valign', 'colspan', 'rowspan', 'border', 'cellpadding', 'cellspacing', 'data-url'
|
||||||
"rel",
|
|
||||||
"class",
|
|
||||||
"src",
|
|
||||||
"alt",
|
|
||||||
"title",
|
|
||||||
"width",
|
|
||||||
"height",
|
|
||||||
"style",
|
|
||||||
"align",
|
|
||||||
"valign",
|
|
||||||
"colspan",
|
|
||||||
"rowspan",
|
|
||||||
"border",
|
|
||||||
"cellpadding",
|
|
||||||
"cellspacing",
|
|
||||||
"data-url",
|
|
||||||
],
|
],
|
||||||
});
|
}).replace(/<span[^>]*data-url="qortal:\/\/use-embed\/[^"]*"[^>]*>.*?<\/span>/g, '');;
|
||||||
|
|
||||||
const handleClick = async (e) => {
|
const handleClick = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const target = e.target;
|
const target = e.target;
|
||||||
if (target.tagName === "A") {
|
if (target.tagName === 'A') {
|
||||||
const href = target.getAttribute("href");
|
const href = target.getAttribute('href');
|
||||||
if (chrome && chrome.tabs) {
|
window.electronAPI.openExternal(href);
|
||||||
chrome.tabs.create({ url: href }, (tab) => {
|
} else if (target.getAttribute('data-url')) {
|
||||||
if (chrome.runtime.lastError) {
|
const url = target.getAttribute('data-url');
|
||||||
console.error("Error opening tab:", chrome.runtime.lastError);
|
|
||||||
} else {
|
|
||||||
console.log("Tab opened successfully:", tab);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (target.getAttribute("data-url")) {
|
|
||||||
const url = target.getAttribute("data-url");
|
|
||||||
const res = extractComponents(url);
|
const res = extractComponents(url);
|
||||||
if (res) {
|
if (res) {
|
||||||
const { service, name, identifier, path } = res;
|
const { service, name, identifier, path } = res;
|
||||||
@ -151,11 +106,24 @@ export const MessageDisplay = ({ htmlContent, isReply }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const embedLink = htmlContent?.match(/qortal:\/\/use-embed\/[^\s<>]+/);
|
||||||
|
|
||||||
|
let embedData = null;
|
||||||
|
|
||||||
|
if (embedLink) {
|
||||||
|
embedData = embedLink[0]
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{embedLink && (
|
||||||
|
<Embed embedLink={embedData} />
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
className={`tiptap ${isReply ? "isReply" : ""}`}
|
className={`tiptap ${isReply ? 'isReply' : ''}`}
|
||||||
dangerouslySetInnerHTML={{ __html: sanitizedContent }}
|
dangerouslySetInnerHTML={{ __html: sanitizedContent }}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Message } from "@chatscope/chat-ui-kit-react";
|
import { Message } from "@chatscope/chat-ui-kit-react";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useInView } from "react-intersection-observer";
|
import { useInView } from "react-intersection-observer";
|
||||||
import { MessageDisplay } from "./MessageDisplay";
|
import { MessageDisplay } from "./MessageDisplay";
|
||||||
import { Avatar, Box, ButtonBase, Typography } from "@mui/material";
|
import { Avatar, Box, Button, ButtonBase, List, ListItem, ListItemText, Popover, Typography } from "@mui/material";
|
||||||
import { formatTimestamp } from "../../utils/time";
|
import { formatTimestamp } from "../../utils/time";
|
||||||
import { getBaseApi } from "../../background";
|
import { getBaseApi } from "../../background";
|
||||||
import { getBaseApiReact } from "../../App";
|
import { getBaseApiReact } from "../../App";
|
||||||
@ -16,6 +16,10 @@ import ReplyIcon from "@mui/icons-material/Reply";
|
|||||||
import { Spacer } from "../../common/Spacer";
|
import { Spacer } from "../../common/Spacer";
|
||||||
import { ReactionPicker } from "../ReactionPicker";
|
import { ReactionPicker } from "../ReactionPicker";
|
||||||
import KeyOffIcon from '@mui/icons-material/KeyOff';
|
import KeyOffIcon from '@mui/icons-material/KeyOff';
|
||||||
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
|
import Mention from "@tiptap/extension-mention";
|
||||||
|
import TextStyle from '@tiptap/extension-text-style';
|
||||||
|
|
||||||
export const MessageItem = ({
|
export const MessageItem = ({
|
||||||
message,
|
message,
|
||||||
onSeen,
|
onSeen,
|
||||||
@ -30,21 +34,32 @@ export const MessageItem = ({
|
|||||||
handleReaction,
|
handleReaction,
|
||||||
reactions,
|
reactions,
|
||||||
isUpdating,
|
isUpdating,
|
||||||
lastSignature
|
lastSignature,
|
||||||
|
onEdit,
|
||||||
|
isPrivate,
|
||||||
|
setMobileViewModeKeepOpen
|
||||||
}) => {
|
}) => {
|
||||||
|
const [anchorEl, setAnchorEl] = useState(null);
|
||||||
|
const [selectedReaction, setSelectedReaction] = useState(null);
|
||||||
const { ref, inView } = useInView({
|
const { ref, inView } = useInView({
|
||||||
threshold: 0.7, // Fully visible
|
threshold: 0.7, // Fully visible
|
||||||
triggerOnce: true, // Only trigger once when it becomes visible
|
triggerOnce: false, // Only trigger once when it becomes visible
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inView && message.unread) {
|
if (inView && isLast && onSeen) {
|
||||||
onSeen(message.id);
|
onSeen(message.id);
|
||||||
}
|
}
|
||||||
}, [inView, message.id, message.unread, onSeen]);
|
}, [inView, message.id, isLast]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{message?.divide && (
|
||||||
|
<div className="unread-divider" id="unread-divider-id">
|
||||||
|
Unread messages below
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
ref={lastSignature === message?.signature ? ref : null}
|
ref={lastSignature === message?.signature ? ref : null}
|
||||||
style={{
|
style={{
|
||||||
@ -76,9 +91,9 @@ export const MessageItem = ({
|
|||||||
color: "white",
|
color: "white",
|
||||||
}}
|
}}
|
||||||
alt={message?.senderName}
|
alt={message?.senderName}
|
||||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
src={message?.senderName ? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||||
message?.senderName
|
message?.senderName
|
||||||
}/qortal_avatar?async=true`}
|
}/qortal_avatar?async=true` : ''}
|
||||||
>
|
>
|
||||||
{message?.senderName?.charAt(0)}
|
{message?.senderName?.charAt(0)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
@ -122,6 +137,15 @@ export const MessageItem = ({
|
|||||||
gap: '10px',
|
gap: '10px',
|
||||||
alignItems: 'center'
|
alignItems: 'center'
|
||||||
}}>
|
}}>
|
||||||
|
{message?.sender === myAddress && (!message?.isNotEncrypted || isPrivate === false) && (
|
||||||
|
<ButtonBase
|
||||||
|
onClick={() => {
|
||||||
|
onEdit(message);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EditIcon />
|
||||||
|
</ButtonBase>
|
||||||
|
)}
|
||||||
{!isShowingAsReply && (
|
{!isShowingAsReply && (
|
||||||
<ButtonBase
|
<ButtonBase
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -182,13 +206,16 @@ export const MessageItem = ({
|
|||||||
StarterKit,
|
StarterKit,
|
||||||
Underline,
|
Underline,
|
||||||
Highlight,
|
Highlight,
|
||||||
|
Mention,
|
||||||
|
TextStyle
|
||||||
])}
|
])}
|
||||||
|
setMobileViewModeKeepOpen={setMobileViewModeKeepOpen}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{reply?.decryptedData?.type === "notification" ? (
|
{reply?.decryptedData?.type === "notification" ? (
|
||||||
<MessageDisplay htmlContent={reply.decryptedData?.data?.message} />
|
<MessageDisplay htmlContent={reply.decryptedData?.data?.message} />
|
||||||
) : (
|
) : (
|
||||||
<MessageDisplay isReply htmlContent={reply.text} />
|
<MessageDisplay setMobileViewModeKeepOpen={setMobileViewModeKeepOpen} isReply htmlContent={reply.text} />
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@ -200,13 +227,16 @@ export const MessageItem = ({
|
|||||||
StarterKit,
|
StarterKit,
|
||||||
Underline,
|
Underline,
|
||||||
Highlight,
|
Highlight,
|
||||||
|
Mention,
|
||||||
|
TextStyle
|
||||||
])}
|
])}
|
||||||
|
setMobileViewModeKeepOpen={setMobileViewModeKeepOpen}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{message?.decryptedData?.type === "notification" ? (
|
{message?.decryptedData?.type === "notification" ? (
|
||||||
<MessageDisplay htmlContent={message.decryptedData?.data?.message} />
|
<MessageDisplay htmlContent={message.decryptedData?.data?.message} />
|
||||||
) : (
|
) : (
|
||||||
<MessageDisplay htmlContent={message.text} />
|
<MessageDisplay setMobileViewModeKeepOpen={setMobileViewModeKeepOpen} htmlContent={message.text} />
|
||||||
)}
|
)}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@ -225,17 +255,15 @@ export const MessageItem = ({
|
|||||||
// const myReaction = reactions
|
// const myReaction = reactions
|
||||||
if(numberOfReactions === 0) return null
|
if(numberOfReactions === 0) return null
|
||||||
return (
|
return (
|
||||||
<ButtonBase sx={{
|
<ButtonBase key={reaction} sx={{
|
||||||
height: '30px',
|
height: '30px',
|
||||||
minWidth: '45px',
|
minWidth: '45px',
|
||||||
background: 'var(--bg-2)',
|
background: 'var(--bg-2)',
|
||||||
borderRadius: '7px'
|
borderRadius: '7px'
|
||||||
}} onClick={()=> {
|
}} onClick={(event) => {
|
||||||
if(reactions[reaction] && reactions[reaction]?.find((item)=> item?.sender === myAddress)){
|
event.stopPropagation(); // Prevent event bubbling
|
||||||
handleReaction(reaction, message, false)
|
setAnchorEl(event.currentTarget);
|
||||||
} else {
|
setSelectedReaction(reaction);
|
||||||
handleReaction(reaction, message, true)
|
|
||||||
}
|
|
||||||
}}>
|
}}>
|
||||||
<div>{reaction}</div> {numberOfReactions > 1 && (
|
<div>{reaction}</div> {numberOfReactions > 1 && (
|
||||||
<Typography sx={{
|
<Typography sx={{
|
||||||
@ -246,12 +274,79 @@ export const MessageItem = ({
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</Box>
|
</Box>
|
||||||
|
{selectedReaction && (
|
||||||
|
<Popover
|
||||||
|
open={Boolean(anchorEl)}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
onClose={() => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
setSelectedReaction(null);
|
||||||
|
}}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: "top",
|
||||||
|
horizontal: "center",
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: "bottom",
|
||||||
|
horizontal: "center",
|
||||||
|
}}
|
||||||
|
PaperProps={{
|
||||||
|
style: {
|
||||||
|
backgroundColor: "#232428",
|
||||||
|
color: "white",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ p: 2 }}>
|
||||||
|
<Typography variant="subtitle1" sx={{ marginBottom: 1 }}>
|
||||||
|
People who reacted with {selectedReaction}
|
||||||
|
</Typography>
|
||||||
|
<List sx={{
|
||||||
|
overflow: 'auto',
|
||||||
|
maxWidth: '80vw',
|
||||||
|
maxHeight: '300px'
|
||||||
|
}}>
|
||||||
|
{reactions[selectedReaction]?.map((reactionItem) => (
|
||||||
|
<ListItem key={reactionItem.sender}>
|
||||||
|
<ListItemText
|
||||||
|
primary={reactionItem.senderName || reactionItem.sender}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
if (
|
||||||
|
reactions[selectedReaction]?.find(
|
||||||
|
(item) => item?.sender === myAddress
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
handleReaction(selectedReaction, message, false); // Remove reaction
|
||||||
|
} else {
|
||||||
|
handleReaction(selectedReaction, message, true); // Add reaction
|
||||||
|
}
|
||||||
|
setAnchorEl(null);
|
||||||
|
setSelectedReaction(null);
|
||||||
|
}}
|
||||||
|
sx={{ marginTop: 2 }}
|
||||||
|
>
|
||||||
|
{reactions[selectedReaction]?.find(
|
||||||
|
(item) => item?.sender === myAddress
|
||||||
|
)
|
||||||
|
? "Remove Reaction"
|
||||||
|
: "Add Reaction"}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '15px'
|
gap: '15px'
|
||||||
}}>
|
}}>
|
||||||
{message?.isNotEncrypted && (
|
{message?.isNotEncrypted && isPrivate && (
|
||||||
<KeyOffIcon sx={{
|
<KeyOffIcon sx={{
|
||||||
color: 'white',
|
color: 'white',
|
||||||
marginLeft: '10px'
|
marginLeft: '10px'
|
||||||
@ -279,6 +374,19 @@ export const MessageItem = ({
|
|||||||
{message?.status === 'failed-permanent' ? 'Failed to send' : 'Sending...'}
|
{message?.status === 'failed-permanent' ? 'Failed to send' : 'Sending...'}
|
||||||
</Typography>
|
</Typography>
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
|
{message?.isEdit && (
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "gray",
|
||||||
|
fontFamily: "Inter",
|
||||||
|
fontStyle: 'italic'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Edited
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
<Typography
|
<Typography
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: "14px",
|
fontSize: "14px",
|
||||||
@ -288,6 +396,7 @@ export const MessageItem = ({
|
|||||||
>
|
>
|
||||||
{formatTimestamp(message.timestamp)}
|
{formatTimestamp(message.timestamp)}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@ -305,11 +414,12 @@ export const MessageItem = ({
|
|||||||
></Message> */}
|
></Message> */}
|
||||||
{/* {!message.unread && <span style={{ color: 'green' }}> Seen</span>} */}
|
{/* {!message.unread && <span style={{ color: 'green' }}> Seen</span>} */}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const ReplyPreview = ({message})=> {
|
export const ReplyPreview = ({message, isEdit})=> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@ -333,16 +443,26 @@ export const ReplyPreview = ({message})=> {
|
|||||||
<Box sx={{
|
<Box sx={{
|
||||||
padding: '5px'
|
padding: '5px'
|
||||||
}}>
|
}}>
|
||||||
|
{isEdit ? (
|
||||||
|
<Typography sx={{
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: 600
|
||||||
|
}}>Editing Message</Typography>
|
||||||
|
) : (
|
||||||
<Typography sx={{
|
<Typography sx={{
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
fontWeight: 600
|
fontWeight: 600
|
||||||
}}>Replied to {message?.senderName || message?.senderAddress}</Typography>
|
}}>Replied to {message?.senderName || message?.senderAddress}</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
{message?.messageText && (
|
{message?.messageText && (
|
||||||
<MessageDisplay
|
<MessageDisplay
|
||||||
htmlContent={generateHTML(message?.messageText, [
|
htmlContent={generateHTML(message?.messageText, [
|
||||||
StarterKit,
|
StarterKit,
|
||||||
Underline,
|
Underline,
|
||||||
Highlight,
|
Highlight,
|
||||||
|
Mention,
|
||||||
|
TextStyle
|
||||||
])}
|
])}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { EditorProvider, useCurrentEditor } from "@tiptap/react";
|
import { EditorProvider, useCurrentEditor, useEditor } from "@tiptap/react";
|
||||||
import StarterKit from "@tiptap/starter-kit";
|
import StarterKit from "@tiptap/starter-kit";
|
||||||
import { Color } from "@tiptap/extension-color";
|
import { Color } from "@tiptap/extension-color";
|
||||||
import ListItem from "@tiptap/extension-list-item";
|
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 FormatHeadingIcon from "@mui/icons-material/FormatSize";
|
||||||
import DeveloperModeIcon from "@mui/icons-material/DeveloperMode";
|
import DeveloperModeIcon from "@mui/icons-material/DeveloperMode";
|
||||||
import Compressor from "compressorjs";
|
import Compressor from "compressorjs";
|
||||||
|
import Mention from '@tiptap/extension-mention';
|
||||||
import ImageResize from "tiptap-extension-resize-image"; // Import the ResizeImage extension
|
import ImageResize from "tiptap-extension-resize-image"; // Import the ResizeImage extension
|
||||||
import { isMobile } from "../../App";
|
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 MenuBar = ({ setEditorRef, isChat }) => {
|
||||||
const { editor } = useCurrentEditor();
|
const { editor } = useCurrentEditor();
|
||||||
const fileInputRef = useRef(null);
|
const fileInputRef = useRef(null);
|
||||||
@ -279,8 +297,10 @@ export default ({
|
|||||||
isFocusedParent,
|
isFocusedParent,
|
||||||
overrideMobile,
|
overrideMobile,
|
||||||
customEditorHeight,
|
customEditorHeight,
|
||||||
|
membersWithNames,
|
||||||
|
enableMentions
|
||||||
}) => {
|
}) => {
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
|
||||||
const extensionsFiltered = isChat
|
const extensionsFiltered = isChat
|
||||||
? extensions.filter((item) => item?.name !== "image")
|
? extensions.filter((item) => item?.name !== "image")
|
||||||
: extensions;
|
: extensions;
|
||||||
@ -290,6 +310,32 @@ export default ({
|
|||||||
setEditorRef(editorInstance);
|
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 = () => {
|
const handleFocus = () => {
|
||||||
if (!isMobile) return;
|
if (!isMobile) return;
|
||||||
setIsFocusedParent(true);
|
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 (
|
return (
|
||||||
|
<div>
|
||||||
<EditorProvider
|
<EditorProvider
|
||||||
slotBefore={
|
slotBefore={
|
||||||
(isFocusedParent || !isMobile || overrideMobile) && (
|
(isFocusedParent || !isMobile || overrideMobile) && (
|
||||||
<MenuBar setEditorRef={setEditorRefFunc} isChat={isChat} />
|
<MenuBar setEditorRef={setEditorRefFunc} isChat={isChat} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
extensions={extensionsFiltered}
|
extensions={[...extensionsFiltered, ...additionalExtensions
|
||||||
|
]}
|
||||||
content={content}
|
content={content}
|
||||||
onCreate={({ editor }) => {
|
onCreate={({ editor }) => {
|
||||||
editor.on("focus", handleFocus); // Listen for focus event
|
editor.on("focus", handleFocus); // Listen for focus event
|
||||||
@ -348,5 +469,7 @@ export default ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -71,6 +71,7 @@
|
|||||||
margin: 1.5rem 0;
|
margin: 1.5rem 0;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
text-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tiptap pre code {
|
.tiptap pre code {
|
||||||
@ -123,3 +124,51 @@
|
|||||||
.isReply p {
|
.isReply p {
|
||||||
font-size: 12px !important;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
139
src/components/ContextMenuMentions.tsx
Normal file
139
src/components/ContextMenuMentions.tsx
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
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 = ()=> {
|
||||||
|
|
||||||
|
|
||||||
|
chrome?.runtime?.sendMessage(
|
||||||
|
{
|
||||||
|
action: "addTimestampMention",
|
||||||
|
payload: {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
groupId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(response) => {
|
||||||
|
if (!response?.error) {
|
||||||
|
getTimestampMention()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
59
src/components/CoreSyncStatus.css
Normal file
59
src/components/CoreSyncStatus.css
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
.lineHeight {
|
||||||
|
line-height: 33%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip .bottom {
|
||||||
|
min-width: 225px;
|
||||||
|
max-width: 250px;
|
||||||
|
top: 35px;
|
||||||
|
right: 0px;
|
||||||
|
/* transform: translate(-50%, 0); */
|
||||||
|
padding: 10px 10px;
|
||||||
|
color: var(--black);
|
||||||
|
background-color: var(--bg-2);
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 13px;
|
||||||
|
border-radius: 8px;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 99999999;
|
||||||
|
box-sizing: border-box;
|
||||||
|
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.5);
|
||||||
|
border: 1px solid var(--black);
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip:hover .bottom {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip .bottom i {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -12px;
|
||||||
|
width: 24px;
|
||||||
|
height: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip .bottom i::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 50%) rotate(45deg);
|
||||||
|
background-color: var(--white);
|
||||||
|
border: 1px solid var(--black);
|
||||||
|
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
113
src/components/CoreSyncStatus.tsx
Normal file
113
src/components/CoreSyncStatus.tsx
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import syncedImg from '../assets/syncStatus/synced.png'
|
||||||
|
import syncedMintingImg from '../assets/syncStatus/synced_minting.png'
|
||||||
|
import syncingImg from '../assets/syncStatus/syncing.png'
|
||||||
|
import { getBaseApiReact } from '../App';
|
||||||
|
import './CoreSyncStatus.css'
|
||||||
|
export const CoreSyncStatus = ({imageSize, position}) => {
|
||||||
|
const [nodeInfos, setNodeInfos] = useState({});
|
||||||
|
const [coreInfos, setCoreInfos] = useState({});
|
||||||
|
const [isUsingGateway, setIsUsingGateway] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getNodeInfos = async () => {
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsUsingGateway(!!getBaseApiReact()?.includes('ext-node.qortal.link'))
|
||||||
|
const url = `${getBaseApiReact()}/admin/status`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
setNodeInfos(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Request failed', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCoreInfos = async () => {
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = `${getBaseApiReact()}/admin/info`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
setCoreInfos(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Request failed', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getNodeInfos();
|
||||||
|
getCoreInfos();
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
getNodeInfos();
|
||||||
|
getCoreInfos();
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renderSyncStatusIcon = () => {
|
||||||
|
const { isSynchronizing = false, syncPercent = 0, isMintingPossible = false, height = 0, numberOfConnections = 0 } = nodeInfos;
|
||||||
|
const buildVersion = coreInfos?.buildVersion ? coreInfos?.buildVersion.substring(0, 12) : '';
|
||||||
|
|
||||||
|
let imagePath = syncingImg;
|
||||||
|
let message = `Synchronizing`
|
||||||
|
if (isMintingPossible && !isUsingGateway) {
|
||||||
|
imagePath = syncedMintingImg;
|
||||||
|
message = `${isSynchronizing ? 'Synchronizing' : 'Synchronized'} ${'(Minting)'}`
|
||||||
|
} else if (isSynchronizing === true && syncPercent === 99) {
|
||||||
|
imagePath = syncingImg
|
||||||
|
} else if (isSynchronizing && !isMintingPossible && syncPercent === 100) {
|
||||||
|
imagePath = syncingImg;
|
||||||
|
message = `Synchronizing ${isUsingGateway ? '' :'(Not Minting)'}`
|
||||||
|
} else if (!isSynchronizing && !isMintingPossible && syncPercent === 100) {
|
||||||
|
imagePath = syncedImg
|
||||||
|
message = `Synchronized ${isUsingGateway ? '' :'(Not Minting)'}`
|
||||||
|
} else if (isSynchronizing && isMintingPossible && syncPercent === 100) {
|
||||||
|
imagePath = syncingImg;
|
||||||
|
message = `Synchronizing ${isUsingGateway ? '' :'(Minting)'}`
|
||||||
|
} else if (!isSynchronizing && isMintingPossible && syncPercent === 100) {
|
||||||
|
imagePath = syncedMintingImg;
|
||||||
|
message = `Synchronized ${isUsingGateway ? '' :'(Minting)'}`
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="tooltip" style={{ display: 'inline' }}>
|
||||||
|
<span><img src={imagePath} style={{ height: 'auto', width: imageSize ? imageSize : '24px' }} alt="sync status" /></span>
|
||||||
|
<div className="bottom" style={{
|
||||||
|
right: position && 'unset',
|
||||||
|
left: position && '0px'
|
||||||
|
}}>
|
||||||
|
<h3>Core Information</h3>
|
||||||
|
<h4 className="lineHeight">Core Version: <span style={{ color: '#03a9f4' }}>{buildVersion}</span></h4>
|
||||||
|
<h4 className="lineHeight">{message}</h4>
|
||||||
|
<h4 className="lineHeight">Block Height: <span style={{ color: '#03a9f4' }}>{height || ''}</span></h4>
|
||||||
|
<h4 className="lineHeight">Connected Peers: <span style={{ color: '#03a9f4' }}>{numberOfConnections || ''}</span></h4>
|
||||||
|
<h4 className="lineHeight">Using gateway: <span style={{ color: '#03a9f4' }}>{isUsingGateway?.toString()}</span></h4>
|
||||||
|
<i></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="core-sync-status-id">
|
||||||
|
{renderSyncStatusIcon()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -78,7 +78,8 @@ export const DesktopFooter = ({
|
|||||||
desktopViewMode,
|
desktopViewMode,
|
||||||
hide,
|
hide,
|
||||||
setIsOpenSideViewDirects,
|
setIsOpenSideViewDirects,
|
||||||
setIsOpenSideViewGroups
|
setIsOpenSideViewGroups,
|
||||||
|
myName
|
||||||
|
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
@ -178,7 +179,7 @@ export const DesktopFooter = ({
|
|||||||
</IconWrapper>
|
</IconWrapper>
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
|
|
||||||
<Save isDesktop />
|
<Save isDesktop myName={myName} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
327
src/components/Embeds/AttachmentEmbed.tsx
Normal file
327
src/components/Embeds/AttachmentEmbed.tsx
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { MyContext, getBaseApiReact } from "../../App";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
Typography,
|
||||||
|
RadioGroup,
|
||||||
|
Radio,
|
||||||
|
FormControlLabel,
|
||||||
|
Button,
|
||||||
|
Box,
|
||||||
|
ButtonBase,
|
||||||
|
Divider,
|
||||||
|
Dialog,
|
||||||
|
IconButton,
|
||||||
|
CircularProgress,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { base64ToBlobUrl } from "../../utils/fileReading";
|
||||||
|
import { saveFileToDiskGeneric } from "../../utils/generateWallet/generateWallet";
|
||||||
|
import AttachmentIcon from '@mui/icons-material/Attachment';
|
||||||
|
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||||
|
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
|
||||||
|
import { CustomLoader } from "../../common/CustomLoader";
|
||||||
|
import { Spacer } from "../../common/Spacer";
|
||||||
|
import { FileAttachmentContainer, FileAttachmentFont } from "./Embed-styles";
|
||||||
|
import DownloadIcon from "@mui/icons-material/Download";
|
||||||
|
import SaveIcon from '@mui/icons-material/Save';
|
||||||
|
import { useSetRecoilState } from "recoil";
|
||||||
|
import { decodeIfEncoded } from "../../utils/decode";
|
||||||
|
|
||||||
|
|
||||||
|
export const AttachmentCard = ({
|
||||||
|
resourceData,
|
||||||
|
resourceDetails,
|
||||||
|
owner,
|
||||||
|
refresh,
|
||||||
|
openExternal,
|
||||||
|
external,
|
||||||
|
isLoadingParent,
|
||||||
|
errorMsg,
|
||||||
|
encryptionType,
|
||||||
|
selectedGroupId
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(true);
|
||||||
|
const { downloadResource } = useContext(MyContext);
|
||||||
|
|
||||||
|
const saveToDisk = async ()=> {
|
||||||
|
const { name, service, identifier } = resourceData;
|
||||||
|
|
||||||
|
const url = `${getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}`;
|
||||||
|
fetch(url)
|
||||||
|
.then(response => response.blob())
|
||||||
|
.then(async blob => {
|
||||||
|
await saveFileToDiskGeneric(blob, resourceData?.fileName)
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Error fetching the video:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveToDiskEncrypted = async ()=> {
|
||||||
|
let blobUrl
|
||||||
|
try {
|
||||||
|
const { name, service, identifier,key } = resourceData;
|
||||||
|
|
||||||
|
const url = `${getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}?encoding=base64`;
|
||||||
|
const res = await fetch(url)
|
||||||
|
const data = await res.text();
|
||||||
|
let decryptedData
|
||||||
|
try {
|
||||||
|
if(key && encryptionType === 'private'){
|
||||||
|
|
||||||
|
decryptedData = await new Promise((res, rej) => {
|
||||||
|
chrome?.runtime?.sendMessage(
|
||||||
|
{
|
||||||
|
action: "DECRYPT_DATA_WITH_SHARING_KEY",
|
||||||
|
type: "qortalRequest",
|
||||||
|
payload: {
|
||||||
|
encryptedData: data,
|
||||||
|
key: decodeURIComponent(key),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
(response) => {
|
||||||
|
if (response.error) {
|
||||||
|
rej(response?.message);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
res(response);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(encryptionType === 'group'){
|
||||||
|
|
||||||
|
decryptedData = await new Promise((res, rej) => {
|
||||||
|
chrome?.runtime?.sendMessage(
|
||||||
|
{
|
||||||
|
action: "DECRYPT_QORTAL_GROUP_DATA",
|
||||||
|
type: "qortalRequest",
|
||||||
|
payload: {
|
||||||
|
data64: data,
|
||||||
|
groupId: selectedGroupId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
(response) => {
|
||||||
|
if (response.error) {
|
||||||
|
rej(response?.message);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
res(response);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('Unable to decrypt')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!decryptedData || decryptedData?.error) throw new Error("Could not decrypt data");
|
||||||
|
blobUrl = base64ToBlobUrl(decryptedData, resourceData?.mimeType)
|
||||||
|
const response = await fetch(blobUrl);
|
||||||
|
const blob = await response.blob();
|
||||||
|
await saveFileToDiskGeneric(blob, resourceData?.fileName)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
if(blobUrl){
|
||||||
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "#1F2023",
|
||||||
|
height: "250px",
|
||||||
|
// height: isOpen ? "auto" : "150px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
padding: "16px 16px 0px 16px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AttachmentIcon
|
||||||
|
sx={{
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography>ATTACHMENT embed</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ButtonBase>
|
||||||
|
<RefreshIcon
|
||||||
|
onClick={refresh}
|
||||||
|
sx={{
|
||||||
|
fontSize: "24px",
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ButtonBase>
|
||||||
|
{external && (
|
||||||
|
<ButtonBase>
|
||||||
|
<OpenInNewIcon
|
||||||
|
onClick={openExternal}
|
||||||
|
sx={{
|
||||||
|
fontSize: "24px",
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ButtonBase>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: "8px 16px 8px 16px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "12px",
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Created by {decodeIfEncoded(owner)}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "12px",
|
||||||
|
color: "cadetblue",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{encryptionType === 'private' ? "ENCRYPTED" : encryptionType === 'group' ? 'GROUP ENCRYPTED' : "Not encrypted"}
|
||||||
|
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Divider sx={{ borderColor: "rgb(255 255 255 / 10%)" }} />
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
width: "100%",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
|
{isLoadingParent && isOpen && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
<CustomLoader />{" "}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{errorMsg && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "var(--danger)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{errorMsg}
|
||||||
|
</Typography>{" "}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<CardContent>
|
||||||
|
{resourceData?.fileName && (
|
||||||
|
<>
|
||||||
|
<Typography sx={{
|
||||||
|
fontSize: '14px'
|
||||||
|
}}>{resourceData?.fileName}</Typography>
|
||||||
|
<Spacer height="10px" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<ButtonBase sx={{
|
||||||
|
width: '90%',
|
||||||
|
maxWidth: '400px'
|
||||||
|
}} onClick={()=> {
|
||||||
|
if(resourceDetails?.status?.status === 'READY'){
|
||||||
|
if(encryptionType){
|
||||||
|
saveToDiskEncrypted()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
saveToDisk()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadResource(resourceData)
|
||||||
|
|
||||||
|
|
||||||
|
}}>
|
||||||
|
|
||||||
|
<FileAttachmentContainer >
|
||||||
|
<Typography>{resourceDetails?.status?.status === 'DOWNLOADED' ? 'BUILDING' : resourceDetails?.status?.status}</Typography>
|
||||||
|
{!resourceDetails && (
|
||||||
|
<>
|
||||||
|
<DownloadIcon />
|
||||||
|
<FileAttachmentFont>Download File</FileAttachmentFont>
|
||||||
|
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{resourceDetails && resourceDetails?.status?.status !== 'READY' && resourceDetails?.status?.status !== 'FAILED_TO_DOWNLOAD' && (
|
||||||
|
<>
|
||||||
|
<CircularProgress sx={{
|
||||||
|
color: 'white'
|
||||||
|
}} size={20} />
|
||||||
|
<FileAttachmentFont>Downloading: {resourceDetails?.status?.percentLoaded || '0'}%</FileAttachmentFont>
|
||||||
|
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{resourceDetails && resourceDetails?.status?.status === 'READY' && (
|
||||||
|
<>
|
||||||
|
<SaveIcon />
|
||||||
|
<FileAttachmentFont>Save to Disk</FileAttachmentFont>
|
||||||
|
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
</FileAttachmentContainer>
|
||||||
|
</ButtonBase>
|
||||||
|
|
||||||
|
</CardContent>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
18
src/components/Embeds/Embed-styles.tsx
Normal file
18
src/components/Embeds/Embed-styles.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Box, Typography, styled } from "@mui/material";
|
||||||
|
|
||||||
|
export const FileAttachmentContainer = styled(Box)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "5px 10px",
|
||||||
|
border: `1px solid ${theme.palette.text.primary}`,
|
||||||
|
width: "100%",
|
||||||
|
gap: '20px'
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const FileAttachmentFont = styled(Typography)(({ theme }) => ({
|
||||||
|
fontSize: "20px",
|
||||||
|
letterSpacing: 0,
|
||||||
|
fontWeight: 400,
|
||||||
|
userSelect: "none",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}));
|
406
src/components/Embeds/Embed.tsx
Normal file
406
src/components/Embeds/Embed.tsx
Normal file
@ -0,0 +1,406 @@
|
|||||||
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { getBaseApiReact } from "../../App";
|
||||||
|
|
||||||
|
|
||||||
|
import { CustomizedSnackbars } from "../Snackbar/Snackbar";
|
||||||
|
|
||||||
|
import { extractComponents } from "../Chat/MessageDisplay";
|
||||||
|
import { executeEvent } from "../../utils/events";
|
||||||
|
|
||||||
|
import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil";
|
||||||
|
import { blobControllerAtom, blobKeySelector, resourceKeySelector, selectedGroupIdAtom } from "../../atoms/global";
|
||||||
|
import { parseQortalLink } from "./embed-utils";
|
||||||
|
import { PollCard } from "./PollEmbed";
|
||||||
|
import { ImageCard } from "./ImageEmbed";
|
||||||
|
import { AttachmentCard } from "./AttachmentEmbed";
|
||||||
|
import { base64ToBlobUrl } from "../../utils/fileReading";
|
||||||
|
|
||||||
|
const getPoll = async (name) => {
|
||||||
|
const pollName = name;
|
||||||
|
const url = `${getBaseApiReact()}/polls/${pollName}`;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseData = await response.json();
|
||||||
|
if (responseData?.message?.includes("POLL_NO_EXISTS")) {
|
||||||
|
throw new Error("POLL_NO_EXISTS");
|
||||||
|
} else if (responseData?.pollName) {
|
||||||
|
const urlVotes = `${getBaseApiReact()}/polls/votes/${pollName}`;
|
||||||
|
|
||||||
|
const responseVotes = await fetch(urlVotes, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseDataVotes = await responseVotes.json();
|
||||||
|
return {
|
||||||
|
info: responseData,
|
||||||
|
votes: responseDataVotes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Embed = ({ embedLink }) => {
|
||||||
|
const [errorMsg, setErrorMsg] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [poll, setPoll] = useState(null);
|
||||||
|
const [type, setType] = useState("");
|
||||||
|
const hasFetched = useRef(false);
|
||||||
|
const [openSnack, setOpenSnack] = useState(false);
|
||||||
|
const [infoSnack, setInfoSnack] = useState(null);
|
||||||
|
const [external, setExternal] = useState(null);
|
||||||
|
const [imageUrl, setImageUrl] = useState("");
|
||||||
|
const [parsedData, setParsedData] = useState(null);
|
||||||
|
const setBlobs = useSetRecoilState(blobControllerAtom);
|
||||||
|
const [selectedGroupId] = useRecoilState(selectedGroupIdAtom)
|
||||||
|
const resourceData = useMemo(()=> {
|
||||||
|
const parsedDataOnTheFly = parseQortalLink(embedLink);
|
||||||
|
if(parsedDataOnTheFly?.service && parsedDataOnTheFly?.name && parsedDataOnTheFly?.identifier){
|
||||||
|
return {
|
||||||
|
service : parsedDataOnTheFly?.service,
|
||||||
|
name: parsedDataOnTheFly?.name,
|
||||||
|
identifier: parsedDataOnTheFly?.identifier,
|
||||||
|
fileName: parsedDataOnTheFly?.fileName ? decodeURIComponent(parsedDataOnTheFly?.fileName) : null,
|
||||||
|
mimeType: parsedDataOnTheFly?.mimeType ? decodeURIComponent(parsedDataOnTheFly?.mimeType) : null,
|
||||||
|
key: parsedDataOnTheFly?.key ? decodeURIComponent(parsedDataOnTheFly?.key) : null,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}, [embedLink])
|
||||||
|
|
||||||
|
const keyIdentifier = useMemo(()=> {
|
||||||
|
|
||||||
|
if(resourceData){
|
||||||
|
return `${resourceData.service}-${resourceData.name}-${resourceData.identifier}`
|
||||||
|
} else {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}, [resourceData])
|
||||||
|
const blobUrl = useRecoilValue(blobKeySelector(keyIdentifier));
|
||||||
|
|
||||||
|
const handlePoll = async (parsedData) => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setErrorMsg("");
|
||||||
|
setType("POLL");
|
||||||
|
if (!parsedData?.name)
|
||||||
|
throw new Error("Invalid poll embed link. Missing name.");
|
||||||
|
const pollRes = await getPoll(parsedData.name);
|
||||||
|
setPoll(pollRes);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
setErrorMsg(error?.message || "Invalid embed link");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getImage = async ({ identifier, name, service }, key, parsedData) => {
|
||||||
|
try {
|
||||||
|
if(blobUrl?.blobUrl){
|
||||||
|
return blobUrl?.blobUrl
|
||||||
|
}
|
||||||
|
let numberOfTries = 0;
|
||||||
|
let imageFinalUrl = null;
|
||||||
|
|
||||||
|
const tryToGetImageStatus = async () => {
|
||||||
|
const urlStatus = `${getBaseApiReact()}/arbitrary/resource/status/${service}/${name}/${identifier}?build=true`;
|
||||||
|
|
||||||
|
const responseStatus = await fetch(urlStatus, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseData = await responseStatus.json();
|
||||||
|
if (responseData?.status === "READY") {
|
||||||
|
if (parsedData?.encryptionType) {
|
||||||
|
const urlData = `${getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}?encoding=base64`;
|
||||||
|
|
||||||
|
const responseData = await fetch(urlData, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const data = await responseData.text();
|
||||||
|
if (data) {
|
||||||
|
let decryptedData
|
||||||
|
try {
|
||||||
|
if(key && encryptionType === 'private'){
|
||||||
|
|
||||||
|
decryptedData = await new Promise((res, rej) => {
|
||||||
|
chrome?.runtime?.sendMessage(
|
||||||
|
{
|
||||||
|
action: "DECRYPT_DATA_WITH_SHARING_KEY",
|
||||||
|
type: "qortalRequest",
|
||||||
|
payload: {
|
||||||
|
encryptedData: data,
|
||||||
|
key: decodeURIComponent(key),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
(response) => {
|
||||||
|
if (response.error) {
|
||||||
|
rej(response?.message);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
res(response);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(encryptionType === 'group'){
|
||||||
|
|
||||||
|
decryptedData = await new Promise((res, rej) => {
|
||||||
|
chrome?.runtime?.sendMessage(
|
||||||
|
{
|
||||||
|
action: "DECRYPT_QORTAL_GROUP_DATA",
|
||||||
|
type: "qortalRequest",
|
||||||
|
payload: {
|
||||||
|
data64: data,
|
||||||
|
groupId: selectedGroupId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
(response) => {
|
||||||
|
if (response.error) {
|
||||||
|
rej(response?.message);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
res(response);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('Unable to decrypt')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!decryptedData || decryptedData?.error) throw new Error("Could not decrypt data");
|
||||||
|
imageFinalUrl = base64ToBlobUrl(decryptedData, parsedData?.mimeType ? decodeURIComponent(parsedData?.mimeType) : undefined)
|
||||||
|
setBlobs((prev=> {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[`${service}-${name}-${identifier}`]: {
|
||||||
|
blobUrl: imageFinalUrl,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
throw new Error('No data for image')
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
imageFinalUrl = `${getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}?async=true`;
|
||||||
|
|
||||||
|
// If parsedData is used here, it must be defined somewhere
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Retry logic
|
||||||
|
while (!imageFinalUrl && numberOfTries < 3) {
|
||||||
|
await tryToGetImageStatus();
|
||||||
|
if (!imageFinalUrl) {
|
||||||
|
numberOfTries++;
|
||||||
|
await new Promise((res) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
res(null);
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageFinalUrl) {
|
||||||
|
|
||||||
|
return imageFinalUrl;
|
||||||
|
} else {
|
||||||
|
setErrorMsg(
|
||||||
|
"Unable to download IMAGE. Please try again later by clicking the refresh button"
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching image:", error);
|
||||||
|
setErrorMsg(
|
||||||
|
error?.error || error?.message || "An unexpected error occurred while trying to download the image"
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImage = async (parsedData) => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setErrorMsg("");
|
||||||
|
if (!parsedData?.name || !parsedData?.service || !parsedData?.identifier)
|
||||||
|
throw new Error("Invalid image embed link. Missing param.");
|
||||||
|
let image = await getImage({
|
||||||
|
name: parsedData.name,
|
||||||
|
service: parsedData.service,
|
||||||
|
identifier: parsedData?.identifier,
|
||||||
|
}, parsedData?.key, parsedData);
|
||||||
|
|
||||||
|
setImageUrl(image);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
setErrorMsg(error?.message || "Invalid embed link");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const handleLink = () => {
|
||||||
|
try {
|
||||||
|
const parsedData = parseQortalLink(embedLink);
|
||||||
|
setParsedData(parsedData);
|
||||||
|
const type = parsedData?.type;
|
||||||
|
try {
|
||||||
|
if (parsedData?.ref) {
|
||||||
|
const res = extractComponents(decodeURIComponent(parsedData.ref));
|
||||||
|
if (res?.service && res?.name) {
|
||||||
|
setExternal(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
}
|
||||||
|
switch (type) {
|
||||||
|
case "POLL":
|
||||||
|
{
|
||||||
|
handlePoll(parsedData);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "IMAGE":
|
||||||
|
setType("IMAGE");
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "ATTACHMENT":
|
||||||
|
setType("ATTACHMENT");
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setErrorMsg(error?.message || "Invalid embed link");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchImage = () => {
|
||||||
|
try {
|
||||||
|
const parsedData = parseQortalLink(embedLink);
|
||||||
|
handleImage(parsedData);
|
||||||
|
} catch (error) {
|
||||||
|
setErrorMsg(error?.message || "Invalid embed link");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openExternal = () => {
|
||||||
|
executeEvent("addTab", { data: external });
|
||||||
|
executeEvent("open-apps-mode", {});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!embedLink || hasFetched.current) return;
|
||||||
|
handleLink();
|
||||||
|
hasFetched.current = true;
|
||||||
|
}, [embedLink]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const resourceDetails = useRecoilValue(resourceKeySelector(keyIdentifier));
|
||||||
|
|
||||||
|
const { parsedType, encryptionType } = useMemo(() => {
|
||||||
|
let parsedType;
|
||||||
|
let encryptionType = false;
|
||||||
|
try {
|
||||||
|
const parsedDataOnTheFly = parseQortalLink(embedLink);
|
||||||
|
if (parsedDataOnTheFly?.type) {
|
||||||
|
parsedType = parsedDataOnTheFly.type;
|
||||||
|
}
|
||||||
|
if (parsedDataOnTheFly?.encryptionType) {
|
||||||
|
encryptionType = parsedDataOnTheFly?.encryptionType
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
return { parsedType, encryptionType };
|
||||||
|
}, [embedLink]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{parsedType === "POLL" && (
|
||||||
|
<PollCard
|
||||||
|
poll={poll}
|
||||||
|
refresh={handleLink}
|
||||||
|
setInfoSnack={setInfoSnack}
|
||||||
|
setOpenSnack={setOpenSnack}
|
||||||
|
external={external}
|
||||||
|
openExternal={openExternal}
|
||||||
|
isLoadingParent={isLoading}
|
||||||
|
errorMsg={errorMsg}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{parsedType === "IMAGE" && (
|
||||||
|
<ImageCard
|
||||||
|
image={imageUrl}
|
||||||
|
owner={parsedData?.name}
|
||||||
|
fetchImage={fetchImage}
|
||||||
|
refresh={fetchImage}
|
||||||
|
setInfoSnack={setInfoSnack}
|
||||||
|
setOpenSnack={setOpenSnack}
|
||||||
|
external={external}
|
||||||
|
openExternal={openExternal}
|
||||||
|
isLoadingParent={isLoading}
|
||||||
|
errorMsg={errorMsg}
|
||||||
|
encryptionType={encryptionType}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{parsedType === 'ATTACHMENT' && (
|
||||||
|
<AttachmentCard
|
||||||
|
resourceData={resourceData}
|
||||||
|
resourceDetails={resourceDetails}
|
||||||
|
owner={parsedData?.name}
|
||||||
|
refresh={fetchImage}
|
||||||
|
setInfoSnack={setInfoSnack}
|
||||||
|
setOpenSnack={setOpenSnack}
|
||||||
|
external={external}
|
||||||
|
openExternal={openExternal}
|
||||||
|
isLoadingParent={isLoading}
|
||||||
|
errorMsg={errorMsg}
|
||||||
|
encryptionType={encryptionType}
|
||||||
|
selectedGroupId={selectedGroupId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<CustomizedSnackbars
|
||||||
|
duration={2000}
|
||||||
|
open={openSnack}
|
||||||
|
setOpen={setOpenSnack}
|
||||||
|
info={infoSnack}
|
||||||
|
setInfo={setInfoSnack}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
265
src/components/Embeds/ImageEmbed.tsx
Normal file
265
src/components/Embeds/ImageEmbed.tsx
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Typography,
|
||||||
|
|
||||||
|
Box,
|
||||||
|
ButtonBase,
|
||||||
|
Divider,
|
||||||
|
Dialog,
|
||||||
|
IconButton,
|
||||||
|
|
||||||
|
} from "@mui/material";
|
||||||
|
|
||||||
|
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||||
|
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
|
||||||
|
import { CustomLoader } from "../../common/CustomLoader";
|
||||||
|
import ImageIcon from "@mui/icons-material/Image";
|
||||||
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
|
import { decodeIfEncoded } from "../../utils/decode";
|
||||||
|
|
||||||
|
export const ImageCard = ({
|
||||||
|
image,
|
||||||
|
fetchImage,
|
||||||
|
owner,
|
||||||
|
refresh,
|
||||||
|
openExternal,
|
||||||
|
external,
|
||||||
|
isLoadingParent,
|
||||||
|
errorMsg,
|
||||||
|
encryptionType,
|
||||||
|
}) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(true);
|
||||||
|
const [height, setHeight] = useState('400px')
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
fetchImage();
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// useEffect(()=> {
|
||||||
|
// if(errorMsg){
|
||||||
|
// setHeight('300px')
|
||||||
|
// }
|
||||||
|
// }, [errorMsg])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "#1F2023",
|
||||||
|
height: height,
|
||||||
|
transition: "height 0.6s ease-in-out",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
padding: "16px 16px 0px 16px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ImageIcon
|
||||||
|
sx={{
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography>IMAGE embed</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ButtonBase>
|
||||||
|
<RefreshIcon
|
||||||
|
onClick={refresh}
|
||||||
|
sx={{
|
||||||
|
fontSize: "24px",
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ButtonBase>
|
||||||
|
{external && (
|
||||||
|
<ButtonBase>
|
||||||
|
<OpenInNewIcon
|
||||||
|
onClick={openExternal}
|
||||||
|
sx={{
|
||||||
|
fontSize: "24px",
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ButtonBase>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: "8px 16px 8px 16px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "12px",
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Created by {decodeIfEncoded(owner)}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "12px",
|
||||||
|
color: "cadetblue",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{encryptionType === 'private' ? "ENCRYPTED" : encryptionType === 'group' ? 'GROUP ENCRYPTED' : "Not encrypted"}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Divider sx={{ borderColor: "rgb(255 255 255 / 10%)" }} />
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
width: "100%",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
|
{isLoadingParent && isOpen && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
<CustomLoader />{" "}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{errorMsg && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "var(--danger)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{errorMsg}
|
||||||
|
</Typography>{" "}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<CardContent>
|
||||||
|
<ImageViewer src={image} />
|
||||||
|
</CardContent>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ImageViewer({ src, alt = "" }) {
|
||||||
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
|
||||||
|
const handleOpenFullscreen = () => setIsFullscreen(true);
|
||||||
|
const handleCloseFullscreen = () => setIsFullscreen(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Image in container */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
maxWidth: "100%", // Prevent horizontal overflow
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
onClick={handleOpenFullscreen}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
style={{
|
||||||
|
maxWidth: "100%",
|
||||||
|
maxHeight: "450px", // Adjust max height for small containers
|
||||||
|
objectFit: "contain", // Preserve aspect ratio
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Fullscreen Viewer */}
|
||||||
|
<Dialog
|
||||||
|
open={isFullscreen}
|
||||||
|
onClose={handleCloseFullscreen}
|
||||||
|
maxWidth="lg"
|
||||||
|
fullWidth
|
||||||
|
fullScreen
|
||||||
|
sx={{
|
||||||
|
"& .MuiDialog-paper": {
|
||||||
|
margin: 0,
|
||||||
|
maxWidth: "100%",
|
||||||
|
width: "100%",
|
||||||
|
height: "100vh",
|
||||||
|
overflow: "hidden", // Prevent scrollbars
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "relative",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
backgroundColor: "#000", // Optional: dark background for fullscreen mode
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Close Button */}
|
||||||
|
<IconButton
|
||||||
|
onClick={handleCloseFullscreen}
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
zIndex: 10,
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
{/* Fullscreen Image */}
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
style={{
|
||||||
|
maxWidth: "100%",
|
||||||
|
maxHeight: "100%",
|
||||||
|
objectFit: "contain", // Preserve aspect ratio
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
395
src/components/Embeds/PollEmbed.tsx
Normal file
395
src/components/Embeds/PollEmbed.tsx
Normal file
@ -0,0 +1,395 @@
|
|||||||
|
import React, { useContext, useEffect, useState } from "react";
|
||||||
|
import { MyContext } from "../../App";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
Typography,
|
||||||
|
RadioGroup,
|
||||||
|
Radio,
|
||||||
|
FormControlLabel,
|
||||||
|
Button,
|
||||||
|
Box,
|
||||||
|
ButtonBase,
|
||||||
|
Divider,
|
||||||
|
|
||||||
|
} from "@mui/material";
|
||||||
|
import { getNameInfo } from "../Group/Group";
|
||||||
|
import PollIcon from "@mui/icons-material/Poll";
|
||||||
|
import { getFee } from "../../background";
|
||||||
|
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||||
|
import { Spacer } from "../../common/Spacer";
|
||||||
|
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
|
||||||
|
import { CustomLoader } from "../../common/CustomLoader";
|
||||||
|
|
||||||
|
|
||||||
|
export const PollCard = ({
|
||||||
|
poll,
|
||||||
|
setInfoSnack,
|
||||||
|
setOpenSnack,
|
||||||
|
refresh,
|
||||||
|
openExternal,
|
||||||
|
external,
|
||||||
|
isLoadingParent,
|
||||||
|
errorMsg,
|
||||||
|
}) => {
|
||||||
|
const [selectedOption, setSelectedOption] = useState("");
|
||||||
|
const [ownerName, setOwnerName] = useState("");
|
||||||
|
const [showResults, setShowResults] = useState(false);
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const { show, userInfo } = useContext(MyContext);
|
||||||
|
const [isLoadingSubmit, setIsLoadingSubmit] = useState(false);
|
||||||
|
const handleVote = async () => {
|
||||||
|
const fee = await getFee("VOTE_ON_POLL");
|
||||||
|
|
||||||
|
await show({
|
||||||
|
message: `Do you accept this VOTE_ON_POLL transaction? POLLS are public!`,
|
||||||
|
publishFee: fee.fee + " QORT",
|
||||||
|
});
|
||||||
|
setIsLoadingSubmit(true);
|
||||||
|
|
||||||
|
window
|
||||||
|
.sendMessage(
|
||||||
|
"voteOnPoll",
|
||||||
|
{
|
||||||
|
pollName: poll?.info?.pollName,
|
||||||
|
optionIndex: +selectedOption,
|
||||||
|
},
|
||||||
|
60000
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
setIsLoadingSubmit(false);
|
||||||
|
if (response.error) {
|
||||||
|
setInfoSnack({
|
||||||
|
type: "error",
|
||||||
|
message: response?.error || "Unable to vote.",
|
||||||
|
});
|
||||||
|
setOpenSnack(true);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
setInfoSnack({
|
||||||
|
type: "success",
|
||||||
|
message:
|
||||||
|
"Successfully voted. Please wait a couple minutes for the network to propogate the changes.",
|
||||||
|
});
|
||||||
|
setOpenSnack(true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setIsLoadingSubmit(false);
|
||||||
|
setInfoSnack({
|
||||||
|
type: "error",
|
||||||
|
message: error?.message || "Unable to vote.",
|
||||||
|
});
|
||||||
|
setOpenSnack(true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getName = async (owner) => {
|
||||||
|
try {
|
||||||
|
const res = await getNameInfo(owner);
|
||||||
|
if (res) {
|
||||||
|
setOwnerName(res);
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (poll?.info?.owner) {
|
||||||
|
getName(poll.info.owner);
|
||||||
|
}
|
||||||
|
}, [poll?.info?.owner]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "#1F2023",
|
||||||
|
height: isOpen ? "auto" : "150px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
padding: "16px 16px 0px 16px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PollIcon
|
||||||
|
sx={{
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography>POLL embed</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ButtonBase>
|
||||||
|
<RefreshIcon
|
||||||
|
onClick={refresh}
|
||||||
|
sx={{
|
||||||
|
fontSize: "24px",
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ButtonBase>
|
||||||
|
{external && (
|
||||||
|
<ButtonBase>
|
||||||
|
<OpenInNewIcon
|
||||||
|
onClick={openExternal}
|
||||||
|
sx={{
|
||||||
|
fontSize: "24px",
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ButtonBase>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: "8px 16px 8px 16px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "12px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Created by {ownerName || poll?.info?.owner}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Divider sx={{ borderColor: "rgb(255 255 255 / 10%)" }} />
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
width: "100%",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!isOpen && !errorMsg && (
|
||||||
|
<>
|
||||||
|
<Spacer height="5px" />
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "var(--green)",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Show poll
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isLoadingParent && isOpen && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
<CustomLoader />{" "}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{errorMsg && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "var(--danger)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{errorMsg}
|
||||||
|
</Typography>{" "}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: isOpen ? "block" : "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardHeader
|
||||||
|
title={poll?.info?.pollName}
|
||||||
|
subheader={poll?.info?.description}
|
||||||
|
sx={{
|
||||||
|
"& .MuiCardHeader-title": {
|
||||||
|
fontSize: "18px", // Custom font size for title
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CardContent>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "18px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Options
|
||||||
|
</Typography>
|
||||||
|
<RadioGroup
|
||||||
|
value={selectedOption}
|
||||||
|
onChange={(e) => setSelectedOption(e.target.value)}
|
||||||
|
>
|
||||||
|
{poll?.info?.pollOptions?.map((option, index) => (
|
||||||
|
<FormControlLabel
|
||||||
|
key={index}
|
||||||
|
value={index}
|
||||||
|
control={
|
||||||
|
<Radio
|
||||||
|
sx={{
|
||||||
|
color: "white", // Unchecked color
|
||||||
|
"&.Mui-checked": {
|
||||||
|
color: "var(--green)", // Checked color
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={option?.optionName}
|
||||||
|
sx={{
|
||||||
|
"& .MuiFormControlLabel-label": {
|
||||||
|
fontSize: "14px",
|
||||||
|
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
disabled={!selectedOption || isLoadingSubmit}
|
||||||
|
onClick={handleVote}
|
||||||
|
>
|
||||||
|
Vote
|
||||||
|
</Button>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
fontStyle: "italic",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
{`${poll?.votes?.totalVotes} ${
|
||||||
|
poll?.votes?.totalVotes === 1 ? " vote" : " votes"
|
||||||
|
}`}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Spacer height="10px" />
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
visibility: poll?.votes?.votes?.find(
|
||||||
|
(item) => item?.voterPublicKey === userInfo?.publicKey
|
||||||
|
)
|
||||||
|
? "visible"
|
||||||
|
: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
You've already voted.
|
||||||
|
</Typography>
|
||||||
|
<Spacer height="10px" />
|
||||||
|
{isLoadingSubmit && (
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "12px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Is processing transaction, please wait...
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<ButtonBase
|
||||||
|
onClick={() => {
|
||||||
|
setShowResults((prev) => !prev);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showResults ? "hide " : "show "} results
|
||||||
|
</ButtonBase>
|
||||||
|
</CardContent>
|
||||||
|
{showResults && <PollResults votes={poll?.votes} />}
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PollResults = ({ votes }) => {
|
||||||
|
const maxVotes = Math.max(
|
||||||
|
...votes?.voteCounts?.map((option) => option.voteCount)
|
||||||
|
);
|
||||||
|
const options = votes?.voteCounts;
|
||||||
|
return (
|
||||||
|
<Box sx={{ width: "100%", p: 2 }}>
|
||||||
|
{options
|
||||||
|
.sort((a, b) => b.voteCount - a.voteCount) // Sort options by votes (highest first)
|
||||||
|
.map((option, index) => (
|
||||||
|
<Box key={index} sx={{ mb: 2 }}>
|
||||||
|
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={{ fontWeight: index === 0 ? "bold" : "normal" , fontSize: "14px"}}
|
||||||
|
>
|
||||||
|
{`${index + 1}. ${option.optionName}`}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={{ fontWeight: index === 0 ? "bold" : "normal" , fontSize: "14px"}}
|
||||||
|
>
|
||||||
|
{option.voteCount} votes
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mt: 1,
|
||||||
|
height: 10,
|
||||||
|
backgroundColor: "#e0e0e0",
|
||||||
|
borderRadius: 5,
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: `${(option.voteCount / maxVotes) * 100}%`,
|
||||||
|
height: "100%",
|
||||||
|
backgroundColor: index === 0 ? "#3f51b5" : "#f50057",
|
||||||
|
transition: "width 0.3s ease-in-out",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
723
src/components/Embeds/VideoPlayer.tsx
Normal file
723
src/components/Embeds/VideoPlayer.tsx
Normal file
@ -0,0 +1,723 @@
|
|||||||
|
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import { Box, IconButton, Slider } from '@mui/material'
|
||||||
|
import { CircularProgress, Typography } from '@mui/material'
|
||||||
|
import { Key } from 'ts-key-enum'
|
||||||
|
import {
|
||||||
|
PlayArrow,
|
||||||
|
Pause,
|
||||||
|
VolumeUp,
|
||||||
|
Fullscreen,
|
||||||
|
PictureInPicture, VolumeOff, Calculate
|
||||||
|
} from '@mui/icons-material'
|
||||||
|
import { styled } from '@mui/system'
|
||||||
|
import { Refresh } from '@mui/icons-material'
|
||||||
|
|
||||||
|
import { Menu, MenuItem } from '@mui/material'
|
||||||
|
import { MoreVert as MoreIcon } from '@mui/icons-material'
|
||||||
|
import { GlobalContext, getBaseApiReact } from '../../App'
|
||||||
|
import { resourceKeySelector } from '../../atoms/global'
|
||||||
|
import { useRecoilValue } from 'recoil'
|
||||||
|
const VideoContainer = styled(Box)`
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const VideoElement = styled('video')`
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-height: calc(100vh - 150px);
|
||||||
|
background: rgb(33, 33, 33);
|
||||||
|
`
|
||||||
|
|
||||||
|
const ControlsContainer = styled(Box)`
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
`
|
||||||
|
|
||||||
|
interface VideoPlayerProps {
|
||||||
|
src?: string
|
||||||
|
poster?: string
|
||||||
|
name?: string
|
||||||
|
identifier?: string
|
||||||
|
service?: string
|
||||||
|
autoplay?: boolean
|
||||||
|
from?: string | null
|
||||||
|
customStyle?: any
|
||||||
|
user?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||||
|
poster,
|
||||||
|
name,
|
||||||
|
identifier,
|
||||||
|
service,
|
||||||
|
autoplay = true,
|
||||||
|
from = null,
|
||||||
|
customStyle = {},
|
||||||
|
node
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const keyIdentifier = useMemo(()=> {
|
||||||
|
|
||||||
|
if(name && identifier && service){
|
||||||
|
return `${service}-${name}-${identifier}`
|
||||||
|
} else {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}, [service, name, identifier])
|
||||||
|
const download = useRecoilValue(resourceKeySelector(keyIdentifier));
|
||||||
|
const { downloadResource } = useContext(GlobalContext);
|
||||||
|
|
||||||
|
const videoRef = useRef<HTMLVideoElement | null>(null)
|
||||||
|
const [playing, setPlaying] = useState(false)
|
||||||
|
const [volume, setVolume] = useState(1)
|
||||||
|
const [mutedVolume, setMutedVolume] = useState(1)
|
||||||
|
const [isMuted, setIsMuted] = useState(false)
|
||||||
|
const [progress, setProgress] = useState(0)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [canPlay, setCanPlay] = useState(false)
|
||||||
|
const [startPlay, setStartPlay] = useState(false)
|
||||||
|
const [isMobileView, setIsMobileView] = useState(false)
|
||||||
|
const [playbackRate, setPlaybackRate] = useState(1)
|
||||||
|
const [anchorEl, setAnchorEl] = useState(null)
|
||||||
|
const reDownload = useRef<boolean>(false)
|
||||||
|
|
||||||
|
const resetVideoState = () => {
|
||||||
|
// Reset all states to their initial values
|
||||||
|
setPlaying(false);
|
||||||
|
setVolume(1);
|
||||||
|
setMutedVolume(1);
|
||||||
|
setIsMuted(false);
|
||||||
|
setProgress(0);
|
||||||
|
setIsLoading(false);
|
||||||
|
setCanPlay(false);
|
||||||
|
setStartPlay(false);
|
||||||
|
setIsMobileView(false);
|
||||||
|
setPlaybackRate(1);
|
||||||
|
setAnchorEl(null);
|
||||||
|
|
||||||
|
// Reset refs to their initial values
|
||||||
|
if (videoRef.current) {
|
||||||
|
videoRef.current.pause(); // Ensure the video is paused
|
||||||
|
videoRef.current.currentTime = 0; // Reset video progress
|
||||||
|
}
|
||||||
|
reDownload.current = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const src = useMemo(() => {
|
||||||
|
if(name && identifier && service){
|
||||||
|
return `${node || getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}`
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}, [service, name, identifier])
|
||||||
|
|
||||||
|
useEffect(()=> {
|
||||||
|
resetVideoState()
|
||||||
|
}, [keyIdentifier])
|
||||||
|
const resourceStatus = useMemo(() => {
|
||||||
|
return download?.status || {}
|
||||||
|
}, [download])
|
||||||
|
|
||||||
|
const minSpeed = 0.25;
|
||||||
|
const maxSpeed = 4.0;
|
||||||
|
const speedChange = 0.25;
|
||||||
|
|
||||||
|
const updatePlaybackRate = (newSpeed: number) => {
|
||||||
|
if (videoRef.current) {
|
||||||
|
if (newSpeed > maxSpeed || newSpeed < minSpeed)
|
||||||
|
newSpeed = minSpeed
|
||||||
|
videoRef.current.playbackRate = newSpeed
|
||||||
|
setPlaybackRate(newSpeed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const increaseSpeed = (wrapOverflow = true) => {
|
||||||
|
const changedSpeed = playbackRate + speedChange
|
||||||
|
let newSpeed = wrapOverflow ? changedSpeed : Math.min(changedSpeed, maxSpeed)
|
||||||
|
|
||||||
|
|
||||||
|
if (videoRef.current) {
|
||||||
|
updatePlaybackRate(newSpeed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const decreaseSpeed = () => {
|
||||||
|
if (videoRef.current) {
|
||||||
|
updatePlaybackRate(playbackRate - speedChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const togglePlay = async () => {
|
||||||
|
if (!videoRef.current) return
|
||||||
|
setStartPlay(true)
|
||||||
|
if (!src || resourceStatus?.status !== 'READY') {
|
||||||
|
ReactDOM.flushSync(() => {
|
||||||
|
setIsLoading(true)
|
||||||
|
})
|
||||||
|
getSrc()
|
||||||
|
}
|
||||||
|
if (playing) {
|
||||||
|
videoRef.current.pause()
|
||||||
|
} else {
|
||||||
|
videoRef.current.play()
|
||||||
|
}
|
||||||
|
setPlaying(!playing)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const onVolumeChange = (_: any, value: number | number[]) => {
|
||||||
|
if (!videoRef.current) return
|
||||||
|
videoRef.current.volume = value as number
|
||||||
|
setVolume(value as number)
|
||||||
|
setIsMuted(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onProgressChange = (_: any, value: number | number[]) => {
|
||||||
|
if (!videoRef.current) return
|
||||||
|
videoRef.current.currentTime = value as number
|
||||||
|
setProgress(value as number)
|
||||||
|
if (!playing) {
|
||||||
|
videoRef.current.play()
|
||||||
|
setPlaying(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEnded = () => {
|
||||||
|
setPlaying(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateProgress = () => {
|
||||||
|
if (!videoRef.current) return
|
||||||
|
setProgress(videoRef.current.currentTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [isFullscreen, setIsFullscreen] = useState(false)
|
||||||
|
|
||||||
|
const enterFullscreen = () => {
|
||||||
|
if (!videoRef.current) return
|
||||||
|
if (videoRef.current.requestFullscreen) {
|
||||||
|
videoRef.current.requestFullscreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const exitFullscreen = () => {
|
||||||
|
if (document.exitFullscreen) {
|
||||||
|
document.exitFullscreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleFullscreen = () => {
|
||||||
|
isFullscreen ? exitFullscreen() : enterFullscreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleFullscreenChange = () => {
|
||||||
|
setIsFullscreen(!!document.fullscreenElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('fullscreenchange', handleFullscreenChange)
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('fullscreenchange', handleFullscreenChange)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const handleCanPlay = () => {
|
||||||
|
setIsLoading(false)
|
||||||
|
setCanPlay(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSrc = React.useCallback(async () => {
|
||||||
|
if (!name || !identifier || !service) return
|
||||||
|
try {
|
||||||
|
downloadResource({
|
||||||
|
name,
|
||||||
|
service,
|
||||||
|
identifier
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}, [identifier, name, service])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function formatTime(seconds: number): string {
|
||||||
|
seconds = Math.floor(seconds)
|
||||||
|
let minutes: number | string = Math.floor(seconds / 60)
|
||||||
|
let hours: number | string = Math.floor(minutes / 60)
|
||||||
|
|
||||||
|
let remainingSeconds: number | string = seconds % 60
|
||||||
|
let remainingMinutes: number | string = minutes % 60
|
||||||
|
|
||||||
|
if (remainingSeconds < 10) {
|
||||||
|
remainingSeconds = '0' + remainingSeconds
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingMinutes < 10) {
|
||||||
|
remainingMinutes = '0' + remainingMinutes
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hours === 0) {
|
||||||
|
hours = ''
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
hours = hours + ':'
|
||||||
|
}
|
||||||
|
|
||||||
|
return hours + remainingMinutes + ':' + remainingSeconds
|
||||||
|
}
|
||||||
|
|
||||||
|
const reloadVideo = () => {
|
||||||
|
if (!videoRef.current) return
|
||||||
|
const currentTime = videoRef.current.currentTime
|
||||||
|
videoRef.current.src = src
|
||||||
|
videoRef.current.load()
|
||||||
|
videoRef.current.currentTime = currentTime
|
||||||
|
if (playing) {
|
||||||
|
videoRef.current.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
resourceStatus?.status === 'DOWNLOADED' &&
|
||||||
|
reDownload?.current === false
|
||||||
|
) {
|
||||||
|
getSrc()
|
||||||
|
reDownload.current = true
|
||||||
|
}
|
||||||
|
}, [getSrc, resourceStatus])
|
||||||
|
|
||||||
|
const handleMenuOpen = (event: any) => {
|
||||||
|
setAnchorEl(event.currentTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMenuClose = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const videoWidth = videoRef?.current?.offsetWidth
|
||||||
|
if (videoWidth && videoWidth <= 600) {
|
||||||
|
setIsMobileView(true)
|
||||||
|
}
|
||||||
|
}, [canPlay])
|
||||||
|
|
||||||
|
const getDownloadProgress = (current: number, total: number) => {
|
||||||
|
const progress = current / total * 100;
|
||||||
|
return Number.isNaN(progress) ? '' : progress.toFixed(0) + '%'
|
||||||
|
}
|
||||||
|
const mute = () => {
|
||||||
|
setIsMuted(true)
|
||||||
|
setMutedVolume(volume)
|
||||||
|
setVolume(0)
|
||||||
|
if (videoRef.current) videoRef.current.volume = 0
|
||||||
|
}
|
||||||
|
const unMute = () => {
|
||||||
|
setIsMuted(false)
|
||||||
|
setVolume(mutedVolume)
|
||||||
|
if (videoRef.current) videoRef.current.volume = mutedVolume
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleMute = () => {
|
||||||
|
isMuted ? unMute() : mute();
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeVolume = (volumeChange: number) => {
|
||||||
|
if (videoRef.current) {
|
||||||
|
const minVolume = 0;
|
||||||
|
const maxVolume = 1;
|
||||||
|
|
||||||
|
|
||||||
|
let newVolume = volumeChange + volume
|
||||||
|
|
||||||
|
newVolume = Math.max(newVolume, minVolume)
|
||||||
|
newVolume = Math.min(newVolume, maxVolume)
|
||||||
|
|
||||||
|
setIsMuted(false)
|
||||||
|
setMutedVolume(newVolume)
|
||||||
|
videoRef.current.volume = newVolume
|
||||||
|
setVolume(newVolume);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
const setProgressRelative = (secondsChange: number) => {
|
||||||
|
if (videoRef.current) {
|
||||||
|
const currentTime = videoRef.current?.currentTime
|
||||||
|
const minTime = 0
|
||||||
|
const maxTime = videoRef.current?.duration || 100
|
||||||
|
|
||||||
|
let newTime = currentTime + secondsChange;
|
||||||
|
newTime = Math.max(newTime, minTime)
|
||||||
|
newTime = Math.min(newTime, maxTime)
|
||||||
|
videoRef.current.currentTime = newTime;
|
||||||
|
setProgress(newTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setProgressAbsolute = (videoPercent: number) => {
|
||||||
|
if (videoRef.current) {
|
||||||
|
videoPercent = Math.min(videoPercent, 100)
|
||||||
|
videoPercent = Math.max(videoPercent, 0)
|
||||||
|
const finalTime = videoRef.current?.duration * videoPercent / 100
|
||||||
|
videoRef.current.currentTime = finalTime
|
||||||
|
setProgress(finalTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const keyboardShortcutsDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
switch (e.key) {
|
||||||
|
case Key.Add: increaseSpeed(false); break;
|
||||||
|
case '+': increaseSpeed(false); break;
|
||||||
|
case '>': increaseSpeed(false); break;
|
||||||
|
|
||||||
|
case Key.Subtract: decreaseSpeed(); break;
|
||||||
|
case '-': decreaseSpeed(); break;
|
||||||
|
case '<': decreaseSpeed(); break;
|
||||||
|
|
||||||
|
case Key.ArrowLeft: {
|
||||||
|
if (e.shiftKey) setProgressRelative(-300);
|
||||||
|
else if (e.ctrlKey) setProgressRelative(-60);
|
||||||
|
else if (e.altKey) setProgressRelative(-10);
|
||||||
|
else setProgressRelative(-5);
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case Key.ArrowRight: {
|
||||||
|
if (e.shiftKey) setProgressRelative(300);
|
||||||
|
else if (e.ctrlKey) setProgressRelative(60);
|
||||||
|
else if (e.altKey) setProgressRelative(10);
|
||||||
|
else setProgressRelative(5);
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case Key.ArrowDown: changeVolume(-0.05); break;
|
||||||
|
case Key.ArrowUp: changeVolume(0.05); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyboardShortcutsUp = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
switch (e.key) {
|
||||||
|
case ' ': togglePlay(); break;
|
||||||
|
case 'm': toggleMute(); break;
|
||||||
|
|
||||||
|
case 'f': enterFullscreen(); break;
|
||||||
|
case Key.Escape: exitFullscreen(); break;
|
||||||
|
|
||||||
|
case '0': setProgressAbsolute(0); break;
|
||||||
|
case '1': setProgressAbsolute(10); break;
|
||||||
|
case '2': setProgressAbsolute(20); break;
|
||||||
|
case '3': setProgressAbsolute(30); break;
|
||||||
|
case '4': setProgressAbsolute(40); break;
|
||||||
|
case '5': setProgressAbsolute(50); break;
|
||||||
|
case '6': setProgressAbsolute(60); break;
|
||||||
|
case '7': setProgressAbsolute(70); break;
|
||||||
|
case '8': setProgressAbsolute(80); break;
|
||||||
|
case '9': setProgressAbsolute(90); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VideoContainer
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyUp={keyboardShortcutsUp}
|
||||||
|
onKeyDown={keyboardShortcutsDown}
|
||||||
|
style={{
|
||||||
|
padding: from === 'create' ? '8px' : 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
top={0}
|
||||||
|
left={0}
|
||||||
|
right={0}
|
||||||
|
bottom={resourceStatus?.status === 'READY' ? '55px ' : 0}
|
||||||
|
display="flex"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
zIndex={25}
|
||||||
|
bgcolor="rgba(0, 0, 0, 0.6)"
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '10px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress color="secondary" />
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
variant="subtitle2"
|
||||||
|
component="div"
|
||||||
|
sx={{
|
||||||
|
color: 'white',
|
||||||
|
fontSize: '15px',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{resourceStatus?.status === 'REFETCHING' ? (
|
||||||
|
<>
|
||||||
|
<>
|
||||||
|
{getDownloadProgress(resourceStatus?.localChunkCount, resourceStatus?.totalChunkCount)}
|
||||||
|
</>
|
||||||
|
|
||||||
|
<> Refetching data in 25 seconds</>
|
||||||
|
</>
|
||||||
|
) : resourceStatus?.status === 'DOWNLOADED' ? (
|
||||||
|
<>Download Completed: building tutorial video...</>
|
||||||
|
) : resourceStatus?.status !== 'READY' ? (
|
||||||
|
<>
|
||||||
|
{getDownloadProgress(resourceStatus?.localChunkCount || 0, resourceStatus?.totalChunkCount || 100)}
|
||||||
|
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>Fetching tutorial from the Qortal Network...</>
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{((!src && !isLoading) || !startPlay) && (
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
top={0}
|
||||||
|
left={0}
|
||||||
|
right={0}
|
||||||
|
bottom={0}
|
||||||
|
display="flex"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
zIndex={500}
|
||||||
|
bgcolor="rgba(0, 0, 0, 0.6)"
|
||||||
|
onClick={() => {
|
||||||
|
togglePlay()
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlayArrow
|
||||||
|
sx={{
|
||||||
|
width: '50px',
|
||||||
|
height: '50px',
|
||||||
|
color: 'white'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexGrow: 1,
|
||||||
|
width: '100%',
|
||||||
|
height: 'calc(100% - 60px)',
|
||||||
|
}}>
|
||||||
|
<VideoElement
|
||||||
|
id={identifier}
|
||||||
|
ref={videoRef}
|
||||||
|
src={!startPlay ? '' : resourceStatus?.status === 'READY' ? src : ''}
|
||||||
|
poster={!startPlay ? poster : ""}
|
||||||
|
onTimeUpdate={updateProgress}
|
||||||
|
autoPlay={autoplay}
|
||||||
|
onClick={togglePlay}
|
||||||
|
onEnded={handleEnded}
|
||||||
|
// onLoadedMetadata={handleLoadedMetadata}
|
||||||
|
onCanPlay={handleCanPlay}
|
||||||
|
preload="metadata"
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
...customStyle
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<ControlsContainer
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
background: 'var(--bg-primary)',
|
||||||
|
width: '100%',
|
||||||
|
flexShrink: 0
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isMobileView && canPlay ? (
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
sx={{
|
||||||
|
color: 'rgba(255, 255, 255, 0.7)'
|
||||||
|
}}
|
||||||
|
onClick={togglePlay}
|
||||||
|
>
|
||||||
|
{playing ? <Pause /> : <PlayArrow />}
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
sx={{
|
||||||
|
color: 'rgba(255, 255, 255, 0.7)',
|
||||||
|
marginLeft: '15px'
|
||||||
|
}}
|
||||||
|
onClick={reloadVideo}
|
||||||
|
>
|
||||||
|
<Refresh />
|
||||||
|
</IconButton>
|
||||||
|
<Slider
|
||||||
|
value={progress}
|
||||||
|
onChange={onProgressChange}
|
||||||
|
min={0}
|
||||||
|
max={videoRef.current?.duration || 100}
|
||||||
|
sx={{ flexGrow: 1, mx: 2 }}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
edge="end"
|
||||||
|
color="inherit"
|
||||||
|
aria-label="menu"
|
||||||
|
onClick={handleMenuOpen}
|
||||||
|
>
|
||||||
|
<MoreIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Menu
|
||||||
|
id="simple-menu"
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
keepMounted
|
||||||
|
open={Boolean(anchorEl)}
|
||||||
|
onClose={handleMenuClose}
|
||||||
|
PaperProps={{
|
||||||
|
style: {
|
||||||
|
width: '250px'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem>
|
||||||
|
<VolumeUp />
|
||||||
|
<Slider
|
||||||
|
value={volume}
|
||||||
|
onChange={onVolumeChange}
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.01} />
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => increaseSpeed()}>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
color: 'rgba(255, 255, 255, 0.7)',
|
||||||
|
fontSize: '14px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Speed: {playbackRate}x
|
||||||
|
</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={toggleFullscreen}>
|
||||||
|
<Fullscreen />
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
) : canPlay ? (
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
sx={{
|
||||||
|
color: 'rgba(255, 255, 255, 0.7)'
|
||||||
|
}}
|
||||||
|
onClick={togglePlay}
|
||||||
|
>
|
||||||
|
{playing ? <Pause /> : <PlayArrow />}
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
sx={{
|
||||||
|
color: 'rgba(255, 255, 255, 0.7)',
|
||||||
|
marginLeft: '15px'
|
||||||
|
}}
|
||||||
|
onClick={reloadVideo}
|
||||||
|
>
|
||||||
|
<Refresh />
|
||||||
|
</IconButton>
|
||||||
|
<Slider
|
||||||
|
value={progress}
|
||||||
|
onChange={onProgressChange}
|
||||||
|
min={0}
|
||||||
|
max={videoRef.current?.duration || 100}
|
||||||
|
sx={{ flexGrow: 1, mx: 2, color: 'var(--Mail-Background)' }}
|
||||||
|
/>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: '14px',
|
||||||
|
marginRight: '5px',
|
||||||
|
color: 'rgba(255, 255, 255, 0.7)',
|
||||||
|
visibility:
|
||||||
|
!videoRef.current?.duration || !progress
|
||||||
|
? 'hidden'
|
||||||
|
: 'visible',
|
||||||
|
flexShrink: 0
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{progress && videoRef.current?.duration && formatTime(progress)}/
|
||||||
|
{progress &&
|
||||||
|
videoRef.current?.duration &&
|
||||||
|
formatTime(videoRef.current?.duration)}
|
||||||
|
</Typography>
|
||||||
|
<IconButton
|
||||||
|
sx={{
|
||||||
|
color: 'rgba(255, 255, 255, 0.7)',
|
||||||
|
marginRight: '10px'
|
||||||
|
}}
|
||||||
|
onClick={toggleMute}
|
||||||
|
>
|
||||||
|
{isMuted ? <VolumeOff /> : <VolumeUp />}
|
||||||
|
</IconButton>
|
||||||
|
<Slider
|
||||||
|
value={volume}
|
||||||
|
onChange={onVolumeChange}
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.01}
|
||||||
|
sx={{
|
||||||
|
maxWidth: '100px',
|
||||||
|
color: 'var(--Mail-Background)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
sx={{
|
||||||
|
color: 'rgba(255, 255, 255, 0.7)',
|
||||||
|
fontSize: '14px',
|
||||||
|
marginLeft: '5px'
|
||||||
|
}}
|
||||||
|
onClick={(e) => increaseSpeed()}
|
||||||
|
>
|
||||||
|
Speed: {playbackRate}x
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
sx={{
|
||||||
|
color: 'rgba(255, 255, 255, 0.7)'
|
||||||
|
}}
|
||||||
|
onClick={toggleFullscreen}
|
||||||
|
>
|
||||||
|
<Fullscreen />
|
||||||
|
</IconButton>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</ControlsContainer>
|
||||||
|
</VideoContainer>
|
||||||
|
)
|
||||||
|
}
|
40
src/components/Embeds/embed-utils.ts
Normal file
40
src/components/Embeds/embed-utils.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
function decodeHTMLEntities(str) {
|
||||||
|
const txt = document.createElement("textarea");
|
||||||
|
txt.innerHTML = str;
|
||||||
|
return txt.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parseQortalLink = (link) => {
|
||||||
|
const prefix = "qortal://use-embed/";
|
||||||
|
if (!link.startsWith(prefix)) {
|
||||||
|
throw new Error("Invalid link format");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode any HTML entities in the link
|
||||||
|
link = decodeHTMLEntities(link);
|
||||||
|
|
||||||
|
// Separate the type and query string
|
||||||
|
const [typePart, queryPart] = link.slice(prefix.length).split("?");
|
||||||
|
|
||||||
|
// Ensure only the type is parsed
|
||||||
|
const type = typePart.split("/")[0].toUpperCase();
|
||||||
|
|
||||||
|
const params = {};
|
||||||
|
if (queryPart) {
|
||||||
|
const queryPairs = queryPart.split("&");
|
||||||
|
|
||||||
|
queryPairs.forEach((pair) => {
|
||||||
|
const [key, value] = pair.split("=");
|
||||||
|
if (key && value) {
|
||||||
|
const decodedKey = decodeURIComponent(key.trim());
|
||||||
|
const decodedValue = value.trim().replace(
|
||||||
|
/<\/?[^>]+(>|$)/g,
|
||||||
|
"" // Remove any HTML tags
|
||||||
|
);
|
||||||
|
params[decodedKey] = decodedValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type, ...params };
|
||||||
|
};
|
@ -92,6 +92,10 @@ import { Apps } from "../Apps/Apps";
|
|||||||
import { AppsNavBar } from "../Apps/AppsNavBar";
|
import { AppsNavBar } from "../Apps/AppsNavBar";
|
||||||
import { AppsDesktop } from "../Apps/AppsDesktop";
|
import { AppsDesktop } from "../Apps/AppsDesktop";
|
||||||
import { formatEmailDate } from "./QMailMessages";
|
import { formatEmailDate } from "./QMailMessages";
|
||||||
|
import LockIcon from '@mui/icons-material/Lock';
|
||||||
|
import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred';
|
||||||
|
import { useSetRecoilState } from "recoil";
|
||||||
|
import { selectedGroupIdAtom } from "../../atoms/global";
|
||||||
|
|
||||||
// let touchStartY = 0;
|
// let touchStartY = 0;
|
||||||
// let disablePullToRefresh = false;
|
// let disablePullToRefresh = false;
|
||||||
@ -187,6 +191,19 @@ export function validateSecretKey(obj) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function areKeysEqual(array1, array2) {
|
||||||
|
// If lengths differ, the arrays cannot be equal
|
||||||
|
if (array1?.length !== array2?.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort both arrays and compare their elements
|
||||||
|
const sortedArray1 = [...array1].sort();
|
||||||
|
const sortedArray2 = [...array2].sort();
|
||||||
|
|
||||||
|
return sortedArray1.every((key, index) => key === sortedArray2[index]);
|
||||||
|
}
|
||||||
|
|
||||||
export const getGroupMembers = async (groupNumber: number) => {
|
export const getGroupMembers = async (groupNumber: number) => {
|
||||||
// const validApi = await findUsableApi();
|
// const validApi = await findUsableApi();
|
||||||
|
|
||||||
@ -441,6 +458,17 @@ export const Group = ({
|
|||||||
const [appsMode, setAppsMode] = useState('home')
|
const [appsMode, setAppsMode] = useState('home')
|
||||||
const [isOpenSideViewDirects, setIsOpenSideViewDirects] = useState(false)
|
const [isOpenSideViewDirects, setIsOpenSideViewDirects] = useState(false)
|
||||||
const [isOpenSideViewGroups, setIsOpenSideViewGroups] = useState(false)
|
const [isOpenSideViewGroups, setIsOpenSideViewGroups] = useState(false)
|
||||||
|
const [isForceShowCreationKeyPopup, setIsForceShowCreationKeyPopup] = useState(false)
|
||||||
|
const setSelectedGroupId = useSetRecoilState(selectedGroupIdAtom)
|
||||||
|
|
||||||
|
const [groupsProperties, setGroupsProperties] = useState({})
|
||||||
|
|
||||||
|
const isPrivate = useMemo(()=> {
|
||||||
|
if(!selectedGroup?.groupId || !groupsProperties[selectedGroup?.groupId]) return null
|
||||||
|
if(groupsProperties[selectedGroup?.groupId]?.isOpen === true) return false
|
||||||
|
if(groupsProperties[selectedGroup?.groupId]?.isOpen === false) return true
|
||||||
|
return null
|
||||||
|
}, [selectedGroup])
|
||||||
|
|
||||||
const toggleSideViewDirects = ()=> {
|
const toggleSideViewDirects = ()=> {
|
||||||
if(isOpenSideViewGroups){
|
if(isOpenSideViewGroups){
|
||||||
@ -467,6 +495,8 @@ export const Group = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
selectedGroupRef.current = selectedGroup;
|
selectedGroupRef.current = selectedGroup;
|
||||||
|
setSelectedGroupId(selectedGroup?.groupId)
|
||||||
|
|
||||||
}, [selectedGroup]);
|
}, [selectedGroup]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -833,15 +863,63 @@ export const Group = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const getAdminsForPublic = async(selectedGroup)=> {
|
||||||
|
try {
|
||||||
|
const { names, addresses, both } =
|
||||||
|
await getGroupAdmins(selectedGroup?.groupId)
|
||||||
|
setAdmins(addresses);
|
||||||
|
setAdminsWithNames(both);
|
||||||
|
} catch (error) {
|
||||||
|
//error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedGroup) {
|
if (selectedGroup && isPrivate !== null) {
|
||||||
|
if(isPrivate){
|
||||||
setTriedToFetchSecretKey(false);
|
setTriedToFetchSecretKey(false);
|
||||||
getSecretKey(true);
|
getSecretKey(true);
|
||||||
|
}
|
||||||
|
|
||||||
getGroupOwner(selectedGroup?.groupId);
|
getGroupOwner(selectedGroup?.groupId);
|
||||||
}
|
}
|
||||||
}, [selectedGroup]);
|
if(isPrivate === false){
|
||||||
|
setTriedToFetchSecretKey(true);
|
||||||
|
getAdminsForPublic(selectedGroup)
|
||||||
|
|
||||||
|
}
|
||||||
|
}, [selectedGroup, isPrivate]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const getGroupsProperties = useCallback(async(address)=> {
|
||||||
|
try {
|
||||||
|
const url = `${getBaseApiReact()}/groups/member/${address}`;
|
||||||
|
const response = await fetch(url);
|
||||||
|
if(!response.ok) throw new Error('Cannot get group properties')
|
||||||
|
let data = await response.json();
|
||||||
|
const transformToObject = data.reduce((result, item) => {
|
||||||
|
|
||||||
|
result[item.groupId] = item
|
||||||
|
return result;
|
||||||
|
}, {});
|
||||||
|
setGroupsProperties(transformToObject)
|
||||||
|
} catch (error) {
|
||||||
|
// error
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(()=> {
|
||||||
|
if(!myAddress) return
|
||||||
|
if(areKeysEqual(groups?.map((grp)=> grp?.groupId), Object.keys(groupsProperties))){
|
||||||
|
} else {
|
||||||
|
getGroupsProperties(myAddress)
|
||||||
|
}
|
||||||
|
}, [groups, myAddress])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -2089,16 +2167,36 @@ export const Group = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<Avatar
|
{groupsProperties[group?.groupId]?.isOpen === false ? (
|
||||||
sx={{
|
<Box sx={{
|
||||||
|
width: '40px',
|
||||||
|
height: '40px',
|
||||||
|
borderRadius: '50%',
|
||||||
background: "#232428",
|
background: "#232428",
|
||||||
color: "white",
|
display: 'flex',
|
||||||
}}
|
alignItems: 'center',
|
||||||
alt={group?.groupName}
|
justifyContent: 'center'
|
||||||
// src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${groupOwner?.name}/qortal_group_avatar_${group.groupId}?async=true`}
|
}}>
|
||||||
>
|
<LockIcon sx={{
|
||||||
{group.groupName?.charAt(0)}
|
color: 'var(--green)'
|
||||||
</Avatar>
|
}} />
|
||||||
|
</Box>
|
||||||
|
): (
|
||||||
|
<Box sx={{
|
||||||
|
width: '40px',
|
||||||
|
height: '40px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: "#232428",
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}>
|
||||||
|
<NoEncryptionGmailerrorredIcon sx={{
|
||||||
|
color: 'var(--danger)'
|
||||||
|
}} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={group.groupName}
|
primary={group.groupName}
|
||||||
@ -2354,6 +2452,7 @@ export const Group = ({
|
|||||||
{!isMobile && selectedGroup && (
|
{!isMobile && selectedGroup && (
|
||||||
|
|
||||||
<DesktopHeader
|
<DesktopHeader
|
||||||
|
isPrivate={isPrivate}
|
||||||
selectedGroup={selectedGroup}
|
selectedGroup={selectedGroup}
|
||||||
groupSection={groupSection}
|
groupSection={groupSection}
|
||||||
isUnread={isUnread}
|
isUnread={isUnread}
|
||||||
@ -2469,6 +2568,7 @@ export const Group = ({
|
|||||||
>
|
>
|
||||||
{triedToFetchSecretKey && (
|
{triedToFetchSecretKey && (
|
||||||
<ChatGroup
|
<ChatGroup
|
||||||
|
isPrivate={isPrivate}
|
||||||
myAddress={myAddress}
|
myAddress={myAddress}
|
||||||
selectedGroup={selectedGroup?.groupId}
|
selectedGroup={selectedGroup?.groupId}
|
||||||
getSecretKey={getSecretKey}
|
getSecretKey={getSecretKey}
|
||||||
@ -2477,16 +2577,18 @@ export const Group = ({
|
|||||||
handleNewEncryptionNotification={
|
handleNewEncryptionNotification={
|
||||||
setNewEncryptionNotification
|
setNewEncryptionNotification
|
||||||
}
|
}
|
||||||
hide={groupSection !== "chat" || !secretKey}
|
hide={groupSection !== "chat" || selectedDirect || newChat}
|
||||||
|
// hideView={!(desktopViewMode === 'chat' && selectedGroup)}
|
||||||
handleSecretKeyCreationInProgress={
|
handleSecretKeyCreationInProgress={
|
||||||
handleSecretKeyCreationInProgress
|
handleSecretKeyCreationInProgress
|
||||||
}
|
}
|
||||||
triedToFetchSecretKey={triedToFetchSecretKey}
|
triedToFetchSecretKey={triedToFetchSecretKey}
|
||||||
myName={userInfo?.name}
|
myName={userInfo?.name}
|
||||||
balance={balance}
|
balance={balance}
|
||||||
|
getTimestampEnterChatParent={getTimestampEnterChat}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{firstSecretKeyInCreation &&
|
{isPrivate && firstSecretKeyInCreation &&
|
||||||
triedToFetchSecretKey &&
|
triedToFetchSecretKey &&
|
||||||
!secretKeyPublishDate && (
|
!secretKeyPublishDate && (
|
||||||
<div
|
<div
|
||||||
@ -2507,7 +2609,7 @@ export const Group = ({
|
|||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!admins.includes(myAddress) &&
|
{isPrivate && !admins.includes(myAddress) &&
|
||||||
!secretKey &&
|
!secretKey &&
|
||||||
triedToFetchSecretKey ? (
|
triedToFetchSecretKey ? (
|
||||||
<>
|
<>
|
||||||
@ -2560,10 +2662,11 @@ export const Group = ({
|
|||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
) : admins.includes(myAddress) &&
|
) : admins.includes(myAddress) &&
|
||||||
!secretKey &&
|
(!secretKey && isPrivate) &&
|
||||||
triedToFetchSecretKey ? null : !triedToFetchSecretKey ? null : (
|
triedToFetchSecretKey ? null : !triedToFetchSecretKey ? null : (
|
||||||
<>
|
<>
|
||||||
<GroupAnnouncements
|
<GroupAnnouncements
|
||||||
|
isPrivate={isPrivate}
|
||||||
myAddress={myAddress}
|
myAddress={myAddress}
|
||||||
selectedGroup={selectedGroup?.groupId}
|
selectedGroup={selectedGroup?.groupId}
|
||||||
getSecretKey={getSecretKey}
|
getSecretKey={getSecretKey}
|
||||||
@ -2577,6 +2680,7 @@ export const Group = ({
|
|||||||
hide={groupSection !== "announcement"}
|
hide={groupSection !== "announcement"}
|
||||||
/>
|
/>
|
||||||
<GroupForum
|
<GroupForum
|
||||||
|
isPrivate={isPrivate}
|
||||||
myAddress={myAddress}
|
myAddress={myAddress}
|
||||||
selectedGroup={selectedGroup}
|
selectedGroup={selectedGroup}
|
||||||
userInfo={userInfo}
|
userInfo={userInfo}
|
||||||
@ -2600,11 +2704,11 @@ export const Group = ({
|
|||||||
zIndex: 100,
|
zIndex: 100,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{admins.includes(myAddress) &&
|
{((isPrivate && admins.includes(myAddress) &&
|
||||||
shouldReEncrypt &&
|
shouldReEncrypt &&
|
||||||
triedToFetchSecretKey &&
|
triedToFetchSecretKey &&
|
||||||
!firstSecretKeyInCreation &&
|
!firstSecretKeyInCreation &&
|
||||||
!hideCommonKeyPopup && (
|
!hideCommonKeyPopup) || isForceShowCreationKeyPopup) && (
|
||||||
<CreateCommonSecret
|
<CreateCommonSecret
|
||||||
setHideCommonKeyPopup={setHideCommonKeyPopup}
|
setHideCommonKeyPopup={setHideCommonKeyPopup}
|
||||||
groupId={selectedGroup?.groupId}
|
groupId={selectedGroup?.groupId}
|
||||||
@ -2678,6 +2782,7 @@ export const Group = ({
|
|||||||
)}
|
)}
|
||||||
{!isMobile && groupSection === "home" && (
|
{!isMobile && groupSection === "home" && (
|
||||||
<DesktopFooter
|
<DesktopFooter
|
||||||
|
isPrivate={isPrivate}
|
||||||
selectedGroup={selectedGroup}
|
selectedGroup={selectedGroup}
|
||||||
groupSection={groupSection}
|
groupSection={groupSection}
|
||||||
isUnread={isUnread}
|
isUnread={isUnread}
|
||||||
|
@ -311,54 +311,7 @@ export const ListOfGroupPromotions = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// const handleCancelInvitation = async (address)=> {
|
|
||||||
// try {
|
|
||||||
// const fee = await getFee('CANCEL_GROUP_INVITE')
|
|
||||||
// await show({
|
|
||||||
// message: "Would you like to perform a CANCEL_GROUP_INVITE transaction?" ,
|
|
||||||
// publishFee: fee.fee + ' QORT'
|
|
||||||
// })
|
|
||||||
// setIsLoadingCancelInvite(true)
|
|
||||||
// await new Promise((res, rej)=> {
|
|
||||||
// window.sendMessage("cancelInvitationToGroup", {
|
|
||||||
// groupId,
|
|
||||||
// qortalAddress: address,
|
|
||||||
// })
|
|
||||||
// .then((response) => {
|
|
||||||
// if (!response?.error) {
|
|
||||||
// setInfoSnack({
|
|
||||||
// type: "success",
|
|
||||||
// message: "Successfully canceled invitation. It may take a couple of minutes for the changes to propagate",
|
|
||||||
// });
|
|
||||||
// setOpenSnack(true);
|
|
||||||
// handlePopoverClose();
|
|
||||||
// setIsLoadingCancelInvite(true);
|
|
||||||
// res(response);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// setInfoSnack({
|
|
||||||
// type: "error",
|
|
||||||
// message: response?.error,
|
|
||||||
// });
|
|
||||||
// setOpenSnack(true);
|
|
||||||
// rej(response.error);
|
|
||||||
// })
|
|
||||||
// .catch((error) => {
|
|
||||||
// setInfoSnack({
|
|
||||||
// type: "error",
|
|
||||||
// message: error.message || "An error occurred",
|
|
||||||
// });
|
|
||||||
// setOpenSnack(true);
|
|
||||||
// rej(error);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// })
|
|
||||||
// } catch (error) {
|
|
||||||
|
|
||||||
// } finally {
|
|
||||||
// setIsLoadingCancelInvite(false)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
const rowRenderer = ({ index, key, parent, style }) => {
|
const rowRenderer = ({ index, key, parent, style }) => {
|
||||||
const promotion = promotions[index];
|
const promotion = promotions[index];
|
||||||
|
@ -1,26 +1,77 @@
|
|||||||
import React, { useContext, useEffect, useMemo, useState } from 'react'
|
import React, { useContext, useEffect, useMemo, useState } from "react";
|
||||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
import { useRecoilState, useSetRecoilState } from "recoil";
|
||||||
import isEqual from 'lodash/isEqual'; // Import deep comparison utility
|
import isEqual from "lodash/isEqual"; // Import deep comparison utility
|
||||||
import { canSaveSettingToQdnAtom, hasSettingsChangedAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from '../../atoms/global';
|
import {
|
||||||
import { ButtonBase } from '@mui/material';
|
canSaveSettingToQdnAtom,
|
||||||
import { objectToBase64 } from '../../qdn/encryption/group-encryption';
|
hasSettingsChangedAtom,
|
||||||
import { MyContext } from '../../App';
|
isUsingImportExportSettingsAtom,
|
||||||
import { getFee } from '../../background';
|
oldPinnedAppsAtom,
|
||||||
import { CustomizedSnackbars } from '../Snackbar/Snackbar';
|
settingsLocalLastUpdatedAtom,
|
||||||
import { SaveIcon } from '../../assets/svgs/SaveIcon';
|
settingsQDNLastUpdatedAtom,
|
||||||
import { IconWrapper } from '../Desktop/DesktopFooter';
|
sortablePinnedAppsAtom,
|
||||||
export const Save = ({isDesktop}) => {
|
} from "../../atoms/global";
|
||||||
|
import { Box, Button, ButtonBase, Popover, Typography } from "@mui/material";
|
||||||
|
import { objectToBase64 } from "../../qdn/encryption/group-encryption";
|
||||||
|
import { MyContext } from "../../App";
|
||||||
|
import { getFee } from "../../background";
|
||||||
|
import { CustomizedSnackbars } from "../Snackbar/Snackbar";
|
||||||
|
import { SaveIcon } from "../../assets/svgs/SaveIcon";
|
||||||
|
import { IconWrapper } from "../Desktop/DesktopFooter";
|
||||||
|
import { Spacer } from "../../common/Spacer";
|
||||||
|
import { LoadingButton } from "@mui/lab";
|
||||||
|
import { saveToLocalStorage } from "../Apps/AppsNavBar";
|
||||||
|
import { saveFileToDiskGeneric } from "../../utils/generateWallet/generateWallet";
|
||||||
|
import { base64ToUint8Array, uint8ArrayToObject } from "../../backgroundFunctions/encryption";
|
||||||
|
|
||||||
|
|
||||||
|
export const handleImportClick = async () => {
|
||||||
|
const fileInput = document.createElement('input');
|
||||||
|
fileInput.type = 'file';
|
||||||
|
fileInput.accept = '.base64,.txt';
|
||||||
|
|
||||||
|
// Create a promise to handle file selection and reading synchronously
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
fileInput.onchange = () => {
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
if (!file) {
|
||||||
|
reject(new Error('No file selected'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
resolve(e.target.result); // Resolve with the file content
|
||||||
|
};
|
||||||
|
reader.onerror = () => {
|
||||||
|
reject(new Error('Error reading file'));
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsText(file); // Read the file as text (Base64 string)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Trigger the file input dialog
|
||||||
|
fileInput.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Save = ({ isDesktop, disableWidth, myName }) => {
|
||||||
const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom);
|
const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom);
|
||||||
const [settingsQdnLastUpdated, setSettingsQdnLastUpdated] = useRecoilState(settingsQDNLastUpdatedAtom);
|
const [settingsQdnLastUpdated, setSettingsQdnLastUpdated] = useRecoilState(
|
||||||
const [settingsLocalLastUpdated] = useRecoilState(settingsLocalLastUpdatedAtom);
|
settingsQDNLastUpdatedAtom
|
||||||
|
);
|
||||||
|
const [settingsLocalLastUpdated] = useRecoilState(
|
||||||
|
settingsLocalLastUpdatedAtom
|
||||||
|
);
|
||||||
const setHasSettingsChangedAtom = useSetRecoilState(hasSettingsChangedAtom);
|
const setHasSettingsChangedAtom = useSetRecoilState(hasSettingsChangedAtom);
|
||||||
|
const [isUsingImportExportSettings, setIsUsingImportExportSettings] = useRecoilState(isUsingImportExportSettingsAtom);
|
||||||
|
|
||||||
const [canSave] = useRecoilState(canSaveSettingToQdnAtom);
|
const [canSave] = useRecoilState(canSaveSettingToQdnAtom);
|
||||||
const [openSnack, setOpenSnack] = useState(false);
|
const [openSnack, setOpenSnack] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [infoSnack, setInfoSnack] = useState(null);
|
const [infoSnack, setInfoSnack] = useState(null);
|
||||||
const [oldPinnedApps, setOldPinnedApps] = useRecoilState(oldPinnedAppsAtom)
|
const [oldPinnedApps, setOldPinnedApps] = useRecoilState(oldPinnedAppsAtom);
|
||||||
|
const [anchorEl, setAnchorEl] = useState(null);
|
||||||
const { show } = useContext(MyContext);
|
const { show } = useContext(MyContext);
|
||||||
|
|
||||||
const hasChanged = useMemo(() => {
|
const hasChanged = useMemo(() => {
|
||||||
@ -28,25 +79,35 @@ export const Save = ({isDesktop}) => {
|
|||||||
sortablePinnedApps: pinnedApps.map((item) => {
|
sortablePinnedApps: pinnedApps.map((item) => {
|
||||||
return {
|
return {
|
||||||
name: item?.name,
|
name: item?.name,
|
||||||
service: item?.service
|
service: item?.service,
|
||||||
}
|
};
|
||||||
})
|
}),
|
||||||
}
|
};
|
||||||
const oldChanges = {
|
const oldChanges = {
|
||||||
sortablePinnedApps: oldPinnedApps.map((item) => {
|
sortablePinnedApps: oldPinnedApps.map((item) => {
|
||||||
return {
|
return {
|
||||||
name: item?.name,
|
name: item?.name,
|
||||||
service: item?.service
|
service: item?.service,
|
||||||
}
|
};
|
||||||
})
|
}),
|
||||||
}
|
};
|
||||||
if(settingsQdnLastUpdated === -100) return false
|
if (settingsQdnLastUpdated === -100) return false;
|
||||||
return !isEqual(oldChanges, newChanges) && settingsQdnLastUpdated < settingsLocalLastUpdated
|
return (
|
||||||
}, [oldPinnedApps, pinnedApps, settingsQdnLastUpdated, settingsLocalLastUpdated])
|
!isEqual(oldChanges, newChanges) &&
|
||||||
|
settingsQdnLastUpdated < settingsLocalLastUpdated
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
oldPinnedApps,
|
||||||
|
pinnedApps,
|
||||||
|
settingsQdnLastUpdated,
|
||||||
|
settingsLocalLastUpdated,
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHasSettingsChangedAtom(hasChanged)
|
setHasSettingsChangedAtom(hasChanged);
|
||||||
}, [hasChanged])
|
}, [hasChanged]);
|
||||||
|
|
||||||
const saveToQdn = async ()=> {
|
const saveToQdn = async ()=> {
|
||||||
try {
|
try {
|
||||||
@ -128,26 +189,414 @@ export const Save = ({isDesktop}) => {
|
|||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const handlePopupClick = (event) => {
|
||||||
|
event.stopPropagation(); // Prevent parent onClick from firing
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const revertChanges = () => {
|
||||||
|
setPinnedApps(oldPinnedApps);
|
||||||
|
saveToLocalStorage("ext_saved_settings", "sortablePinnedApps", null);
|
||||||
|
setAnchorEl(null)
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ButtonBase onClick={saveToQdn} disabled={!hasChanged || !canSave || isLoading || settingsQdnLastUpdated === -100}>
|
<ButtonBase
|
||||||
|
onClick={handlePopupClick}
|
||||||
|
disabled={
|
||||||
|
// !hasChanged ||
|
||||||
|
// !canSave ||
|
||||||
|
isLoading
|
||||||
|
// settingsQdnLastUpdated === -100
|
||||||
|
}
|
||||||
|
>
|
||||||
{isDesktop ? (
|
{isDesktop ? (
|
||||||
<IconWrapper
|
<IconWrapper
|
||||||
|
disableWidth={disableWidth}
|
||||||
color="rgba(250, 250, 250, 0.5)"
|
color="rgba(250, 250, 250, 0.5)"
|
||||||
label="Save"
|
label="Save"
|
||||||
selected={false}
|
selected={false}
|
||||||
>
|
>
|
||||||
<SaveIcon
|
<SaveIcon
|
||||||
color={settingsQdnLastUpdated === -100 ? '#8F8F91' : (hasChanged && !isLoading) ? '#5EB049' : '#8F8F91'}
|
color={
|
||||||
|
settingsQdnLastUpdated === -100
|
||||||
|
? "#8F8F91"
|
||||||
|
: hasChanged && !isLoading
|
||||||
|
? "#5EB049"
|
||||||
|
: "#8F8F91"
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</IconWrapper>
|
</IconWrapper>
|
||||||
) : (
|
) : (
|
||||||
<SaveIcon
|
<SaveIcon
|
||||||
color={settingsQdnLastUpdated === -100 ? '#8F8F91' : (hasChanged && !isLoading) ? '#5EB049' : '#8F8F91'}
|
color={
|
||||||
|
settingsQdnLastUpdated === -100
|
||||||
|
? "#8F8F91"
|
||||||
|
: hasChanged && !isLoading
|
||||||
|
? "#5EB049"
|
||||||
|
: "#8F8F91"
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
|
<Popover
|
||||||
|
open={!!anchorEl}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
onClose={() => setAnchorEl(null)} // Close popover on click outside
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: "bottom",
|
||||||
|
horizontal: "center",
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: "top",
|
||||||
|
horizontal: "center",
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
width: "300px",
|
||||||
|
maxWidth: "90%",
|
||||||
|
maxHeight: "80%",
|
||||||
|
overflow: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isUsingImportExportSettings && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: "15px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 1,
|
||||||
|
width: '100%'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
You are using the export/import way of saving settings.
|
||||||
|
</Typography>
|
||||||
|
<Spacer height="40px" />
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={()=> {
|
||||||
|
saveToLocalStorage("ext_saved_settings_import_export", "sortablePinnedApps", null, true);
|
||||||
|
setIsUsingImportExportSettings(false)
|
||||||
|
}}
|
||||||
|
variant="contained"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "var(--danger)",
|
||||||
|
color: "black",
|
||||||
|
fontWeight: 'bold',
|
||||||
|
opacity: 0.7,
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "var(--danger)",
|
||||||
|
color: "black",
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Use QDN saving
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{!isUsingImportExportSettings && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: "15px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 1,
|
||||||
|
width: '100%'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!myName ? (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
You need a registered Qortal name to save your pinned apps to QDN.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{hasChanged && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
You have unsaved changes to your pinned apps. Save them to QDN.
|
||||||
|
</Typography>
|
||||||
|
<Spacer height="10px" />
|
||||||
|
<LoadingButton
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "var(--green)",
|
||||||
|
color: "black",
|
||||||
|
opacity: 0.7,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "var(--green)",
|
||||||
|
color: "black",
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
loading={isLoading}
|
||||||
|
onClick={saveToQdn}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
Save to QDN
|
||||||
|
</LoadingButton>
|
||||||
|
<Spacer height="20px" />
|
||||||
|
{!isNaN(settingsQdnLastUpdated) && settingsQdnLastUpdated > 0 && (
|
||||||
|
<>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Don't like your current local changes? Would you like to
|
||||||
|
reset to your saved QDN pinned apps?
|
||||||
|
</Typography>
|
||||||
|
<Spacer height="10px" />
|
||||||
|
<LoadingButton
|
||||||
|
size="small"
|
||||||
|
loading={isLoading}
|
||||||
|
onClick={revertChanges}
|
||||||
|
variant="contained"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "var(--danger)",
|
||||||
|
color: "black",
|
||||||
|
fontWeight: 'bold',
|
||||||
|
opacity: 0.7,
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "var(--danger)",
|
||||||
|
color: "black",
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Revert to QDN
|
||||||
|
</LoadingButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!isNaN(settingsQdnLastUpdated) && settingsQdnLastUpdated === 0 && (
|
||||||
|
<>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Don't like your current local changes? Would you like to
|
||||||
|
reset to the default pinned apps?
|
||||||
|
</Typography>
|
||||||
|
<Spacer height="10px" />
|
||||||
|
<LoadingButton
|
||||||
|
loading={isLoading}
|
||||||
|
onClick={revertChanges}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
Revert to default
|
||||||
|
</LoadingButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{!isNaN(settingsQdnLastUpdated) && settingsQdnLastUpdated === -100 && isUsingImportExportSettings !== true && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
The app was unable to download your existing QDN-saved pinned
|
||||||
|
apps. Would you like to overwrite those changes?
|
||||||
|
</Typography>
|
||||||
|
<Spacer height="10px" />
|
||||||
|
<LoadingButton
|
||||||
|
size="small"
|
||||||
|
loading={isLoading}
|
||||||
|
onClick={saveToQdn}
|
||||||
|
variant="contained"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "var(--danger)",
|
||||||
|
color: "black",
|
||||||
|
fontWeight: 'bold',
|
||||||
|
opacity: 0.7,
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "var(--danger)",
|
||||||
|
color: "black",
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Overwrite to QDN
|
||||||
|
</LoadingButton>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{!hasChanged && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
You currently do not have any changes to your pinned apps
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: "15px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 1,
|
||||||
|
width: '100%'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '10px',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
width: '100%'
|
||||||
|
}}>
|
||||||
|
<ButtonBase onClick={async () => {
|
||||||
|
try {
|
||||||
|
const fileContent = await handleImportClick();
|
||||||
|
const decryptedData = await new Promise((res, rej) => {
|
||||||
|
chrome?.runtime?.sendMessage(
|
||||||
|
{
|
||||||
|
action: "DECRYPT_DATA",
|
||||||
|
type: "qortalRequest",
|
||||||
|
payload: {
|
||||||
|
encryptedData: fileContent
|
||||||
|
},
|
||||||
|
},
|
||||||
|
(response) => {
|
||||||
|
console.log('response', response)
|
||||||
|
if (response.error) {
|
||||||
|
rej(response?.message);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
res(response);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const decryptToUnit8ArraySubject =
|
||||||
|
base64ToUint8Array(decryptedData);
|
||||||
|
const responseData = uint8ArrayToObject(
|
||||||
|
decryptToUnit8ArraySubject
|
||||||
|
);
|
||||||
|
console.log('responseData', responseData)
|
||||||
|
if(Array.isArray(responseData)){
|
||||||
|
saveToLocalStorage("ext_saved_settings_import_export", "sortablePinnedApps", responseData, {
|
||||||
|
isUsingImportExport: true
|
||||||
|
});
|
||||||
|
setPinnedApps(responseData)
|
||||||
|
setOldPinnedApps(responseData)
|
||||||
|
setIsUsingImportExportSettings(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log("error", error);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
|
||||||
|
Import
|
||||||
|
</ButtonBase>
|
||||||
|
<ButtonBase onClick={async () => {
|
||||||
|
try {
|
||||||
|
console.log('pinnedApps', pinnedApps)
|
||||||
|
const data64 = await objectToBase64(pinnedApps);
|
||||||
|
|
||||||
|
const encryptedData = await new Promise((res, rej) => {
|
||||||
|
chrome?.runtime?.sendMessage(
|
||||||
|
{
|
||||||
|
action: "ENCRYPT_DATA",
|
||||||
|
type: "qortalRequest",
|
||||||
|
payload: {
|
||||||
|
data64
|
||||||
|
},
|
||||||
|
},
|
||||||
|
(response) => {
|
||||||
|
if (response.error) {
|
||||||
|
rej(response?.message);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
res(response);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const blob = new Blob([encryptedData], {
|
||||||
|
type: "text/plain",
|
||||||
|
});
|
||||||
|
|
||||||
|
const timestamp = new Date()
|
||||||
|
.toISOString()
|
||||||
|
.replace(/:/g, "-"); // Safe timestamp for filenames
|
||||||
|
const filename = `qortal-new-ui-backup-settings-${timestamp}.txt`;
|
||||||
|
await saveFileToDiskGeneric(blob, filename)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log('error', error)
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
Export
|
||||||
|
</ButtonBase>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Popover>
|
||||||
<CustomizedSnackbars
|
<CustomizedSnackbars
|
||||||
duration={3500}
|
duration={3500}
|
||||||
open={openSnack}
|
open={openSnack}
|
||||||
@ -156,6 +605,5 @@ export const Save = ({isDesktop}) => {
|
|||||||
setInfo={setInfoSnack}
|
setInfo={setInfoSnack}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
);
|
||||||
)
|
};
|
||||||
}
|
|
||||||
|
108
src/components/Tutorials/Tutorials.tsx
Normal file
108
src/components/Tutorials/Tutorials.tsx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import React, { useContext, useState } from 'react'
|
||||||
|
import { GlobalContext, MyContext } from '../../App';
|
||||||
|
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Tab, Tabs, Typography } from '@mui/material';
|
||||||
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
import { VideoPlayer } from '../Embeds/VideoPlayer';
|
||||||
|
|
||||||
|
export const Tutorials = () => {
|
||||||
|
const { openTutorialModal, setOpenTutorialModal } = useContext(GlobalContext);
|
||||||
|
const [multiNumber, setMultiNumber] = useState(0)
|
||||||
|
const handleClose = ()=> {
|
||||||
|
setOpenTutorialModal(null)
|
||||||
|
setMultiNumber(0)
|
||||||
|
}
|
||||||
|
if(!openTutorialModal) return null
|
||||||
|
if(openTutorialModal?.multi){
|
||||||
|
const selectedTutorial = openTutorialModal?.multi[multiNumber]
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
onClose={handleClose}
|
||||||
|
aria-labelledby="customized-dialog-title"
|
||||||
|
open={!!openTutorialModal}
|
||||||
|
fullWidth={true}
|
||||||
|
maxWidth="xl"
|
||||||
|
>
|
||||||
|
<Tabs sx={{
|
||||||
|
"& .MuiTabs-indicator": {
|
||||||
|
backgroundColor: "white",
|
||||||
|
},
|
||||||
|
}} value={multiNumber} onChange={(e, value)=> setMultiNumber(value)} aria-label="basic tabs example">
|
||||||
|
{openTutorialModal?.multi?.map((item, index)=> {
|
||||||
|
return (
|
||||||
|
<Tab sx={{
|
||||||
|
"&.Mui-selected": {
|
||||||
|
color: "white",
|
||||||
|
},
|
||||||
|
}} label={item?.title} value={index} />
|
||||||
|
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Tabs>
|
||||||
|
<DialogTitle sx={{ m: 0, p: 2 }} >
|
||||||
|
{selectedTutorial?.title} {` Tutorial`}
|
||||||
|
</DialogTitle>
|
||||||
|
<IconButton
|
||||||
|
aria-label="close"
|
||||||
|
onClick={handleClose}
|
||||||
|
sx={(theme) => ({
|
||||||
|
position: 'absolute',
|
||||||
|
right: 8,
|
||||||
|
top: 8,
|
||||||
|
color: theme.palette.grey[500],
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
<DialogContent dividers sx={{
|
||||||
|
height: '85vh'
|
||||||
|
}}>
|
||||||
|
|
||||||
|
<VideoPlayer node="https://ext-node.qortal.link" {...selectedTutorial?.resource || {}} />
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button variant="contained" onClick={handleClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dialog
|
||||||
|
onClose={handleClose}
|
||||||
|
aria-labelledby="customized-dialog-title"
|
||||||
|
open={!!openTutorialModal}
|
||||||
|
fullWidth={true}
|
||||||
|
maxWidth="xl"
|
||||||
|
>
|
||||||
|
<DialogTitle sx={{ m: 0, p: 2 }} >
|
||||||
|
{openTutorialModal?.title} {` Tutorial`}
|
||||||
|
</DialogTitle>
|
||||||
|
<IconButton
|
||||||
|
aria-label="close"
|
||||||
|
onClick={handleClose}
|
||||||
|
sx={(theme) => ({
|
||||||
|
position: 'absolute',
|
||||||
|
right: 8,
|
||||||
|
top: 8,
|
||||||
|
color: theme.palette.grey[500],
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
<DialogContent dividers sx={{
|
||||||
|
height: '85vh'
|
||||||
|
}}>
|
||||||
|
|
||||||
|
<VideoPlayer node="https://ext-node.qortal.link" {...openTutorialModal?.resource || {}} />
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button variant="contained" onClick={handleClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
169
src/components/Tutorials/useHandleTutorials.tsx
Normal file
169
src/components/Tutorials/useHandleTutorials.tsx
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
|
import { saveToLocalStorage } from "../Apps/AppsNavBar";
|
||||||
|
|
||||||
|
|
||||||
|
const checkIfGatewayIsOnline = async () => {
|
||||||
|
try {
|
||||||
|
const url = `https://ext-node.qortal.link/admin/status`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (data?.height) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
return false
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const useHandleTutorials = () => {
|
||||||
|
const [openTutorialModal, setOpenTutorialModal] = useState<any>(null);
|
||||||
|
const [shownTutorials, setShowTutorials] = useState(null)
|
||||||
|
|
||||||
|
useEffect(()=> {
|
||||||
|
try {
|
||||||
|
const storedData = localStorage.getItem('shown-tutorials');
|
||||||
|
|
||||||
|
|
||||||
|
if (storedData) {
|
||||||
|
setShowTutorials(JSON.parse(storedData));
|
||||||
|
} else {
|
||||||
|
setShowTutorials({})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
//error
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const saveShowTutorial = useCallback((type)=> {
|
||||||
|
try {
|
||||||
|
|
||||||
|
setShowTutorials((prev)=> {
|
||||||
|
return {
|
||||||
|
...(prev || {}),
|
||||||
|
[type]: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
saveToLocalStorage('shown-tutorials', type, true)
|
||||||
|
} catch (error) {
|
||||||
|
//error
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
const showTutorial = useCallback(async (type, isForce) => {
|
||||||
|
try {
|
||||||
|
const isOnline = await checkIfGatewayIsOnline()
|
||||||
|
if(!isOnline) return
|
||||||
|
switch (type) {
|
||||||
|
case "create-account":
|
||||||
|
{
|
||||||
|
if((shownTutorials || {})['create-account'] && !isForce) return
|
||||||
|
saveShowTutorial('create-account')
|
||||||
|
setOpenTutorialModal({
|
||||||
|
title: "Account Creation",
|
||||||
|
resource: {
|
||||||
|
name: "a-test",
|
||||||
|
service: "VIDEO",
|
||||||
|
identifier: "account-creation-hub",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "important-information":
|
||||||
|
{
|
||||||
|
if((shownTutorials || {})['important-information'] && !isForce) return
|
||||||
|
saveShowTutorial('important-information')
|
||||||
|
|
||||||
|
setOpenTutorialModal({
|
||||||
|
title: "Important Information!",
|
||||||
|
resource: {
|
||||||
|
name: "a-test",
|
||||||
|
service: "VIDEO",
|
||||||
|
identifier: "important-information-hub",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "getting-started":
|
||||||
|
{
|
||||||
|
if((shownTutorials || {})['getting-started'] && !isForce) return
|
||||||
|
saveShowTutorial('getting-started')
|
||||||
|
|
||||||
|
setOpenTutorialModal({
|
||||||
|
multi: [
|
||||||
|
|
||||||
|
{
|
||||||
|
title: "1. Getting Started",
|
||||||
|
resource: {
|
||||||
|
name: "a-test",
|
||||||
|
service: "VIDEO",
|
||||||
|
identifier: "getting-started-hub",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "2. Overview",
|
||||||
|
resource: {
|
||||||
|
name: "a-test",
|
||||||
|
service: "VIDEO",
|
||||||
|
identifier: "overview-hub",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "3. Qortal Groups",
|
||||||
|
resource: {
|
||||||
|
name: "a-test",
|
||||||
|
service: "VIDEO",
|
||||||
|
identifier: "groups-hub",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "qapps":
|
||||||
|
{
|
||||||
|
if((shownTutorials || {})['qapps'] && !isForce) return
|
||||||
|
saveShowTutorial('qapps')
|
||||||
|
|
||||||
|
setOpenTutorialModal({
|
||||||
|
multi: [
|
||||||
|
{
|
||||||
|
title: "1. Apps Dashboard",
|
||||||
|
resource: {
|
||||||
|
name: "a-test",
|
||||||
|
service: "VIDEO",
|
||||||
|
identifier: "apps-dashboard-hub",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "2. Apps Navigation",
|
||||||
|
resource: {
|
||||||
|
name: "a-test",
|
||||||
|
service: "VIDEO",
|
||||||
|
identifier: "apps-navigation-hub",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
//error
|
||||||
|
}
|
||||||
|
}, [shownTutorials]);
|
||||||
|
return {
|
||||||
|
showTutorial,
|
||||||
|
openTutorialModal,
|
||||||
|
setOpenTutorialModal,
|
||||||
|
shownTutorialsInitiated: !!shownTutorials
|
||||||
|
};
|
||||||
|
};
|
@ -35,7 +35,10 @@
|
|||||||
--bg-2: #27282c;
|
--bg-2: #27282c;
|
||||||
--bg-3: rgba(0, 0, 0, 0.1);
|
--bg-3: rgba(0, 0, 0, 0.1);
|
||||||
--unread: #B14646;
|
--unread: #B14646;
|
||||||
--apps-circle: #1F2023
|
--apps-circle: #1F2023;
|
||||||
|
--green: #5EB049;
|
||||||
|
--danger: #B14646;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@ -73,17 +76,43 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 16px;
|
width: 14px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background-color: white;
|
background-color: #444444;;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-clip: content-box;
|
background-clip: content-box;
|
||||||
border: 4px solid transparent;
|
border: 4px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@property --var1 {
|
||||||
|
syntax: "<color>";
|
||||||
|
inherits: true;
|
||||||
|
initial-value: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.scrollable-container {
|
||||||
|
transition: --var1 0.4s;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollable-container:hover {
|
||||||
|
--var1: #444444;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.scrollable-container::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--var1);
|
||||||
|
border-radius: 8px;
|
||||||
|
background-clip: content-box;
|
||||||
|
border: 4px solid transparent;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Mobile-specific scrollbar styles */
|
/* Mobile-specific scrollbar styles */
|
||||||
@media only screen and (max-width: 600px) {
|
@media only screen and (max-width: 600px) {
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
|
@ -137,7 +137,7 @@ export const encryptDataGroup = ({ data64, publicKeys, privateKey, userPublicKey
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const encryptSingle = async ({ data64, secretKeyObject, typeNumber = 1 }: any) => {
|
export const encryptSingle = async ({ data64, secretKeyObject, typeNumber = 2 }: any) => {
|
||||||
// Find the highest key in the secretKeyObject
|
// Find the highest key in the secretKeyObject
|
||||||
const highestKey = Math.max(...Object.keys(secretKeyObject).filter(item => !isNaN(+item)).map(Number));
|
const highestKey = Math.max(...Object.keys(secretKeyObject).filter(item => !isNaN(+item)).map(Number));
|
||||||
const highestKeyObject = secretKeyObject[highestKey];
|
const highestKeyObject = secretKeyObject[highestKey];
|
||||||
@ -180,26 +180,42 @@ export const encryptSingle = async ({ data64, secretKeyObject, typeNumber = 1 }:
|
|||||||
|
|
||||||
// Concatenate the highest key, type number, nonce, and encrypted data (new format)
|
// Concatenate the highest key, type number, nonce, and encrypted data (new format)
|
||||||
const highestKeyStr = highestKey.toString().padStart(10, '0'); // Fixed length of 10 digits
|
const highestKeyStr = highestKey.toString().padStart(10, '0'); // Fixed length of 10 digits
|
||||||
finalEncryptedData = btoa(highestKeyStr + typeNumberStr + nonceBase64 + encryptedDataBase64);
|
|
||||||
|
const highestKeyBytes = new TextEncoder().encode(highestKeyStr.padStart(10, '0'));
|
||||||
|
const typeNumberBytes = new TextEncoder().encode(typeNumberStr.padStart(3, '0'));
|
||||||
|
|
||||||
|
// Step 3: Concatenate all binary
|
||||||
|
const combinedBinary = new Uint8Array(
|
||||||
|
highestKeyBytes.length + typeNumberBytes.length + nonce.length + encryptedData.length
|
||||||
|
);
|
||||||
|
// finalEncryptedData = btoa(highestKeyStr) + btoa(typeNumberStr) + nonceBase64 + encryptedDataBase64;
|
||||||
|
combinedBinary.set(highestKeyBytes, 0);
|
||||||
|
combinedBinary.set(typeNumberBytes, highestKeyBytes.length);
|
||||||
|
combinedBinary.set(nonce, highestKeyBytes.length + typeNumberBytes.length);
|
||||||
|
combinedBinary.set(encryptedData, highestKeyBytes.length + typeNumberBytes.length + nonce.length);
|
||||||
|
|
||||||
|
// Step 4: Base64 encode once
|
||||||
|
finalEncryptedData = uint8ArrayToBase64(combinedBinary);
|
||||||
}
|
}
|
||||||
|
|
||||||
return finalEncryptedData;
|
return finalEncryptedData;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const decodeBase64ForUIChatMessages = (messages)=> {
|
export const decodeBase64ForUIChatMessages = (messages)=> {
|
||||||
|
|
||||||
let msgs = []
|
let msgs = []
|
||||||
for(const msg of messages){
|
for(const msg of messages){
|
||||||
try {
|
try {
|
||||||
const decoded = atob(msg?.data);
|
const decoded = atob(msg?.data);
|
||||||
const parseDecoded = JSON.parse(decoded)
|
const parseDecoded =JSON.parse(decodeURIComponent(escape(decoded)))
|
||||||
if(parseDecoded?.messageText){
|
|
||||||
msgs.push({
|
msgs.push({
|
||||||
...msg,
|
...msg,
|
||||||
...parseDecoded
|
...parseDecoded
|
||||||
})
|
})
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -208,7 +224,6 @@ export const decodeBase64ForUIChatMessages = (messages)=> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const decryptSingle = async ({ data64, secretKeyObject, skipDecodeBase64 }: any) => {
|
export const decryptSingle = async ({ data64, secretKeyObject, skipDecodeBase64 }: any) => {
|
||||||
// First, decode the base64-encoded input (if skipDecodeBase64 is not set)
|
// First, decode the base64-encoded input (if skipDecodeBase64 is not set)
|
||||||
const decodedData = skipDecodeBase64 ? data64 : atob(data64);
|
const decodedData = skipDecodeBase64 ? data64 : atob(data64);
|
||||||
@ -241,6 +256,28 @@ export const decodeBase64ForUIChatMessages = (messages)=> {
|
|||||||
encryptedDataBase64 = decodeForNumber.slice(10); // The remaining part is the encrypted data
|
encryptedDataBase64 = decodeForNumber.slice(10); // The remaining part is the encrypted data
|
||||||
} else {
|
} else {
|
||||||
if (hasTypeNumber) {
|
if (hasTypeNumber) {
|
||||||
|
// const typeNumberStr = new TextDecoder().decode(typeNumberBytes);
|
||||||
|
if(decodeForNumber.slice(10, 13) !== '001'){
|
||||||
|
const decodedBinary = base64ToUint8Array(decodedData);
|
||||||
|
const highestKeyBytes = decodedBinary.slice(0, 10); // if ASCII digits only
|
||||||
|
const highestKeyStr = new TextDecoder().decode(highestKeyBytes);
|
||||||
|
|
||||||
|
const nonce = decodedBinary.slice(13, 13 + 24);
|
||||||
|
const encryptedData = decodedBinary.slice(13 + 24);
|
||||||
|
const highestKey = parseInt(highestKeyStr, 10);
|
||||||
|
|
||||||
|
const messageKey = base64ToUint8Array(secretKeyObject[+highestKey].messageKey);
|
||||||
|
const decryptedBytes = nacl.secretbox.open(encryptedData, nonce, messageKey);
|
||||||
|
|
||||||
|
// Check if decryption was successful
|
||||||
|
if (!decryptedBytes) {
|
||||||
|
throw new Error("Decryption failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the decrypted Uint8Array back to a Base64 string
|
||||||
|
return uint8ArrayToBase64(decryptedBytes);
|
||||||
|
|
||||||
|
}
|
||||||
// New format: Extract type number and nonce
|
// New format: Extract type number and nonce
|
||||||
typeNumberStr = possibleTypeNumberStr; // Extract type number
|
typeNumberStr = possibleTypeNumberStr; // Extract type number
|
||||||
nonceBase64 = decodeForNumber.slice(13, 45); // Extract nonce (next 32 characters after type number)
|
nonceBase64 = decodeForNumber.slice(13, 45); // Extract nonce (next 32 characters after type number)
|
||||||
@ -275,6 +312,7 @@ export const decodeBase64ForUIChatMessages = (messages)=> {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function decryptGroupDataQortalRequest(data64EncryptedData, privateKey) {
|
export function decryptGroupDataQortalRequest(data64EncryptedData, privateKey) {
|
||||||
const allCombined = base64ToUint8Array(data64EncryptedData)
|
const allCombined = base64ToUint8Array(data64EncryptedData)
|
||||||
const str = "qortalGroupEncryptedData"
|
const str = "qortalGroupEncryptedData"
|
||||||
@ -421,3 +459,43 @@ export function decryptDeprecatedSingle(uint8Array, publicKey, privateKey) {
|
|||||||
}
|
}
|
||||||
return uint8ArrayToBase64(_decryptedData)
|
return uint8ArrayToBase64(_decryptedData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const decryptGroupEncryptionWithSharingKey = async ({ data64EncryptedData, key }: any) => {
|
||||||
|
|
||||||
|
const allCombined = base64ToUint8Array(data64EncryptedData)
|
||||||
|
const str = "qortalGroupEncryptedData"
|
||||||
|
const strEncoder = new TextEncoder()
|
||||||
|
const strUint8Array = strEncoder.encode(str)
|
||||||
|
// Extract the nonce
|
||||||
|
const nonceStartPosition = strUint8Array.length
|
||||||
|
const nonceEndPosition = nonceStartPosition + 24 // Nonce is 24 bytes
|
||||||
|
const nonce = allCombined.slice(nonceStartPosition, nonceEndPosition)
|
||||||
|
// Extract the shared keyNonce
|
||||||
|
const keyNonceStartPosition = nonceEndPosition
|
||||||
|
const keyNonceEndPosition = keyNonceStartPosition + 24 // Nonce is 24 bytes
|
||||||
|
const keyNonce = allCombined.slice(keyNonceStartPosition, keyNonceEndPosition)
|
||||||
|
// Extract the sender's public key
|
||||||
|
const senderPublicKeyStartPosition = keyNonceEndPosition
|
||||||
|
const senderPublicKeyEndPosition = senderPublicKeyStartPosition + 32 // Public keys are 32 bytes
|
||||||
|
|
||||||
|
// Calculate count first
|
||||||
|
const countStartPosition = allCombined.length - 4 // 4 bytes before the end, since count is stored in Uint32 (4 bytes)
|
||||||
|
const countArray = allCombined.slice(countStartPosition, countStartPosition + 4)
|
||||||
|
const count = new Uint32Array(countArray.buffer)[0]
|
||||||
|
// Then use count to calculate encryptedData
|
||||||
|
const encryptedDataStartPosition = senderPublicKeyEndPosition // start position of encryptedData
|
||||||
|
const encryptedDataEndPosition = allCombined.length - ((count * (32 + 16)) + 4)
|
||||||
|
const encryptedData = allCombined.slice(encryptedDataStartPosition, encryptedDataEndPosition)
|
||||||
|
const symmetricKey = base64ToUint8Array(key);
|
||||||
|
|
||||||
|
// Decrypt the data using the nonce and messageKey
|
||||||
|
const decryptedData = nacl.secretbox.open(encryptedData, nonce, symmetricKey)
|
||||||
|
|
||||||
|
|
||||||
|
// Check if decryption was successful
|
||||||
|
if (!decryptedData) {
|
||||||
|
throw new Error("Decryption failed");
|
||||||
|
}
|
||||||
|
// Convert the decrypted Uint8Array back to a Base64 string
|
||||||
|
return uint8ArrayToBase64(decryptedData);
|
||||||
|
};
|
@ -1,5 +1,5 @@
|
|||||||
import { getApiKeyFromStorage } from "./background";
|
import { gateways, getApiKeyFromStorage } from "./background";
|
||||||
import { addForeignServer, addListItems, adminAction, cancelSellOrder, createBuyOrder, createPoll, createSellOrder, decryptData, deleteListItems, deployAt, encryptData, getCrossChainServerInfo, getDaySummary, getForeignFee, getListItems, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getWalletBalance, joinGroup, publishMultipleQDNResources, publishQDNResource, removeForeignServer, saveFile, sendChatMessage, sendCoin, setCurrentForeignServer, updateForeignFee, voteOnPoll } from "./qortalRequests/get";
|
import { addForeignServer, addListItems, adminAction, cancelSellOrder, createBuyOrder, createPoll, createSellOrder, decryptData, decryptDataWithSharingKey, decryptQortalGroupData, deleteListItems, deployAt, encryptData, encryptDataWithSharingKey, encryptQortalGroupData, getCrossChainServerInfo, getDaySummary, getForeignFee, getListItems, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getWalletBalance, joinGroup, publishMultipleQDNResources, publishQDNResource, removeForeignServer, saveFile, sendChatMessage, sendCoin, setCurrentForeignServer, signTransaction, updateForeignFee, voteOnPoll } from "./qortalRequests/get";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -480,6 +480,8 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "ADMIN_ACTION": {
|
case "ADMIN_ACTION": {
|
||||||
|
const data = request.payload;
|
||||||
|
|
||||||
adminAction(data, isFromExtension).then((res) => {
|
adminAction(data, isFromExtension).then((res) => {
|
||||||
sendResponse(res);
|
sendResponse(res);
|
||||||
})
|
})
|
||||||
@ -489,6 +491,69 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "SIGN_TRANSACTION": {
|
||||||
|
const data = request.payload;
|
||||||
|
|
||||||
|
signTransaction(data, isFromExtension).then((res) => {
|
||||||
|
sendResponse(res);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({ error: error?.message });
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "DECRYPT_QORTAL_GROUP_DATA": {
|
||||||
|
const data = request.payload;
|
||||||
|
|
||||||
|
decryptQortalGroupData(data, isFromExtension).then((res) => {
|
||||||
|
sendResponse(res);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({ error: error?.message });
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "ENCRYPT_DATA_WITH_SHARING_KEY": {
|
||||||
|
const data = request.payload;
|
||||||
|
|
||||||
|
encryptDataWithSharingKey(data, isFromExtension).then((res) => {
|
||||||
|
sendResponse(res);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({ error: error?.message });
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "DECRYPT_DATA_WITH_SHARING_KEY": {
|
||||||
|
const data = request.payload;
|
||||||
|
|
||||||
|
decryptDataWithSharingKey(data, isFromExtension).then((res) => {
|
||||||
|
sendResponse(res);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({ error: error?.message });
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "ENCRYPT_QORTAL_GROUP_DATA": {
|
||||||
|
const data = request.payload;
|
||||||
|
|
||||||
|
encryptQortalGroupData(data, isFromExtension).then((res) => {
|
||||||
|
sendResponse(res);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({ error: error?.message });
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -13,16 +13,24 @@ import {
|
|||||||
sendQortFee,
|
sendQortFee,
|
||||||
sendCoin as sendCoinFunc,
|
sendCoin as sendCoinFunc,
|
||||||
isUsingLocal,
|
isUsingLocal,
|
||||||
createBuyOrderTxQortalRequest
|
createBuyOrderTxQortalRequest,
|
||||||
|
groupSecretkeys,
|
||||||
|
getBaseApi,
|
||||||
|
getArbitraryEndpoint
|
||||||
} from "../background";
|
} from "../background";
|
||||||
import { getNameInfo } from "../backgroundFunctions/encryption";
|
import { decryptGroupEncryption, getNameInfo, uint8ArrayToObject } from "../backgroundFunctions/encryption";
|
||||||
import { QORT_DECIMALS } from "../constants/constants";
|
import { QORT_DECIMALS } from "../constants/constants";
|
||||||
import Base58 from "../deps/Base58";
|
import Base58 from "../deps/Base58";
|
||||||
import {
|
import {
|
||||||
base64ToUint8Array,
|
base64ToUint8Array,
|
||||||
|
createSymmetricKeyAndNonce,
|
||||||
decryptDeprecatedSingle,
|
decryptDeprecatedSingle,
|
||||||
decryptGroupDataQortalRequest,
|
decryptGroupDataQortalRequest,
|
||||||
|
decryptGroupEncryptionWithSharingKey,
|
||||||
|
decryptSingle,
|
||||||
encryptDataGroup,
|
encryptDataGroup,
|
||||||
|
encryptSingle,
|
||||||
|
objectToBase64,
|
||||||
uint8ArrayStartsWith,
|
uint8ArrayStartsWith,
|
||||||
uint8ArrayToBase64,
|
uint8ArrayToBase64,
|
||||||
} from "../qdn/encryption/group-encryption";
|
} from "../qdn/encryption/group-encryption";
|
||||||
@ -48,6 +56,7 @@ const sellerForeignFee = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function roundUpToDecimals(number, decimals = 8) {
|
function roundUpToDecimals(number, decimals = 8) {
|
||||||
const factor = Math.pow(10, decimals); // Create a factor based on the number of decimals
|
const factor = Math.pow(10, decimals); // Create a factor based on the number of decimals
|
||||||
return Math.ceil(+number * factor) / factor;
|
return Math.ceil(+number * factor) / factor;
|
||||||
@ -97,6 +106,139 @@ const _createPoll = async ({pollName, pollDescription, options}, isFromExtension
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function validateSecretKey(obj) {
|
||||||
|
// Check if the input is an object
|
||||||
|
if (typeof obj !== "object" || obj === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate over each key in the object
|
||||||
|
for (let key in obj) {
|
||||||
|
// Ensure the key is a string representation of a positive integer
|
||||||
|
if (!/^\d+$/.test(key)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the corresponding value for the key
|
||||||
|
const value = obj[key];
|
||||||
|
|
||||||
|
// Check that value is an object and not null
|
||||||
|
if (typeof value !== "object" || value === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for messageKey
|
||||||
|
if (!value.hasOwnProperty("messageKey")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure messageKey and nonce are non-empty strings
|
||||||
|
if (
|
||||||
|
typeof value.messageKey !== "string" ||
|
||||||
|
value.messageKey.trim() === ""
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all checks passed, return true
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPublishesFromAdminsAdminSpace = async (
|
||||||
|
admins: string[],
|
||||||
|
groupId
|
||||||
|
) => {
|
||||||
|
const queryString = admins.map((name) => `name=${name}`).join("&");
|
||||||
|
const baseUrl = await getBaseApi()
|
||||||
|
const url = `${baseUrl}/arbitrary/resources/searchsimple?mode=ALL&service=DOCUMENT_PRIVATE&identifier=admins-symmetric-qchat-group-${groupId}&exactmatchnames=true&limit=0&reverse=true&${queryString}&prefix=true`;
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("network error");
|
||||||
|
}
|
||||||
|
const adminData = await response.json();
|
||||||
|
|
||||||
|
const filterId = adminData.filter(
|
||||||
|
(data: any) => data.identifier === `admins-symmetric-qchat-group-${groupId}`
|
||||||
|
);
|
||||||
|
if (filterId?.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const sortedData = filterId.sort((a: any, b: any) => {
|
||||||
|
// Get the most recent date for both a and b
|
||||||
|
const dateA = a.updated ? new Date(a.updated) : new Date(a.created);
|
||||||
|
const dateB = b.updated ? new Date(b.updated) : new Date(b.created);
|
||||||
|
|
||||||
|
// Sort by most recent
|
||||||
|
return dateB.getTime() - dateA.getTime();
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortedData[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPublishesFromAdmins = async (admins: string[], groupId) => {
|
||||||
|
const baseUrl = await getBaseApi()
|
||||||
|
|
||||||
|
const queryString = admins.map((name) => `name=${name}`).join("&");
|
||||||
|
const url = `${baseUrl}/arbitrary/resources/searchsimple?mode=ALL&service=DOCUMENT_PRIVATE&identifier=symmetric-qchat-group-${
|
||||||
|
groupId
|
||||||
|
}&exactmatchnames=true&limit=0&reverse=true&${queryString}&prefix=true`;
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("network error");
|
||||||
|
}
|
||||||
|
const adminData = await response.json();
|
||||||
|
|
||||||
|
const filterId = adminData.filter(
|
||||||
|
(data: any) =>
|
||||||
|
data.identifier === `symmetric-qchat-group-${groupId}`
|
||||||
|
);
|
||||||
|
if (filterId?.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const sortedData = filterId.sort((a: any, b: any) => {
|
||||||
|
// Get the most recent date for both a and b
|
||||||
|
const dateA = a.updated ? new Date(a.updated) : new Date(a.created);
|
||||||
|
const dateB = b.updated ? new Date(b.updated) : new Date(b.created);
|
||||||
|
|
||||||
|
// Sort by most recent
|
||||||
|
return dateB.getTime() - dateA.getTime();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return sortedData[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGroupAdmins = async (groupNumber: number) => {
|
||||||
|
// const validApi = await findUsableApi();
|
||||||
|
const baseUrl = await getBaseApi()
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${baseUrl}/groups/members/${groupNumber}?limit=0&onlyAdmins=true`
|
||||||
|
);
|
||||||
|
const groupData = await response.json();
|
||||||
|
let members: any = [];
|
||||||
|
let membersAddresses = [];
|
||||||
|
let both = [];
|
||||||
|
|
||||||
|
|
||||||
|
const getMemNames = groupData?.members?.map(async (member) => {
|
||||||
|
if (member?.member) {
|
||||||
|
const name = await getNameInfo(member.member);
|
||||||
|
if (name) {
|
||||||
|
members.push(name);
|
||||||
|
both.push({ name, address: member.member });
|
||||||
|
}
|
||||||
|
membersAddresses.push(member.member);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
await Promise.all(getMemNames);
|
||||||
|
console.log('members', members)
|
||||||
|
return { names: members, addresses: membersAddresses, both };
|
||||||
|
};
|
||||||
|
|
||||||
const _deployAt = async (
|
const _deployAt = async (
|
||||||
{name,
|
{name,
|
||||||
description,
|
description,
|
||||||
@ -2824,46 +2966,102 @@ export const cancelSellOrder = async (data, isFromExtension) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const adminAction = async (data, isFromExtension) => {
|
export const adminAction = async (data, isFromExtension) => {
|
||||||
const requiredFields = [
|
const requiredFields = ["type"];
|
||||||
"type",
|
|
||||||
];
|
|
||||||
const missingFields: string[] = [];
|
const missingFields: string[] = [];
|
||||||
requiredFields.forEach((field) => {
|
requiredFields.forEach((field) => {
|
||||||
if (!data[field]) {
|
if (!data[field]) {
|
||||||
missingFields.push(field);
|
missingFields.push(field);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// For actions that require a value, check for 'value' field
|
||||||
|
const actionsRequiringValue = [
|
||||||
|
"addpeer",
|
||||||
|
"removepeer",
|
||||||
|
"forcesync",
|
||||||
|
"addmintingaccount",
|
||||||
|
"removemintingaccount",
|
||||||
|
];
|
||||||
|
if (
|
||||||
|
actionsRequiringValue.includes(data.type.toLowerCase()) &&
|
||||||
|
!data.value
|
||||||
|
) {
|
||||||
|
missingFields.push("value");
|
||||||
|
}
|
||||||
if (missingFields.length > 0) {
|
if (missingFields.length > 0) {
|
||||||
const missingFieldsString = missingFields.join(", ");
|
const missingFieldsString = missingFields.join(", ");
|
||||||
const errorMsg = `Missing fields: ${missingFieldsString}`;
|
const errorMsg = `Missing fields: ${missingFieldsString}`;
|
||||||
throw new Error(errorMsg);
|
throw new Error(errorMsg);
|
||||||
}
|
}
|
||||||
const isGateway = await isRunningGateway()
|
const isGateway = await isRunningGateway();
|
||||||
if (isGateway) {
|
if (isGateway) {
|
||||||
throw new Error('This action cannot be done through a gateway')
|
throw new Error("This action cannot be done through a gateway");
|
||||||
}
|
}
|
||||||
|
|
||||||
let apiEndpoint = '';
|
let apiEndpoint = "";
|
||||||
|
let method = "GET"; // Default method
|
||||||
|
let includeValueInBody = false;
|
||||||
switch (data.type.toLowerCase()) {
|
switch (data.type.toLowerCase()) {
|
||||||
case 'stop':
|
case "stop":
|
||||||
apiEndpoint = await createEndpoint('/admin/stop');
|
apiEndpoint = await createEndpoint("/admin/stop");
|
||||||
break;
|
break;
|
||||||
case 'restart':
|
case "restart":
|
||||||
apiEndpoint = await createEndpoint('/admin/restart');
|
apiEndpoint = await createEndpoint("/admin/restart");
|
||||||
break;
|
break;
|
||||||
case 'bootstrap':
|
case "bootstrap":
|
||||||
apiEndpoint = await createEndpoint('/admin/bootstrap');
|
apiEndpoint = await createEndpoint("/admin/bootstrap");
|
||||||
|
break;
|
||||||
|
case "addmintingaccount":
|
||||||
|
apiEndpoint = await createEndpoint("/admin/mintingaccounts");
|
||||||
|
method = "POST";
|
||||||
|
includeValueInBody = true;
|
||||||
|
break;
|
||||||
|
case "removemintingaccount":
|
||||||
|
apiEndpoint = await createEndpoint("/admin/mintingaccounts");
|
||||||
|
method = "DELETE";
|
||||||
|
includeValueInBody = true;
|
||||||
|
break;
|
||||||
|
case "forcesync":
|
||||||
|
apiEndpoint = await createEndpoint("/admin/forcesync");
|
||||||
|
method = "POST";
|
||||||
|
includeValueInBody = true;
|
||||||
|
break;
|
||||||
|
case "addpeer":
|
||||||
|
apiEndpoint = await createEndpoint("/peers");
|
||||||
|
method = "POST";
|
||||||
|
includeValueInBody = true;
|
||||||
|
break;
|
||||||
|
case "removepeer":
|
||||||
|
apiEndpoint = await createEndpoint("/peers");
|
||||||
|
method = "DELETE";
|
||||||
|
includeValueInBody = true;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown admin action type: ${data.type}`);
|
throw new Error(`Unknown admin action type: ${data.type}`);
|
||||||
}
|
}
|
||||||
|
// Prepare the permission prompt text
|
||||||
|
let permissionText = `Do you give this application permission to perform the admin action: ${data.type}`;
|
||||||
|
if (data.value) {
|
||||||
|
permissionText += ` with value: ${data.value}`;
|
||||||
|
}
|
||||||
|
|
||||||
const resPermission = await getUserPermission({
|
const resPermission = await getUserPermission(
|
||||||
text1: `Do you give this application permission to perform a node ${data.type}?`,
|
{
|
||||||
}, isFromExtension);
|
text1: permissionText,
|
||||||
|
},
|
||||||
|
isFromExtension
|
||||||
|
);
|
||||||
const { accepted } = resPermission;
|
const { accepted } = resPermission;
|
||||||
if (accepted) {
|
if (accepted) {
|
||||||
const response = await fetch(apiEndpoint);
|
// Set up options for the API call
|
||||||
|
const options: RequestInit = {
|
||||||
|
method: method,
|
||||||
|
headers: {},
|
||||||
|
};
|
||||||
|
if (includeValueInBody) {
|
||||||
|
options.headers["Content-Type"] = "text/plain";
|
||||||
|
options.body = data.value;
|
||||||
|
}
|
||||||
|
const response = await fetch(apiEndpoint, options);
|
||||||
if (!response.ok) throw new Error("Failed to perform request");
|
if (!response.ok) throw new Error("Failed to perform request");
|
||||||
|
|
||||||
let res;
|
let res;
|
||||||
@ -2876,5 +3074,325 @@ export const adminAction = async (data, isFromExtension) => {
|
|||||||
} else {
|
} else {
|
||||||
throw new Error("User declined request");
|
throw new Error("User declined request");
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const signTransaction = async (data, isFromExtension) => {
|
||||||
|
const requiredFields = ["unsignedBytes"];
|
||||||
|
const missingFields: string[] = [];
|
||||||
|
requiredFields.forEach((field) => {
|
||||||
|
if (!data[field]) {
|
||||||
|
missingFields.push(field);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (missingFields.length > 0) {
|
||||||
|
const missingFieldsString = missingFields.join(", ");
|
||||||
|
const errorMsg = `Missing fields: ${missingFieldsString}`;
|
||||||
|
throw new Error(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let _url = await createEndpoint(
|
||||||
|
"/transactions/decode?ignoreValidityChecks=false"
|
||||||
|
);
|
||||||
|
|
||||||
|
let _body = data.unsignedBytes;
|
||||||
|
const response = await fetch(_url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: _body,
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error("Failed to decode transaction");
|
||||||
|
const decodedData = await response.json();
|
||||||
|
const resPermission = await getUserPermission(
|
||||||
|
{
|
||||||
|
text1: `Do you give this application permission to sign a transaction?`,
|
||||||
|
highlightedText: "Read the transaction carefully before accepting!",
|
||||||
|
text2: `Tx type: ${decodedData.type}`,
|
||||||
|
json: decodedData,
|
||||||
|
},
|
||||||
|
isFromExtension
|
||||||
|
);
|
||||||
|
const { accepted } = resPermission;
|
||||||
|
if (accepted) {
|
||||||
|
|
||||||
|
let urlConverted = await createEndpoint("/transactions/convert");
|
||||||
|
|
||||||
|
const responseConverted = await fetch(urlConverted, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: data.unsignedBytes,
|
||||||
|
});
|
||||||
|
const resKeyPair = await getKeyPair();
|
||||||
|
const parsedData = resKeyPair;
|
||||||
|
const uint8PrivateKey = Base58.decode(parsedData.privateKey);
|
||||||
|
const uint8PublicKey = Base58.decode(parsedData.publicKey);
|
||||||
|
const keyPair = {
|
||||||
|
privateKey: uint8PrivateKey,
|
||||||
|
publicKey: uint8PublicKey,
|
||||||
|
};
|
||||||
|
const convertedBytes = await responseConverted.text();
|
||||||
|
const txBytes = Base58.decode(data.unsignedBytes);
|
||||||
|
const _arbitraryBytesBuffer = Object.keys(txBytes).map(function (key) {
|
||||||
|
return txBytes[key];
|
||||||
|
});
|
||||||
|
const arbitraryBytesBuffer = new Uint8Array(_arbitraryBytesBuffer);
|
||||||
|
const txByteSigned = Base58.decode(convertedBytes);
|
||||||
|
const _bytesForSigningBuffer = Object.keys(txByteSigned).map(function (
|
||||||
|
key
|
||||||
|
) {
|
||||||
|
return txByteSigned[key];
|
||||||
|
});
|
||||||
|
const bytesForSigningBuffer = new Uint8Array(_bytesForSigningBuffer);
|
||||||
|
const signature = nacl.sign.detached(
|
||||||
|
bytesForSigningBuffer,
|
||||||
|
keyPair.privateKey
|
||||||
|
);
|
||||||
|
const signedBytes = utils.appendBuffer(arbitraryBytesBuffer, signature);
|
||||||
|
return uint8ArrayToBase64(signedBytes);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new Error("User declined request");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const decryptQortalGroupData = async (data, sender) => {
|
||||||
|
console.log('data', data)
|
||||||
|
let data64 = data.data64;
|
||||||
|
let groupId = data?.groupId
|
||||||
|
let isAdmins = data?.isAdmins
|
||||||
|
if(!groupId){
|
||||||
|
throw new Error('Please provide a groupId')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data64) {
|
||||||
|
throw new Error("Please include data to encrypt");
|
||||||
|
}
|
||||||
|
|
||||||
|
let secretKeyObject
|
||||||
|
if(!isAdmins){
|
||||||
|
if(groupSecretkeys[groupId] && groupSecretkeys[groupId].secretKeyObject && groupSecretkeys[groupId]?.timestamp && (Date.now() - groupSecretkeys[groupId]?.timestamp) < 1200000){
|
||||||
|
secretKeyObject = groupSecretkeys[groupId].secretKeyObject
|
||||||
|
}
|
||||||
|
if(!secretKeyObject){
|
||||||
|
const { names } =
|
||||||
|
await getGroupAdmins(groupId)
|
||||||
|
|
||||||
|
const publish =
|
||||||
|
await getPublishesFromAdmins(names, groupId);
|
||||||
|
if(publish === false) throw new Error('No group key found.')
|
||||||
|
const url = await createEndpoint(`/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${
|
||||||
|
publish.identifier
|
||||||
|
}?encoding=base64`);
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
url
|
||||||
|
);
|
||||||
|
const resData = await res.text();
|
||||||
|
const decryptedKey: any = await decryptGroupEncryption({data: resData});
|
||||||
|
|
||||||
|
const dataint8Array = base64ToUint8Array(decryptedKey.data);
|
||||||
|
const decryptedKeyToObject = uint8ArrayToObject(dataint8Array);
|
||||||
|
if (!validateSecretKey(decryptedKeyToObject))
|
||||||
|
throw new Error("SecretKey is not valid");
|
||||||
|
secretKeyObject = decryptedKeyToObject
|
||||||
|
groupSecretkeys[groupId] = {
|
||||||
|
secretKeyObject,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(groupSecretkeys[`admins-${groupId}`] && groupSecretkeys[`admins-${groupId}`].secretKeyObject && groupSecretkeys[`admins-${groupId}`]?.timestamp && (Date.now() - groupSecretkeys[`admins-${groupId}`]?.timestamp) < 1200000){
|
||||||
|
secretKeyObject = groupSecretkeys[`admins-${groupId}`].secretKeyObject
|
||||||
|
}
|
||||||
|
if(!secretKeyObject){
|
||||||
|
const { names } =
|
||||||
|
await getGroupAdmins(groupId)
|
||||||
|
|
||||||
|
const publish =
|
||||||
|
await getPublishesFromAdminsAdminSpace(names, groupId);
|
||||||
|
if(publish === false) throw new Error('No group key found.')
|
||||||
|
const url = await createEndpoint(`/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${
|
||||||
|
publish.identifier
|
||||||
|
}?encoding=base64`);
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
url
|
||||||
|
);
|
||||||
|
const resData = await res.text();
|
||||||
|
const decryptedKey: any = await decryptGroupEncryption({data: resData});
|
||||||
|
|
||||||
|
const dataint8Array = base64ToUint8Array(decryptedKey.data);
|
||||||
|
const decryptedKeyToObject = uint8ArrayToObject(dataint8Array);
|
||||||
|
if (!validateSecretKey(decryptedKeyToObject))
|
||||||
|
throw new Error("SecretKey is not valid");
|
||||||
|
secretKeyObject = decryptedKeyToObject
|
||||||
|
groupSecretkeys[`admins-${groupId}`] = {
|
||||||
|
secretKeyObject,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
console.log('secretKeyObject', secretKeyObject)
|
||||||
|
|
||||||
|
const resGroupDecryptResource = decryptSingle({
|
||||||
|
data64, secretKeyObject: secretKeyObject, skipDecodeBase64: true
|
||||||
|
})
|
||||||
|
if (resGroupDecryptResource) {
|
||||||
|
return resGroupDecryptResource;
|
||||||
|
} else {
|
||||||
|
throw new Error("Unable to decrypt");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const encryptDataWithSharingKey = async (data, sender) => {
|
||||||
|
let data64 = data.data64;
|
||||||
|
let publicKeys = data.publicKeys || [];
|
||||||
|
if (data.fileId) {
|
||||||
|
data64 = await getFileFromContentScript(data.fileId, sender);
|
||||||
|
}
|
||||||
|
if (!data64) {
|
||||||
|
throw new Error("Please include data to encrypt");
|
||||||
|
}
|
||||||
|
const symmetricKey = createSymmetricKeyAndNonce()
|
||||||
|
const dataObject = {
|
||||||
|
data: data64,
|
||||||
|
key:symmetricKey.messageKey
|
||||||
|
}
|
||||||
|
const dataObjectBase64 = await objectToBase64(dataObject)
|
||||||
|
|
||||||
|
const resKeyPair = await getKeyPair();
|
||||||
|
const parsedData = resKeyPair;
|
||||||
|
const privateKey = parsedData.privateKey;
|
||||||
|
const userPublicKey = parsedData.publicKey;
|
||||||
|
|
||||||
|
const encryptDataResponse = encryptDataGroup({
|
||||||
|
data64: dataObjectBase64,
|
||||||
|
publicKeys: publicKeys,
|
||||||
|
privateKey,
|
||||||
|
userPublicKey,
|
||||||
|
customSymmetricKey: symmetricKey.messageKey
|
||||||
|
});
|
||||||
|
if (encryptDataResponse) {
|
||||||
|
return encryptDataResponse;
|
||||||
|
} else {
|
||||||
|
throw new Error("Unable to encrypt");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decryptDataWithSharingKey = async (data, sender) => {
|
||||||
|
const { encryptedData, key } = data;
|
||||||
|
|
||||||
|
|
||||||
|
if (!encryptedData) {
|
||||||
|
throw new Error("Please include data to decrypt");
|
||||||
|
}
|
||||||
|
const decryptedData = await decryptGroupEncryptionWithSharingKey({data64EncryptedData: encryptedData, key})
|
||||||
|
const base64ToObject = JSON.parse(atob(decryptedData))
|
||||||
|
if(!base64ToObject.data) throw new Error('No data in the encrypted resource')
|
||||||
|
return base64ToObject.data
|
||||||
|
};
|
||||||
|
|
||||||
|
export const encryptQortalGroupData = async (data, sender) => {
|
||||||
|
let data64 = data.data64;
|
||||||
|
let groupId = data?.groupId
|
||||||
|
let isAdmins = data?.isAdmins
|
||||||
|
if(!groupId){
|
||||||
|
throw new Error('Please provide a groupId')
|
||||||
|
}
|
||||||
|
if (data.fileId) {
|
||||||
|
data64 = await getFileFromContentScript(data.fileId, sender);
|
||||||
|
}
|
||||||
|
if (!data64) {
|
||||||
|
throw new Error("Please include data to encrypt");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let secretKeyObject
|
||||||
|
if(!isAdmins){
|
||||||
|
if(groupSecretkeys[groupId] && groupSecretkeys[groupId].secretKeyObject && groupSecretkeys[groupId]?.timestamp && (Date.now() - groupSecretkeys[groupId]?.timestamp) < 1200000){
|
||||||
|
secretKeyObject = groupSecretkeys[groupId].secretKeyObject
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!secretKeyObject){
|
||||||
|
const { names } =
|
||||||
|
await getGroupAdmins(groupId)
|
||||||
|
|
||||||
|
const publish =
|
||||||
|
await getPublishesFromAdmins(names, groupId);
|
||||||
|
if(publish === false) throw new Error('No group key found.')
|
||||||
|
const url = await createEndpoint(`/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${
|
||||||
|
publish.identifier
|
||||||
|
}?encoding=base64`);
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
url
|
||||||
|
);
|
||||||
|
const resData = await res.text();
|
||||||
|
const decryptedKey: any = await decryptGroupEncryption({data: resData});
|
||||||
|
|
||||||
|
const dataint8Array = base64ToUint8Array(decryptedKey.data);
|
||||||
|
const decryptedKeyToObject = uint8ArrayToObject(dataint8Array);
|
||||||
|
|
||||||
|
if (!validateSecretKey(decryptedKeyToObject))
|
||||||
|
throw new Error("SecretKey is not valid");
|
||||||
|
secretKeyObject = decryptedKeyToObject
|
||||||
|
groupSecretkeys[groupId] = {
|
||||||
|
secretKeyObject,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if(groupSecretkeys[`admins-${groupId}`] && groupSecretkeys[`admins-${groupId}`].secretKeyObject && groupSecretkeys[`admins-${groupId}`]?.timestamp && (Date.now() - groupSecretkeys[`admins-${groupId}`]?.timestamp) < 1200000){
|
||||||
|
secretKeyObject = groupSecretkeys[`admins-${groupId}`].secretKeyObject
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!secretKeyObject){
|
||||||
|
const { names } =
|
||||||
|
await getGroupAdmins(groupId)
|
||||||
|
|
||||||
|
const publish =
|
||||||
|
await getPublishesFromAdminsAdminSpace(names, groupId);
|
||||||
|
if(publish === false) throw new Error('No group key found.')
|
||||||
|
const url = await createEndpoint(`/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${
|
||||||
|
publish.identifier
|
||||||
|
}?encoding=base64`);
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
url
|
||||||
|
);
|
||||||
|
const resData = await res.text();
|
||||||
|
const decryptedKey: any = await decryptGroupEncryption({data: resData});
|
||||||
|
|
||||||
|
const dataint8Array = base64ToUint8Array(decryptedKey.data);
|
||||||
|
const decryptedKeyToObject = uint8ArrayToObject(dataint8Array);
|
||||||
|
|
||||||
|
if (!validateSecretKey(decryptedKeyToObject))
|
||||||
|
throw new Error("SecretKey is not valid");
|
||||||
|
secretKeyObject = decryptedKeyToObject
|
||||||
|
groupSecretkeys[`admins-${groupId}`] = {
|
||||||
|
secretKeyObject,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const resGroupEncryptedResource = encryptSingle({
|
||||||
|
data64, secretKeyObject: secretKeyObject,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (resGroupEncryptedResource) {
|
||||||
|
return resGroupEncryptedResource;
|
||||||
|
} else {
|
||||||
|
throw new Error("Unable to encrypt");
|
||||||
|
}
|
||||||
};
|
};
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback, useEffect } from 'react'
|
import React, { useCallback, useEffect } from 'react'
|
||||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||||
import { canSaveSettingToQdnAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from './atoms/global';
|
import { canSaveSettingToQdnAtom, isUsingImportExportSettingsAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from './atoms/global';
|
||||||
import { getArbitraryEndpointReact, getBaseApiReact } from './App';
|
import { getArbitraryEndpointReact, getBaseApiReact } from './App';
|
||||||
import { decryptResource } from './components/Group/Group';
|
import { decryptResource } from './components/Group/Group';
|
||||||
import { base64ToUint8Array, uint8ArrayToObject } from './backgroundFunctions/encryption';
|
import { base64ToUint8Array, uint8ArrayToObject } from './backgroundFunctions/encryption';
|
||||||
@ -53,13 +53,13 @@ const getPublishRecord = async (myName) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useQortalGetSaveSettings = (myName) => {
|
export const useQortalGetSaveSettings = (myName, isAuthenticated) => {
|
||||||
const setSortablePinnedApps = useSetRecoilState(sortablePinnedAppsAtom);
|
const setSortablePinnedApps = useSetRecoilState(sortablePinnedAppsAtom);
|
||||||
const setCanSave = useSetRecoilState(canSaveSettingToQdnAtom);
|
const setCanSave = useSetRecoilState(canSaveSettingToQdnAtom);
|
||||||
const setSettingsQDNLastUpdated = useSetRecoilState(settingsQDNLastUpdatedAtom);
|
const setSettingsQDNLastUpdated = useSetRecoilState(settingsQDNLastUpdatedAtom);
|
||||||
const [settingsLocalLastUpdated] = useRecoilState(settingsLocalLastUpdatedAtom);
|
const [settingsLocalLastUpdated] = useRecoilState(settingsLocalLastUpdatedAtom);
|
||||||
const [oldPinnedApps, setOldPinnedApps] = useRecoilState(oldPinnedAppsAtom)
|
const [oldPinnedApps, setOldPinnedApps] = useRecoilState(oldPinnedAppsAtom)
|
||||||
|
const [isUsingImportExportSettings] = useRecoilState(isUsingImportExportSettingsAtom);
|
||||||
const getSavedSettings = useCallback(async (myName, settingsLocalLastUpdated)=> {
|
const getSavedSettings = useCallback(async (myName, settingsLocalLastUpdated)=> {
|
||||||
try {
|
try {
|
||||||
const {hasPublishRecord, timestamp} = await getPublishRecord(myName)
|
const {hasPublishRecord, timestamp} = await getPublishRecord(myName)
|
||||||
@ -87,8 +87,9 @@ export const useQortalGetSaveSettings = (myName) => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
useEffect(()=> {
|
useEffect(()=> {
|
||||||
if(!myName || !settingsLocalLastUpdated) return
|
if(!myName || !settingsLocalLastUpdated || !isAuthenticated || isUsingImportExportSettings === null) return
|
||||||
|
if(isUsingImportExportSettings) return
|
||||||
getSavedSettings(myName, settingsLocalLastUpdated)
|
getSavedSettings(myName, settingsLocalLastUpdated)
|
||||||
}, [getSavedSettings, myName, settingsLocalLastUpdated])
|
}, [getSavedSettings, myName, settingsLocalLastUpdated, isAuthenticated, isUsingImportExportSettings])
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback, useEffect } from 'react'
|
import React, { useCallback, useEffect } from 'react'
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from './atoms/global';
|
import { isUsingImportExportSettingsAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from './atoms/global';
|
||||||
|
|
||||||
function fetchFromLocalStorage(key) {
|
function fetchFromLocalStorage(key) {
|
||||||
try {
|
try {
|
||||||
@ -19,17 +19,38 @@ function fetchFromLocalStorage(key) {
|
|||||||
export const useRetrieveDataLocalStorage = () => {
|
export const useRetrieveDataLocalStorage = () => {
|
||||||
const setSortablePinnedApps = useSetRecoilState(sortablePinnedAppsAtom);
|
const setSortablePinnedApps = useSetRecoilState(sortablePinnedAppsAtom);
|
||||||
const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom);
|
const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom);
|
||||||
|
const setIsUsingImportExportSettings = useSetRecoilState(isUsingImportExportSettingsAtom)
|
||||||
|
const setSettingsQDNLastUpdated = useSetRecoilState(settingsQDNLastUpdatedAtom);
|
||||||
|
const setOldPinnedApps = useSetRecoilState(oldPinnedAppsAtom)
|
||||||
|
|
||||||
const getSortablePinnedApps = useCallback(()=> {
|
const getSortablePinnedApps = useCallback(()=> {
|
||||||
const pinnedAppsLocal = fetchFromLocalStorage('ext_saved_settings')
|
const pinnedAppsLocal = fetchFromLocalStorage('ext_saved_settings')
|
||||||
if(pinnedAppsLocal?.sortablePinnedApps){
|
if(pinnedAppsLocal?.sortablePinnedApps){
|
||||||
setSortablePinnedApps(pinnedAppsLocal?.sortablePinnedApps)
|
setSortablePinnedApps(pinnedAppsLocal?.sortablePinnedApps)
|
||||||
}
|
|
||||||
setSettingsLocalLastUpdated(pinnedAppsLocal?.timestamp || -1)
|
setSettingsLocalLastUpdated(pinnedAppsLocal?.timestamp || -1)
|
||||||
|
} else {
|
||||||
|
setSettingsLocalLastUpdated(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [])
|
||||||
|
const getSortablePinnedAppsImportExport = useCallback(()=> {
|
||||||
|
const pinnedAppsLocal = fetchFromLocalStorage('ext_saved_settings_import_export')
|
||||||
|
if(pinnedAppsLocal?.sortablePinnedApps){
|
||||||
|
setOldPinnedApps(pinnedAppsLocal?.sortablePinnedApps)
|
||||||
|
|
||||||
|
|
||||||
|
setIsUsingImportExportSettings(true)
|
||||||
|
setSettingsQDNLastUpdated(pinnedAppsLocal?.timestamp || 0)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
setIsUsingImportExportSettings(false)
|
||||||
|
}
|
||||||
|
|
||||||
}, [])
|
}, [])
|
||||||
useEffect(()=> {
|
useEffect(()=> {
|
||||||
|
|
||||||
getSortablePinnedApps()
|
getSortablePinnedApps()
|
||||||
|
getSortablePinnedAppsImportExport()
|
||||||
}, [getSortablePinnedApps])
|
}, [getSortablePinnedApps])
|
||||||
|
|
||||||
}
|
}
|
||||||
|
16
src/utils/decode.ts
Normal file
16
src/utils/decode.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
export function decodeIfEncoded(input) {
|
||||||
|
try {
|
||||||
|
// Check if input is URI-encoded by encoding and decoding
|
||||||
|
const encoded = encodeURIComponent(decodeURIComponent(input));
|
||||||
|
if (encoded === input) {
|
||||||
|
// Input is URI-encoded, so decode it
|
||||||
|
return decodeURIComponent(input);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// decodeURIComponent throws an error if input is not encoded
|
||||||
|
console.error("Error decoding URI:", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return input as-is if not URI-encoded
|
||||||
|
return input;
|
||||||
|
}
|
@ -55,3 +55,13 @@ export const fileToBase64 = (file) => new Promise(async (resolve, reject) => {
|
|||||||
semaphore.release()
|
semaphore.release()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const base64ToBlobUrl = (base64, mimeType = "image/png") => {
|
||||||
|
const binary = atob(base64);
|
||||||
|
const array = [];
|
||||||
|
for (let i = 0; i < binary.length; i++) {
|
||||||
|
array.push(binary.charCodeAt(i));
|
||||||
|
}
|
||||||
|
const blob = new Blob([new Uint8Array(array)], { type: mimeType });
|
||||||
|
return URL.createObjectURL(blob);
|
||||||
|
};
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { crypto, walletVersion } from '../../constants/decryptWallet';
|
import { crypto, walletVersion } from '../../constants/decryptWallet';
|
||||||
import { doInitWorkers, kdf } from '../../deps/kdf';
|
import { doInitWorkers, kdf } from '../../deps/kdf';
|
||||||
|
import { mimeToExtensionMap } from '../memeTypes';
|
||||||
import PhraseWallet from './phrase-wallet';
|
import PhraseWallet from './phrase-wallet';
|
||||||
import * as WORDLISTS from './wordlists';
|
import * as WORDLISTS from './wordlists';
|
||||||
import { saveAs } from 'file-saver';
|
import { saveAs } from 'file-saver';
|
||||||
@ -74,6 +75,10 @@ export function generateRandomSentence(template = 'adverb verb noun adjective no
|
|||||||
return parse(template);
|
return parse(template);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasExtension = (filename) => {
|
||||||
|
return filename.includes(".") && filename.split(".").pop().length > 0;
|
||||||
|
};
|
||||||
|
|
||||||
export const createAccount = async()=> {
|
export const createAccount = async()=> {
|
||||||
const generatedSeedPhrase = generateRandomSentence()
|
const generatedSeedPhrase = generateRandomSentence()
|
||||||
const threads = doInitWorkers(crypto.kdfThreads)
|
const threads = doInitWorkers(crypto.kdfThreads)
|
||||||
@ -100,3 +105,16 @@ export const createAccount = async()=> {
|
|||||||
FileSaver.saveAs(blob, fileName); // Ensure FileSaver is properly imported or available in your environment.
|
FileSaver.saveAs(blob, fileName); // Ensure FileSaver is properly imported or available in your environment.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const saveFileToDiskGeneric = async (blob, filename) => {
|
||||||
|
const timestamp = new Date()
|
||||||
|
.toISOString()
|
||||||
|
.replace(/:/g, "-"); // Safe timestamp for filenames
|
||||||
|
|
||||||
|
const fileExtension = mimeToExtensionMap[blob.type]
|
||||||
|
let fileName = filename || "qortal_file_" + timestamp + "." + fileExtension;
|
||||||
|
fileName = hasExtension(fileName) ? fileName : fileName + "." + fileExtension;
|
||||||
|
|
||||||
|
await saveAs(blob, fileName);
|
||||||
|
|
||||||
|
}
|
@ -12,10 +12,13 @@ export const mimeToExtensionMap = {
|
|||||||
"application/vnd.oasis.opendocument.presentation": ".odp",
|
"application/vnd.oasis.opendocument.presentation": ".odp",
|
||||||
"text/plain": ".txt",
|
"text/plain": ".txt",
|
||||||
"text/csv": ".csv",
|
"text/csv": ".csv",
|
||||||
"text/html": ".html",
|
|
||||||
"application/xhtml+xml": ".xhtml",
|
"application/xhtml+xml": ".xhtml",
|
||||||
"application/xml": ".xml",
|
"application/xml": ".xml",
|
||||||
"application/json": ".json",
|
"application/rtf": ".rtf",
|
||||||
|
"application/vnd.apple.pages": ".pages",
|
||||||
|
"application/vnd.google-apps.document": ".gdoc",
|
||||||
|
"application/vnd.google-apps.spreadsheet": ".gsheet",
|
||||||
|
"application/vnd.google-apps.presentation": ".gslides",
|
||||||
|
|
||||||
// Images
|
// Images
|
||||||
"image/jpeg": ".jpg",
|
"image/jpeg": ".jpg",
|
||||||
@ -25,6 +28,11 @@ export const mimeToExtensionMap = {
|
|||||||
"image/svg+xml": ".svg",
|
"image/svg+xml": ".svg",
|
||||||
"image/tiff": ".tif",
|
"image/tiff": ".tif",
|
||||||
"image/bmp": ".bmp",
|
"image/bmp": ".bmp",
|
||||||
|
"image/x-icon": ".ico",
|
||||||
|
"image/heic": ".heic",
|
||||||
|
"image/heif": ".heif",
|
||||||
|
"image/apng": ".apng",
|
||||||
|
"image/avif": ".avif",
|
||||||
|
|
||||||
// Audio
|
// Audio
|
||||||
"audio/mpeg": ".mp3",
|
"audio/mpeg": ".mp3",
|
||||||
@ -32,6 +40,11 @@ export const mimeToExtensionMap = {
|
|||||||
"audio/wav": ".wav",
|
"audio/wav": ".wav",
|
||||||
"audio/webm": ".weba",
|
"audio/webm": ".weba",
|
||||||
"audio/aac": ".aac",
|
"audio/aac": ".aac",
|
||||||
|
"audio/flac": ".flac",
|
||||||
|
"audio/x-m4a": ".m4a",
|
||||||
|
"audio/x-ms-wma": ".wma",
|
||||||
|
"audio/midi": ".midi",
|
||||||
|
"audio/x-midi": ".mid",
|
||||||
|
|
||||||
// Video
|
// Video
|
||||||
"video/mp4": ".mp4",
|
"video/mp4": ".mp4",
|
||||||
@ -45,6 +58,7 @@ export const mimeToExtensionMap = {
|
|||||||
"video/3gpp2": ".3g2",
|
"video/3gpp2": ".3g2",
|
||||||
"video/x-matroska": ".mkv",
|
"video/x-matroska": ".mkv",
|
||||||
"video/x-flv": ".flv",
|
"video/x-flv": ".flv",
|
||||||
|
"video/x-ms-asf": ".asf",
|
||||||
|
|
||||||
// Archives
|
// Archives
|
||||||
"application/zip": ".zip",
|
"application/zip": ".zip",
|
||||||
@ -53,4 +67,57 @@ export const mimeToExtensionMap = {
|
|||||||
"application/x-7z-compressed": ".7z",
|
"application/x-7z-compressed": ".7z",
|
||||||
"application/x-gzip": ".gz",
|
"application/x-gzip": ".gz",
|
||||||
"application/x-bzip2": ".bz2",
|
"application/x-bzip2": ".bz2",
|
||||||
}
|
"application/x-apple-diskimage": ".dmg",
|
||||||
|
"application/vnd.android.package-archive": ".apk",
|
||||||
|
"application/x-iso9660-image": ".iso",
|
||||||
|
|
||||||
|
// Code Files
|
||||||
|
"text/javascript": ".js",
|
||||||
|
"text/css": ".css",
|
||||||
|
"text/html": ".html",
|
||||||
|
"application/json": ".json",
|
||||||
|
"text/xml": ".xml",
|
||||||
|
"application/x-sh": ".sh",
|
||||||
|
"application/x-csh": ".csh",
|
||||||
|
"text/x-python": ".py",
|
||||||
|
"text/x-java-source": ".java",
|
||||||
|
"application/java-archive": ".jar",
|
||||||
|
"application/vnd.microsoft.portable-executable": ".exe",
|
||||||
|
"application/x-msdownload": ".msi",
|
||||||
|
"text/x-c": ".c",
|
||||||
|
"text/x-c++": ".cpp",
|
||||||
|
"text/x-go": ".go",
|
||||||
|
"application/x-perl": ".pl",
|
||||||
|
"text/x-php": ".php",
|
||||||
|
"text/x-ruby": ".rb",
|
||||||
|
"text/x-sql": ".sql",
|
||||||
|
"application/x-httpd-php": ".php",
|
||||||
|
"application/x-python-code": ".pyc",
|
||||||
|
|
||||||
|
// ROM Files
|
||||||
|
"application/x-nintendo-nes-rom": ".nes",
|
||||||
|
"application/x-snes-rom": ".smc",
|
||||||
|
"application/x-gameboy-rom": ".gb",
|
||||||
|
"application/x-gameboy-advance-rom": ".gba",
|
||||||
|
"application/x-n64-rom": ".n64",
|
||||||
|
"application/x-sega-genesis-rom": ".gen",
|
||||||
|
"application/x-sega-master-system-rom": ".sms",
|
||||||
|
"application/x-psx-rom": ".iso", // PlayStation ROMs
|
||||||
|
"application/x-bios-rom": ".rom",
|
||||||
|
"application/x-flash-rom": ".bin",
|
||||||
|
"application/x-eeprom": ".eep",
|
||||||
|
"application/x-c64-rom": ".prg",
|
||||||
|
|
||||||
|
// Miscellaneous
|
||||||
|
"application/octet-stream": ".bin", // General binary files
|
||||||
|
"application/x-shockwave-flash": ".swf",
|
||||||
|
"application/x-silverlight-app": ".xap",
|
||||||
|
"application/x-ms-shortcut": ".lnk",
|
||||||
|
"application/vnd.ms-fontobject": ".eot",
|
||||||
|
"font/woff": ".woff",
|
||||||
|
"font/woff2": ".woff2",
|
||||||
|
"font/ttf": ".ttf",
|
||||||
|
"font/otf": ".otf",
|
||||||
|
"application/vnd.visio": ".vsd",
|
||||||
|
"application/vnd.ms-project": ".mpp",
|
||||||
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user