From f75efc8cf61bce90ce71678fe94b3ebf79afbcf8 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Mon, 9 Sep 2024 20:36:39 +0300 Subject: [PATCH] version 2 - beta --- index.html | 2 +- package-lock.json | 3026 ++++++++++++++- package.json | 36 +- public/content-script.js | 5 +- public/favicon.ico | Bin 0 -> 15406 bytes public/manifest.json | 4 +- public/msg-not1.wav | Bin 0 -> 27712 bytes src/App-styles.ts | 9 + src/App.tsx | 1310 +++++-- src/assets/svgs/ArrowDown.svg | 3 + src/assets/svgs/Attachment.svg | 3 + src/assets/svgs/Check.svg | 3 + src/assets/svgs/ComposeIcon copy.svg | 9 + src/assets/svgs/ComposeIcon.svg | 9 + src/assets/svgs/CreateThreadIcon.tsx | 20 + src/assets/svgs/ModalClose.svg | 3 + src/assets/svgs/More.svg | 3 + src/assets/svgs/SendNewMessage.tsx | 19 + src/assets/svgs/Sort.svg | 4 + src/assets/svgs/interfaces.ts | 6 + src/background.ts | 3272 +++++++++++++++-- src/backgroundFunctions/encryption.ts | 208 ++ src/common/CustomLoader.tsx | 7 + src/common/LazyLoad.tsx | 47 + src/common/customloader.css | 64 + src/common/useModal.tsx | 64 + .../Chat/AnnouncementDiscussion.tsx | 344 ++ src/components/Chat/AnnouncementItem.tsx | 167 + src/components/Chat/AnnouncementList.tsx | 96 + src/components/Chat/ChatContainer.tsx | 56 + src/components/Chat/ChatDirect.tsx | 305 ++ src/components/Chat/ChatGroup.tsx | 377 ++ src/components/Chat/ChatList.tsx | 144 + src/components/Chat/CreateCommonSecret.tsx | 79 + src/components/Chat/CustomImage.ts | 59 + src/components/Chat/GroupAnnouncements.tsx | 607 +++ src/components/Chat/GroupForum.tsx | 52 + src/components/Chat/MessageDisplay.tsx | 66 + src/components/Chat/MessageItem.tsx | 93 + src/components/Chat/ResizableImage.tsx | 63 + src/components/Chat/TipTap.tsx | 292 ++ src/components/Chat/styles.css | 121 + src/components/Group/AddGroup.tsx | 474 +++ src/components/Group/AddGroupList.tsx | 272 ++ src/components/Group/Forum/DisplayHtml.tsx | 45 + src/components/Group/Forum/GroupMail.tsx | 777 ++++ src/components/Group/Forum/Mail-styles.ts | 799 ++++ src/components/Group/Forum/NewThread.tsx | 554 +++ src/components/Group/Forum/ReadOnlySlate.tsx | 129 + src/components/Group/Forum/ReusableModal.tsx | 57 + .../Group/Forum/ShowMessageWithoutModal.tsx | 224 ++ src/components/Group/Forum/TextEditor.tsx | 39 + src/components/Group/Forum/Thread copy.tsx | 329 ++ src/components/Group/Forum/Thread.tsx | 663 ++++ src/components/Group/Forum/texteditor.css | 71 + src/components/Group/Group.tsx | 1943 ++++++++++ src/components/Group/GroupInvites.tsx | 114 + src/components/Group/GroupJoinRequests.tsx | 170 + src/components/Group/InviteMember.tsx | 108 + src/components/Group/ListOfBans.tsx | 188 + src/components/Group/ListOfInvites.tsx | 189 + src/components/Group/ListOfJoinRequests.tsx | 189 + src/components/Group/ListOfMembers.tsx | 385 ++ .../Group/ListOfThreadPostsWatched.tsx | 138 + src/components/Group/ManageMembers.tsx | 316 ++ src/components/Group/ThingsToDoInitial.tsx | 166 + src/components/Group/UserListOfInvites.tsx | 206 ++ src/components/Group/WebsocketActive.tsx | 109 + src/components/Snackbar/LoadingSnackbar.tsx | 21 + src/components/Snackbar/Snackbar.tsx | 38 + src/components/TaskManager/TaskManger.tsx | 175 + src/constants/codes.ts | 1 + src/constants/forum.ts | 0 src/index.css | 28 + src/main.tsx | 45 + src/qdn/encryption/group-encryption.ts | 266 ++ src/qdn/publish/pubish.ts | 267 ++ src/transactions/AddGroupAdminTransaction.ts | 37 + src/transactions/CancelGroupBanTransaction.ts | 37 + .../CancelGroupInviteTransaction.ts | 36 + src/transactions/ChatBase.ts | 3 + src/transactions/CreateGroupTransaction.ts | 64 + src/transactions/GroupBanTransaction.ts | 50 + src/transactions/GroupChatTransaction.ts | 72 + src/transactions/GroupInviteTransaction.ts | 42 + src/transactions/GroupKickTransaction.ts | 46 + src/transactions/JoinGroupTransaction.ts | 38 + src/transactions/LeaveGroupTransaction.ts | 35 + src/transactions/RegisterNameTransaction.ts | 42 + .../RemoveGroupAdminTransaction.ts | 38 + src/transactions/transactions.ts | 29 +- src/utils/Size/index.ts | 11 + src/utils/decryptChatMessage.ts | 13 +- src/utils/events.ts | 11 + src/utils/generateWallet/generateWallet.ts | 2 +- src/utils/helpers.ts | 34 + src/utils/qortalLink/index.ts | 12 + src/utils/queue/queue.ts | 56 + src/utils/time.ts | 38 + 99 files changed, 20541 insertions(+), 757 deletions(-) create mode 100644 public/favicon.ico create mode 100644 public/msg-not1.wav create mode 100644 src/assets/svgs/ArrowDown.svg create mode 100644 src/assets/svgs/Attachment.svg create mode 100644 src/assets/svgs/Check.svg create mode 100644 src/assets/svgs/ComposeIcon copy.svg create mode 100644 src/assets/svgs/ComposeIcon.svg create mode 100644 src/assets/svgs/CreateThreadIcon.tsx create mode 100644 src/assets/svgs/ModalClose.svg create mode 100644 src/assets/svgs/More.svg create mode 100644 src/assets/svgs/SendNewMessage.tsx create mode 100644 src/assets/svgs/Sort.svg create mode 100644 src/assets/svgs/interfaces.ts create mode 100644 src/backgroundFunctions/encryption.ts create mode 100644 src/common/CustomLoader.tsx create mode 100644 src/common/LazyLoad.tsx create mode 100644 src/common/customloader.css create mode 100644 src/common/useModal.tsx create mode 100644 src/components/Chat/AnnouncementDiscussion.tsx create mode 100644 src/components/Chat/AnnouncementItem.tsx create mode 100644 src/components/Chat/AnnouncementList.tsx create mode 100644 src/components/Chat/ChatContainer.tsx create mode 100644 src/components/Chat/ChatDirect.tsx create mode 100644 src/components/Chat/ChatGroup.tsx create mode 100644 src/components/Chat/ChatList.tsx create mode 100644 src/components/Chat/CreateCommonSecret.tsx create mode 100644 src/components/Chat/CustomImage.ts create mode 100644 src/components/Chat/GroupAnnouncements.tsx create mode 100644 src/components/Chat/GroupForum.tsx create mode 100644 src/components/Chat/MessageDisplay.tsx create mode 100644 src/components/Chat/MessageItem.tsx create mode 100644 src/components/Chat/ResizableImage.tsx create mode 100644 src/components/Chat/TipTap.tsx create mode 100644 src/components/Chat/styles.css create mode 100644 src/components/Group/AddGroup.tsx create mode 100644 src/components/Group/AddGroupList.tsx create mode 100644 src/components/Group/Forum/DisplayHtml.tsx create mode 100644 src/components/Group/Forum/GroupMail.tsx create mode 100644 src/components/Group/Forum/Mail-styles.ts create mode 100644 src/components/Group/Forum/NewThread.tsx create mode 100644 src/components/Group/Forum/ReadOnlySlate.tsx create mode 100644 src/components/Group/Forum/ReusableModal.tsx create mode 100644 src/components/Group/Forum/ShowMessageWithoutModal.tsx create mode 100644 src/components/Group/Forum/TextEditor.tsx create mode 100644 src/components/Group/Forum/Thread copy.tsx create mode 100644 src/components/Group/Forum/Thread.tsx create mode 100644 src/components/Group/Forum/texteditor.css create mode 100644 src/components/Group/Group.tsx create mode 100644 src/components/Group/GroupInvites.tsx create mode 100644 src/components/Group/GroupJoinRequests.tsx create mode 100644 src/components/Group/InviteMember.tsx create mode 100644 src/components/Group/ListOfBans.tsx create mode 100644 src/components/Group/ListOfInvites.tsx create mode 100644 src/components/Group/ListOfJoinRequests.tsx create mode 100644 src/components/Group/ListOfMembers.tsx create mode 100644 src/components/Group/ListOfThreadPostsWatched.tsx create mode 100644 src/components/Group/ManageMembers.tsx create mode 100644 src/components/Group/ThingsToDoInitial.tsx create mode 100644 src/components/Group/UserListOfInvites.tsx create mode 100644 src/components/Group/WebsocketActive.tsx create mode 100644 src/components/Snackbar/LoadingSnackbar.tsx create mode 100644 src/components/Snackbar/Snackbar.tsx create mode 100644 src/components/TaskManager/TaskManger.tsx create mode 100644 src/constants/codes.ts create mode 100644 src/constants/forum.ts create mode 100644 src/qdn/encryption/group-encryption.ts create mode 100644 src/qdn/publish/pubish.ts create mode 100644 src/transactions/AddGroupAdminTransaction.ts create mode 100644 src/transactions/CancelGroupBanTransaction.ts create mode 100644 src/transactions/CancelGroupInviteTransaction.ts create mode 100644 src/transactions/CreateGroupTransaction.ts create mode 100644 src/transactions/GroupBanTransaction.ts create mode 100644 src/transactions/GroupChatTransaction.ts create mode 100644 src/transactions/GroupInviteTransaction.ts create mode 100644 src/transactions/GroupKickTransaction.ts create mode 100644 src/transactions/JoinGroupTransaction.ts create mode 100644 src/transactions/LeaveGroupTransaction.ts create mode 100644 src/transactions/RegisterNameTransaction.ts create mode 100644 src/transactions/RemoveGroupAdminTransaction.ts create mode 100644 src/utils/Size/index.ts create mode 100644 src/utils/events.ts create mode 100644 src/utils/helpers.ts create mode 100644 src/utils/qortalLink/index.ts create mode 100644 src/utils/queue/queue.ts create mode 100644 src/utils/time.ts diff --git a/index.html b/index.html index e24c53d..2dab011 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + Qortal Extension diff --git a/package-lock.json b/package-lock.json index 5639f96..9d8df9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,36 +8,66 @@ "name": "ext-one", "version": "0.0.0", "dependencies": { + "@chatscope/chat-ui-kit-react": "^2.0.3", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.16.4", - "@mui/material": "^5.15.14", + "@mui/lab": "^5.0.0-alpha.173", + "@mui/material": "^5.16.7", + "@reduxjs/toolkit": "^2.2.7", "@testing-library/jest-dom": "^6.4.6", "@testing-library/user-event": "^14.5.2", + "@tiptap/extension-color": "^2.5.9", + "@tiptap/extension-image": "^2.6.6", + "@tiptap/extension-placeholder": "^2.6.2", + "@tiptap/extension-text-style": "^2.5.9", + "@tiptap/pm": "^2.5.9", + "@tiptap/react": "^2.5.9", + "@tiptap/starter-kit": "^2.5.9", "@types/chrome": "^0.0.263", "asmcrypto.js": "2.3.2", "bcryptjs": "2.4.3", "buffer": "6.0.3", + "compressorjs": "^1.2.1", + "dompurify": "^3.1.6", "file-saver": "^2.0.5", "jssha": "3.3.1", + "lodash": "^4.17.21", + "mime": "^4.0.4", + "moment": "^2.30.1", + "quill-image-resize-module-react": "^3.0.0", "react": "^18.2.0", "react-copy-to-clipboard": "^5.1.0", "react-countdown-circle-timer": "^3.2.1", "react-dom": "^18.2.0", - "react-dropzone": "^14.2.3" + "react-dropzone": "^14.2.3", + "react-infinite-scroller": "^1.2.6", + "react-intersection-observer": "^9.13.0", + "react-quill": "^2.0.0", + "react-redux": "^9.1.2", + "react-virtualized": "^9.22.5", + "short-unique-id": "^5.2.0", + "slate": "^0.103.0", + "slate-react": "^0.109.0", + "tiptap-extension-resize-image": "^1.1.8" }, "devDependencies": { "@testing-library/dom": "^10.3.0", "@testing-library/react": "^16.0.0", + "@types/dompurify": "^3.0.5", + "@types/lodash": "^4.17.7", "@types/react": "^18.2.64", "@types/react-copy-to-clipboard": "^5.0.7", "@types/react-dom": "^18.2.21", + "@types/react-infinite-scroller": "^1.2.5", + "@types/react-virtualized": "^9.21.30", "@typescript-eslint/eslint-plugin": "^7.1.1", "@typescript-eslint/parser": "^7.1.1", "@vitejs/plugin-react": "^4.2.1", "eslint": "^8.57.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", + "rename-cli": "^6.2.1", "typescript": "^5.2.2", "vite": "^5.1.6", "vitest": "^1.6.0" @@ -422,6 +452,30 @@ "node": ">=6.9.0" } }, + "node_modules/@chatscope/chat-ui-kit-react": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@chatscope/chat-ui-kit-react/-/chat-ui-kit-react-2.0.3.tgz", + "integrity": "sha512-0IkjFskRec7SHrFivOQPiZMie5GLQL+ZnROiIbj4yptbC3aMEMFdHRAZrfqlid3uQx9kYhdtn34wMLh1vVNMLA==", + "dependencies": { + "@chatscope/chat-ui-kit-styles": "^1.2.0", + "@fortawesome/fontawesome-free": "^5.12.1", + "@fortawesome/fontawesome-svg-core": "^1.2.26", + "@fortawesome/free-solid-svg-icons": "^5.12.0", + "@fortawesome/react-fontawesome": "^0.1.8", + "classnames": "^2.2.6", + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "prop-types": "^15.7.2", + "react": "^16.12.0 || ^17.0.0 || ^18.2.0", + "react-dom": "^16.12.0 || ^17.0.0 || ^18.2.0" + } + }, + "node_modules/@chatscope/chat-ui-kit-styles": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@chatscope/chat-ui-kit-styles/-/chat-ui-kit-styles-1.4.0.tgz", + "integrity": "sha512-016mBJD3DESw7Nh+lkKcPd22xG92ghA0VpIXIbjQtmXhC7Ve6wRazTy8z1Ahut+Tbv179+JxrftuMngsj/yV8Q==" + }, "node_modules/@emotion/babel-plugin": { "version": "11.11.0", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", @@ -1066,6 +1120,60 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "0.2.36", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz", + "integrity": "sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "5.15.4", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz", + "integrity": "sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "1.2.36", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.36.tgz", + "integrity": "sha512-YUcsLQKYb6DmaJjIHdDWpBIGCcyE/W+p/LMGvjQem55Mm2XWVAP5kWTMKWLv9lwpCVjpLxPyOMOyUocP1GxrtA==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "^0.2.36" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "5.15.4", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.4.tgz", + "integrity": "sha512-JLmQfz6tdtwxoihXLg6lT78BorrFyCf59SAwBM6qV/0zXyVeDygJVb3fk+j5Qat+Yvcxp1buLTY5iDh1ZSAQ8w==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "^0.2.36" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/react-fontawesome": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.19.tgz", + "integrity": "sha512-Hyb+lB8T18cvLNX0S3llz7PcSOAJMLwiVKBuuzwM/nI5uoBw+gQjnf9il0fR1C3DKOI5Kc79pkJ4/xB0Uw9aFQ==", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~1 || ~6", + "react": ">=16.x" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -1182,6 +1290,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@juggle/resize-observer": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", + "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==" + }, "node_modules/@mui/base": { "version": "5.0.0-beta.40", "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", @@ -1214,9 +1327,9 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "5.15.14", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.14.tgz", - "integrity": "sha512-on75VMd0XqZfaQW+9pGjSNiqW+ghc5E2ZSLRBXwcXl/C4YzjfyjrLPhrEpKnR9Uym9KXBvxrhoHfPcczYHweyA==", + "version": "5.16.7", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.7.tgz", + "integrity": "sha512-RtsCt4Geed2/v74sbihWzzRs+HsIQCfclHeORh5Ynu2fS4icIKozcSubwuG7vtzq2uW3fOR1zITSP84TNt2GoQ==", "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" @@ -1247,22 +1360,62 @@ } } }, - "node_modules/@mui/material": { - "version": "5.15.14", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.14.tgz", - "integrity": "sha512-kEbRw6fASdQ1SQ7LVdWR5OlWV3y7Y54ZxkLzd6LV5tmz+NpO3MJKZXSfgR0LHMP7meKsPiMm4AuzV0pXDpk/BQ==", + "node_modules/@mui/lab": { + "version": "5.0.0-alpha.173", + "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.173.tgz", + "integrity": "sha512-Gt5zopIWwxDgGy/MXcp6GueD84xFFugFai4hYiXY0zowJpTVnIrTQCQXV004Q7rejJ7aaCntX9hpPJqCrioshA==", "dependencies": { "@babel/runtime": "^7.23.9", "@mui/base": "5.0.0-beta.40", - "@mui/core-downloads-tracker": "^5.15.14", - "@mui/system": "^5.15.14", - "@mui/types": "^7.2.14", - "@mui/utils": "^5.15.14", + "@mui/system": "^5.16.5", + "@mui/types": "^7.2.15", + "@mui/utils": "^5.16.5", + "clsx": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material": ">=5.15.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "5.16.7", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.7.tgz", + "integrity": "sha512-cwwVQxBhK60OIOqZOVLFt55t01zmarKJiJUWbk0+8s/Ix5IaUzAShqlJchxsIQ4mSrWqgcKCCXKtIlG5H+/Jmg==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/core-downloads-tracker": "^5.16.7", + "@mui/system": "^5.16.7", + "@mui/types": "^7.2.15", + "@mui/utils": "^5.16.6", + "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.10", "clsx": "^2.1.0", "csstype": "^3.1.3", "prop-types": "^15.8.1", - "react-is": "^18.2.0", + "react-is": "^18.3.1", "react-transition-group": "^4.4.5" }, "engines": { @@ -1292,17 +1445,17 @@ } }, "node_modules/@mui/material/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, "node_modules/@mui/private-theming": { - "version": "5.15.14", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.14.tgz", - "integrity": "sha512-UH0EiZckOWcxiXLX3Jbb0K7rC8mxTr9L9l6QhOZxYc4r8FHUkefltV9VDGLrzCaWh30SQiJvAEd7djX3XXY6Xw==", + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.6.tgz", + "integrity": "sha512-rAk+Rh8Clg7Cd7shZhyt2HGTTE5wYKNSJ5sspf28Fqm/PZ69Er9o6KX25g03/FG2dfpg5GCwZh/xOojiTfm3hw==", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/utils": "^5.15.14", + "@mui/utils": "^5.16.6", "prop-types": "^15.8.1" }, "engines": { @@ -1323,9 +1476,9 @@ } }, "node_modules/@mui/styled-engine": { - "version": "5.15.14", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.14.tgz", - "integrity": "sha512-RILkuVD8gY6PvjZjqnWhz8fu68dVkqhM5+jYWfB5yhlSQKg+2rHkmEwm75XIeAqI3qwOndK6zELK5H6Zxn4NHw==", + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.6.tgz", + "integrity": "sha512-zaThmS67ZmtHSWToTiHslbI8jwrmITcN93LQaR2lKArbvS7Z3iLkwRoiikNWutx9MBs8Q6okKvbZq1RQYB3v7g==", "dependencies": { "@babel/runtime": "^7.23.9", "@emotion/cache": "^11.11.0", @@ -1354,15 +1507,15 @@ } }, "node_modules/@mui/system": { - "version": "5.15.14", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.14.tgz", - "integrity": "sha512-auXLXzUaCSSOLqJXmsAaq7P96VPRXg2Rrz6OHNV7lr+kB8lobUF+/N84Vd9C4G/wvCXYPs5TYuuGBRhcGbiBGg==", + "version": "5.16.7", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.7.tgz", + "integrity": "sha512-Jncvs/r/d/itkxh7O7opOunTqbbSSzMTHzZkNLM+FjAOg+cYAZHrPDlYe1ZGKUYORwwb2XexlWnpZp0kZ4AHuA==", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/private-theming": "^5.15.14", - "@mui/styled-engine": "^5.15.14", - "@mui/types": "^7.2.14", - "@mui/utils": "^5.15.14", + "@mui/private-theming": "^5.16.6", + "@mui/styled-engine": "^5.16.6", + "@mui/types": "^7.2.15", + "@mui/utils": "^5.16.6", "clsx": "^2.1.0", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -1393,9 +1546,9 @@ } }, "node_modules/@mui/types": { - "version": "7.2.14", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.14.tgz", - "integrity": "sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ==", + "version": "7.2.15", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.15.tgz", + "integrity": "sha512-nbo7yPhtKJkdf9kcVOF8JZHPZTmqXjJ/tI0bdWgHg5tp9AnIN4Y7f7wm9T+0SyGYJk76+GYZ8Q5XaTYAsUHN0Q==", "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0" }, @@ -1406,14 +1559,16 @@ } }, "node_modules/@mui/utils": { - "version": "5.15.14", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.14.tgz", - "integrity": "sha512-0lF/7Hh/ezDv5X7Pry6enMsbYyGKjADzvHyo3Qrc/SSlTsQ1VkbDMbH0m2t3OR5iIVLwMoxwM7yGd+6FCMtTFA==", + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.6.tgz", + "integrity": "sha512-tWiQqlhxAt3KENNiSRL+DIn9H5xNVK6Jjf70x3PnfQPz1MPBdh7yyIcAyVBT9xiw7hP3SomRhPR7hzBMBCjqEA==", "dependencies": { "@babel/runtime": "^7.23.9", - "@types/prop-types": "^15.7.11", + "@mui/types": "^7.2.15", + "@types/prop-types": "^15.7.12", + "clsx": "^2.1.1", "prop-types": "^15.8.1", - "react-is": "^18.2.0" + "react-is": "^18.3.1" }, "engines": { "node": ">=12.0.0" @@ -1433,9 +1588,9 @@ } }, "node_modules/@mui/utils/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -1481,6 +1636,34 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.7.tgz", + "integrity": "sha512-faI3cZbSdFb8yv9dhDTmGwclW0vk0z5o1cia+kf7gCbaCwHI5e+7tP57mJUv22pNcNbeA62GSrPpfrUfdXcQ6g==", + "dependencies": { + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@remirror/core-constants": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-2.0.2.tgz", + "integrity": "sha512-dyHY+sMF0ihPus3O27ODd4+agdHMEmuRdyiZJ2CCWjPV5UFmn17ZbElvk6WOGVE4rdCJKZQCrPV2BcikOMLUGQ==" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.13.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz", @@ -1937,6 +2120,401 @@ "@testing-library/dom": ">=7.21.4" } }, + "node_modules/@tiptap/core": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.6.6.tgz", + "integrity": "sha512-VO5qTsjt6rwworkuo0s5AqYMfDA0ZwiTiH6FHKFSu2G/6sS7HKcc/LjPq+5Legzps4QYdBDl3W28wGsGuS1GdQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "^2.6.6" + } + }, + "node_modules/@tiptap/extension-blockquote": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.5.9.tgz", + "integrity": "sha512-LhGyigmd/v1OjYPeoVK8UvFHbH6ffh175ZuNvseZY4PsBd7kZhrSUiuMG8xYdNX8FxamsxAzr2YpsYnOzu3W7A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.5.9" + } + }, + "node_modules/@tiptap/extension-bold": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.5.9.tgz", + "integrity": "sha512-XUJdzFb31t0+bwiRquJf0btBpqOB3axQNHTKM9XADuL4S+Z6OBPj0I5rYINeElw/Q7muvdWrHWHh/ovNJA1/5A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.5.9" + } + }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.5.9.tgz", + "integrity": "sha512-NddZ8Qn5dgPPa1W4yk0jdhF4tDBh0FwzBpbnDu2Xz/0TUHrA36ugB2CvR5xS1we4zUKckgpVqOqgdelrmqqFVg==", + "dependencies": { + "tippy.js": "^6.3.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.5.9", + "@tiptap/pm": "^2.5.9" + } + }, + "node_modules/@tiptap/extension-bullet-list": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.5.9.tgz", + "integrity": "sha512-hJTv1x4omFgaID4LMRT5tOZb/VKmi8Kc6jsf4JNq4Grxd2sANmr9qpmKtBZvviK+XD5PpTXHvL+1c8C1SQtuHQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.5.9" + } + }, + "node_modules/@tiptap/extension-code": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.5.9.tgz", + "integrity": "sha512-Q1PL3DUXiEe5eYUwOug1haRjSaB0doAKwx7KFVI+kSGbDwCV6BdkKAeYf3us/O2pMP9D0im8RWX4dbSnatgwBA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.5.9" + } + }, + "node_modules/@tiptap/extension-code-block": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.5.9.tgz", + "integrity": "sha512-+MUwp0VFFv2aFiZ/qN6q10vfIc6VhLoFFpfuETX10eIRks0Xuj2nGiqCDj7ca0/M44bRg2VvW8+tg/ZEHFNl9g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.5.9", + "@tiptap/pm": "^2.5.9" + } + }, + "node_modules/@tiptap/extension-color": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/@tiptap/extension-color/-/extension-color-2.5.9.tgz", + "integrity": "sha512-VUGCT9iqD/Ni9arLIxkCbykAElRMFyew7uk2kbbNvttzdwzmZkbslEgCiaEZQTqKr8w4wjuQL14YOtXc6iwEww==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.5.9", + "@tiptap/extension-text-style": "^2.5.9" + } + }, + "node_modules/@tiptap/extension-document": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.5.9.tgz", + "integrity": "sha512-VdNZYDyCzC3W430UdeRXR9IZzPeODSbi5Xz/JEdV93THVp8AC9CrZR7/qjqdBTgbTB54VP8Yr6bKfCoIAF0BeQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.5.9" + } + }, + "node_modules/@tiptap/extension-dropcursor": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.5.9.tgz", + "integrity": "sha512-nEOb37UryG6bsU9JAs/HojE6Jg43LupNTAMISbnuB1CPAeAqNsFMwORd9eEPkyEwnQT7MkhsMOSJM44GoPGIFA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.5.9", + "@tiptap/pm": "^2.5.9" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.5.9.tgz", + "integrity": "sha512-MWJIQQT6e5MgqHny8neeH2Dx926nVPF7sv4p84nX4E0dnkRbEYUP8mCsWYhSUvxxIif6e+yY+4654f2Q9qTx1w==", + "dependencies": { + "tippy.js": "^6.3.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.5.9", + "@tiptap/pm": "^2.5.9" + } + }, + "node_modules/@tiptap/extension-gapcursor": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.5.9.tgz", + "integrity": "sha512-yW7V2ebezsa7mWEDWCg4A1ZGsmSV5bEHKse9wzHCDkb7TutSVhLZxGo72U6hNN9PnAksv+FJQk03NuZNYvNyRQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.5.9", + "@tiptap/pm": "^2.5.9" + } + }, + "node_modules/@tiptap/extension-hard-break": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.5.9.tgz", + "integrity": "sha512-8hQ63SgZRG4BqHOeSfeaowG2eMr2beced018pOGbpHbE3XSYoISkMVuFz4Z8UEVR3W9dTbKo4wxNufSTducocQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.5.9" + } + }, + "node_modules/@tiptap/extension-heading": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.5.9.tgz", + "integrity": "sha512-HHowAlGUbFn1qvmY02ydM7qiPPMTGhAJn2A46enDRjNHW5UoqeMfkMpTEYaioOexyguRFSfDT3gpK68IHkQORQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.5.9" + } + }, + "node_modules/@tiptap/extension-history": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.5.9.tgz", + "integrity": "sha512-hGPtJgoZSwnVVqi/xipC2ET/9X2G2UI/Y+M3IYV1ZlM0tCYsv4spNi3uXlZqnXRwYcBXLk5u6e/dmsy5QFbL8g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.5.9", + "@tiptap/pm": "^2.5.9" + } + }, + "node_modules/@tiptap/extension-horizontal-rule": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.5.9.tgz", + "integrity": "sha512-/ES5NdxCndBmZAgIXSpCJH8YzENcpxR0S8w34coSWyv+iW0Sq7rW/mksQw8ZIVsj8a7ntpoY5OoRFpSlqcvyGw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.5.9", + "@tiptap/pm": "^2.5.9" + } + }, + "node_modules/@tiptap/extension-image": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-2.6.6.tgz", + "integrity": "sha512-dwJKvoqsr72B4tcTH8hXhfBJzUMs/jXUEE9MnfzYnSXf+CYALLjF8r/IkGYbxce62GP/bMDoj8BgpF8saeHtqA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.6.6" + } + }, + "node_modules/@tiptap/extension-italic": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.5.9.tgz", + "integrity": "sha512-Bw+P139L4cy+B56zpUiRjP8BZSaAUl3JFMnr/FO+FG55QhCxFMXIc6XrC3vslNy5ef3B3zv4gCttS3ee8ByMiw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.5.9" + } + }, + "node_modules/@tiptap/extension-list-item": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.5.9.tgz", + "integrity": "sha512-d9Eo+vBz74SMxP0r25aqiErV256C+lGz+VWMjOoqJa6xWLM1keYy12JtGQWJi8UDVZrDskJKCHq81A0uLt27WA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.5.9" + } + }, + "node_modules/@tiptap/extension-ordered-list": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.5.9.tgz", + "integrity": "sha512-9MsWpvVvzILuEOd/GdroF7RI7uDuE1M6at9rzsaVGvCPVHZBvu1XR3MSVK5OdiJbbJuPGttlzEFLaN/rQdCGFg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.5.9" + } + }, + "node_modules/@tiptap/extension-paragraph": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.5.9.tgz", + "integrity": "sha512-HDXGiHTJ/V85dbDMjcFj4XfqyTQZqry6V21ucMzgBZYX60X3gIn7VpQTQnnRjvULSgtfOASSJP6BELc5TyiK0w==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.5.9" + } + }, + "node_modules/@tiptap/extension-placeholder": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-2.6.2.tgz", + "integrity": "sha512-Aou6lH456j5mpry36jyAdZzINxFx6fjqvmapmmORJKV+9J889P7RN7laRRsosWHez0Oxg4KuWL3FuDexx6ZJOQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.6.2", + "@tiptap/pm": "^2.6.2" + } + }, + "node_modules/@tiptap/extension-strike": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.5.9.tgz", + "integrity": "sha512-QezkOZpczpl09S8lp5JL7sRkwREoPY16Y/lTvBcFKm3TZbVzYZZ/KwS0zpwK9HXTfXr8os4L9AGjQf0tHonX+w==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.5.9" + } + }, + "node_modules/@tiptap/extension-text": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.5.9.tgz", + "integrity": "sha512-W0pfiQUPsMkwaV5Y/wKW4cFsyXAIkyOFt7uN5u6LrZ/iW9KZ/IsDODPJDikWp0aeQnXzT9NNQULTpCjbHzzS6g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.5.9" + } + }, + "node_modules/@tiptap/extension-text-style": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-2.5.9.tgz", + "integrity": "sha512-1pNnl/a5EdY7g3IeFomm0B6eiTvAFOBeJGswoYxogzHmkWbLFhXFdgZ6qz7+k985w4qscsG1GpvtOW3IrJ9J6g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.5.9" + } + }, + "node_modules/@tiptap/pm": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.6.6.tgz", + "integrity": "sha512-56FGLPn3fwwUlIbLs+BO21bYfyqP9fKyZQbQyY0zWwA/AG2kOwoXaRn7FOVbjP6CylyWpFJnpRRmgn694QKHEg==", + "dependencies": { + "prosemirror-changeset": "^2.2.1", + "prosemirror-collab": "^1.3.1", + "prosemirror-commands": "^1.5.2", + "prosemirror-dropcursor": "^1.8.1", + "prosemirror-gapcursor": "^1.3.2", + "prosemirror-history": "^1.4.1", + "prosemirror-inputrules": "^1.4.0", + "prosemirror-keymap": "^1.2.2", + "prosemirror-markdown": "^1.13.0", + "prosemirror-menu": "^1.2.4", + "prosemirror-model": "^1.22.2", + "prosemirror-schema-basic": "^1.2.3", + "prosemirror-schema-list": "^1.4.1", + "prosemirror-state": "^1.4.3", + "prosemirror-tables": "^1.4.0", + "prosemirror-trailing-node": "^2.0.9", + "prosemirror-transform": "^1.9.0", + "prosemirror-view": "^1.33.9" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/react": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-2.5.9.tgz", + "integrity": "sha512-NZYAslIb79oxIOFHx9T9ey5oX0aJ1uRbtT2vvrvvyRaO6fKWgAwMYN92bOu5/f2oUVGUp6l7wkYZGdjz/XP5bA==", + "dependencies": { + "@tiptap/extension-bubble-menu": "^2.5.9", + "@tiptap/extension-floating-menu": "^2.5.9", + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.5.9", + "@tiptap/pm": "^2.5.9", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + } + }, + "node_modules/@tiptap/starter-kit": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.5.9.tgz", + "integrity": "sha512-nZ4V+vRayomjxUsajFMHv1iJ5SiSaEA65LAXze/CzyZXGMXfL2OLzY7wJoaVJ4BgwINuO0dOSAtpNDN6jI+6mQ==", + "dependencies": { + "@tiptap/core": "^2.5.9", + "@tiptap/extension-blockquote": "^2.5.9", + "@tiptap/extension-bold": "^2.5.9", + "@tiptap/extension-bullet-list": "^2.5.9", + "@tiptap/extension-code": "^2.5.9", + "@tiptap/extension-code-block": "^2.5.9", + "@tiptap/extension-document": "^2.5.9", + "@tiptap/extension-dropcursor": "^2.5.9", + "@tiptap/extension-gapcursor": "^2.5.9", + "@tiptap/extension-hard-break": "^2.5.9", + "@tiptap/extension-heading": "^2.5.9", + "@tiptap/extension-history": "^2.5.9", + "@tiptap/extension-horizontal-rule": "^2.5.9", + "@tiptap/extension-italic": "^2.5.9", + "@tiptap/extension-list-item": "^2.5.9", + "@tiptap/extension-ordered-list": "^2.5.9", + "@tiptap/extension-paragraph": "^2.5.9", + "@tiptap/extension-strike": "^2.5.9", + "@tiptap/extension-text": "^2.5.9" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -1993,6 +2571,15 @@ "@types/har-format": "*" } }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dev": true, + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -2023,15 +2610,29 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", + "dev": true + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" }, "node_modules/@types/prop-types": { - "version": "15.7.11", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", - "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" + }, + "node_modules/@types/quill": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz", + "integrity": "sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==", + "dependencies": { + "parchment": "^1.1.2" + } }, "node_modules/@types/react": { "version": "18.2.67", @@ -2061,6 +2662,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-infinite-scroller": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/react-infinite-scroller/-/react-infinite-scroller-1.2.5.tgz", + "integrity": "sha512-fJU1jhMgoL6NJFrqTM0Ob7tnd2sQWGxe2ESwiU6FZWbJK/VO/Er5+AOhc+e2zbT0dk5pLygqctsulOLJ8xnSzw==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.10", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", @@ -2069,6 +2679,16 @@ "@types/react": "*" } }, + "node_modules/@types/react-virtualized": { + "version": "9.21.30", + "resolved": "https://registry.npmjs.org/@types/react-virtualized/-/react-virtualized-9.21.30.tgz", + "integrity": "sha512-4l2TFLQ8BCjNDQlvH85tU6gctuZoEdgYzENQyZHpgTHU7hoLzYgPSOALMAeA58LOWua8AzC6wBivPj1lfl6JgQ==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/react": "*" + } + }, "node_modules/@types/scheduler": { "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", @@ -2080,6 +2700,17 @@ "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.3.1.tgz", @@ -2463,6 +3094,33 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2482,11 +3140,30 @@ "node": ">=4" } }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/aria-query": { "version": "5.3.0", @@ -2552,6 +3229,16 @@ "npm": ">=6" } }, + "node_modules/bail": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", + "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2582,6 +3269,23 @@ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" }, + "node_modules/blessed": { + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/blessed/-/blessed-0.1.81.tgz", + "integrity": "sha512-LoF5gae+hlmfORcG1M5+5XZi4LBmvlXTzwJWzUlPryN/SJdSflZvROM2TwkT0GMpq7oqT48NRd4GS7BiVBc5OQ==", + "dev": true, + "bin": { + "blessed": "bin/tput.js" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/blueimp-canvas-to-blob": { + "version": "3.29.0", + "resolved": "https://registry.npmjs.org/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.29.0.tgz", + "integrity": "sha512-0pcSSGxC0QxT+yVkivxIqW0Y4VlO2XSDPofBAqoJ1qJxgH9eiUDLv50Rixij2cDuEfx4M6DpD9UGZpRhT5Q8qg==" + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -2668,6 +3372,24 @@ "node": ">=8" } }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2676,6 +3398,15 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001599", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001599.tgz", @@ -2696,6 +3427,16 @@ } ] }, + "node_modules/ccount": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.1.0.tgz", + "integrity": "sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chai": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", @@ -2728,6 +3469,52 @@ "node": ">=4" } }, + "node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-1.1.4.tgz", + "integrity": "sha512-HRcDxZuZqMx3/a+qrzxdBKBPUpxWEq9xw2OPZ3a/174ihfrQKVsFhqtthBInFy1zZ9GgZyFXOatNujm8M+El3g==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, "node_modules/check-error": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", @@ -2741,14 +3528,216 @@ "node": "*" } }, - "node_modules/clsx": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", - "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, + "node_modules/cli-clear": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cli-clear/-/cli-clear-1.0.4.tgz", + "integrity": "sha512-/WdwW67j4CO5PHQtd7+qisNNxHxbAcuq3s9T64pz3hiToFPHRpkv+3YdwOPKQlk8twroU76feyHmJHjJ0K2p6w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/clipboardy": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-2.3.0.tgz", + "integrity": "sha512-mKhiIL2DrQIsuXMgBgnfEHOZOryC7kY7YO//TN6c63wlEm3NG5tz+YgY5rVi29KCmq/QQjKYvM7a19+MDOTHOQ==", + "dev": true, + "dependencies": { + "arch": "^2.1.1", + "execa": "^1.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/clipboardy/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/clipboardy/node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, "engines": { "node": ">=6" } }, + "node_modules/clipboardy/node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clipboardy/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clipboardy/node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/clipboardy/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/clipboardy/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/clipboardy/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clipboardy/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clipboardy/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/clipboardy/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/collapse-white-space": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz", + "integrity": "sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -2777,6 +3766,20 @@ "node": ">= 0.8" } }, + "node_modules/compressorjs": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/compressorjs/-/compressorjs-1.2.1.tgz", + "integrity": "sha512-+geIjeRnPhQ+LLvvA7wxBQE5ddeLU7pJ3FsKFWirDw6veY3s9iLxAQEw7lXGHnhCJvBujEQWuNnGzZcvCvdkLQ==", + "dependencies": { + "blueimp-canvas-to-blob": "^3.29.0", + "is-blob": "^2.1.0" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz", + "integrity": "sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2819,6 +3822,11 @@ "node": ">=10" } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2884,6 +3892,15 @@ "node": ">=18" } }, + "node_modules/dateformat": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz", + "integrity": "sha512-GODcnWq3YGoTnygPfi02ygEiRxqUxpJwuRHjdhJYuxpcZmDq4rjBiXYmbCCzStxo176ixfLT6i4NPwQooRySnw==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2901,6 +3918,15 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decimal.js": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", @@ -2923,12 +3949,63 @@ "node": ">=6" } }, + "node_modules/deep-equal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", + "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", + "dependencies": { + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.5.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2972,6 +4049,18 @@ "node": ">=8" } }, + "node_modules/direction": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/direction/-/direction-1.0.4.tgz", + "integrity": "sha512-GYqKi1aH7PJXxdhTeZBFrg8vUBeKXi+cNprXsC1kpJcbcVnV9wBsrOu1cQEdG0WeQwlfHiy3XvnKfIrJ2R0NzQ==", + "bin": { + "direction": "cli.js" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -2999,20 +4088,37 @@ "csstype": "^3.0.2" } }, + "node_modules/dompurify": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz", + "integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==" + }, "node_modules/electron-to-chromium": { "version": "1.4.710", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.710.tgz", "integrity": "sha512-w+9yAVHoHhysCa+gln7AzbO9CdjFcL/wN/5dd+XW/Msl2d/4+WisEaCF1nty0xbAKaxdaJfgLB2296U7zZB7BA==", "dev": true }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, "license": "BSD-2-Clause", - "optional": true, - "peer": true, "engines": { "node": ">=0.12" }, @@ -3028,6 +4134,25 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.19.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", @@ -3375,6 +4500,11 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", + "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==" + }, "node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -3399,12 +4529,48 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/external-editor/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-diff": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", + "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==" + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -3454,6 +4620,21 @@ "reusify": "^1.0.4" } }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -3552,6 +4733,29 @@ "node": ">= 6" } }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-extra/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3580,6 +4784,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3589,6 +4801,15 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -3599,6 +4820,24 @@ "node": "*" } }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-stream": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", @@ -3695,6 +4934,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -3709,6 +4965,53 @@ "node": ">=4" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -3828,6 +5131,15 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -3877,11 +5189,170 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/inquirer": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/inquirer/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/inquirer/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/inquirer/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumeric": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-alphanumeric/-/is-alphanumeric-1.0.0.tgz", + "integrity": "sha512-ZmRL7++ZkcMOfDuWZuMJyIVLr2keE1o/DeNWh1EmgqGhUcV+9BIVsx0BcSBOHTZqzjs4+dISzr2KAeBEWGgXeA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "dev": true, + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, + "node_modules/is-blob": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-blob/-/is-blob-2.1.0.tgz", + "integrity": "sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, "node_modules/is-core-module": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", @@ -3893,6 +5364,45 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3902,6 +5412,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -3914,6 +5433,21 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-hotkey": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/is-hotkey/-/is-hotkey-0.2.0.tgz", + "integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==" + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -3932,6 +5466,23 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -3941,6 +5492,21 @@ "optional": true, "peer": true }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", @@ -3954,12 +5520,50 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-whitespace-character": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz", + "integrity": "sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-word-character": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.4.tgz", + "integrity": "sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "devOptional": true }, + "node_modules/jpeg-exif": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz", + "integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==", + "dev": true + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4067,6 +5671,15 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jssha": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.3.1.tgz", @@ -4102,6 +5715,14 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/local-pkg": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", @@ -4137,8 +5758,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -4146,6 +5766,16 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/longest-streak": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.4.tgz", + "integrity": "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -4195,6 +5825,56 @@ "@jridgewell/sourcemap-codec": "^1.4.15" } }, + "node_modules/markdown-escapes": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz", + "integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-table": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-1.1.3.tgz", + "integrity": "sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q==", + "dev": true + }, + "node_modules/mdast-util-compact": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mdast-util-compact/-/mdast-util-compact-1.0.4.tgz", + "integrity": "sha512-3YDMQHI5vRiS2uygEFYaqckibpJtKq5Sj2c8JioeOQBU6INpKbdWzfyLqFFnDwEcEnRFIdMsguzs5pC1Jp4Isg==", + "dev": true, + "dependencies": { + "unist-util-visit": "^1.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==" + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -4224,6 +5904,20 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.4.tgz", + "integrity": "sha512-v8yqInVjhXyqP6+Kw4fV3ZzeMRqEW6FotRsKXjRS5VMTNIuXsdRoAvklpoRgSqXm6o9VNH4/C0mgedko9DdLsQ==", + "funding": [ + "https://github.com/sponsors/broofa" + ], + "bin": { + "mime": "bin/cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -4301,12 +5995,32 @@ "ufo": "^1.5.3" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "devOptional": true }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "node_modules/named-js-regexp": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/named-js-regexp/-/named-js-regexp-1.3.5.tgz", + "integrity": "sha512-XO0DPujDP9IWpkt690iWLreKztb/VB811DGl5N3z7BfhkMJuiVZXOi6YN/fEB9qkvtMVTgSZDW8pzdVt8vj/FA==", + "dev": true + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -4331,6 +6045,12 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, "node_modules/node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", @@ -4366,6 +6086,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg==", + "dev": true + }, "node_modules/nwsapi": { "version": "2.2.10", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.10.tgz", @@ -4383,6 +6109,29 @@ "node": ">=0.10.0" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4408,6 +6157,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/opn": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", + "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", + "dev": true, + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/opn/node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -4425,6 +6195,29 @@ "node": ">= 0.8.0" } }, + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==" + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -4455,6 +6248,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parchment": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz", + "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4466,6 +6273,20 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-1.2.2.tgz", + "integrity": "sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg==", + "dev": true, + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + } + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -4656,6 +6477,36 @@ "devOptional": true, "license": "MIT" }, + "node_modules/prompt-sync": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/prompt-sync/-/prompt-sync-4.2.0.tgz", + "integrity": "sha512-BuEzzc5zptP5LsgV5MZETjDaKSWfchl5U9Luiu8SKp7iZWD5tZalOxvNcZRwv+d2phNFr8xlbxmFNcRKfJOzJw==", + "dev": true, + "dependencies": { + "strip-ansi": "^5.0.0" + } + }, + "node_modules/prompt-sync/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/prompt-sync/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -4666,6 +6517,193 @@ "react-is": "^16.13.1" } }, + "node_modules/prosemirror-changeset": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.2.1.tgz", + "integrity": "sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ==", + "dependencies": { + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-collab": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz", + "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==", + "dependencies": { + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-commands": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.6.0.tgz", + "integrity": "sha512-xn1U/g36OqXn2tn5nGmvnnimAj/g1pUx2ypJJIe8WkVX83WyJVC5LTARaxZa2AtQRwntu9Jc5zXs9gL9svp/mg==", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-dropcursor": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.1.tgz", + "integrity": "sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw==", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "node_modules/prosemirror-gapcursor": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.2.tgz", + "integrity": "sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==", + "dependencies": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "node_modules/prosemirror-history": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.4.1.tgz", + "integrity": "sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==", + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-inputrules": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.4.0.tgz", + "integrity": "sha512-6ygpPRuTJ2lcOXs9JkefieMst63wVJBgHZGl5QOytN7oSZs3Co/BYbc3Yx9zm9H37Bxw8kVzCnDsihsVsL4yEg==", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-keymap": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.2.tgz", + "integrity": "sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==", + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "node_modules/prosemirror-markdown": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.0.tgz", + "integrity": "sha512-UziddX3ZYSYibgx8042hfGKmukq5Aljp2qoBiJRejD/8MH70siQNz5RB1TrdTPheqLMy4aCe4GYNF10/3lQS5g==", + "dependencies": { + "markdown-it": "^14.0.0", + "prosemirror-model": "^1.20.0" + } + }, + "node_modules/prosemirror-menu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.4.tgz", + "integrity": "sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA==", + "dependencies": { + "crelt": "^1.0.0", + "prosemirror-commands": "^1.0.0", + "prosemirror-history": "^1.0.0", + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-model": { + "version": "1.22.3", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.22.3.tgz", + "integrity": "sha512-V4XCysitErI+i0rKFILGt/xClnFJaohe/wrrlT2NSZ+zk8ggQfDH4x2wNK7Gm0Hp4CIoWizvXFP7L9KMaCuI0Q==", + "dependencies": { + "orderedmap": "^2.0.0" + } + }, + "node_modules/prosemirror-schema-basic": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.3.tgz", + "integrity": "sha512-h+H0OQwZVqMon1PNn0AG9cTfx513zgIG2DY00eJ00Yvgb3UD+GQ/VlWW5rcaxacpCGT1Yx8nuhwXk4+QbXUfJA==", + "dependencies": { + "prosemirror-model": "^1.19.0" + } + }, + "node_modules/prosemirror-schema-list": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.4.1.tgz", + "integrity": "sha512-jbDyaP/6AFfDfu70VzySsD75Om2t3sXTOdl5+31Wlxlg62td1haUpty/ybajSfJ1pkGadlOfwQq9kgW5IMo1Rg==", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.7.3" + } + }, + "node_modules/prosemirror-state": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.3.tgz", + "integrity": "sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-tables": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.4.0.tgz", + "integrity": "sha512-fxryZZkQG12fSCNuZDrYx6Xvo2rLYZTbKLRd8rglOPgNJGMKIS8uvTt6gGC38m7UCu/ENnXIP9pEz5uDaPc+cA==", + "dependencies": { + "prosemirror-keymap": "^1.1.2", + "prosemirror-model": "^1.8.1", + "prosemirror-state": "^1.3.1", + "prosemirror-transform": "^1.2.1", + "prosemirror-view": "^1.13.3" + } + }, + "node_modules/prosemirror-trailing-node": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-2.0.9.tgz", + "integrity": "sha512-YvyIn3/UaLFlFKrlJB6cObvUhmwFNZVhy1Q8OpW/avoTbD/Y7H5EcjK4AZFKhmuS6/N6WkGgt7gWtBWDnmFvHg==", + "dependencies": { + "@remirror/core-constants": "^2.0.2", + "escape-string-regexp": "^4.0.0" + }, + "peerDependencies": { + "prosemirror-model": "^1.22.1", + "prosemirror-state": "^1.4.2", + "prosemirror-view": "^1.33.8" + } + }, + "node_modules/prosemirror-trailing-node/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prosemirror-transform": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.9.0.tgz", + "integrity": "sha512-5UXkr1LIRx3jmpXXNKDhv8OyAOeLTGuXNwdVfg8x27uASna/wQkr9p6fD3eupGOi4PLJfbezxTyi/7fSJypXHg==", + "dependencies": { + "prosemirror-model": "^1.21.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.33.9", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.33.9.tgz", + "integrity": "sha512-xV1A0Vz9cIcEnwmMhKKFAOkfIp8XmJRnaZoPqNXrPS7EK5n11Ov8V76KhR0RsfQd/SIzmWY+bg+M44A2Lx/Nnw==", + "dependencies": { + "prosemirror-model": "^1.20.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -4675,6 +6713,16 @@ "optional": true, "peer": true }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4684,6 +6732,14 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "engines": { + "node": ">=6" + } + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -4713,6 +6769,47 @@ } ] }, + "node_modules/quill": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz", + "integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==", + "dependencies": { + "clone": "^2.1.1", + "deep-equal": "^1.0.1", + "eventemitter3": "^2.0.3", + "extend": "^3.0.2", + "parchment": "^1.1.4", + "quill-delta": "^3.6.2" + } + }, + "node_modules/quill-delta": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz", + "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==", + "dependencies": { + "deep-equal": "^1.0.1", + "extend": "^3.0.2", + "fast-diff": "1.1.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/quill-image-resize-module-react": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quill-image-resize-module-react/-/quill-image-resize-module-react-3.0.0.tgz", + "integrity": "sha512-3jVChLoXh+fwEELx3OswOEEuF+1KU3r/B9RAqZ//s+d+UMduVZzUepU1g/XoxjKoBJvWD2lJwBIFBRUNb8ebCw==", + "dependencies": { + "lodash": "^4.17.4", + "quill": "^1.2.2", + "raw-loader": "^0.5.1" + } + }, + "node_modules/raw-loader": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-0.5.1.tgz", + "integrity": "sha512-sf7oGoLuaYAScB4VGr0tzetsYlS8EJH6qnTCfQ/WVEa89hALQ4RQfCKt5xCyPQKPDUbVUAIP1QsxAwfAjlDp7Q==" + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -4772,11 +6869,82 @@ "react": ">= 16.8 || 18.0.0" } }, + "node_modules/react-infinite-scroller": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/react-infinite-scroller/-/react-infinite-scroller-1.2.6.tgz", + "integrity": "sha512-mGdMyOD00YArJ1S1F3TVU9y4fGSfVVl6p5gh/Vt4u99CJOptfVu/q5V/Wlle72TMgYlBwIhbxK5wF0C/R33PXQ==", + "dependencies": { + "prop-types": "^15.5.8" + }, + "peerDependencies": { + "react": "^0.14.9 || ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-intersection-observer": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.13.0.tgz", + "integrity": "sha512-y0UvBfjDiXqC8h0EWccyaj4dVBWMxgEx0t5RGNzQsvkfvZwugnKwxpu70StY4ivzYuMajavwUDjH4LJyIki9Lw==", + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "node_modules/react-quill": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0.tgz", + "integrity": "sha512-4qQtv1FtCfLgoD3PXAur5RyxuUbPXQGOHgTlFie3jtxp43mXDtzCKaOgQ3mLyZfi1PUlyjycfivKelFhy13QUg==", + "dependencies": { + "@types/quill": "^1.3.10", + "lodash": "^4.17.4", + "quill": "^1.3.7" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "react-dom": "^16 || ^17 || ^18" + } + }, + "node_modules/react-redux": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", + "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==", + "dependencies": { + "@types/use-sync-external-store": "^0.0.3", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25", + "react": "^18.0", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-redux/node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, "node_modules/react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -4801,6 +6969,31 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-virtualized": { + "version": "9.22.5", + "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.22.5.tgz", + "integrity": "sha512-YqQMRzlVANBv1L/7r63OHa2b0ZsAaDp1UhVNEdUaXI8A5u6hTpA5NYtUueLH2rFuY/27mTGIBl7ZhqFKzw18YQ==", + "dependencies": { + "@babel/runtime": "^7.7.2", + "clsx": "^1.0.4", + "dom-helpers": "^5.1.3", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0", + "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-virtualized/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -4814,11 +7007,167 @@ "node": ">=8" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "dependencies": { + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/remark": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/remark/-/remark-8.0.0.tgz", + "integrity": "sha512-K0PTsaZvJlXTl9DN6qYlvjTkqSZBFELhROZMrblm2rB+085flN84nz4g/BscKRMqDvhzlK1oQ/xnWQumdeNZYw==", + "dev": true, + "dependencies": { + "remark-parse": "^4.0.0", + "remark-stringify": "^4.0.0", + "unified": "^6.0.0" + } + }, + "node_modules/remark-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-4.0.0.tgz", + "integrity": "sha512-XZgICP2gJ1MHU7+vQaRM+VA9HEL3X253uwUM/BGgx3iv6TH2B3bF3B8q00DKcyP9YrJV+/7WOWEWBFF/u8cIsw==", + "dev": true, + "dependencies": { + "collapse-white-space": "^1.0.2", + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-whitespace-character": "^1.0.0", + "is-word-character": "^1.0.0", + "markdown-escapes": "^1.0.0", + "parse-entities": "^1.0.2", + "repeat-string": "^1.5.4", + "state-toggle": "^1.0.0", + "trim": "0.0.1", + "trim-trailing-lines": "^1.0.0", + "unherit": "^1.0.4", + "unist-util-remove-position": "^1.0.0", + "vfile-location": "^2.0.0", + "xtend": "^4.0.1" + } + }, + "node_modules/remark-stringify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-4.0.0.tgz", + "integrity": "sha512-xLuyKTnuQer3ke9hkU38SUYLiTmS078QOnoFavztmbt/pAJtNSkNtFgR0U//uCcmG0qnyxao+PDuatQav46F1w==", + "dev": true, + "dependencies": { + "ccount": "^1.0.0", + "is-alphanumeric": "^1.0.0", + "is-decimal": "^1.0.0", + "is-whitespace-character": "^1.0.0", + "longest-streak": "^2.0.1", + "markdown-escapes": "^1.0.0", + "markdown-table": "^1.1.0", + "mdast-util-compact": "^1.0.0", + "parse-entities": "^1.0.2", + "repeat-string": "^1.5.4", + "state-toggle": "^1.0.0", + "stringify-entities": "^1.0.1", + "unherit": "^1.0.4", + "xtend": "^4.0.1" + } + }, + "node_modules/rename-cli": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/rename-cli/-/rename-cli-6.2.1.tgz", + "integrity": "sha512-9UV1/tEEEtMKH54T6GCjo+VGXBbSMqTCzbXeB3AXJrK5JImxEpSRrUnKNGu+ByGwCqnXqOPirkiRApedLQC8Lg==", + "dev": true, + "dependencies": { + "blessed": "^0.1.81", + "chalk": "^2.3.2", + "cli-clear": "^1.0.4", + "clipboardy": "^2.2.0", + "dateformat": "^2.2.0", + "fs-extra": "^8.1.0", + "globby": "^11.0.0", + "inquirer": "^7.1.0", + "jpeg-exif": "^1.1.4", + "named-js-regexp": "^1.3.3", + "num2fraction": "^1.2.2", + "opn": "^5.3.0", + "path-exists": "^3.0.0", + "prompt-sync": "^4.1.6", + "remark": "^8.0.0", + "yargs": "^15.3.0" + }, + "bin": { + "rename": "bin.js", + "rname": "bin.js" + } + }, + "node_modules/rename-cli/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha512-vuNYXC7gG7IeVNBC1xUllqCcZKRbJoSPOBhnTEcAIiKCsbuef6zO3F0Rve3isPMMoNoQRWjQwbAgAjHUHniyEA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -4828,6 +7177,11 @@ "optional": true, "peer": true }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -4852,6 +7206,49 @@ "node": ">=4" } }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -4909,6 +7306,11 @@ "fsevents": "~2.3.2" } }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==" + }, "node_modules/rrweb-cssom": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", @@ -4918,6 +7320,15 @@ "optional": true, "peer": true }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4941,14 +7352,30 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/rxjs/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, - "license": "MIT", - "optional": true, - "peer": true + "license": "MIT" }, "node_modules/saxes": { "version": "6.0.0", @@ -4973,6 +7400,14 @@ "loose-envify": "^1.1.0" } }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, "node_modules/semver": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", @@ -5006,6 +7441,42 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5027,6 +7498,15 @@ "node": ">=8" } }, + "node_modules/short-unique-id": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/short-unique-id/-/short-unique-id-5.2.0.tgz", + "integrity": "sha512-cMGfwNyfDZ/nzJ2k2M+ClthBIh//GlZl1JEf47Uoa9XR11bz8Pa2T2wQO4bVrRdH48LrIDWJahQziKo3MjhsWg==", + "bin": { + "short-unique-id": "bin/short-unique-id", + "suid": "bin/short-unique-id" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -5056,6 +7536,35 @@ "node": ">=8" } }, + "node_modules/slate": { + "version": "0.103.0", + "resolved": "https://registry.npmjs.org/slate/-/slate-0.103.0.tgz", + "integrity": "sha512-eCUOVqUpADYMZ59O37QQvUdnFG+8rin0OGQAXNHvHbQeVJ67Bu0spQbcy621vtf8GQUXTEQBlk6OP9atwwob4w==", + "dependencies": { + "immer": "^10.0.3", + "is-plain-object": "^5.0.0", + "tiny-warning": "^1.0.3" + } + }, + "node_modules/slate-react": { + "version": "0.109.0", + "resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.109.0.tgz", + "integrity": "sha512-tzSJFqwzAvy4PmIPobuKp7PX2Q/R/jwG0DU7AJTnMLVQpGpzS0yacsDcFeGRaGAQpFZYlUteFkKiBm9MKgDEyg==", + "dependencies": { + "@juggle/resize-observer": "^3.4.0", + "direction": "^1.0.4", + "is-hotkey": "^0.2.0", + "is-plain-object": "^5.0.0", + "lodash": "^4.17.21", + "scroll-into-view-if-needed": "^3.1.0", + "tiny-invariant": "1.3.1" + }, + "peerDependencies": { + "react": ">=18.2.0", + "react-dom": ">=18.2.0", + "slate": ">=0.99.0" + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -5080,6 +7589,16 @@ "devOptional": true, "license": "MIT" }, + "node_modules/state-toggle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz", + "integrity": "sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/std-env": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", @@ -5087,6 +7606,32 @@ "devOptional": true, "license": "MIT" }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stringify-entities": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-1.3.2.tgz", + "integrity": "sha512-nrBAQClJAPN2p+uGCVJRPIPakKeKWZ9GtBCmormE7pWOSlHat7+x5A8gx85M7HM5Dt0BP3pP5RhVW77WdbJJ3A==", + "dev": true, + "dependencies": { + "character-entities-html4": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-hexadecimal": "^1.0.0" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -5099,6 +7644,15 @@ "node": ">=8" } }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-final-newline": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", @@ -5198,6 +7752,22 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/tiny-invariant": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", + "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "node_modules/tinybench": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz", @@ -5225,6 +7795,36 @@ "node": ">=14.0.0" } }, + "node_modules/tippy.js": { + "version": "6.3.7", + "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz", + "integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==", + "dependencies": { + "@popperjs/core": "^2.9.0" + } + }, + "node_modules/tiptap-extension-resize-image": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/tiptap-extension-resize-image/-/tiptap-extension-resize-image-1.1.8.tgz", + "integrity": "sha512-dRPCfkCCUPtlVCn7w9HHE01ANJE6pRQMPAYXmsd1Qlk8KUasePdvCQIVJMqKriAqvKYJtnRc3HfojmzZtkQq0w==", + "peerDependencies": { + "@tiptap/core": "^2.0.0", + "@tiptap/extension-image": "^2.0.0", + "@tiptap/pm": "^2.0.0" + } + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -5283,6 +7883,33 @@ "node": ">=18" } }, + "node_modules/trim": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", + "integrity": "sha512-YzQV+TZg4AxpKxaTHK3c3D+kRDCGVEE7LemdlQZoQXn0iennk10RsIoY6ikzAqJTc9Xjl9C1/waHom/J86ziAQ==", + "deprecated": "Use String.prototype.trim() instead", + "dev": true + }, + "node_modules/trim-trailing-lines": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.4.tgz", + "integrity": "sha512-rjUWSqnfTNrjbB9NQWfPMH/xRK1deHeGsHoVfpxJ++XeYXE0d6B1En37AHfw3jtfTU7dzMzZL2jjpe8Qb5gLIQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", + "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -5347,6 +7974,11 @@ "node": ">=14.17" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==" + }, "node_modules/ufo": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz", @@ -5354,6 +7986,77 @@ "devOptional": true, "license": "MIT" }, + "node_modules/unherit": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz", + "integrity": "sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.0", + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/unified": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/unified/-/unified-6.2.0.tgz", + "integrity": "sha512-1k+KPhlVtqmG99RaTbAv/usu85fcSRu3wY8X+vnsEhIxNP5VbVIDiXnLqyKIG+UMdyTg0ZX9EI6k2AfjJkHPtA==", + "dev": true, + "dependencies": { + "bail": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^1.1.0", + "trough": "^1.0.0", + "vfile": "^2.0.0", + "x-is-string": "^0.1.0" + } + }, + "node_modules/unist-util-is": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz", + "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==", + "dev": true + }, + "node_modules/unist-util-remove-position": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-1.1.4.tgz", + "integrity": "sha512-tLqd653ArxJIPnKII6LMZwH+mb5q+n/GtXQZo6S6csPRs5zB0u79Yw8ouR3wTw8wxvdJFhpP6Y7jorWdCgLO0A==", + "dev": true, + "dependencies": { + "unist-util-visit": "^1.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz", + "integrity": "sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ==", + "dev": true + }, + "node_modules/unist-util-visit": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.1.tgz", + "integrity": "sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==", + "dev": true, + "dependencies": { + "unist-util-visit-parents": "^2.0.0" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz", + "integrity": "sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==", + "dev": true, + "dependencies": { + "unist-util-is": "^3.0.0" + } + }, "node_modules/universalify": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", @@ -5418,6 +8121,45 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/vfile": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-2.3.0.tgz", + "integrity": "sha512-ASt4mBUHcTpMKD/l5Q+WJXNtshlWxOogYyGYYrg4lt/vuRjC1EFQtlAofL5VmtVNIZJzWYFJjzGWZ0Gw8pzW1w==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.4", + "replace-ext": "1.0.0", + "unist-util-stringify-position": "^1.0.0", + "vfile-message": "^1.0.0" + } + }, + "node_modules/vfile-location": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-2.0.6.tgz", + "integrity": "sha512-sSFdyCP3G6Ka0CEmN83A2YCMKIieHx0EDaj5IDP4g1pa5ZJ4FJDvpO0WODLxo4LUX4oe52gmSCK7Jw4SBghqxA==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-1.1.1.tgz", + "integrity": "sha512-1WmsopSGhWt5laNir+633LszXvZ+Z/lxveBf6yhGsqnQIhlhzooZae7zV6YVM1Sdkw68dtAW3ow0pOdPANugvA==", + "dev": true, + "dependencies": { + "unist-util-stringify-position": "^1.1.1" + } + }, "node_modules/vite": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.6.tgz", @@ -5562,6 +8304,11 @@ } } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", @@ -5647,6 +8394,12 @@ "node": ">= 8" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true + }, "node_modules/why-is-node-running": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", @@ -5664,6 +8417,53 @@ "node": ">=8" } }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -5694,6 +8494,12 @@ } } }, + "node_modules/x-is-string": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz", + "integrity": "sha512-GojqklwG8gpzOVEVki5KudKNoq7MbbjYZCbyWzEz7tyPA7eleiE0+ePwOWQQRb5fm86rD3S8Tc0tSFf3AOv50w==", + "dev": true + }, "node_modules/xml-name-validator": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", @@ -5715,6 +8521,21 @@ "optional": true, "peer": true }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -5729,6 +8550,93 @@ "node": ">= 6" } }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 72083a2..9ca8095 100644 --- a/package.json +++ b/package.json @@ -5,43 +5,73 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc && vite build", + "build": "vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", "test": "vitest", "coverage": "vitest run --coverage" }, "dependencies": { + "@chatscope/chat-ui-kit-react": "^2.0.3", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.16.4", - "@mui/material": "^5.15.14", + "@mui/lab": "^5.0.0-alpha.173", + "@mui/material": "^5.16.7", + "@reduxjs/toolkit": "^2.2.7", "@testing-library/jest-dom": "^6.4.6", "@testing-library/user-event": "^14.5.2", + "@tiptap/extension-color": "^2.5.9", + "@tiptap/extension-image": "^2.6.6", + "@tiptap/extension-placeholder": "^2.6.2", + "@tiptap/extension-text-style": "^2.5.9", + "@tiptap/pm": "^2.5.9", + "@tiptap/react": "^2.5.9", + "@tiptap/starter-kit": "^2.5.9", "@types/chrome": "^0.0.263", "asmcrypto.js": "2.3.2", "bcryptjs": "2.4.3", "buffer": "6.0.3", + "compressorjs": "^1.2.1", + "dompurify": "^3.1.6", "file-saver": "^2.0.5", "jssha": "3.3.1", + "lodash": "^4.17.21", + "mime": "^4.0.4", + "moment": "^2.30.1", + "quill-image-resize-module-react": "^3.0.0", "react": "^18.2.0", "react-copy-to-clipboard": "^5.1.0", "react-countdown-circle-timer": "^3.2.1", "react-dom": "^18.2.0", - "react-dropzone": "^14.2.3" + "react-dropzone": "^14.2.3", + "react-infinite-scroller": "^1.2.6", + "react-intersection-observer": "^9.13.0", + "react-quill": "^2.0.0", + "react-redux": "^9.1.2", + "react-virtualized": "^9.22.5", + "short-unique-id": "^5.2.0", + "slate": "^0.103.0", + "slate-react": "^0.109.0", + "tiptap-extension-resize-image": "^1.1.8" }, "devDependencies": { "@testing-library/dom": "^10.3.0", "@testing-library/react": "^16.0.0", + "@types/dompurify": "^3.0.5", + "@types/lodash": "^4.17.7", "@types/react": "^18.2.64", "@types/react-copy-to-clipboard": "^5.0.7", "@types/react-dom": "^18.2.21", + "@types/react-infinite-scroller": "^1.2.5", + "@types/react-virtualized": "^9.21.30", "@typescript-eslint/eslint-plugin": "^7.1.1", "@typescript-eslint/parser": "^7.1.1", "@vitejs/plugin-react": "^4.2.1", "eslint": "^8.57.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", + "rename-cli": "^6.2.1", "typescript": "^5.2.2", "vite": "^5.1.6", "vitest": "^1.6.0" diff --git a/public/content-script.js b/public/content-script.js index 2fc77f7..fd2b902 100644 --- a/public/content-script.js +++ b/public/content-script.js @@ -1,3 +1,4 @@ + async function connection(hostname) { const isConnected = await chrome.storage.local.get([hostname]); let connected = false @@ -48,6 +49,7 @@ document.addEventListener('qortalExtensionRequests', async (event) => { } }); } else if (type === 'REQUEST_CONNECTION') { + console.log('REQUEST_CONNECTION') const hostname = window.location.hostname chrome.runtime.sendMessage({ action: "connection", payload: { hostname @@ -122,7 +124,7 @@ document.addEventListener('qortalExtensionRequests', async (event) => { } }); } else if(type === 'REQUEST_LTC_BALANCE'){ - + const hostname = window.location.hostname const res = await connection(hostname) @@ -137,6 +139,7 @@ document.addEventListener('qortalExtensionRequests', async (event) => { chrome.runtime.sendMessage({ action: "ltcBalance", payload: { hostname }, timeout }, (response) => { + if (response.error) { document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { detail: { type: "LTC_BALANCE", data: { diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..f5f7e84822bcbfa333adfb1babf4966bb4690ca2 GIT binary patch literal 15406 zcmeHOX>46b6&}ZK3JGZwpcF&_BMJ(%f(o$-NCl#zH9{aNH53IQegY*eMHHy|ClVnd zxUroW)0%9pT}YHbLYIg|ZPKuWv<)U&Ri>1qDs}`#R~6o^_?=MKB1HYfF{tNmZ*X6Z0%%9ovKv>i&hgzNtzzqWu)w>afH* zq$kTBldzCV6hUmR-I<~iH6Nw?IL zfCiI=jabci4d$+fTl7fxb6V%J(seR3pqYl-cj(S7??c_Qnr%pfeH>YlW=$#D4obh= zDup8hKO->h?>%QkFl;}AJZ9%WNwXTUqBdv<hk zA7x3I^6|@$bC93ETlL7X;QUEP$c1%X4Yzvck2*kI5DX%RdAe8hr$xH)4|R!UEzm8e z9dypDg?Rn_p^npEvkpjudThqx@F6BUv5r`c?wh{m&9&g@WRdIF&?xrJeWo1gb4c8u zX7cDAs$yR2lUId_w0twc%Qwe&%jHHh2hI^_0WboyldV^oc#+5dNtR^L1+d{3tBc zhmG!ak$P0!4&FgM$G+PA+mGpy&Rse;b-LVCIyQV<_wT!3x7J-}Vdc4JpM9-$i*(zsmjwBP3Tct1dyM3ns#4#x_j6BO zfIoPjZQi0gHoY6|EPJhEbGbfq*))mFyeTLe+**}gy z*ErXj|8cLsBE~Ox$`NM? z<)=@c#zy8@i2CUBw`(`f-|;-*gdE$BpbyV(b3XX^JLfOYVA=vn_DA0{UlaSo_~4-s zFgLVO*p@bnkEi(^us?{wwzp~Xns$MA+hcFRI{Pv9exLn?b!eRd4{5fEqk+1nD0>`! z%reYNg6$h(e>VL_cW%8IeDiUPFbt%9B*Olr3@*3*Z7|Rdw{782do*lpF=d?Z6lEJ{ z`;)i(nN9!8Z*RN##N}z1(>CT>MY^Q^ryG~CQ@LLvMf%@nKR5fDo5t0AxC9s{L5Nm; z8)>F-5|9Wyt5&IbC+pRM)@oILv{K#Cv`*cRwa|#UI*j*lOO4CtV;*I8T_fAF5Brh^ zX$2U}QNfOTRB3aadV6z?x)W=AHE``kV!WB>)CAI$2fNv~t?@9{%r+V6!@i(FTBJ$Z za|Bz+wyjl*z^?+Sf&8&%Mv*)=4(gK538$WYNy8pLW7-wuG23HkUZv(YRjLnzekJys z0lcS;Ic9uWn|396>1oEt{A0Iez5=xOLN9{0OcC;q!S2k7@TgMHV@{UObS^qKs2ec9 zk6_)U!8`Jvq##_yjdKz-ds6GvBgEjBX(7LxYSfLOu?F%uGaY=i)9Al5#n^0lxrdF< z;=P7g3c+mJaLmLGxt}T;JK~STeDa0w>hnE+k;J^jBJ%e*@wi=Mu`#SEbRMX4oVViT zUZPunwMh3p{S~p%r^bfhtKnV?iFw-XtYe!bdomz1%mei}X+3bC!8*Mc{kfkPD5Da& zFQHFy`qlM%^8ATVe~W%EW1+7f{2Ag=iMDuvx4i~e5Tnr(|6BG*uKjAo^%J1sFJnBR z>&aX6;6Hu}U+|#B3q$=EQz$;;LEZJl(nQ*RUidx5J&+jvxcU1?rCNwKm4)q*CGcmK z=-2-IEuBg4)7kM6k6&-#2HwCwHgE`I{*Z&8blU0`<9ED|BIB|4?@GTO@!ZCKdzXEF zIJAQ@fQ}coq=z@rVFke?b6$iSFNfA80iQO)LHxKMosXh1=G^+IR=B z^8AiCSAn~B0q!(DrH5Wwi~GfvM8BK&vf1%rNzBU=f`9PuKM}nnP0}V0@`{-cVr-zj zM7^QyN7>spNQmr9B+k(P;t%xLsaJ6?JDR8~yt^Gec>r;n?@MA{?sFsXOWYB4q)i^= zWyLlk<^r}CZ9ZcIV?3+HjtW_`sSR&H+-kGj=lIpZ|C@Ia6T`>!(4k68$7{+E=O~jr z%O()-W<0*@i4WRxD$$g`KW@tVBl09~U<)K0>uO`aL-u=77OFd~wQmxheypC~&=1Bb z$g^kHo%uMLi?^(sU0>7#FFuHSi<@HclQzdm-eLGrMyxLw-y>Rgl+Ylp@FypG56{{3 zp0Dfn`Wqo{^Q{EVh4t4+a&#GY64dRKt4)~v3&78D9dB8uz94>6Sd7e;U9|jjdi-p= z9z@)zV-su~2NUA#nD_3+kLcH5`ne?L!*wBCe+tBZ0b^JR+4EZl`b42~6@{PoO+zoQ zfjzL$>BoB~-gk1{W=`zKnzAzWdnSd6Jj+lRi^31v#(&4)8Asgt zf4bIC__{S~?0+7A!Wlb_JY_8w`gZ*<3&aVe_=Cyz3&oAWr z_dO6ezj*fX4CGnpe-i8;u3tI7X3KtX->32W!S1I%nztX2_k2?bJBT(1Z4cTYVS8=? z_{Dx;9C&u@x8=I6{%r~NTdA~3?^!N!74bVG&Wl9+@BvH=8~A6*e%_L3Kl2V;z7cWT z7k=fz?>PKo>jSfizeAS_@Lw*3aPb!Lem2^F`6c|9u0+3+6pQg+=z~ct--_XfoNKt( zgs$>_b?nsPnfZ_2;d*~L{^McrdPsO0+t8e)&fDcb(g&t5jJgE>ec?3x_Yv`L9RJ;m z*!jhI^ZM_!Pa}L^%lC5pcjA}$iS>OYWBVaO+ijj9-o)PgY;^)KI9Ak7CN2EXFo*#a@ zXT=X6MqKR`th)>l#@o;I$;Jj=M$GYH=^OI>UmmYl7bdY!J|7i7B!0$5fp3K!D~+P# zD`)(60lo?OIKD}Vi{IjZArRZ;cPnhmJ_X4C5fLSRz2(SbNnv|zY&|u z_&Mi*&y{?CVT}N5;R?P#GGpL8$X$$epHa}AfO~zPx# literal 0 HcmV?d00001 diff --git a/public/manifest.json b/public/manifest.json index 0d0bd1e..6f12e9d 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -14,7 +14,7 @@ }, "action": { }, - "permissions": [ "storage", "system.display", "activeTab", "tabs" + "permissions": [ "storage", "system.display", "activeTab", "tabs", "notifications", "alarms" ], "content_scripts": [ @@ -24,6 +24,6 @@ } ], "content_security_policy": { - "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; connect-src 'self' https://api.qortal.org https://api2.qortal.org https://appnode.qortal.org https://apinode.qortalnodes.live https://apinode1.qortalnodes.live https://apinode2.qortalnodes.live https://apinode3.qortalnodes.live https://apinode4.qortalnodes.live;" + "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; connect-src 'self' https://api.qortal.org https://api2.qortal.org https://appnode.qortal.org https://apinode.qortalnodes.live https://apinode1.qortalnodes.live https://apinode2.qortalnodes.live https://apinode3.qortalnodes.live https://apinode4.qortalnodes.live https://ext-node.qortal.link wss://appnode.qortal.org wss://ext-node.qortal.link ws://127.0.0.1:12391 http://127.0.0.1:12391 https://ext-node.qortal.link; " } } diff --git a/public/msg-not1.wav b/public/msg-not1.wav new file mode 100644 index 0000000000000000000000000000000000000000..475c2100cf3ab731bb890a9f83d583cbf33d3a02 GIT binary patch literal 27712 zcmZU*2V7Iv`#*lO2O(ka4GJotAc_d?RTS%1+={rW)=@_t-Hy6vZQZNIz4x9dLu4Gh$D2ifH zY-;#5in6-Rrnr<96+eIN{HkRXZ>XU^Xy;h9JX1&3?_vw$p7tD7GlNG<9kERQsv+c;)fRgwljk$rs5N zP9eAOT;7?yklfz6_jB&%bj$6YtISj7wH7K0dzXZje6B34e9-Wq;j!|uvRYrGm-Cu= z@2uWiN!=uFTl|0YZw_q=EgVodz~(EPuLe&TG@(4UGFCWOFt`7L{tH^-o8!;MUx=sY zY3Jp~7RSA*H#EO|KY#mf_Bq^4?xMD(Z9}RCR-Mi{pX2b}=6&&_@<-#Y zkG{V5!r=@06YVE%AANB2!{NlkGY(Hbyzq}De)!Okiips=9$$VHJuzZ&$_l1a-Zy`A-XmMmTtZ@1KO z>6c{%%Yv5oT<-Rb`!}fzKP(KK8!~sz)Rj|P#wfi1AS8qlp^i8;T`tj-M$EF+`c68*?U;p^&57D0% ze>NR&IxagWJ=gDY#N{D(2jA^@(eXn1QT8$7bHr!Adf)mLniZN{L7u?I(aP~wm%Ck- z^!pTb~t7gh)uAaYc{`>g1@l6X`7Usw2#Gjaddj6PM(X)6{_){9j zG>!=!(rbuQALl;x{&oJP&gIUdEut;f8P*xzHotA&Ub?;1BFiFcf8xQ!aZg4+`Q_G* zTbnMeyENi_^m+HwZl}FYx}O|=a@5IPr+1z(5gLI4Zl zl~I+(R(-38C1lxIIa+0SqclB`4s!fp~${yciqmq1szK}mUH8|{x&^rHhF&M`K3pBkAdNl;r)h0 z4S5m$D0nO=h(zC z@5fC3YSLFDh7BL~Eb2wn=dgmXydH%;D!fa)dpm|Z+DmLC52+{Avi7Czvm53#l$Dm1 zdgXcLO;4Mi_BioT;)<86ULJjN=*h^3qaU8Sf9(E|`+wdKec1EiH&0eR>Gsn1<+Q|^ ziOC<|f1H{do10UTUBas8)ZbEFR~@F0&_9ZP7Im}hY8UG~`MeLvKCq}9kbqu!1xf;MgB)RCixMGt#8@czIL{gV0}3po}tt=qJ2pb%QEUS>mR7@s&b5lMuR{rXtr*$Ugo;O zHQH~a-`K$Mf#Z9R@7*J!Tg0OQPX^Qut{!}5=-Ht~(9RAyJEVSK-N2&$CH;Tuv#F0h zmdq3*kwO>HL36T>b0|#^;aCN}70hv*m zQD^&~=^qpx623QdM`%`HL10kVu3hhXJoY&2aMHnE>LeY;8^&8?h&P;7ol^xj1~>Mp z>|I$=R9h66J1aLeJtKWg>Q|{PALJiW5>pau-qpSvmpC?2^}gf%=j4LqnzZUPDw~__ zQRq}imvYO_)E=+BD}N~Oq3Nc1L%pV^ieg2#t#4TW?R?Q$10s;*5zO_6PV+I}@`GgNV_xB-@3EdA`e z+h1}!@3znPknfdlm%5b)mj?@bi+Vfub?mDQSBCEg|85Oeh1>L%_N7C)z0-qJg71w zq{-6gWNos0l6TVX4?7X+Nr5RrDO1uWr~RGzPbQsb%o|ZWvUq&exGHs{w$Y|d*0$L2 zjUkhp&b70&x0KjR?0PA*q%F52XY3&a`hOm?E~weE|uTsfw3RAb+&epM;O z$;G|}eg&(tS7tv*f0*u;=8|?c^+M{j)JdsfANzkC`YH0$tjxH~-MKq*PkuiAd0W}m zGW%MG+TJZ8EtlJ`wZ|H#8ihOo?}+42Nv>^{?QxeAE-$=ZdHHws?Ru&E_3i_MBZ43I zdfuyhXxGr?p{qhqg!~n9x#!iM*8{Hw-VL}HkmsA@+sh-wqsFn$(b?MGS}tl69c3S6 z_ti(}J5(JiK{Ky;cg?<$t)*|RfeWU@1O8Sm2HrXS2Wkg+yv zUDo8>$+_JN0}8W>^NNR84z7%@A5)(uPm}-MalYfS@tW~I_b&H@#R-ePHhpd4oEA7S z?nd_tpK2eDKhw2e_sH&>12+Y}33?r*4blho3F;lRsK=5XR|BpD@Vaoi?Dsn2wcK@) zYl8hX`wg=1WDUYbVHmp)dy9UP{!3eN+wUzqTTa#;uj^YGQTbcRPbE7(@BX|he?@*& z?x0*=4l5@yJ0*Kh&W@a@yuo>m1q}sDzbyXppzKN6gX%lggBpi5j%Xdyx<`FLEv5zZ zHtr5?mAG1Lw9;AKaJcPI@NN;^;=0f6ezg0s?j_xdy7dYO z3%JihgVR&+W(Lr{(Sqjt#ce zF4cF+u9q3VFkhTMJAalGhzlC>n)7bvUC)clkISbD*@c^nHWa-pNiIpPcwe!hc1!K` zrprw+%CX9DO+U?7^hkO(cP=+p94nStSz5*0FR_nu33plJvEGC7F?c`od*gSj>*cNw z{Ga<*`j_~3^Y8AzyUV^VKECe0b)HS0TsMx}Hpi`wKiK?i<8J9}xl*u3aF%t3RiH1> zdv<79B(|ei-yrQbA%DLXLKBGCKxm;PHjMPME?in8%`*FfK=Y?m4pDeR1Z`<6kad-4|6uXLD zXL!VVXub4aHGlyNKMTLb&~&~!-z1-8pB%3&ubv*E9?~6h{^#xJ@r zxy1#=t4hBueN_Iq{BqUhs?fU7y8cc5o31OaDb}>FZC|Hbqub5wXRdQEa}z|DMQf#N zq_sA+HmwdShhJQNa#`uV%01CD(Q}^nJnv|qXrFIxYXV>Qa^qQ!NKB>r3edsZ^*vEiX1QT;*9C>iDUmaQ$T8rC#?t|_cZugs`i zf-$x9TIto2>m`dz;!6rkzLdzyq-BcomhvB~eyjSY_Dt>g#>tH<;>#Tf-pgmMX^PcRg~2LyZ&|`98(=rToPS^+(X?J9!(xPPow8_uUIdwr^d6v zqtTY48{bXfw%cW=%W|icPMP+f>~GuLwTY00%jR0dSKoMyU?exQ4* zyU}r}BU|}d87rSC4{Qo)+F8G~eroNE+LzVOs_#`jtctE0RkfvRbJd9IQPo#!F4z23 z_g9@oqoh&Z+|nH08s3`TmeF=elb|_o_{VUAx=P*SKIC2ioKBHUm!!&)Wo0&HHhb;& z+P`vq?YPu=vGX>U-(4(Rg|2EBjmt=vQ7$59fpag%P{(q+GCQe_%;t-tx9qL5Y2i4b#7)AHy9?hE@wloZ?8&G$!=5Wor>hG#ss#>Z9 z)uQUB)sL&WwY=K$y7Ib#jRP7TTO3;^w@z!VZL4T|sClM&Xt-l|OMRfyxT)Ny!so(` zk`0m;S&NKsE4005f7`y=vEFgF^A6{8E*D&!T%BDR7lTWb%U~CkQ;U-Xez#n;yJYv# zI^BAQbemKqZWFr*oCOdDI3h;O&^lTtQ%lv8RFhR@@=|$HQ%aM6gI|NXR$n`}W=74p z>PgiVRi#xH)za$h>Wu2(+R$3BdYAfbjk_Dyw5({k)q1yeV*7-453Q%x%jjYBWp!nB z=XK@rMIzB7$peX#m5Y^^t-I|5`^WYfj%kkboa3G6yUcP)c1d$N=5o{}!#T@&z0-Q9 zLkv{fR_9lqK=q_1X2FP2NoKL_{`wiDk=qfyKan9ni^oumfI>|cPZlv8rhp7$~jwOzzPL)ov&QqPE zoFkq0I~{VG={U{tr2TpOm9|T4ldRIL(kxRge-!^DrujzxM9w76Ipz#gtFPBj*Tia? z+M3$ZTQgee7JbW6_>p;`;dFyfeL#I!U2xs&+Qi!G+REB}b^Gg{*T1M=(iq>kt$A0o zR3TE-C>xbl9hMz)wez$^h9bi(Y6g|VDd6-M^c5@=FA>kOoMEYvX=Q_L2H9BI+1PEg zUuQqrVYD^-S?nalGYNOLkLM(=UzN8m~4aG>mMBZdl&1tbyOiYjkh& zYP#H<(0p8eQm$x~w;pUe(DuE0v-*hcpze(^(I{r|Sy9|U+y#O-!36OHvDnhWa=Gjq z*(U2P*3Pz$wqxwZ+Oh2^`$6`D?c41->>?p<(KfUV+B;e{N>(f>m2?*eh$9410vM#Y zEmSM@-1yjdME9reu==3-O{*_!?O1N!OoOu8ql2dj;v;Uw_S^Sg-rMc-Mh zwb*F6!7@zNM|Q~Spw&U^!`8tzJ#1#%%(n@$39#8|z0dlf)gG&!vM||p%U>;bTI{kI zCz>E?;WzMS!d&46>nY2Dabh+aHX1bA4((C(arH|04f=~}hpM8rr1gm6sN#(Ll$kbFUba1No7FM9BSaIT>80zb8*BL5@IAeOzCqoj zirHV-Dz1vF;J5N$Kn$HJo+-X>@z5e$k}f%6dD_xl>L!hrj+XjCyK8yJQZ8wcR9e(n z9EVjrYmvQZo?w>XXWlQo-4Ne@Vg1DVj#!jR{z>7fnO&e6@%UDw~zTN^EnKhVF> zDn`M?vEo_D?DuRBt~YlIZ!&KUtZl{#;sv3?Uc!&UG+~6Ozi72+m1qOBXlS*TL;X{Iks;q8 zp=I<)W(2d9`jxuNdc>+@SFn{FHAl^D=VtS=czgJJ`CbAa!A`+eL89QJpg@o*cp`Wy zSO;@rj({&%!C%V1#=Ffch@^&YkO$@waprZCSQ}QY1A}p{I!1C?b=;hYn`R;mhP@@Bg_xJ27E>t z2O1;k2zmfBkQqh|qoP@(ScBQa*&&=jj(}^){m4z^uHdcZwe#e#oH(6-l>Y~4?gsxX z{~Tz0HQy218Qxi*h$rMtokt7x53YjiTY8pj&O z7>?+V>NUD{-FnciGjLp>E!4iyzSO?gzSSzUN^O)bQg=yrS=UP+qR-do>h~EA7-k!1 z7(?hjv@>JRXc?NRq)Mo#te32V?0xL#+sEO^d551x!C z;Z?#K#Yyfd?hqKuk~qnnL7ah{^X$LbdcfyM)>o`U)B!4o$z?pDEu)vwAB^vfJ&Zxd zzkzn3p_d^?pP~O)@V#M*VTNHM;BdNOwqY}TzHYc_Xfr4cql}}Cw~g0XCP0d!bKe5vQCHvW5L2G5n zS%+AEumV}#Sg)wJFd|Q+YMFXwGxI$l%b9sZ->2i~g|riGM;E}4%uB|L#;wp+8dn*= zF)lH#H-2y2ZQNvrXSFcX$wZg#4`(-XV4re2kKjDC9ICUqGT*9 z))dxc)_&FjRwDRC9W()3z_x|v1kDnfk)>zl0ZQ(%Zm>49egiKY!s4<7tarfU7HTuq zmx`d|OcQg3xy~$SmN0=#FUCM?=_ERZzCvH3chEcOjr8~QxAY2nDZQLtPk%@MN^hf2 z(T`7EEY{wQ*~4}l}_ED?oxkJhd|cvsBzS2sv8wZ@u5{Q70f&419O>4 zVD>Txm`$MHMbKt2GnsMF#xgO?C?=Yj!b}8<}krghM^GTWI`VCNgK$&d>7e`ZRV zHl~%~L7%8of1nUfg;4{jA=FpYa4H6JyN*C(!>`JI76;+0-%OxJ5W`?6ZKhVde$&iObyrr zQB%QG!r3~oNflht1h0*7HIDblDQd?6yrI@azz@G#|>(WPS3k z3*`%u!-MjKr`^9z1hVm=7qlbV6Q)Z*3wf#s)D?r(D*+QZfPx~xL$2w4E?}bo-hDBl zqQt~^18}YY4eEeDJ=jP9y^5jPKy!fR4$a5JPZufxcL+6~HB8v>3R_hgJyh3*m^UEd%~a;i_u5 zt_JRC1gx8_OaqUI9>k5*WGP#)pp8k3-k?c0;KLhu_5uz1ncn-rdoR!&dXgKU*%hw0 z2ff>xd`bj;pp6lgh+jQ$+X~d$L8Hy4CI^kyns}%IJ{p0ea(JR7X5P>j%B0$esz0Z%Q!FCw)ObXf&YMHB{bAA9*L$A2h|{x z!Xz`1zyQ5zfqS8;cOlS~!PCyflLzP$wcG`s-hhJuz=$v4qpL~pT}_BUKKEV^Qfl;mquBm`4t3j?- zxK{-n(!eJV#sUdw6aCv2=(&TvF@gpFT6_7o(TINK3)F+an^1#2fg^n14ScExyn4g;7!6#ZAr3Hl5qonfjHen? zG$K6Km?+kOcF?Y6;8)m+;ECQ;1h18F1wN54YzmMO5!yKxU}VRr{(c#A8< z9vlZyudqyS@*o?NBw~m|79bZFFoM|~^uK7wh^z$J(eII3vk8SQCPX&Cr&f5^4Ciqy zgC$fW9yqp}!zs=r0&CiN8qxe-FTO$b}^|0dRn$B#tL+h&?Q@ zB%+ug8KuQhLtLmW+orbfm!SRyugCC9`KdOt=O)Vl^KAyxAc{Q<`?@)={X4jNh+qcTQ396xbH zLr*6tK-#Dyv55=hMP%X&Kcf~fGN2YQx>3LZ13h8G7=fOR4QV26vsRSwSq+*XXFGr& z998iSBNIj@Vl~7D>IARACt8FH?o)} z7_=I4g!duVOrn8?5gBNCq^vNZqzR%7;ud2+Vird+GMB<{(8m$ys7J&v;us$!CUZ>E z3xFDl+1B7WID%tr#~BySKy1LHTuol$1IgBwF zMesdJL1wae2ImSG3-ErF0I4C5C=1T6P-pnWaaCaAjPQ+|<8?S|!l)<)o(bAf0`w$| z4TNLt87bl{UJn#0ig1iGX|w=xNw`CwMhlQnXeE>n^-Uxuza3D^*eCXcoZ&c)`bNED zHX&*hqa8*`)FpC``b4ebuK^OZh>XZBY5?_&52T6uz^G!@G0`-6HNbhIbCih4f%-*F z;z)zq!`Uv5TsR6MuY@+4TXt#``5|`3Bhp7n%y*(hs5N4dPWz)4kuuunzql};Cz`=4 zFqRoiN0f)49%&%As5O)k`@uWSb|Uk{8IpC!GJu^%a+oiQd5 zPVo+Wc5-c&<9}sEpCL9ee{ZIPxJ9>Z)?nOjl%p%AqJs=toGl)h+GFlz+ zgZk{`8YM(d2nU2FB8kL%@)^AfF^N5rxe-c*GkqNOD3ex@GU1kBkI*2xG2_WBC*p%3 zfZ(5Kq!Yc^6XFB=Lv0h@5#dBfa0)m^DT!8y){!omwV~7~0iqCfVx~*hn6BooQZmL_z` zInqD*hRB8S2_rm8jyl1f5px)qFftSE;k9UC;?vj*u{!DnbxNd18rTc5xfvzIf<)Iu z8syjP;Yf#gy4m{3FH%Cy61|XUgcik~Q9Gzba}WQE8^R%>Nv^{?%+ezlC?$S_ybymT z82aybW*eD%`>zbBJ?s%*39rZta*BFD-U)_CJSC%RC))5n{6zGJdL-6BJQ1mp8|()? zo_Icy)l8GB;uK+H6zdLu^4Y~bi&wa zMicfzDF3f)|7#V3bQ04#5kPzcsi806Jch)2qH82t(h~_ zxmhP>jFMjeuP>m633dr@p!9!r*r{_O5BbK-8=*=tOMIQ!i*S$kB8O(bCy|~+IkVRA zh!3-e{&zgXagBH)evguncx1Md`G|DP9CaehJi?KPNP5E2iTL~f%13;Y;Ge`ceMoM-^Azz4Acp-uWH7Bt5zl+bK5yxweObr8B+-K4lEh=8M|0f3yNS*IZ)`@M zu{RR8$W@(k<7a|nvIdGCPi7uwwk!5Q|4JY)YT4bk_1uQ8*+Y#H+v$Rim6P&%_^h+iSTF#ee5 zCDy+P5p}2s65|QB@OtxHj9{Di zVy8w)WH8&2Xv-|S*&C4Zf30u+WrQ?{?8JwOhm+BQ;0GhWc^oreO(-HITn|9Bp{`Ij z_=?y>gb;d&cKmILyJWbU)&{E$xTb~c54bLYYbW??p8TG}6+I5DB9JvjM_B71>&Li) zjO(4aQ-JFpWZe~i(W7>7-2nT-wG3QG#WiePZNZfxTp_~!AzT5$U$m$ZT(iSp%(!-e z`)5)U74l91s{&{*{2hU-V7Qjl2z}tnJy|Ek)f=(~fnJNN3HGMd87Ejf#=Q<-(~7Yl ztYZhjTCxXxc8Ap^+^a)P;a&!=|7I}R%p2w<^MHBGT!o$3OUy;s-@MA)g5ApJ%nRlt zv?^%$I~-Slf?)-EFziQ8qQ+D6pq6JL6;CaJHk+DDO@*_O)Iihz53aePP77h}8~02f zGWVEk(Cba;{Tg!z`hLtLGVkI0B9opFbGUZv1#8Q=dk_kgaBUvf=)1sry*Hf2l`&j- zvH~e^tsGaOaUHxD6#_g(!M^DPY7XS~%?8fq0Y4Lg$0+y)_s>x4A3)MwkiUStv=Z1i z&Y?5ubM)V^$Go1NP0ypJ&=csn^a9vp{+WIX4RbkH!M<`G*l#_xhGMZetRt+$EPu8? z8}k>$93kgx&N$9W&MMA#@Epz=#Zj_b*=yLV*&kUMEHPZ~5Bv{iBA94;9OShuHlBfu z3XQ%)zYcO$d?3$4uQTc#^>+H%kdaZNuh1VeoHYDu{0Z`bexmpYxycSMa~(=R;;l8@Gd-&B@|yVsBtKP;zPl>`32$ zi~~1=i{YZ~qHex6PCHvOTk~A~Or56wq&}wkQ&XpH(msPs3IQ#lA5-_JA348phC=Sx z4dFH6F7aORDaej`YVq76N&H@XQ+Qi=n0Ju3p7lNJCge2yrv61eL^(`3qj_4hC)D-v zAWw8`)%dCT(^D608Lgv}biPf>y0zFHA+xnq(nSYW0>&V2& z;W0yE%3>>GmGfHX*~Ht#2gU`(Et$D!rpI`%@wJ0%2k#8t5nS$E>CECX91-L?`WAL6 z9Q!`{{qGO%N;U9;oJz{%Q4X7M2Y3$^&t+6e!9`k(W9g5o@=P}Q7URi8; zY|pWQWB(a&X@Izgs7H)LjKd=E){PCD8~(~Yn)~rh+8gy9?VXl{`h>_cL(YskIpk#U z>E5Sn|E~O7bW3u}=cVV%nVEAk>#J+3U+7=!bF4G1?fsqnSN31tUp-nk`t!8HY3t{$ zn>RatPW<8pix(W8eQ0*SN&P1!5C1S6Qeu0*@p$856w<;E%J<5!;y%SQlBXw!J_>oX z^U9tpHs@sLB2Gp@4d&33-OhD8cjL;9D>EO>ezYrjNAkqtsl^$}RAq^asI^3vo_7*#Bt-ULi=vcnmK!??3=Rwt94&xg=dAY^IPvXPc~QfhxSix z->TkKgEEF>B))k2V*jm!x0WU>O?Ywk`Pt$#C1-LjWL=nlE&iI@W0%KMlBXnxe+mDR zu1Hsi`4;>lmm-&t;9kLyP&?>e%)OW;Qv_$XU`!YcO?X9k#rlrj9g7$I z7ZGVuX_^vs$wcKukF^fT&%wMAmL#` z$7S{9?{2TTo%Jm1*)OR-r6zs(_$6JQD#!fCuN_A_rgzKgHm-k6|LRcJA7d@`N z(RgForNx&>4&9UDD0dTQ#p)Nyl1%p0*Yd`GyttETI2yWMtOSY24}n%_0g zE1FmIIpuQ-=Q;cNuXncIDZ5&Hb?xO1m#1Hyc6H0`A8&7c`peU{Bz2Nkp+{kGW6#Dg z$fFx&J=}VZZ=CPt-U+=UhYT3<?lYsy zj4s8t#kM~|-f5`3mprRDw|Ls8sh^g;UiSL!!*>tg+_G{$|5dqnrB?y4|(SZP#M)cqm1 zhXf4^9yWByupw4amQj_x>v{)w3+T4aZL`}d=`!gX7-a{w4s6|9xu_|*E?U2cs1%(?>D{P^!d>513#UYJ`f7pCs$9azNWmS{EIqH&6mbW7r4%MZRx7$ z`hBnUy>^E03QvwqiWChH59l8m5&2W!U;0XVih7RiGQP`m=cmp}iAu5yM%ez!2<3(9 z3)PE1fAiTUQsvokhwpz>2vevt~Fh14l9o+Z^F#r zwdA!V*g42~qTgh{i$VVc-3_}N<`Ur^ai{-{{!=4nM0D@dwa>KR8Nma)M0JUA8S7#r zwU%~+yzV}2z1xawOKYEhdGcj`_JZulkC7j5e7OFhHnA?z;)D1@drC*jj?5jIb3f1h zY*j6*wrRC%O@jRTBjUfrxei$ljowY(6MIbVu`hII=&-R1!F>G7# zufd$Ig0BC#o_B4PDP`YtHgd{3iaVY(yll8rdcAaL-iW-0Pj#Q@WGdy&hu0s*B#lYh zo4Pl3S>`vHI|_Fe?yB5bS>ICM;;MJoZx#G3_|0~Q?H-TcJ$M1)fZaW}_q-Z*BP=4k zU${lMIJ`$#P}q~;N5MjWk^dt1cy}DEHuKl=@;xrw{cZQFXuGJ*s4#9-{;o`~O{<+) zGO=WP-p;(VjHHYOX-m?6{rJ;I&re>TAmnCkFW6S_zAUM1d&Aa-nD(*lhpEHVCyO+T zMGlJ{Dm^Pb-}t}wzZ`TWC_3b;kWHbRL%W3pge(kN9JI`TiT^s!4W3$it^Fd4c#Gjw zH09Us-oC%#XoGFJWw|$uzrNW%*-O$Fr#pUf{xmi{CS8@K%KBVTRPeIwSy@m+uZ9

g&=ve&U*g@HwZ`Caq6e)cHz7-K)q z{)+e?@%PLYW_;V&wg>eO>j#w$DVtp|qu{5kU$g2zRewrP%S!8?9+B>w?Uy~eaCG5n zsLpb2bZsnZ|J?2azB^AcQ=*5|n;*P3dBp}y3kV7B6Z|6NNk~#?YUr4d(IFFp#s%5I zI9Uwz9PM4~2Z^J^MG!-OSN*1XSC?EDS{hUulpm5Gnb|LMU)rIxB_HEHrlsYi-OId@ zS)bpW?^Nbk)>vO(p9b|*F_3%s!6M1xj>BDtonG6$UIn}jFb3;_8$;!x)50c&_3Rzm z``4a7_uSI$hi>0^FZI6dc+W9ik|G%Zd6o(7f46T1yR0tzuBi(qb)2_FJZU(Ii zT@(5+>|xkSXh%c-2$>!@J+RqN?)TjFiEFTRsC5P}mDg1ls7q@}ZW#r2V(vv=Menj- zW^ezr@6-F#7pa?I97;<|P1~KdJ8M(nmcnrrF%`cz?P_|iey;wSGnR8-dS4pnyukUR zPpZ$>?pwQe=@rmx1^g#ONLb&nWuc2hdj^LD-}S%iKh`tGbH06?{deN;#6qZFx~96K zQr5NC%`cr@x+H&j{`$;ynV-@=rXBhC=f{S$+O$iVS2M5XU&=pIdcM@J-n;&>>Y1vX zE}^AjYw-=cYjz_%MtMx`Ig;%pzony1( zC(9&D4$Sp?X@WGbo8L5#t{Pnx@umNl*4&od=b5iF^V4(Ehh+@U5M&Fp)%orD=_Q#Z zFs-c3Q{*a~^)C9oygj@GS%R#^N$#ZdQhJT?AMJmq$K4)^AZ+rW8$E9InB+gf-_G04 zd!+MM&e2vQtqws}`Z2>Fh7zc6F09Y5*OY6^&lR05a?N+mKaq1PXK>EI9CjWjub?o$ za6{>a(o0a2$CY#Bo*FOB9>^m8DE=s3ZM)i505x_)y@zyp@Ialpa=UN?TXlm4gt zzxEsB*UvM`^P1CTC$*K@YMXF}@Hz8@aZo#}TQukE;ME;9gE#AXChk9DMTDk7E-)Aq931pdq9Km?Vam~{dXmVS#TaPpzZCqRPUCrMW|5Pk2 zTTu42^m*yCvZrO^E5}!ks~ul^plN^8bL9)=9_?=JIjCtkEjS_Ym-dj#Z5wRQI{od$ zcjLNg+%@i#Jf?UAxcj+(=d!^C=hzX}gRJ{m^tQkp`2u5}F$C%_q^*{%kD$6~Q|;Q? zU#oUh2`Zrwp^{&Dr0Q_hueHC{zJ(enajUr1QSGQcVLWM!fV}oo;#1;PR;!>kINm*Qc&2ZmDjuZd2VBxh!#UaI|yWXLHcT%F@!Zm|w=91lhZfw9mAgR2x)T&DqWV z_1)^vRiCWhQ2AqJW<^THKb4m&b=BJHi}eZhKehbaGOF#Xw(Ghpx_H(SmZeZ6oGP6z zt+B1L{oZM#Q@U%q>qGYk?(QC59vZ0YoaQpaWtzhjhal^o*6tQ=7Hha`xI3X1;d*p)&evnI3*arB!{Z64vwUm$ZD-^0Mt|+e!U-eJVSV zog?}zT5h$-D$-$~gWN^#66-O|Bg$)#*8#6RUXh*yJ(svGa&vHUayoBw&L&V2B=O;S z^Dsw$TgQ%$d-6N-i}jc5Z&lr@5|<0gD@tlgW|d4USzo%PG^oPAVlvb_zHNNb*jw3G zIY~Q7n?|Kj1BF9{lVuZRSM0Ca8(oYpj0fYf&3n7|a;O8o>V4IOl_{>%T|d6FNc8_V?P&+Fr^Y%3m6HG!|9oRWB=FQT`*;rw0{xFWz0eulR83!P3ym(8`{5 zA$4H&;dILVoO_E`lh+BK6Fz5r&ibglTD@+% z-*(qIX`HNVZEOcfq9l3TTyBIh()gnNX?sRXPRrA}hjkw+Qz{3RMU@SL8twYx`r`hj z5vAT0eifT)Hq?llESh#JcPnpbZ)h>Ue4%imaD;4_Y`Xm{`}r<&U2HsT;pgT!FBLR{ zr_pn>`)2oj&ikB~*e$jjFC8b{CpaM3NPSN&(=OLOS3XrfZFjX{tM_FH|K|-Ke@+#iGJ>lUkuxjDWhj4!uU7!AfIo5^NFNm)w(# zupVJu3-ysdIc|2WaH@5(auz!qKnZW{U)ld+^Q+B#>3r!)(Mb{F)Rl2%8ng{s+jiUb z5=DvPTd3Wm8;lJn>(A8Bt)Epts9{J$NK>ySce%U#qw=HDPu)eGuK%RJ3N^)xcnf(c zqBPMZ%fBsOTP0d)Y}#$Y?ZWNi?H1YXwcTU;*!r-OmOb?okNRk=f*>paDlmLFPnHvbN7ck@oD8=R|%Q`o6&RC7D#bp%0;+W@E?ie=AY zkK@PiOGRa(21%o2AjGzNR*$SESx>MwS{ba~$ll5BTHdm(6E}$ugYEL5o)mLsDFfFq zMKfL_ZMSbiGXODk9{tVCWC zZ!*+RE|M&gWJoimKUi(H8VbK>0<8V5y{&w#hDwJ@&sdzcm@b?lJPonz1a*SiZP;x% zr#Y{Yv|F_Elsx54`E7ZxmcW+%&4-)i&CSgr@(?*Jd9;pci)kxW7pqJ3WqN;D4_wJz z!QCU=Cmb#rDp?>~C`+_{XFc3@xNW-aC);VZGi;l!>#g_8j>uL?zLA^~o)^yJ&f=Cp z?&<{nI6b9isZm$S@)Y^d=3&hj8~jo_R01;?6=zwv>Rw=ur^rdz<9U``bYIo>z_3|X-I7P(DX$9SiTu* z0}Is!>Z69g47*u7S@HbEe1ljk7Rp4jc~H|?YFBJG8Di!ehj$Kk4pt7+>}J{}SYNQV zmfA?ah>Ar%JTG1ltT~42L-lJr)^-Fa15E!f@BnIHtLw|^o$5X6*VHeo&uGYR7}`9b z`FF)G#qqY|ZQZrqwAN5pW6hCp$_2H8>5_?(V5>e>gKeX1ajf{!;U|Y3(0;W4!G5Xj za@*Bb%dIX-u1Y2d#|!`99O2xjAJWy@3hnatZ`)V4u4;{F32(`6%xzo=Rl>3cTlkgn zv|&Zl>L!_7B44BYPPwJyhYp3VMfWFjj2Xloz)cWd5*~y9E0N0-vUN~b>tyF__tNee zc*Hk$?zT?0rB+o|)s~f(L&QVGJ@`HN1+0A5cgD5G0onoDCGFp|dnjF$zsk4EUpGH* z{?b(1RM%A7RNh?Hd`W&)K1?}SxuShlyQ|hwD=>>@ph?mv>A4J-8NeRE{+buVlL>8v z3&iuqJz)HawVZ013IBg_9a@?sO)}qNzD2w!UIe8@f*9^t?rPRb)*gC4{YjsqAFcgb z+e6(=y|sO7`@*)xZB?o=l}2SyU2aQgt7)%mKdwHeexiM%buc&@&e3OR{5^S`bDWdS z%i!G*+!1sWbrHFU-Nn1byG;K(mm*3Q{VhxodJ5bG{dkc)PmVjMnyR5L(h2k)!#=~` zy7M|4t*y35U8MF^d#ejNiaH$CwlLFuqy7Swi!r+KIjP^ItX5ZZ>$qNg z4?fLj_}k$B70wIJ38o0939j%j@z3(k@y3DiIwfbLlzv$w$ z3$>?U-ME*gFZ^>#nC1k~U7}s0-3Il89k8x_(0Ir=otegXv;0`qP#e01`vbR}SHaWr zJNWknj|DbDsgNra2=@y12p;pF@b~d{^BlRZ+|}$A>~m22oI_{OBB)jVR=-@&*70;r zng&f*O@Jl^{_`VCovR+K8KAL-|E7x6^)uDZPBBh0!t{YrQEk)<_A_=I{0~|I)JNj~ z2c!uy1cRV{cbIULuu)JafS>UKOTHEV3O9i(;mA1APd&nwOe4nm16*X`#2$pD-LXMAI>}6!vT2uW(qKIh#3Ju9h3l z59j*{`~;6c#uh;%RI|Sl^b+(G#PG-R9eEDC%bY75E2xQ^1A8Vn=xemhC^a6{AJfm) z&Co5-#%n)lK4`XRHiKr5X?W0KIpj)Ktrw`W;Hw-guHvVY*1T}a8u#@v6tdBisKW3+MaybvUx45yq znY_o)GI*bOr+MdjT_A3*;cnpm&iNJS{m71H4P#-3a3x(%CmWNENrqGdt=H*i>gT|! zJfo}D)#x~So_?u*q28!x8D1Nn8Lq+}#WngO{S@}0-a~caYp5tZ$3DZJ$(hB0zhmQI z-RJ`DLO|tME}zTgZi9Nnp3N@B$N&`DgvD9tmKI6hTFnj2IP*>?hzl1u_-;CRh zOJR=(|5N6q@szRNSYsSPe+AX3Pw9coU?z!4X7G1GI+a5WXANSVXC=VD8P&3!*v=q{ zAKM*{%`6q`KIf8s+k8fCGh+8FZvJoM}xWaV0r)@M)##h zfPXKim%xhTKXfZCr$;cunG3Lwhr6qDsm0V|>Nf0M@mT{{k*vk6g{+NW)8AM>u$Hq{ zv9LDU2`VCUr~;^!+y!-pgJ9pTnK{FpgSy7)P?77!G}BFVI-La-ug~dM^jkU`d{jZJ zXb;96YGEfs)%GD%Z6WTHPJ&&)z0^*qx_wDiQRSd510{rgFbe3a;r}NJs4q~B`HD)KWH_Se0xQkc-JBPRr zj1_SssZronbEtJNf8GtW_k$#dU@dA7eEJRQ)K|ma-E8=NIMi1Lz!>ic`-e){Rjq>R zmt-adDqEjJ1?pAP&SwJiH|&94hSlr4@N4)X*enewWy9>J26kW-uy;;@=UbWTU9bwL zH|#$4g4g~~aT*R4t-Ya+s5k6hV%?4>>{??dMvVEMoGI7P6I zg%xIjdcuI2Y~yaPS88n3*p|V7wqr)n|3;V;G7$L z;{tMGO&FVi4AlWx!-Z8yPVkD=pjeNF`_NXfLu(D1!b(!y$wt{3*yF{_ZmiYBY=5k^ z!5Rvb4{Pdh&m7-lRV3DQ;J!CjPGH_JW+dQlKFK@Bd~U4l!JTt_#VP=d$e8gj`IjPQ z`eOzFR-<4J2WB*21_5RbU|ud(a$)WS))!&EKh}0&-Zoa7U`3`9K9Rk5Qk#R>1DM4@ zst_o?okAd}qXm_k9vx9H3{>c)wfI06Z z=NM~tuyzJ(XfQhgD^qKsnQO%mcUZfK`3qP}i?t6V&jV{_`9KT%#`;Pp6aGlWpBLcW z9Z-Nz7x>;Du9KQLM9X3A1Xk)`%@@)`q?JJ{H&q^*u~%k79#*Yjz5^l;bB8b+gVgC_ zHJ&rjM1*^pD&btf|GSvZVwI;IP_zZQ79cC?nE_j3Ef-c{wm|(0smVpZ!I>yWxuQrUuf#Ohm8 z3yhkr_?MSjlRgmXn0bZ%g{VP96KkR!(aY_CwhJ81xesVSImk*k0sZUoHV3} zn8$vwA@At9=%1MVk5vWe*(5Vj4|LEHSdWHvf9Uy`LnMS(j3tODk|9QVLrF*_48{P= zkHXL9*kI1dLJB0O2z?lR9yLPhkIdC|<`{sQ!mE*Q%xl7|Z<1L=YKbteVKp6zgjn5z zRp2NOMjpHxbNDe@k*I}opmxyaXeUxbM=(HWV?7vp8Rl}aK~A&`Y5?gVHM9)o5VZkS zw3eAR!2(7$L=oPLx5+;lq>33)IC_y9 zHI$8LnV<-5MWQ09%EwwRq>D0RJSVuo(F-%ONCOtZ+N5D#5~&fzx-7&Wp@ni{gg2Nz zqfPM>Ms1R}X2v3^y+Ycgo($iSI8EyR(2A&89KX!jd1TZ;{!wDI6h=4f8)={>5j_~I z5y$2%UhDx;L^9;?8|)FWglNQ1_&v@q2s+6p@|k4Cp%nNTDI&7)E*uHaGtdgC74!j= zkz{0&99SG>aO}b-)`uXkSfPSgBl&K`1OA&0h|(keP~)fpq8%LHuu2Wlfg><_9eNhl z9pl|Z3eqPlBKiHPf@NTLhs7qyP`kWYdZCR)fV+RuzPl!Z{hBle9{No+>{06U?Kn zi0$wlxeI-vlY25QA|=!iQbI4l-bwr+N5ss3{mG06v%RrT64x>QkZ%!vh<3ySK8R#Q z`^YPaapsth=toOntVO*bAE+gBrYutLvtZo7k$+YpRg*}Y zco*?Af_5S~>H@WhI3#*ReG<&$8T0`3aDqXz2Jm{6lUNjWhwl;9s9B7ZVAu6E)OqncTp1eIuIj7TK& znHyr7ixMD@=InLiX`PxvZy>rh$8&-d^a;Wv=^OL4(c2J%h%b}?dHmne0lND4 zfm~q^cnx|EN`goHHG^EDG-fNCeV^EXSiy`}^gc2ZLk`Tj@MbKNYf&b&6Y)nfP9c4> zKa)`csh}PRRT3+R9uNsQN|QP^vxe|~d=UCX$9O-9P@O)E{h3Fj&ggv>##$Dd2rX7Z|J28bq^5;lK2V&k;)hS7tm*_#hfUjv@QSCEkgv5WXXzadc>?=jy?qBN1fj99a5cqFXM`asSQnZ$STdZdY#!>b9$om$3gQO9T- zNhkB?&rhbFX7HxIz#~G(80C#DF~fI6iAQqb&W+AspUQL(gB+!)JX)$?o^)BOWL_4) zK<+@NH@$@0xa2Ka5}Ps{wMIiYv0}15oCnM!pbrN3iN>${GvuZ>%$evtK4ee*6DBztMfb&Y$^MD Ytu@+R+{@*3otInL+T-(HUu}DqFHwX?ZvX%Q literal 0 HcmV?d00001 diff --git a/src/App-styles.ts b/src/App-styles.ts index fa4a0eb..b33bb0a 100644 --- a/src/App-styles.ts +++ b/src/App-styles.ts @@ -40,6 +40,15 @@ export const AuthenticatedContainerInnerRight = styled(Box)(({ theme }) => ({ background: "rgba(0, 0, 0, 0.1)" })); +export const AuthenticatedContainerInnerTop = styled(Box)(({ theme }) => ({ + display: "flex", + alignItems: "center", + justifyContent: "flex-start", + width: "100%px", + height: "60px", + background: "rgba(0, 0, 0, 0.1)", + padding: '20px' +})); export const TextP = styled(Typography)(({ theme }) => ({ fontSize: "13px", diff --git a/src/App.tsx b/src/App.tsx index a23c7e5..c9698eb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,18 +1,40 @@ -import { useEffect, useMemo, useRef, useState } from "react"; +import { + createContext, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import reactLogo from "./assets/react.svg"; import viteLogo from "/vite.svg"; import "./App.css"; import { useDropzone } from "react-dropzone"; -import { Box, CircularProgress, Input, InputLabel, Tooltip, Typography } from "@mui/material"; +import { + Box, + Button, + Checkbox, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Input, + InputLabel, + Popover, + Tooltip, + Typography, +} from "@mui/material"; import { decryptStoredWallet } from "./utils/decryptWallet"; import { CountdownCircleTimer } from "react-countdown-circle-timer"; import Logo1 from "./assets/svgs/Logo1.svg"; import Logo1Dark from "./assets/svgs/Logo1Dark.svg"; -import RefreshIcon from '@mui/icons-material/Refresh'; +import RefreshIcon from "@mui/icons-material/Refresh"; import Logo2 from "./assets/svgs/Logo2.svg"; import Copy from "./assets/svgs/Copy.svg"; -import ltcLogo from './assets/ltc.png'; -import qortLogo from './assets/qort.png' +import ltcLogo from "./assets/ltc.png"; +import qortLogo from "./assets/qort.png"; import { CopyToClipboard } from "react-copy-to-clipboard"; import Download from "./assets/svgs/Download.svg"; import Logout from "./assets/svgs/Logout.svg"; @@ -44,6 +66,23 @@ import { import { Spacer } from "./common/Spacer"; import { Loader } from "./components/Loader"; import { PasswordField, ErrorText } from "./components"; +import { ChatGroup } from "./components/Chat/ChatGroup"; +import { Group, requestQueueMemberNames } from "./components/Group/Group"; +import { TaskManger } from "./components/TaskManager/TaskManger"; +import { useModal } from "./common/useModal"; +import { LoadingButton } from "@mui/lab"; +import { Label } from "./components/Group/AddGroup"; +import { CustomizedSnackbars } from "./components/Snackbar/Snackbar"; +import { + getFee, + groupApi, + groupApiLocal, + groupApiSocket, + groupApiSocketLocal, +} from "./background"; +import { executeEvent } from "./utils/events"; +import { requestQueueCommentCount, requestQueuePublishedAccouncements } from "./components/Chat/GroupAnnouncements"; +import { requestQueueGroupJoinRequests } from "./components/Group/GroupJoinRequests"; type extStates = | "not-authenticated" @@ -59,22 +98,113 @@ type extStates = | "wallet-dropped" | "web-app-request-buy-order" | "buy-order-submitted" - ; + | "group"; +interface MyContextInterface { + txList: any[]; + memberGroups: any[]; + setTxList: (val) => void; + setMemberGroups: (val) => void; + isShow: boolean; + onCancel: () => void; + onOk: () => void; + show: () => void; + message: any; +} + +const defaultValues: MyContextInterface = { + txList: [], + memberGroups: [], + setTxList: () => {}, + setMemberGroups: () => {}, + isShow: false, + onCancel: () => {}, + onOk: () => {}, + show: () => {}, + message: { + publishFee: "", + message: "", + }, +}; + +export const allQueues = { + requestQueueCommentCount: requestQueueCommentCount, + requestQueuePublishedAccouncements: requestQueuePublishedAccouncements, + requestQueueMemberNames: requestQueueMemberNames, + requestQueueGroupJoinRequests: requestQueueGroupJoinRequests +} + +const controlAllQueues = (action) => { + Object.keys(allQueues).forEach((key) => { + const val = allQueues[key]; + try { + if (typeof val[action] === 'function') { + val[action](); + } + } catch (error) { + console.error(error); + } + }); +}; + +export const clearAllQueues = () => { + Object.keys(allQueues).forEach((key) => { + const val = allQueues[key]; + try { + val.clear(); + } catch (error) { + console.error(error); + } + }); +} + +export const pauseAllQueues = () => controlAllQueues('pause'); +export const resumeAllQueues = () => controlAllQueues('resume'); + + +export const MyContext = createContext(defaultValues); + +export let globalApiKey: string | null = null; + +export const getBaseApiReact = (customApi?: string) => { + + if (customApi) { + return customApi; + } + + if (globalApiKey) { + return groupApiLocal; + } else { + return groupApi; + } +}; +export const getBaseApiReactSocket = (customApi?: string) => { + + if (customApi) { + return customApi; + } + + if (globalApiKey) { + return groupApiSocketLocal; + } else { + return groupApiSocket; + } +}; +export const isMainWindow = window?.location?.href?.includes("?main=true"); function App() { const [extState, setExtstate] = useState("not-authenticated"); const [backupjson, setBackupjson] = useState(null); const [rawWallet, setRawWallet] = useState(null); const [ltcBalanceLoading, setLtcBalanceLoading] = useState(false); - const [qortBalanceLoading, setQortBalanceLoading] = useState(false) + const [qortBalanceLoading, setQortBalanceLoading] = useState(false); const [decryptedWallet, setdecryptedWallet] = useState(null); const [requestConnection, setRequestConnection] = useState(null); const [requestBuyOrder, setRequestBuyOrder] = useState(null); - const [authenticatedMode, setAuthenticatedMode] = useState('qort') + const [authenticatedMode, setAuthenticatedMode] = useState("qort"); const [requestAuthentication, setRequestAuthentication] = useState(null); const [userInfo, setUserInfo] = useState(null); const [balance, setBalance] = useState(null); - const [ltcBalance, setLtcBalance] = useState(null) + const [ltcBalance, setLtcBalance] = useState(null); const [paymentTo, setPaymentTo] = useState(""); const [paymentAmount, setPaymentAmount] = useState(0); const [paymentPassword, setPaymentPassword] = useState(""); @@ -84,10 +214,13 @@ function App() { const [walletToBeDownloaded, setWalletToBeDownloaded] = useState(null); const [walletToBeDownloadedPassword, setWalletToBeDownloadedPassword] = useState(""); - const [authenticatePassword, setAuthenticatePassword] = - useState(""); + const [isMain, setIsMain] = useState( + window?.location?.href?.includes("?main=true") + ); + const isMainRef = useRef(false); + const [authenticatePassword, setAuthenticatePassword] = useState(""); const [sendqortState, setSendqortState] = useState(null); - const [isLoading, setIsLoading] = useState(false) + const [isLoading, setIsLoading] = useState(false); const [ walletToBeDownloadedPasswordConfirm, setWalletToBeDownloadedPasswordConfirm, @@ -96,13 +229,80 @@ function App() { useState(""); const [walletToBeDecryptedError, setWalletToBeDecryptedError] = useState(""); - const holdRefExtState = useRef("not-authenticated") + const [txList, setTxList] = useState([]); + const [memberGroups, setMemberGroups] = useState([]); + const [isFocused, setIsFocused] = useState(true); + + const holdRefExtState = useRef("not-authenticated"); + const isFocusedRef = useRef(true); + const { isShow, onCancel, onOk, show, message } = useModal(); + const [openRegisterName, setOpenRegisterName] = useState(false); + const registerNamePopoverRef = useRef(null); + const [isLoadingRegisterName, setIsLoadingRegisterName] = useState(false); + const [registerNameValue, setRegisterNameValue] = useState(""); + const [infoSnack, setInfoSnack] = useState(null); + const [openSnack, setOpenSnack] = useState(false); + const [hasLocalNode, setHasLocalNode] = useState(false); + const [openAdvancedSettings, setOpenAdvancedSettings] = useState(false); + const [useLocalNode, setUseLocalNode] = useState(false); + const [confirmUseOfLocal, setConfirmUseOfLocal] = useState(false); + + const [apiKey, setApiKey] = useState(""); + + useEffect(() => { + chrome.runtime.sendMessage({ action: "getApiKey" }, (response) => { + if (response) { + + globalApiKey = response; + setApiKey(response); + setUseLocalNode(true) + setConfirmUseOfLocal(true) + setOpenAdvancedSettings(true) + } + }); + }, []); useEffect(() => { if (extState) { - holdRefExtState.current = extState + holdRefExtState.current = extState; } - }, [extState]) + }, [extState]); + useEffect(() => { + isFocusedRef.current = isFocused; + }, [isFocused]); + + // Handler for file selection + const handleFileChangeApiKey = (event) => { + const file = event.target.files[0]; // Get the selected file + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + const text = e.target.result; // Get the file content + setApiKey(text); // Store the file content in the state + }; + reader.readAsText(file); // Read the file as text + } + }; + + const checkIfUserHasLocalNode = useCallback(async () => { + try { + const url = `http://127.0.0.1:12391/admin/status`; + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + const data = await response.json(); + if (data?.isSynchronizing === false && data?.syncPercent === 100) { + setHasLocalNode(true); + } + } catch (error) {} + }, []); + + useEffect(() => { + checkIfUserHasLocalNode(); + }, [extState]); const address = useMemo(() => { if (!rawWallet?.address0) return ""; @@ -135,7 +335,7 @@ function App() { try { if (typeof fileContents !== "string") return; pf = JSON.parse(fileContents); - } catch (e) { } + } catch (e) {} try { const requiredFields = [ @@ -163,8 +363,6 @@ function App() { }, }); - - const saveWalletFunc = async (password: string) => { let wallet = structuredClone(rawWallet); @@ -173,7 +371,7 @@ function App() { wallet = await wallet2.generateSaveWalletData( password, crypto.kdfThreads, - () => { } + () => {} ); setWalletToBeDownloaded({ @@ -201,31 +399,28 @@ function App() { chrome.tabs.sendMessage( tabs[0].id, { from: "popup", subject: "anySubject" }, - function (response) { - - } + function (response) {} ); } ); }; - const getBalanceFunc = () => { - setQortBalanceLoading(true) + setQortBalanceLoading(true); chrome.runtime.sendMessage({ action: "balance" }, (response) => { if (!response?.error && !isNaN(+response)) { setBalance(response); } - setQortBalanceLoading(false) + setQortBalanceLoading(false); }); }; const getLtcBalanceFunc = () => { - setLtcBalanceLoading(true) + setLtcBalanceLoading(true); chrome.runtime.sendMessage({ action: "ltcBalance" }, (response) => { if (!response?.error && !isNaN(+response)) { setLtcBalance(response); } - setLtcBalanceLoading(false) + setLtcBalanceLoading(false); }); }; const sendCoinFunc = () => { @@ -243,7 +438,7 @@ function App() { setSendPaymentError("Please enter your wallet password"); return; } - setIsLoading(true) + setIsLoading(true); chrome.runtime.sendMessage( { action: "sendCoin", @@ -260,12 +455,11 @@ function App() { setExtstate("transfer-success-regular"); // setSendPaymentSuccess("Payment successfully sent"); } - setIsLoading(false) + setIsLoading(false); } ); }; - const clearAllStates = () => { setRequestConnection(null); setRequestAuthentication(null); @@ -274,29 +468,73 @@ function App() { useEffect(() => { // Listen for messages from the background script chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - // Check if the message is to update the state - if (message.action === "UPDATE_STATE_CONFIRM_SEND_QORT") { + if ( + message.action === "UPDATE_STATE_CONFIRM_SEND_QORT" && + !isMainWindow + ) { // Update the component state with the received 'sendqort' state setSendqortState(message.payload); setExtstate("web-app-request-payment"); - } else if (message.action === "closePopup") { + } else if (message.action === "closePopup" && !isMainWindow) { // Update the component state with the received 'sendqort' state window.close(); - } else if (message.action === "UPDATE_STATE_REQUEST_CONNECTION") { + } else if ( + message.action === "UPDATE_STATE_REQUEST_CONNECTION" && + !isMainWindow + ) { + // Update the component state with the received 'sendqort' state setRequestConnection(message.payload); setExtstate("web-app-request-connection"); - } else if (message.action === "UPDATE_STATE_REQUEST_BUY_ORDER") { + } else if ( + message.action === "UPDATE_STATE_REQUEST_BUY_ORDER" && + !isMainWindow + ) { // Update the component state with the received 'sendqort' state setRequestBuyOrder(message.payload); setExtstate("web-app-request-buy-order"); - } else if (message.action === "UPDATE_STATE_REQUEST_AUTHENTICATION") { + } else if ( + message.action === "UPDATE_STATE_REQUEST_AUTHENTICATION" && + !isMainWindow + ) { // Update the component state with the received 'sendqort' state setRequestAuthentication(message.payload); setExtstate("web-app-request-authentication"); - } else if (message.action === "SET_COUNTDOWN") { + } else if (message.action === "SET_COUNTDOWN" && !isMainWindow) { setCountdown(message.payload); + } else if (message.action === "INITIATE_MAIN") { + // Update the component state with the received 'sendqort' state + setIsMain(true); + isMainRef.current = true; + } else if (message.action === "CHECK_FOCUS" && isMainWindow) { + + sendResponse(isFocusedRef.current); + } else if ( + message.action === "NOTIFICATION_OPEN_DIRECT" && + isMainWindow + ) { + executeEvent("openDirectMessage", { + from: message.payload.from, + }); + } else if (message.action === "NOTIFICATION_OPEN_GROUP" && isMainWindow) { + executeEvent("openGroupMessage", { + from: message.payload.from, + }); + } else if ( + message.action === "NOTIFICATION_OPEN_ANNOUNCEMENT_GROUP" && + isMainWindow + ) { + executeEvent("openGroupAnnouncement", { + from: message.payload.from, + }); + } else if ( + message.action === "NOTIFICATION_OPEN_THREAD_NEW_POST" && + isMainWindow + ) { + executeEvent("openThreadNewPost", { + data: message.payload.data, + }); } }); }, []); @@ -327,7 +565,7 @@ function App() { return; } - setIsLoading(true) + setIsLoading(true); chrome.runtime.sendMessage( { action: "sendQortConfirmation", @@ -344,12 +582,11 @@ function App() { setExtstate("transfer-success-request"); setCountdown(null); } else { - setSendPaymentError( response?.error || "Unable to perform payment. Please try again." ); } - setIsLoading(false) + setIsLoading(false); } ); }; @@ -372,7 +609,7 @@ function App() { return; } - setIsLoading(true) + setIsLoading(true); chrome.runtime.sendMessage( { action: "buyOrderConfirmation", @@ -387,12 +624,11 @@ function App() { setExtstate("buy-order-submitted"); setCountdown(null); } else { - setSendPaymentError( response?.error || "Unable to perform payment. Please try again." ); } - setIsLoading(false) + setIsLoading(false); } ); }; @@ -422,29 +658,48 @@ function App() { useEffect(() => { try { - setIsLoading(true) + setIsLoading(true); chrome.runtime.sendMessage({ action: "getWalletInfo" }, (response) => { if (response && response?.walletInfo) { setRawWallet(response?.walletInfo); - if (holdRefExtState.current === 'web-app-request-payment' || holdRefExtState.current === 'web-app-request-connection' || holdRefExtState.current === 'web-app-request-buy-order') return + if ( + holdRefExtState.current === "web-app-request-payment" || + holdRefExtState.current === "web-app-request-connection" || + holdRefExtState.current === "web-app-request-buy-order" + ) + return; + + setExtstate("authenticated"); } }); - } catch (error) { } finally { - setIsLoading(false) + } catch (error) { + } finally { + setIsLoading(false); } }, []); - useEffect(() => { - if (!address) return; + const getUserInfo = useCallback(async (useTimer?: boolean) => { try { + if (useTimer) { + await new Promise((res) => { + setTimeout(() => { + res(null); + }, 10000); + }); + } chrome.runtime.sendMessage({ action: "userInfo" }, (response) => { if (response && !response.error) { setUserInfo(response); } }); getBalanceFunc(); - } catch (error) { } + } catch (error) {} + }, []); + + useEffect(() => { + if (!address) return; + getUserInfo(); }, [address]); useEffect(() => { @@ -453,11 +708,15 @@ function App() { }; }, []); - useEffect(()=> { - if(authenticatedMode === 'ltc' && !ltcBalanceLoading && ltcBalance === null ){ - getLtcBalanceFunc() + useEffect(() => { + if ( + authenticatedMode === "ltc" && + !ltcBalanceLoading && + ltcBalance === null + ) { + getLtcBalanceFunc(); } - }, [authenticatedMode]) + }, [authenticatedMode]); const confirmPasswordToDownload = async () => { try { @@ -466,18 +725,17 @@ function App() { setSendPaymentError("Please enter your password"); return; } - setIsLoading(true) + setIsLoading(true); await new Promise((res) => { setTimeout(() => { - res() - }, 250) - }) + res(); + }, 250); + }); const res = await saveWalletFunc(walletToBeDownloadedPassword); } catch (error: any) { setWalletToBeDownloadedError(error?.message); } finally { - setIsLoading(false) - + setIsLoading(false); } }; @@ -509,48 +767,49 @@ function App() { setWalletToBeDownloadedError("Password fields do not match!"); return; } - setIsLoading(true) + setIsLoading(true); await new Promise((res) => { setTimeout(() => { - res() - }, 250) - }) + res(); + }, 250); + }); const res = await createAccount(); const wallet = await res.generateSaveWalletData( walletToBeDownloadedPassword, crypto.kdfThreads, - () => { } + () => {} ); - chrome.runtime.sendMessage({ - action: "decryptWallet", payload: { - password: walletToBeDownloadedPassword, - wallet - } - }, (response) => { - if (response && !response?.error) { - setRawWallet(wallet); - setWalletToBeDownloaded({ + chrome.runtime.sendMessage( + { + action: "decryptWallet", + payload: { + password: walletToBeDownloadedPassword, wallet, - qortAddress: wallet.address0, - }); - chrome.runtime.sendMessage({ action: "userInfo" }, (response2) => { - setIsLoading(false) - if (response2 && !response2.error) { - setUserInfo(response); - } - }); - getBalanceFunc(); - } else if (response?.error) { - setIsLoading(false) - setWalletToBeDecryptedError(response.error) + }, + }, + (response) => { + if (response && !response?.error) { + setRawWallet(wallet); + setWalletToBeDownloaded({ + wallet, + qortAddress: wallet.address0, + }); + chrome.runtime.sendMessage({ action: "userInfo" }, (response2) => { + setIsLoading(false); + if (response2 && !response2.error) { + setUserInfo(response); + } + }); + getBalanceFunc(); + } else if (response?.error) { + setIsLoading(false); + setWalletToBeDecryptedError(response.error); + } } - }); - - - + ); } catch (error: any) { setWalletToBeDownloadedError(error?.message); - setIsLoading(false) + setIsLoading(false); } }; @@ -561,7 +820,7 @@ function App() { resetAllStates(); } }); - } catch (error) { } + } catch (error) {} }; const returnToMain = () => { @@ -578,16 +837,16 @@ function App() { const resetAllStates = () => { setExtstate("not-authenticated"); - setAuthenticatedMode('qort') + setAuthenticatedMode("qort"); setBackupjson(null); setRawWallet(null); setdecryptedWallet(null); setRequestConnection(null); - setRequestBuyOrder(null) + setRequestBuyOrder(null); setRequestAuthentication(null); setUserInfo(null); setBalance(null); - setLtcBalance(null) + setLtcBalance(null); setPaymentTo(""); setPaymentAmount(0); setPaymentPassword(""); @@ -599,58 +858,217 @@ function App() { setWalletToBeDownloadedPasswordConfirm(""); setWalletToBeDownloadedError(""); setSendqortState(null); + globalApiKey = null; + setApiKey(""); + setUseLocalNode(false); + setHasLocalNode(false); + setOpenAdvancedSettings(false); + setConfirmUseOfLocal(false) }; const authenticateWallet = async () => { try { - setIsLoading(true) - setWalletToBeDecryptedError('') + setIsLoading(true); + setWalletToBeDecryptedError(""); await new Promise((res) => { setTimeout(() => { - res() - }, 250) - }) - chrome.runtime.sendMessage({ - action: "decryptWallet", payload: { - password: authenticatePassword, - wallet: rawWallet + res(); + }, 250); + }); + chrome.runtime.sendMessage( + { + action: "decryptWallet", + payload: { + password: authenticatePassword, + wallet: rawWallet, + }, + }, + (response) => { + if (response && !response?.error) { + setAuthenticatePassword(""); + setExtstate("authenticated"); + setWalletToBeDecryptedError(""); + chrome.runtime.sendMessage({ action: "userInfo" }, (response) => { + setIsLoading(false); + if (response && !response.error) { + setUserInfo(response); + } + }); + getBalanceFunc(); + chrome.runtime.sendMessage( + { action: "getWalletInfo" }, + (response) => { + if (response && response?.walletInfo) { + setRawWallet(response?.walletInfo); + } + } + ); + } else if (response?.error) { + setIsLoading(false); + setWalletToBeDecryptedError(response.error); + } } - }, (response) => { - if (response && !response?.error) { - setAuthenticatePassword(""); - setExtstate("authenticated"); - setWalletToBeDecryptedError('') - chrome.runtime.sendMessage({ action: "userInfo" }, (response) => { - setIsLoading(false) - if (response && !response.error) { - setUserInfo(response); + ); + } catch (error) { + setWalletToBeDecryptedError("Unable to authenticate. Wrong password"); + } + }; + + // const handleBeforeUnload = (e)=> { + // const shouldClose = confirm('Are you sure you want to close this window? You may have unsaved changes.'); + + // if (!shouldClose) { + // // Prevent the window from closing + // e.preventDefault(); + // e.returnValue = ''; // Required for Chrome + // } else { + // // Allow the window to close + // // No need to call preventDefault here; returnValue must be left empty + // } + // } + + // useEffect(()=> { + // window.addEventListener('beforeunload', handleBeforeUnload); + + // return ()=> { + // window.removeEventListener('beforeunload', handleBeforeUnload); + // } + // }, []) + + useEffect(() => { + if (!isMainWindow) return; + const handleBeforeUnload = (e) => { + e.preventDefault(); + e.returnValue = ""; // This is required for Chrome to display the confirmation dialog. + }; + + // Add the event listener when the component mounts + window.addEventListener("beforeunload", handleBeforeUnload); + + // Clean up the event listener when the component unmounts + return () => { + window.removeEventListener("beforeunload", handleBeforeUnload); + }; + }, []); + + useEffect(() => { + if (!isMainWindow) return; + // Handler for when the window gains focus + const handleFocus = () => { + setIsFocused(true); + console.log("Webview is focused"); + }; + + // Handler for when the window loses focus + const handleBlur = () => { + setIsFocused(false); + console.log("Webview is not focused"); + }; + + // Attach the event listeners + window.addEventListener("focus", handleFocus); + window.addEventListener("blur", handleBlur); + + // Optionally, listen for visibility changes + const handleVisibilityChange = () => { + if (document.visibilityState === "visible") { + setIsFocused(true); + console.log("Webview is visible"); + } else { + setIsFocused(false); + console.log("Webview is hidden"); + } + }; + + document.addEventListener("visibilitychange", handleVisibilityChange); + + // Cleanup the event listeners on component unmount + return () => { + window.removeEventListener("focus", handleFocus); + window.removeEventListener("blur", handleBlur); + document.removeEventListener("visibilitychange", handleVisibilityChange); + }; + }, []); + + const registerName = async () => { + try { + if (!userInfo?.address) throw new Error("Your address was not found"); + const fee = await getFee("REGISTER_NAME"); + await show({ + message: "Would you like to register this name?", + publishFee: fee.fee + " QORT", + }); + setIsLoadingRegisterName(true); + new Promise((res, rej) => { + chrome.runtime.sendMessage( + { + action: "registerName", + payload: { + name: registerNameValue, + }, + }, + (response) => { + + if (!response?.error) { + res(response); + setIsLoadingRegisterName(false); + setInfoSnack({ + type: "success", + message: + "Successfully registered. It may take a couple of minutes for the changes to propagate", + }); + setOpenRegisterName(false); + setRegisterNameValue(""); + setOpenSnack(true); + setTxList((prev) => [ + { + ...response, + type: "register-name", + label: `Registered name: awaiting confirmation. This may take a couple minutes.`, + labelDone: `Registered name: success!`, + done: false, + }, + ...prev.filter((item) => !item.done), + ]); + return; } - }); - getBalanceFunc(); - chrome.runtime.sendMessage({ action: "getWalletInfo" }, (response) => { - if (response && response?.walletInfo) { - setRawWallet(response?.walletInfo); - } - }); - } else if (response?.error) { - setIsLoading(false) - setWalletToBeDecryptedError(response.error) - } + setInfoSnack({ + type: "error", + message: response?.error, + }); + setOpenSnack(true); + rej(response.error); + } + ); }); } catch (error) { - setWalletToBeDecryptedError('Unable to authenticate. Wrong password') + if (error?.message) { + setInfoSnack({ + type: "error", + message: error?.message, + }); + } + } finally { + setIsLoadingRegisterName(false); } - } + }; return ( + {/* {extState === 'group' && ( + + )} */} + {extState === "not-authenticated" && ( <> -

+
@@ -709,168 +1127,361 @@ function App() { }} /> + {hasLocalNode && ( + <> + + + + { + setOpenAdvancedSettings(true); + }} + > + Advanced settings + + + {openAdvancedSettings && ( + <> + + { + setUseLocalNode(event.target.checked); + }} + disabled={confirmUseOfLocal} + sx={{ + "&.Mui-checked": { + color: "white", // Customize the color when checked + }, + "& .MuiSvgIcon-root": { + color: "white", + }, + }} + /> + + Use local node + + {useLocalNode && ( + <> + + + + {apiKey} + + + + + )} + + )} + + + )} )} {/* {extState !== "not-authenticated" && ( )} */} - {extState === "authenticated" && ( - - - + {extState === "authenticated" && isMainWindow && ( + + + + + + - {authenticatedMode === 'ltc' ? ( - <> - - - - - {rawWallet?.ltcAddress?.slice(0, 6)}... - {rawWallet?.ltcAddress?.slice(-4)} - - - - {ltcBalanceLoading && } - {!isNaN(+ltcBalance) && !ltcBalanceLoading && ( - + {authenticatedMode === "ltc" ? ( + <> + + + + + {rawWallet?.ltcAddress?.slice(0, 6)}... + {rawWallet?.ltcAddress?.slice(-4)} + + + + {ltcBalanceLoading && ( + + )} + {!isNaN(+ltcBalance) && !ltcBalanceLoading && ( + + + {ltcBalance} LTC + + + + )} + + ) : ( + <> + + - {ltcBalance} LTC + {userInfo?.name} - - + + + + {rawWallet?.address0?.slice(0, 6)}... + {rawWallet?.address0?.slice(-4)} + + + + {qortBalanceLoading && ( + + )} + {!qortBalanceLoading && balance >= 0 && ( + + + {balance?.toFixed(2)} QORT + + + + )} + + {userInfo && !userInfo?.name && ( + { + setOpenRegisterName(true); + }} + > + REGISTER NAME + + )} + + { + setExtstate("send-qort"); + }} + > + Transfer QORT + + )} - - - ) : ( - <> - - - {userInfo?.name} - - - - - {rawWallet?.address0?.slice(0, 6)}... - {rawWallet?.address0?.slice(-4)} - - - - {qortBalanceLoading && } - {!qortBalanceLoading && (balance >= 0) && ( - - - {balance?.toFixed(2)} QORT - - - - )} - - { - setExtstate("send-qort"); + chrome.tabs.create({ url: "https://www.qort.trade" }); }} > - Transfer QORT - - - )} - { - chrome.tabs.create({ url: 'https://www.qort.trade' }); - }} - > - Get QORT at qort.trade - - - - - { - setExtstate("download-wallet"); - }} - src={Download} - style={{ - cursor: "pointer", - }} - /> - - - - {authenticatedMode === 'qort' && ( - { - setAuthenticatedMode('ltc') - }} src={ltcLogo} style={{ - cursor: "pointer", - width: '20px', - height: 'auto' - }} /> - )} - {authenticatedMode === 'ltc' && ( - { - setAuthenticatedMode('qort') - }} src={qortLogo} style={{ - cursor: "pointer", - width: '20px', - height: 'auto' - }} /> - )} - - + Get QORT at qort.trade + + + + + { + setExtstate("download-wallet"); + }} + src={Download} + style={{ + cursor: "pointer", + }} + /> + + + + {authenticatedMode === "qort" && ( + { + setAuthenticatedMode("ltc"); + }} + src={ltcLogo} + style={{ + cursor: "pointer", + width: "20px", + height: "auto", + }} + /> + )} + {authenticatedMode === "ltc" && ( + { + setAuthenticatedMode("qort"); + }} + src={qortLogo} + style={{ + cursor: "pointer", + width: "20px", + height: "auto", + }} + /> + )} + + + + + + + )} - {extState === "send-qort" && ( + {extState === "send-qort" && isMainWindow && ( <> )} - {extState === "web-app-request-buy-order" && ( + {extState === "web-app-request-buy-order" && !isMainWindow && ( <> @@ -1023,7 +1634,8 @@ function App() { fontWeight: 700, }} > - {requestBuyOrder?.crosschainAtInfo?.expectedForeignAmount} {requestBuyOrder?.crosschainAtInfo?.foreignBlockchain} + {requestBuyOrder?.crosschainAtInfo?.expectedForeignAmount}{" "} + {requestBuyOrder?.crosschainAtInfo?.foreignBlockchain} {/* @@ -1064,7 +1676,7 @@ function App() { {sendPaymentError} )} - {extState === "web-app-request-payment" && ( + {extState === "web-app-request-payment" && !isMainWindow && ( <> @@ -1138,13 +1750,16 @@ function App() { {sendPaymentError} )} - {extState === "web-app-request-connection" && ( + {extState === "web-app-request-connection" && !isMainWindow && ( <> -
+
@@ -1198,13 +1813,16 @@ function App() { )} - {extState === "web-app-request-authentication" && ( + {extState === "web-app-request-authentication" && !isMainWindow && ( <> -
+
@@ -1242,7 +1860,7 @@ function App() { )} - {rawWallet && extState === 'wallet-dropped' && ( + {rawWallet && extState === "wallet-dropped" && ( <> -
+
@@ -1302,9 +1923,7 @@ function App() { - setAuthenticatePassword(e.target.value) - } + onChange={(e) => setAuthenticatePassword(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { authenticateWallet(); @@ -1312,12 +1931,10 @@ function App() { }} /> - + Authenticate - - {walletToBeDecryptedError} - + {walletToBeDecryptedError} )} @@ -1342,10 +1959,13 @@ function App() { /> -
+
@@ -1386,9 +2006,7 @@ function App() { Confirm password - - {walletToBeDownloadedError} - + {walletToBeDownloadedError} )} @@ -1420,16 +2038,19 @@ function App() { cursor: "pointer", }} onClick={() => { - setExtstate("not-authenticated") + setExtstate("not-authenticated"); }} src={Return} /> -
+
@@ -1471,9 +2092,7 @@ function App() { Create Account - - {walletToBeDownloadedError} - + {walletToBeDownloadedError} )} @@ -1597,6 +2216,83 @@ function App() { )} {isLoading && } + {isShow && ( + + {"Publish"} + + + {message.message} + + + publish fee: {message.publishFee} + + + + + + + + )} + { + setOpenRegisterName(false); + setRegisterNameValue(""); + }} + anchorOrigin={{ + vertical: "bottom", + horizontal: "center", + }} + transformOrigin={{ + vertical: "top", + horizontal: "center", + }} + style={{ marginTop: "8px" }} + > + + + setRegisterNameValue(e.target.value)} + value={registerNameValue} + placeholder="Choose a name" + /> + + + Register Name + + + + ); } diff --git a/src/assets/svgs/ArrowDown.svg b/src/assets/svgs/ArrowDown.svg new file mode 100644 index 0000000..99dfdcd --- /dev/null +++ b/src/assets/svgs/ArrowDown.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svgs/Attachment.svg b/src/assets/svgs/Attachment.svg new file mode 100644 index 0000000..394344b --- /dev/null +++ b/src/assets/svgs/Attachment.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svgs/Check.svg b/src/assets/svgs/Check.svg new file mode 100644 index 0000000..fb6cf50 --- /dev/null +++ b/src/assets/svgs/Check.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svgs/ComposeIcon copy.svg b/src/assets/svgs/ComposeIcon copy.svg new file mode 100644 index 0000000..e2fc3fc --- /dev/null +++ b/src/assets/svgs/ComposeIcon copy.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/svgs/ComposeIcon.svg b/src/assets/svgs/ComposeIcon.svg new file mode 100644 index 0000000..e2fc3fc --- /dev/null +++ b/src/assets/svgs/ComposeIcon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/svgs/CreateThreadIcon.tsx b/src/assets/svgs/CreateThreadIcon.tsx new file mode 100644 index 0000000..549ec2e --- /dev/null +++ b/src/assets/svgs/CreateThreadIcon.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { styled } from '@mui/system'; +import { SVGProps } from './interfaces'; + +// Create a styled container with hover effects +const SvgContainer = styled('svg')({ + '& path': { + fill: 'rgba(41, 41, 43, 1)', // Default to red if no color prop + } +}); + +export const CreateThreadIcon:React.FC = ({ color, opacity }) => { + return ( + + + + + + ); +}; diff --git a/src/assets/svgs/ModalClose.svg b/src/assets/svgs/ModalClose.svg new file mode 100644 index 0000000..1a8b18e --- /dev/null +++ b/src/assets/svgs/ModalClose.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svgs/More.svg b/src/assets/svgs/More.svg new file mode 100644 index 0000000..84b0158 --- /dev/null +++ b/src/assets/svgs/More.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svgs/SendNewMessage.tsx b/src/assets/svgs/SendNewMessage.tsx new file mode 100644 index 0000000..33d7e86 --- /dev/null +++ b/src/assets/svgs/SendNewMessage.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { styled } from '@mui/system'; +import { SVGProps } from './interfaces'; + +// Create a styled container with hover effects +const SvgContainer = styled('svg')({ + '& path': { + fill: 'rgba(41, 41, 43, 1)', // Default to red if no color prop + } +}); + +export const SendNewMessage:React.FC = ({ color, opacity }) => { + return ( + + + + + ); +}; diff --git a/src/assets/svgs/Sort.svg b/src/assets/svgs/Sort.svg new file mode 100644 index 0000000..c399db8 --- /dev/null +++ b/src/assets/svgs/Sort.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/svgs/interfaces.ts b/src/assets/svgs/interfaces.ts new file mode 100644 index 0000000..0cbd14f --- /dev/null +++ b/src/assets/svgs/interfaces.ts @@ -0,0 +1,6 @@ +export interface SVGProps { + color: string + height: string + width: string + opacity?: number +} diff --git a/src/background.ts b/src/background.ts index d60c23a..dae068c 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,12 +1,90 @@ // @ts-nocheck +// import { encryptAndPublishSymmetricKeyGroupChat } from "./backgroundFunctions/encryption"; +import { + decryptGroupEncryption, + encryptAndPublishSymmetricKeyGroupChat, + publishGroupEncryptedResource, + uint8ArrayToObject, +} from "./backgroundFunctions/encryption"; +import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from "./constants/codes"; +import { QORT_DECIMALS } from "./constants/constants"; import Base58 from "./deps/Base58"; +import { + base64ToUint8Array, + decryptSingle, + encryptSingle, + objectToBase64, +} from "./qdn/encryption/group-encryption"; +import { reusableGet } from "./qdn/publish/pubish"; import { signChat } from "./transactions/signChat"; import { createTransaction } from "./transactions/transactions"; import { decryptChatMessage } from "./utils/decryptChatMessage"; import { decryptStoredWallet } from "./utils/decryptWallet"; import PhraseWallet from "./utils/generateWallet/phrase-wallet"; import { validateAddress } from "./utils/validateAddress"; -import { Sha256 } from 'asmcrypto.js' +import { Sha256 } from "asmcrypto.js"; + +let lastGroupNotification +export const groupApi = "https://ext-node.qortal.link" +export const groupApiSocket = "wss://ext-node.qortal.link" +export const groupApiLocal = "http://127.0.0.1:12391" +export const groupApiSocketLocal = "ws://127.0.0.1:12391" +const timeDifferenceForNotificationChatsBackground = 600000 + +const checkDifference = (createdTimestamp)=> { + return (Date.now() - createdTimestamp) < timeDifferenceForNotificationChatsBackground +} +const getApiKeyFromStorage = async () => { + return new Promise((resolve, reject) => { + chrome.storage.local.get('apiKey', (result) => { + if (chrome.runtime.lastError) { + return reject(chrome.runtime.lastError); + } + resolve(result.apiKey || null); // Return null if apiKey isn't found + }); + }); +}; + +export const getBaseApi = async (customApi?: string) => { + + + if (customApi) { + return customApi; + } + + const apiKey = await getApiKeyFromStorage(); // Retrieve apiKey asynchronously + if (apiKey) { + return groupApiLocal; + } else { + return groupApi; + } +}; + +export const createEndpointSocket = async (endpoint) => { + const apiKey = await getApiKeyFromStorage(); // Retrieve apiKey asynchronously + + if (apiKey) { + return `${groupApiSocketLocal}${endpoint}`; + } else { + return `${groupApiSocket}${endpoint}`; + } +}; + +export const createEndpoint = async (endpoint, customApi) => { + if (customApi) { + return `${customApi}${endpoint}`; + } + + const apiKey = await getApiKeyFromStorage(); // Retrieve apiKey asynchronously + + if (apiKey) { + // Check if the endpoint already contains a query string + const separator = endpoint.includes('?') ? '&' : '?'; + return `${groupApiLocal}${endpoint}${separator}apiKey=${apiKey}`; + } else { + return `${groupApi}${endpoint}`; + } +}; export const walletVersion = 2; @@ -22,13 +100,20 @@ const apiEndpoints = [ "https://apinode4.qortalnodes.live", ]; -const buyTradeNodeBaseUrl = 'https://appnode.qortal.org' -const proxyAccountAddress = 'QXPejUe5Za1KD3zCMViWCX35AreMQ9H7ku' -const proxyAccountPublicKey = '5hP6stDWybojoDw5t8z9D51nV945oMPX7qBd29rhX1G7' +const buyTradeNodeBaseUrl = "https://appnode.qortal.org"; +const proxyAccountAddress = "QXPejUe5Za1KD3zCMViWCX35AreMQ9H7ku"; +const proxyAccountPublicKey = "5hP6stDWybojoDw5t8z9D51nV945oMPX7qBd29rhX1G7"; const pendingResponses = new Map(); +let groups = null +let socket +let timeoutId; +let groupSocketTimeout; +let socketTimeout: any; +let interval +let intervalThreads // Function to check each API endpoint -async function findUsableApi() { +export async function findUsableApi() { for (const endpoint of apiEndpoints) { try { const response = await fetch(`${endpoint}/admin/status`); @@ -49,10 +134,603 @@ async function findUsableApi() { throw new Error("No usable API found"); } + +async function checkWebviewFocus() { + return new Promise((resolve) => { + // Set a timeout for 1 second + const timeout = setTimeout(() => { + resolve(false); // No response within 1 second, assume not focused + }, 1000); + + // Send message to the content script to check focus + chrome.runtime.sendMessage({ action: 'CHECK_FOCUS' }, (response) => { + clearTimeout(timeout); // Clear the timeout if we get a response + + if (chrome.runtime.lastError) { + resolve(false); // Error occurred, assume not focused + } else { + resolve(response); // Resolve based on the response + } + }); + }); +} + +function playNotificationSound() { + chrome.runtime.sendMessage({ action: 'PLAY_NOTIFICATION_SOUND' }); +} + +const handleNotificationDirect = async (directs)=> { + let isFocused + const wallet = await getSaveWallet(); + const address = wallet.address0; + const dataDirects = directs.filter((direct)=> direct?.sender !== address) + try { + + isFocused = await checkWebviewFocus() + + if(isFocused){ + throw new Error('isFocused') + } + const newActiveChats= dataDirects + const oldActiveChats = await getChatHeadsDirect() + +if(newActiveChats?.length === 0) return + +let newestLatestTimestamp +let oldestLatestTimestamp +// Find the latest timestamp from newActiveChats +newActiveChats?.forEach(newChat => { +if (!newestLatestTimestamp || newChat?.timestamp > newestLatestTimestamp?.timestamp) { + newestLatestTimestamp = newChat; +} +}); + +// Find the latest timestamp from oldActiveChats +oldActiveChats?.forEach(oldChat => { +if (!oldestLatestTimestamp || oldChat?.timestamp > oldestLatestTimestamp?.timestamp) { + oldestLatestTimestamp = oldChat; +} +}); + + + if(checkDifference(newestLatestTimestamp.timestamp) && !oldestLatestTimestamp || (newestLatestTimestamp && newestLatestTimestamp?.timestamp > oldestLatestTimestamp?.timestamp)){ + const notificationId = 'chat_notification_' + Date.now() + '_type=direct' + `_from=${newestLatestTimestamp.address}`; + chrome.notifications.create(notificationId, { + type: 'basic', + iconUrl: 'qort.png', // Add an appropriate icon for chat notifications + title: `New Direct message! ${newestLatestTimestamp?.name && `from ${newestLatestTimestamp.name}`}`, + message: 'You have received a new direct message', + priority: 2, // Use the maximum priority to ensure it's noticeable + // buttons: [ + // { title: 'Go to group' } + // ] + }); + setTimeout(() => { + chrome.notifications.clear(notificationId); + }, 7000); + // chrome.runtime.sendMessage( + // { + // action: "notification", + // payload: { + // }, + // } + // ) + // audio.play(); + playNotificationSound() + + + + } + + } catch (error) { + + if(!isFocused){ + chrome.runtime.sendMessage( + { + action: "notification", + payload: { + }, + }, + (response) => { + + if (!response?.error) { + + } + + } + ); + const notificationId = 'chat_notification_' + Date.now() + chrome.notifications.create(notificationId, { + type: 'basic', + iconUrl: 'qort.png', // Add an appropriate icon for chat notifications + title: `New Direct message!`, + message: 'You have received a new direct message', + priority: 2, // Use the maximum priority to ensure it's noticeable + // buttons: [ + // { title: 'Go to group' } + // ] + }); + setTimeout(() => { + chrome.notifications.clear(notificationId); + }, 7000); + playNotificationSound() + // audio.play(); + // } + } + + } finally { + setChatHeadsDirect(dataDirects) + // chrome.runtime.sendMessage( + // { + // action: "setChatHeads", + // payload: { + // data, + // }, + // } + // ); + + } +} +async function getThreadActivity(){ + const wallet = await getSaveWallet(); + const address = wallet.address0; + const key = `threadactivity-${address}` + const res = await chrome.storage.local.get([key]); +if (res?.[key]) { + const parsedData = JSON.parse(res[key]) + return parsedData; +} else { + return null +} +} + +async function updateThreadActivity({threadId, qortalName, groupId, thread}) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const ONE_WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000; // One week in milliseconds + let lastResetTime = 0 + // Retrieve the last reset timestamp from storage + const key = `threadactivity-${address}` + + chrome.storage.local.get([key], (data) => { + let threads + + if (!data[key] || Object.keys(data?.[key]?.length === 0)) { + threads = { createdThreads: [], mostVisitedThreads: [], recentThreads: [] }; + } else { + threads = JSON.parse(data[key]) + } + if(threads?.lastResetTime){ + lastResetTime = threads.lastResetTime + } + + const currentTime = Date.now(); + + // Check if a week has passed since the last reset + if (!lastResetTime || currentTime - lastResetTime > ONE_WEEK_IN_MS) { + // Reset the visit counts for all most visited threads + threads.mostVisitedThreads.forEach(thread => thread.visitCount = 0); + lastResetTime = currentTime; // Update the last reset time + threads.lastResetTime = lastResetTime + } + + // Update the recent threads list + threads.recentThreads = threads.recentThreads.filter(t => t.threadId !== threadId); + threads.recentThreads.unshift({ threadId, qortalName, groupId, thread, visitCount: 1, lastVisited: Date.now() }); + + // Sort the recent threads by lastVisited time (descending) + threads.recentThreads.sort((a, b) => b.lastVisited - a.lastVisited); + // Limit the recent threads list to 2 items + threads.recentThreads = threads.recentThreads.slice(0, 2); + + // Update the most visited threads list + const existingThread = threads.mostVisitedThreads.find(t => t.threadId === threadId); + if (existingThread) { + existingThread.visitCount += 1; + existingThread.lastVisited = Date.now(); // Update the last visited time as well + } else { + threads.mostVisitedThreads.push({ threadId, qortalName, groupId, thread, visitCount: 1, lastVisited: Date.now() }); + } + + // Sort the most visited threads by visitCount (descending) + threads.mostVisitedThreads.sort((a, b) => b.visitCount - a.visitCount); + // Limit the most visited threads list to 2 items + threads.mostVisitedThreads = threads.mostVisitedThreads.slice(0, 2); + + // Store the updated thread information and last reset time + // chrome.storage.local.set({ threads, lastResetTime }); + + const dataString = JSON.stringify(threads); + chrome.storage.local.set({ [`threadactivity-${address}`]: dataString }) + }); +} + + +const handleNotification = async (groups)=> { + const wallet = await getSaveWallet(); + const address = wallet.address0; + let isFocused + const data = groups.filter((group)=> group?.sender !== address) + try { + if(!data || data?.length === 0) return + isFocused = await checkWebviewFocus() + + if(isFocused){ + throw new Error('isFocused') + } + const newActiveChats= data + const oldActiveChats = await getChatHeads() + + + let results = [] + let newestLatestTimestamp + let oldestLatestTimestamp + // Find the latest timestamp from newActiveChats + newActiveChats?.forEach(newChat => { + if (!newestLatestTimestamp || newChat?.timestamp > newestLatestTimestamp?.timestamp) { + newestLatestTimestamp = newChat; + } + }); + + // Find the latest timestamp from oldActiveChats + oldActiveChats?.forEach(oldChat => { + if (!oldestLatestTimestamp || oldChat?.timestamp > oldestLatestTimestamp?.timestamp) { + oldestLatestTimestamp = oldChat; + } + }); + + + if(checkDifference(newestLatestTimestamp.timestamp) && !oldestLatestTimestamp || (newestLatestTimestamp && newestLatestTimestamp?.timestamp > oldestLatestTimestamp?.timestamp)){ + if (!lastGroupNotification || ((Date.now() - lastGroupNotification) >= 120000)) { + + const notificationId = 'chat_notification_' + Date.now() + '_type=group' + `_from=${newestLatestTimestamp.groupId}`; + + chrome.notifications.create(notificationId, { + type: 'basic', + iconUrl: 'qort.png', // Add an appropriate icon for chat notifications + title: 'New Group Message!', + message: `You have received a new message from ${newestLatestTimestamp?.groupName}`, + priority: 2, // Use the maximum priority to ensure it's noticeable + // buttons: [ + // { title: 'Go to group' } + // ] + }); + setTimeout(() => { + chrome.notifications.clear(notificationId); + }, 7000); + // chrome.runtime.sendMessage( + // { + // action: "notification", + // payload: { + // }, + // } + // ) + // audio.play(); + playNotificationSound() + lastGroupNotification = Date.now() + + } + } + + } catch (error) { + + if(!isFocused){ + chrome.runtime.sendMessage( + { + action: "notification", + payload: { + }, + }, + (response) => { + + if (!response?.error) { + + } + + } + ); + const notificationId = 'chat_notification_' + Date.now(); + chrome.notifications.create(notificationId, { + type: 'basic', + iconUrl: 'qort.png', // Add an appropriate icon for chat notifications + title: 'New Group Message!', + message: 'You have received a new message from one of your groups', + priority: 2, // Use the maximum priority to ensure it's noticeable + // buttons: [ + // { title: 'Go to group' } + // ] + }); + setTimeout(() => { + chrome.notifications.clear(notificationId); + }, 7000); + playNotificationSound() + // audio.play(); + lastGroupNotification = Date.now() + // } + } + + } finally { + if(!data || data?.length === 0) return + setChatHeads(data) + // chrome.runtime.sendMessage( + // { + // action: "setChatHeads", + // payload: { + // data, + // }, + // } + // ); + + } +} + +const checkThreads = async (bringBack) => { + try { + + let myName = "" + const userData = await getUserInfo() + if(userData?.name){ + myName = userData.name + } + let newAnnouncements = [] + let dataToBringBack = [] + const threadActivity = await getThreadActivity() + if(!threadActivity) return null + + const selectedThreads = [ + ...threadActivity.createdThreads.slice(0, 2), + ...threadActivity.mostVisitedThreads.slice(0, 2), + ...threadActivity.recentThreads.slice(0, 2), + ] + + if(selectedThreads?.length === 0) return null + const tempData = { + + } + for (const thread of selectedThreads){ + try { + const identifier = `thmsg-${thread?.threadId}` + const name = thread?.qortalName + const url = await createEndpoint(`/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=1&includemetadata=false&offset=${0}&reverse=true&prefix=true`); + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }) + const responseData = await response.json() + + const latestMessage = responseData.filter((pub)=> pub?.name !== myName)[0] + // const latestMessage = responseData[0] + + if (!latestMessage) { + continue + } + + if(checkDifference(latestMessage.created) && latestMessage.created > thread?.lastVisited && (!thread?.lastNotified || thread?.lastNotified < thread?.created)){ + tempData[thread.threadId] = latestMessage.created + newAnnouncements.push(thread) + + } + if(latestMessage.created > thread?.lastVisited){ + dataToBringBack.push(thread) + + } + } catch (error) { + conosle.log({error}) + } + } + + + if(bringBack){ + + return dataToBringBack + } + + const updateThreadWithLastNotified = { + ...threadActivity, + createdThreads: (threadActivity?.createdThreads || [])?.map((item)=> { + if(tempData[item.threadId]){ + return { + ...item, + lastNotified: tempData[item.threadId] + } + } else { + return item + } + + }), + mostVisitedThreads: (threadActivity?.mostVisitedThreads || [])?.map((item)=> { + if(tempData[item.threadId]){ + return { + ...item, + lastNotified: tempData[item.threadId] + } + } else { + return item + } + }), + recentThreads: (threadActivity?.recentThreads || [])?.map((item)=> { + if(tempData[item.threadId]){ + return { + ...item, + lastNotified: tempData[item.threadId] + } + } else { + return item + } + }), + } + + const wallet = await getSaveWallet(); + const address = wallet.address0; + const dataString = JSON.stringify(updateThreadWithLastNotified); + chrome.storage.local.set({ [`threadactivity-${address}`]: dataString }) + + + + if(newAnnouncements.length > 0){ + const notificationId = 'chat_notification_' + Date.now() + '_type=thread-post' + `_data=${JSON.stringify(newAnnouncements[0])}`; + + chrome.notifications.create(notificationId, { + type: 'basic', + iconUrl: 'qort.png', // Add an appropriate icon for chat notifications + title: `New thread post!`, + message: `New post in ${newAnnouncements[0]?.thread?.threadData?.title}`, + priority: 2, // Use the maximum priority to ensure it's noticeable + // buttons: [ + // { title: 'Go to group' } + // ] + }); + setTimeout(() => { + chrome.notifications.clear(notificationId); + }, 7000); + playNotificationSound() + } + const savedtimestampAfter = await getTimestampGroupAnnouncement() + chrome.runtime.sendMessage({ + action: "SET_GROUP_ANNOUNCEMENTS", + payload: savedtimestampAfter, + }); + } catch (error) { + + } finally { + } +} +const checkNewMessages = + async () => { + try { + let myName = "" + const userData = await getUserInfo() + if(userData?.name){ + myName = userData.name + } + + let newAnnouncements = [] + const activeData = await getStoredData('active-groups-directs') || { groups: [], directs: [] }; + const groups = activeData?.groups + if(!groups || groups?.length === 0) return + const savedtimestamp = await getTimestampGroupAnnouncement() + + for (const group of groups){ + try { + + const identifier = `grp-${group.groupId}-anc-`; + const url = await createEndpoint(`/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=1&includemetadata=false&offset=${0}&reverse=true&prefix=true`); + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }) + const responseData = await response.json() + + const latestMessage = responseData.filter((pub)=> pub?.name !== myName)[0] + if (!latestMessage) { + continue + } + + if(checkDifference(latestMessage.created) && !savedtimestamp[group.groupId] || latestMessage.created > savedtimestamp?.[group.groupId]?.notification){ + newAnnouncements.push(group) + await addTimestampGroupAnnouncement({groupId: group.groupId, timestamp: Date.now()}) + //save new timestamp + } + } catch (error) { + + } + } + if(newAnnouncements.length > 0){ + const notificationId = 'chat_notification_' + Date.now() + '_type=group-announcement' + `_from=${newAnnouncements[0]?.groupId}`; + + chrome.notifications.create(notificationId, { + type: 'basic', + iconUrl: 'qort.png', // Add an appropriate icon for chat notifications + title: `New group announcement!`, + message: `You have received a new announcement from ${newAnnouncements[0]?.groupName}`, + priority: 2, // Use the maximum priority to ensure it's noticeable + // buttons: [ + // { title: 'Go to group' } + // ] + }); + setTimeout(() => { + chrome.notifications.clear(notificationId); + }, 7000); + playNotificationSound() + } + const savedtimestampAfter = await getTimestampGroupAnnouncement() + chrome.runtime.sendMessage({ + action: "SET_GROUP_ANNOUNCEMENTS", + payload: savedtimestampAfter, + }); + } catch (error) { + + } finally { + } + } + +const listenForNewGroupAnnouncements = async ()=> { + try { + setTimeout(() => { + checkNewMessages() + }, 500); + if(interval){ + clearInterval(interval) + } + + let isCalling = false + interval = setInterval(async () => { + if (isCalling) return + isCalling = true + const res = await checkNewMessages() + isCalling = false + }, 180000) + } catch (error) { + + } +} +const listenForThreadUpdates = async ()=> { + try { + setTimeout(() => { + checkThreads() + }, 500); + if(intervalThreads){ + clearInterval(intervalThreads) + } + + let isCalling = false + intervalThreads = setInterval(async () => { + if (isCalling) return + isCalling = true + const res = await checkThreads() + isCalling = false + }, 60000) + } catch (error) { + + } +} + + + + +const forceCloseWebSocket = () => { + if (socket) { + + + clearTimeout(timeoutId); + clearTimeout(groupSocketTimeout); + clearTimeout(socketTimeout); + timeoutId = null + groupSocketTimeout = null + socket.close(1000, 'forced') + socket = null + } +}; + + async function getNameInfo() { const wallet = await getSaveWallet(); const address = wallet.address0; - const validApi = await findUsableApi(); + const validApi = await getBaseApi() const response = await fetch(validApi + "/names/address/" + address); const nameData = await response.json(); if (nameData?.length > 0) { @@ -62,7 +740,7 @@ async function getNameInfo() { } } async function getAddressInfo(address) { - const validApi = await findUsableApi(); + const validApi = await getBaseApi() const response = await fetch(validApi + "/addresses/" + address); const data = await response.json(); @@ -112,7 +790,9 @@ async function connection(hostname) { } async function getTradeInfo(qortalAtAddress) { - const response = await fetch(buyTradeNodeBaseUrl + "/crosschain/trade/" + qortalAtAddress); + const response = await fetch( + buyTradeNodeBaseUrl + "/crosschain/trade/" + qortalAtAddress + ); if (!response?.ok) throw new Error("Cannot crosschain trade information"); const data = await response.json(); return data; @@ -121,7 +801,7 @@ async function getTradeInfo(qortalAtAddress) { async function getBalanceInfo() { const wallet = await getSaveWallet(); const address = wallet.address0; - const validApi = await findUsableApi(); + const validApi = await getBaseApi() const response = await fetch(validApi + "/addresses/balance/" + address); if (!response?.ok) throw new Error("Cannot fetch balance"); @@ -131,28 +811,26 @@ async function getBalanceInfo() { async function getLTCBalance() { const wallet = await getSaveWallet(); let _url = `${buyTradeNodeBaseUrl}/crosschain/ltc/walletbalance`; - const keyPair = await getKeyPair() - const parsedKeyPair = JSON.parse(keyPair) - let _body = parsedKeyPair.ltcPublicKey + const keyPair = await getKeyPair(); + const parsedKeyPair = JSON.parse(keyPair); + let _body = parsedKeyPair.ltcPublicKey; const response = await fetch(_url, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json' + "Content-Type": "application/json", }, - body: _body -}); -if(response?.ok){ - const data = await response.text(); - const dataLTCBalance = (Number(data) / 1e8).toFixed(8); - return +dataLTCBalance - -} else throw new Error('Onable to get LTC balance') - + body: _body, + }); + if (response?.ok) { + const data = await response.text(); + const dataLTCBalance = (Number(data) / 1e8).toFixed(8); + return +dataLTCBalance; + } else throw new Error("Onable to get LTC balance"); } -const processTransactionVersion2Chat = async (body: any, validApi: string) => { +const processTransactionVersion2Chat = async (body: any, customApi) => { // const validApi = await findUsableApi(); - const url = validApi + "/transactions/process?apiVersion=2"; + const url = await createEndpoint("/transactions/process?apiVersion=2", customApi); return fetch(url, { method: "POST", headers: {}, @@ -167,24 +845,41 @@ const processTransactionVersion2Chat = async (body: any, validApi: string) => { }); }; +const processTransactionVersion2 = async (body: any) => { + const url = await createEndpoint(`/transactions/process?apiVersion=2`); + + try { + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", // Ensure the body is correctly parsed + }, + body, // Convert body to JSON string + }); + + // if (!response.ok) { + // // If the response is not successful (status code is not 2xx) + // throw new Error(`HTTP error! Status: ${response.status}`); + // } -const processTransactionVersion2 = async (body: any, validApi: string) => { - // const validApi = await findUsableApi(); - const url = validApi + "/transactions/process?apiVersion=2"; - return fetch(url, { - method: "POST", - headers: {}, - body, - }).then(async (response) => { try { const json = await response.clone().json(); return json; - } catch (e) { - return await response.text(); + } catch (jsonError) { + try { + const text = await response.text(); + return text + } catch (textError) { + throw new Error(`Failed to parse response as both JSON and text.`); + } } - }); + } catch (error) { + console.error("Error processing transaction:", error); + throw error; // Re-throw the error after logging it + } }; + const transaction = async ( { type, params, apiVersion, keyPair }: any, validApi @@ -196,9 +891,13 @@ const transaction = async ( const signedBytes = Base58.encode(tx.signedBytes); res = await processTransactionVersion2(signedBytes, validApi); } + let success = true + if(res?.error){ + success = false + } return { - success: true, + success, data: res, }; }; @@ -232,7 +931,7 @@ const makeTransactionRequest = async ( const getLastRef = async () => { const wallet = await getSaveWallet(); const address = wallet.address0; - const validApi = await findUsableApi(); + const validApi = await getBaseApi() const response = await fetch( validApi + "/addresses/lastreference/" + address ); @@ -241,7 +940,7 @@ const getLastRef = async () => { return data; }; const sendQortFee = async () => { - const validApi = await findUsableApi(); + const validApi = await getBaseApi() const response = await fetch( validApi + "/transactions/unitfee?txType=PAYMENT" ); @@ -254,13 +953,14 @@ const sendQortFee = async () => { const qortFee = (Number(data) / 1e8).toFixed(8); return qortFee; }; + async function getNameOrAddress(receiver) { try { const isAddress = validateAddress(receiver); if (isAddress) { return receiver; } - const validApi = await findUsableApi(); + const validApi = await getBaseApi() const response = await fetch(validApi + "/names/" + receiver); const data = await response.json(); @@ -275,21 +975,40 @@ async function getNameOrAddress(receiver) { } } +async function getPublicKey(receiver) { + try { + const validApi = await getBaseApi() + + const response = await fetch(validApi + "/addresses/publickey/" + receiver); + if (!response?.ok) throw new Error("Cannot fetch recipient's public key"); + + const data = await response.text(); + if (!data?.error && data !== 'false') return data; + if (data?.error) { + throw new Error("Cannot fetch recipient's public key"); + } + throw new Error("Cannot fetch recipient's public key"); + } catch (error) { + throw new Error(error?.message || "cannot validate address or name"); + } +} + async function decryptWallet({ password, wallet, walletVersion }) { try { const response = await decryptStoredWallet(password, wallet); const wallet2 = new PhraseWallet(response, walletVersion); const keyPair = wallet2._addresses[0].keyPair; - const ltcPrivateKey = wallet2._addresses[0].ltcWallet.derivedMasterPrivateKey - const ltcPublicKey = wallet2._addresses[0].ltcWallet.derivedMasterPublicKey - const ltcAddress = wallet2._addresses[0].ltcWallet.address + const ltcPrivateKey = + wallet2._addresses[0].ltcWallet.derivedMasterPrivateKey; + const ltcPublicKey = wallet2._addresses[0].ltcWallet.derivedMasterPublicKey; + const ltcAddress = wallet2._addresses[0].ltcWallet.address; const toSave = { privateKey: Base58.encode(keyPair.privateKey), publicKey: Base58.encode(keyPair.publicKey), ltcPrivateKey: ltcPrivateKey, - ltcPublicKey : ltcPublicKey - } - const dataString = JSON.stringify(toSave) + ltcPublicKey: ltcPublicKey, + }; + const dataString = JSON.stringify(toSave); await new Promise((resolve, reject) => { chrome.storage.local.set({ keyPair: dataString }, () => { if (chrome.runtime.lastError) { @@ -302,8 +1021,8 @@ async function decryptWallet({ password, wallet, walletVersion }) { const newWallet = { ...wallet, publicKey: Base58.encode(keyPair.publicKey), - ltcAddress: ltcAddress - } + ltcAddress: ltcAddress, + }; await new Promise((resolve, reject) => { chrome.storage.local.set({ walletInfo: newWallet }, () => { if (chrome.runtime.lastError) { @@ -316,142 +1035,388 @@ async function decryptWallet({ password, wallet, walletVersion }) { return true; } catch (error) { - console.log({ error }) + throw new Error(error.message); } } -async function signChatFunc(chatBytesArray, chatNonce, validApi, keyPair) { - let response +async function signChatFunc(chatBytesArray, chatNonce, customApi, keyPair) { + let response; try { - const signedChatBytes = signChat( - chatBytesArray, - chatNonce, - keyPair - ) - const res = await processTransactionVersion2Chat(signedChatBytes, validApi) - response = res + const signedChatBytes = signChat(chatBytesArray, chatNonce, keyPair); + + const res = await processTransactionVersion2Chat(signedChatBytes, customApi); + response = res; } catch (e) { - console.error(e) - console.error(e.message) - response = false + console.error(e); + console.error(e.message); + response = false; } - return response + return response; } function sbrk(size, heap) { - let brk = 512 * 1024 // stack top - let old = brk - brk += size - if (brk > heap.length) throw new Error('heap exhausted') - return old + let brk = 512 * 1024; // stack top + let old = brk; + brk += size; + if (brk > heap.length) throw new Error("heap exhausted"); + return old; } const computePow = async ({ chatBytes, path, difficulty }) => { - let response = null + let response = null; await new Promise((resolve, reject) => { const _chatBytesArray = Object.keys(chatBytes).map(function (key) { - return chatBytes[key] - }) - const chatBytesArray = new Uint8Array(_chatBytesArray) - const chatBytesHash = new Sha256().process(chatBytesArray).finish().result - const memory = new WebAssembly.Memory({ initial: 256, maximum: 256 }) - const heap = new Uint8Array(memory.buffer) + return chatBytes[key]; + }); + const chatBytesArray = new Uint8Array(_chatBytesArray); + const chatBytesHash = new Sha256().process(chatBytesArray).finish().result; + const memory = new WebAssembly.Memory({ initial: 256, maximum: 256 }); + const heap = new Uint8Array(memory.buffer); - const hashPtr = sbrk(32, heap) - const hashAry = new Uint8Array(memory.buffer, hashPtr, 32) - hashAry.set(chatBytesHash) - const workBufferLength = 8 * 1024 * 1024 - const workBufferPtr = sbrk(workBufferLength, heap) + const hashPtr = sbrk(32, heap); + const hashAry = new Uint8Array(memory.buffer, hashPtr, 32); + hashAry.set(chatBytesHash); + const workBufferLength = 8 * 1024 * 1024; + const workBufferPtr = sbrk(workBufferLength, heap); const importObject = { env: { - memory: memory - } - } + memory: memory, + }, + }; function loadWebAssembly(filename, imports) { // Fetch the file and compile it - return fetch(filename).then(response => response.arrayBuffer()).then(buffer => WebAssembly.compile(buffer)).then(module => { - // Create the instance. - return new WebAssembly.Instance(module, importObject) - }) + return fetch(filename) + .then((response) => response.arrayBuffer()) + .then((buffer) => WebAssembly.compile(buffer)) + .then((module) => { + // Create the instance. + return new WebAssembly.Instance(module, importObject); + }); } - loadWebAssembly(path) - .then(wasmModule => { - response = { - nonce: wasmModule.exports.compute2(hashPtr, workBufferPtr, workBufferLength, difficulty), chatBytesArray - } - resolve() - }) - }) - return response + loadWebAssembly(path).then((wasmModule) => { + response = { + nonce: wasmModule.exports.compute2( + hashPtr, + workBufferPtr, + workBufferLength, + difficulty + ), + chatBytesArray, + }; + resolve(); + }); + }); + return response; +}; + +const getStoredData = async (key) => { + return new Promise((resolve, reject) => { + chrome.storage.local.get(key, (result) => { + if (chrome.runtime.lastError) { + return reject(chrome.runtime.lastError); + } + resolve(result[key]); + }); + }); +}; + +async function handleActiveGroupDataFromSocket({groups, directs}){ + try { + + chrome.runtime.sendMessage({ + action: "SET_GROUPS", + payload: groups, + }); + chrome.runtime.sendMessage({ + action: "SET_DIRECTS", + payload: directs, + }); + groups = groups + directs = directs + const activeData = { + groups: groups || [], // Your groups data here + directs: directs || [] // Your directs data here + }; + + // Save the active data to localStorage + chrome.storage.local.set({ 'active-groups-directs': activeData }); + try { + handleNotification(groups) + handleNotificationDirect(directs) + + } catch (error) { + + } + } catch (error) { + + } } async function sendChat({ qortAddress, recipientPublicKey, message }) { - let _reference = new Uint8Array(64); self.crypto.getRandomValues(_reference); - let sendTimestamp = Date.now() + let sendTimestamp = Date.now(); - let reference = Base58.encode(_reference) - const resKeyPair = await getKeyPair() - const parsedData = JSON.parse(resKeyPair) + let reference = Base58.encode(_reference); + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); const uint8PrivateKey = Base58.decode(parsedData.privateKey); const uint8PublicKey = Base58.decode(parsedData.publicKey); const keyPair = { privateKey: uint8PrivateKey, - publicKey: uint8PublicKey + publicKey: uint8PublicKey, }; - const balance = await getBalanceInfo() - const hasEnoughBalance = +balance < 4 ? false : true - const difficulty = 8 + const balance = await getBalanceInfo(); + const hasEnoughBalance = +balance < 4 ? false : true; + const difficulty = 8; const jsonData = { atAddress: message.atAddress, foreignKey: message.foreignKey, - receivingAddress: message.receivingAddress + receivingAddress: message.receivingAddress, }; const finalJson = { callRequest: jsonData, - extra: "whatever additional data goes here" + extra: "whatever additional data goes here", }; - const messageStringified = JSON.stringify(finalJson) - - const tx = await createTransaction( - 18, - keyPair, - { - timestamp: sendTimestamp, - recipient: qortAddress, - recipientPublicKey: recipientPublicKey, - hasChatReference: 0, - message: messageStringified, - lastReference: reference, - proofOfWorkNonce: 0, - isEncrypted: 1, - isText: 1 - }, + const messageStringified = JSON.stringify(finalJson); - ) - if(!hasEnoughBalance){ - const _encryptedMessage = tx._encryptedMessage - const encryptedMessageToBase58 = Base58.encode(_encryptedMessage) + const tx = await createTransaction(18, keyPair, { + timestamp: sendTimestamp, + recipient: qortAddress, + recipientPublicKey: recipientPublicKey, + hasChatReference: 0, + message: messageStringified, + lastReference: reference, + proofOfWorkNonce: 0, + isEncrypted: 1, + isText: 1, + }); + if (!hasEnoughBalance) { + const _encryptedMessage = tx._encryptedMessage; + const encryptedMessageToBase58 = Base58.encode(_encryptedMessage); return { encryptedMessageToBase58, - signature: 'id-' + Date.now() + '-' + Math.floor(Math.random() * 1000), - reference + signature: "id-" + Date.now() + "-" + Math.floor(Math.random() * 1000), + reference, + }; + } + const path = chrome.runtime.getURL("memory-pow.wasm.full"); + + const { nonce, chatBytesArray } = await computePow({ + chatBytes: tx.chatBytes, + path, + difficulty, + }); + let _response = await signChatFunc( + chatBytesArray, + nonce, + "https://appnode.qortal.org", + keyPair + ); + if (_response?.error) { + throw new Error(_response?.message); + } + return _response; +} + + +async function sendChatGroup({ + groupId, + typeMessage, + chatReference, + messageText, +}) { + let _reference = new Uint8Array(64); + self.crypto.getRandomValues(_reference); + + let reference = Base58.encode(_reference); + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + const balance = await getBalanceInfo(); + const hasEnoughBalance = +balance < 4 ? false : true; + const difficulty = 8; + + const tx = await createTransaction(181, keyPair, { + timestamp: Date.now(), + groupID: Number(groupId), + hasReceipient: 0, + hasChatReference: typeMessage === "edit" ? 1 : 0, + // chatReference: chatReference, + message: messageText, + lastReference: reference, + proofOfWorkNonce: 0, + isEncrypted: 0, // Set default to not encrypted for groups + isText: 1, + }); + + if (!hasEnoughBalance) { + throw new Error("Must have at least 4 QORT to send a chat message"); + } + const path = chrome.runtime.getURL("memory-pow.wasm.full"); + + const { nonce, chatBytesArray } = await computePow({ + chatBytes: tx.chatBytes, + path, + difficulty, + }); + let _response = await signChatFunc( + chatBytesArray, + nonce, + null, + keyPair + ); + if (_response?.error) { + throw new Error(_response?.message); + } + return _response; +} + +async function sendChatDirect({ + directTo, + typeMessage, + chatReference, + messageText, +}) { + + + const recipientAddress = await getNameOrAddress(directTo) + const recipientPublicKey = await getPublicKey(recipientAddress) + + if(!recipientPublicKey) throw new Error('Cannot retrieve publickey') + + let _reference = new Uint8Array(64); + self.crypto.getRandomValues(_reference); + + let reference = Base58.encode(_reference); + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + const balance = await getBalanceInfo(); + const hasEnoughBalance = +balance < 4 ? false : true; + + const difficulty = 8; + + const finalJson = { + message: messageText, + version: 2, + }; + const messageStringified = JSON.stringify(finalJson); + const tx = await createTransaction(18, keyPair, { + timestamp: Date.now(), + recipient: recipientAddress, + recipientPublicKey: recipientPublicKey, + hasChatReference: 0, + message: messageStringified, + lastReference: reference, + proofOfWorkNonce: 0, + isEncrypted: 1, + isText: 1, + }); + + if (!hasEnoughBalance) { + throw new Error("Must have at least 4 QORT to send a chat message"); + } + const path = chrome.runtime.getURL("memory-pow.wasm.full"); + + + const { nonce, chatBytesArray } = await computePow({ + chatBytes: tx.chatBytes, + path, + difficulty, + }); + + let _response = await signChatFunc( + chatBytesArray, + nonce, + null, + keyPair + ); + if (_response?.error) { + throw new Error(_response?.message); + } + return _response; +} + +async function decryptSingleFunc({ messages, secretKeyObject, skipDecodeBase64 }) { + let holdMessages = []; + + for (const message of messages) { + try { + const res = await decryptSingle({ + data64: message.data, + secretKeyObject, + skipDecodeBase64 + }); + + const decryptToUnit8Array = base64ToUint8Array(res); + const responseData = uint8ArrayToObject(decryptToUnit8Array); + holdMessages.push({ ...message, text: responseData }); + } catch (error) { + } } - const path = chrome.runtime.getURL('memory-pow.wasm.full'); + return holdMessages; +} +async function decryptSingleForPublishes({ messages, secretKeyObject, skipDecodeBase64 }) { + let holdMessages = []; - + for (const message of messages) { + try { + const res = await decryptSingle({ + data64: message.data, + secretKeyObject, + skipDecodeBase64 + }); + + const decryptToUnit8Array = base64ToUint8Array(res); + const responseData = uint8ArrayToObject(decryptToUnit8Array); + holdMessages.push({ ...message, decryptedData: responseData }); + } catch (error) { - const { nonce, chatBytesArray } = await computePow({ chatBytes: tx.chatBytes, path, difficulty }) - let _response = await signChatFunc(chatBytesArray, - nonce, "https://appnode.qortal.org", keyPair - ) - if (_response?.error) { - throw new Error(_response?.message) + } } - return _response + return holdMessages; +} + +async function decryptDirectFunc({ messages, involvingAddress }) { + const senderPublicKey = await getPublicKey(involvingAddress) + let holdMessages = []; + + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + for (const message of messages) { + try { + const decodedMessage = decryptChatMessage( + message.data, + keyPair.privateKey, + senderPublicKey, + message.reference + ); + const parsedMessage = JSON.parse(decodedMessage); + holdMessages.push({ ...message, ...parsedMessage }); + } catch (error) { + + } + } + return holdMessages; } async function createBuyOrderTx({ crosschainAtInfo }) { @@ -459,63 +1424,450 @@ async function createBuyOrderTx({ crosschainAtInfo }) { const wallet = await getSaveWallet(); const address = wallet.address0; - const resKeyPair = await getKeyPair() - const parsedData = JSON.parse(resKeyPair) + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); const message = { atAddress: crosschainAtInfo.qortalAtAddress, foreignKey: parsedData.ltcPrivateKey, - receivingAddress: address - } - const res = await sendChat({ qortAddress: proxyAccountAddress, recipientPublicKey: proxyAccountPublicKey, message }) + receivingAddress: address, + }; + const res = await sendChat({ + qortAddress: proxyAccountAddress, + recipientPublicKey: proxyAccountPublicKey, + message, + }); if (res?.signature) { - listenForChatMessageForBuyOrder({ nodeBaseUrl: buyTradeNodeBaseUrl, senderAddress: proxyAccountAddress, senderPublicKey: proxyAccountPublicKey, signature: res?.signature, - - }) - if(res?.encryptedMessageToBase58){ - return { atAddress: crosschainAtInfo.qortalAtAddress, encryptedMessageToBase58: res?.encryptedMessageToBase58, node: buyTradeNodeBaseUrl, qortAddress: address, chatSignature: res?.signature, senderPublicKey: parsedData.publicKey, sender: address, reference: res?.reference } + }); + if (res?.encryptedMessageToBase58) { + return { + atAddress: crosschainAtInfo.qortalAtAddress, + encryptedMessageToBase58: res?.encryptedMessageToBase58, + node: buyTradeNodeBaseUrl, + qortAddress: address, + chatSignature: res?.signature, + senderPublicKey: parsedData.publicKey, + sender: address, + reference: res?.reference, + }; } - return { atAddress: crosschainAtInfo.qortalAtAddress, chatSignature: res?.signature, node: buyTradeNodeBaseUrl, qortAddress: address } + return { + atAddress: crosschainAtInfo.qortalAtAddress, + chatSignature: res?.signature, + node: buyTradeNodeBaseUrl, + qortAddress: address, + }; } else { - throw new Error("Unable to send buy order message") + throw new Error("Unable to send buy order message"); } - } catch (error) { throw new Error(error.message); } } +async function sendChatNotification(res, groupId, secretKeyObject, numberOfMembers){ + try { +const data = await objectToBase64({ + type: "notification", + subType: "new-group-encryption", + data: { + timestamp: res.timestamp, + name: res.name, + message: `${res.name} has updated the encryption key`, + numberOfMembers + }, +}) + + encryptSingle({ + data64: data, + secretKeyObject: secretKeyObject, + }) + .then((res2) => { + + sendChatGroup({ + groupId, + typeMessage: undefined, + chatReference: undefined, + messageText: res2, + }) + .then(() => {}) + .catch((error) => { + console.error('1',error.message); + }); + }) + .catch((error) => { + console.error('2',error.message); + + }); + } catch (error) { + + } +} + +export const getFee = async(txType)=> { + + const timestamp = Date.now() + const data = await reusableGet(`/transactions/unitfee?txType=${txType}×tamp=${timestamp}`) + const arbitraryFee = (Number(data) / 1e8).toFixed(8) + + return { + timestamp, + fee: arbitraryFee + } + +} + +async function leaveGroup({groupId}){ + const wallet = await getSaveWallet(); + const address = wallet.address0; + const lastReference = await getLastRef() + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + const feeres = await getFee('LEAVE_GROUP') + + const tx = await createTransaction(32, keyPair, { + fee: feeres.fee, + registrantAddress: address, + rGroupId: groupId, + lastReference: lastReference, + + + + }); + + const signedBytes = Base58.encode(tx.signedBytes); + + const res = await processTransactionVersion2(signedBytes) + if(!res?.signature) throw new Error('Transaction was not able to be processed') + return res +} + +async function joinGroup({groupId}){ + const wallet = await getSaveWallet(); + const address = wallet.address0; + const lastReference = await getLastRef() + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + const feeres = await getFee('JOIN_GROUP') + + const tx = await createTransaction(31, keyPair, { + fee: feeres.fee, + registrantAddress: address, + rGroupId: groupId, + lastReference: lastReference, + + + + }); + + const signedBytes = Base58.encode(tx.signedBytes); + + const res = await processTransactionVersion2(signedBytes) + if(!res?.signature) throw new Error(res?.message || 'Transaction was not able to be processed') + return res +} + +async function cancelInvitationToGroup({groupId, qortalAddress}){ + const lastReference = await getLastRef() + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + const feeres = await getFee('CANCEL_GROUP_INVITE') + + const tx = await createTransaction(30, keyPair, { + fee: feeres.fee, + recipient: qortalAddress, + rGroupId: groupId, + lastReference: lastReference, + + }); + + const signedBytes = Base58.encode(tx.signedBytes); + + const res = await processTransactionVersion2(signedBytes) + if(!res?.signature) throw new Error('Transaction was not able to be processed') + return res +} + +async function cancelBan({groupId, qortalAddress}){ + const lastReference = await getLastRef() + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + const feeres = await getFee('CANCEL_GROUP_BAN') + + const tx = await createTransaction(27, keyPair, { + fee: feeres.fee, + recipient: qortalAddress, + rGroupId: groupId, + lastReference: lastReference, + + }); + + const signedBytes = Base58.encode(tx.signedBytes); + + const res = await processTransactionVersion2(signedBytes) + if(!res?.signature) throw new Error('Transaction was not able to be processed') + return res +} +async function registerName({name}){ + const lastReference = await getLastRef() + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + const feeres = await getFee('REGISTER_NAME') + + const tx = await createTransaction(3, keyPair, { + fee: feeres.fee, + name, + value: "", + lastReference: lastReference, + + }); + + const signedBytes = Base58.encode(tx.signedBytes); + + const res = await processTransactionVersion2(signedBytes) + if(!res?.signature) throw new Error('Transaction was not able to be processed') + return res +} +async function makeAdmin({groupId, qortalAddress}){ + const lastReference = await getLastRef() + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + const feeres = await getFee('ADD_GROUP_ADMIN') + + const tx = await createTransaction(24, keyPair, { + fee: feeres.fee, + recipient: qortalAddress, + rGroupId: groupId, + lastReference: lastReference, + + }); + + const signedBytes = Base58.encode(tx.signedBytes); + + const res = await processTransactionVersion2(signedBytes) + if(!res?.signature) throw new Error('Transaction was not able to be processed') + return res +} + +async function removeAdmin({groupId, qortalAddress}){ + const lastReference = await getLastRef() + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + const feeres = await getFee('REMOVE_GROUP_ADMIN') + + const tx = await createTransaction(25, keyPair, { + fee: feeres.fee, + recipient: qortalAddress, + rGroupId: groupId, + lastReference: lastReference, + + }); + + const signedBytes = Base58.encode(tx.signedBytes); + + const res = await processTransactionVersion2(signedBytes) + if(!res?.signature) throw new Error('Transaction was not able to be processed') + return res +} + +async function banFromGroup({groupId, qortalAddress, rBanReason = "", rBanTime}){ + const lastReference = await getLastRef() + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + const feeres = await getFee('GROUP_BAN') + + const tx = await createTransaction(26, keyPair, { + fee: feeres.fee, + recipient: qortalAddress, + rGroupId: groupId, + rBanReason: rBanReason, + rBanTime, + lastReference: lastReference, + + }); + + const signedBytes = Base58.encode(tx.signedBytes); + + const res = await processTransactionVersion2(signedBytes) + if(!res?.signature) throw new Error('Transaction was not able to be processed') + return res +} + + + +async function kickFromGroup({groupId, qortalAddress, rBanReason = ""}){ + const lastReference = await getLastRef() + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + const feeres = await getFee('GROUP_KICK') + + const tx = await createTransaction(28, keyPair, { + fee: feeres.fee, + recipient: qortalAddress, + rGroupId: groupId, + rBanReason: rBanReason, + lastReference: lastReference, + + }); + + const signedBytes = Base58.encode(tx.signedBytes); + + const res = await processTransactionVersion2(signedBytes) + if(!res?.signature) throw new Error('Transaction was not able to be processed') + return res +} + + + +async function createGroup({ groupName, groupDescription, groupType, groupApprovalThreshold, minBlock, maxBlock}){ + const wallet = await getSaveWallet(); + const address = wallet.address0; + if(!address) throw new Error('Cannot find user') + const lastReference = await getLastRef() + const feeres = await getFee('CREATE_GROUP') + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + + const tx = await createTransaction(22, keyPair, { + fee: feeres.fee, + registrantAddress: address, + rGroupName: groupName, + rGroupDesc: groupDescription, + rGroupType: groupType, + rGroupApprovalThreshold: groupApprovalThreshold, + rGroupMinimumBlockDelay: minBlock, + rGroupMaximumBlockDelay: maxBlock, + lastReference: lastReference, + + }); + + const signedBytes = Base58.encode(tx.signedBytes); + + const res = await processTransactionVersion2(signedBytes) + if(!res?.signature) throw new Error('Transaction was not able to be processed') + return res +} +async function inviteToGroup({groupId, qortalAddress, inviteTime}){ + const address = await getNameOrAddress(qortalAddress) + if(!address) throw new Error('Cannot find user') + const lastReference = await getLastRef() + const feeres = await getFee('GROUP_INVITE') + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + + + const tx = await createTransaction(29, keyPair, { + fee: feeres.fee, + recipient: address, + rGroupId: groupId, + rInviteTime: inviteTime, + lastReference: lastReference, + + }); + + const signedBytes = Base58.encode(tx.signedBytes); + + const res = await processTransactionVersion2(signedBytes) + if(!res?.signature) throw new Error('Transaction was not able to be processed') + return res +} + async function sendCoin({ password, amount, receiver }, skipConfirmPassword) { try { const confirmReceiver = await getNameOrAddress(receiver); if (confirmReceiver.error) throw new Error("Invalid receiver address or name"); const wallet = await getSaveWallet(); - let keyPair = '' + let keyPair = ""; if (skipConfirmPassword) { - const resKeyPair = await getKeyPair() - const parsedData = JSON.parse(resKeyPair) + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); const uint8PrivateKey = Base58.decode(parsedData.privateKey); const uint8PublicKey = Base58.decode(parsedData.publicKey); keyPair = { privateKey: uint8PrivateKey, - publicKey: uint8PublicKey + publicKey: uint8PublicKey, }; } else { const response = await decryptStoredWallet(password, wallet); const wallet2 = new PhraseWallet(response, walletVersion); - keyPair = wallet2._addresses[0].keyPair + keyPair = wallet2._addresses[0].keyPair; } - const lastRef = await getLastRef(); const fee = await sendQortFee(); - const validApi = await findUsableApi(); + const validApi = await findUsableApi() const res = await makeTransactionRequest( confirmReceiver, @@ -525,6 +1877,7 @@ async function sendCoin({ password, amount, receiver }, skipConfirmPassword) { keyPair, validApi ); + return { res, validApi }; } catch (error) { throw new Error(error.message); @@ -565,13 +1918,13 @@ async function fetchMessagesForBuyOrders(apiCall, signature, senderPublicKey) { let retryDelay = 2000; // Start with a 2-second delay const maxDuration = 360000 * 2; // Maximum duration set to 12 minutes const startTime = Date.now(); // Record the start time - let triedChatMessage = [] + let triedChatMessage = []; // Promise to handle polling logic - await new Promise((res)=> { + await new Promise((res) => { setTimeout(() => { - res() + res(); }, 40000); - }) + }); return new Promise((resolve, reject) => { const attemptFetch = async () => { if (Date.now() - startTime > maxDuration) { @@ -581,24 +1934,31 @@ async function fetchMessagesForBuyOrders(apiCall, signature, senderPublicKey) { try { const response = await fetch(apiCall); let data = await response.json(); - data = data.filter((item) => !triedChatMessage.includes(item.signature)) + data = data.filter( + (item) => !triedChatMessage.includes(item.signature) + ); if (data && data.length > 0) { - const encodedMessageObj = data[0] - const resKeyPair = await getKeyPair() - const parsedData = JSON.parse(resKeyPair) + const encodedMessageObj = data[0]; + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); const uint8PrivateKey = Base58.decode(parsedData.privateKey); const uint8PublicKey = Base58.decode(parsedData.publicKey); const keyPair = { privateKey: uint8PrivateKey, - publicKey: uint8PublicKey + publicKey: uint8PublicKey, }; - const decodedMessage = decryptChatMessage(encodedMessageObj.data, keyPair.privateKey, senderPublicKey, encodedMessageObj.reference) - const parsedMessage = JSON.parse(decodedMessage) + const decodedMessage = decryptChatMessage( + encodedMessageObj.data, + keyPair.privateKey, + senderPublicKey, + encodedMessageObj.reference + ); + const parsedMessage = JSON.parse(decodedMessage); if (parsedMessage?.extra?.chatRequestSignature === signature) { resolve(parsedMessage); } else { - triedChatMessage.push(encodedMessageObj.signature) + triedChatMessage.push(encodedMessageObj.signature); setTimeout(attemptFetch, retryDelay); retryDelay = Math.min(retryDelay * 2, 360000); // Ensure delay does not exceed 6 minutes } @@ -616,7 +1976,12 @@ async function fetchMessagesForBuyOrders(apiCall, signature, senderPublicKey) { }); } -async function listenForChatMessage({ nodeBaseUrl, senderAddress, senderPublicKey, timestamp }) { +async function listenForChatMessage({ + nodeBaseUrl, + senderAddress, + senderPublicKey, + timestamp, +}) { try { let validApi = ""; const checkIfNodeBaseUrlIsAcceptable = apiEndpoints.find( @@ -629,29 +1994,39 @@ async function listenForChatMessage({ nodeBaseUrl, senderAddress, senderPublicKe } const wallet = await getSaveWallet(); const address = wallet.address0; - const before = timestamp + 5000 - const after = timestamp - 5000 - const apiCall = `${validApi}/chat/messages?involving=${senderAddress}&involving=${address}&reverse=true&limit=1&before=${before}&after=${after}`; - const encodedMessageObj = await fetchMessages(apiCall) + const before = timestamp + 5000; + const after = timestamp - 5000; + const apiCall = `${validApi}/chat/messages?involving=${senderAddress}&involving=${address}&reverse=true&limit=1&before=${before}&after=${after}&encoding=BASE64`; + const encodedMessageObj = await fetchMessages(apiCall); - const resKeyPair = await getKeyPair() - const parsedData = JSON.parse(resKeyPair) + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); const uint8PrivateKey = Base58.decode(parsedData.privateKey); const uint8PublicKey = Base58.decode(parsedData.publicKey); const keyPair = { privateKey: uint8PrivateKey, - publicKey: uint8PublicKey + publicKey: uint8PublicKey, }; - const decodedMessage = decryptChatMessage(encodedMessageObj.data, keyPair.privateKey, senderPublicKey, encodedMessageObj.reference) + const decodedMessage = decryptChatMessage( + encodedMessageObj.data, + keyPair.privateKey, + senderPublicKey, + encodedMessageObj.reference + ); return { secretCode: decodedMessage }; } catch (error) { - console.error(error) + console.error(error); throw new Error(error.message); } } -async function listenForChatMessageForBuyOrder({ nodeBaseUrl, senderAddress, senderPublicKey, signature }) { +async function listenForChatMessageForBuyOrder({ + nodeBaseUrl, + senderAddress, + senderPublicKey, + signature, +}) { try { let validApi = ""; const checkIfNodeBaseUrlIsAcceptable = apiEndpoints.find( @@ -664,10 +2039,14 @@ async function listenForChatMessageForBuyOrder({ nodeBaseUrl, senderAddress, sen } const wallet = await getSaveWallet(); const address = wallet.address0; - const before = Date.now() + 1200000 - const after = Date.now() - const apiCall = `${validApi}/chat/messages?involving=${senderAddress}&involving=${address}&reverse=true&limit=1&before=${before}&after=${after}`; - const parsedMessageObj = await fetchMessagesForBuyOrders(apiCall, signature, senderPublicKey) + const before = Date.now() + 1200000; + const after = Date.now(); + const apiCall = `${validApi}/chat/messages?involving=${senderAddress}&involving=${address}&reverse=true&limit=1&before=${before}&after=${after}&encoding=BASE64`; + const parsedMessageObj = await fetchMessagesForBuyOrders( + apiCall, + signature, + senderPublicKey + ); // const resKeyPair = await getKeyPair() // const parsedData = JSON.parse(resKeyPair) @@ -681,16 +2060,252 @@ async function listenForChatMessageForBuyOrder({ nodeBaseUrl, senderAddress, sen // const decodedMessage = decryptChatMessage(encodedMessageObj.data, keyPair.privateKey, senderPublicKey, encodedMessageObj.reference) // const parsedMessage = JSON.parse(decodedMessage) chrome.tabs.query({}, function (tabs) { - tabs.forEach(tab => { - chrome.tabs.sendMessage(tab.id, { type: "RESPONSE_FOR_TRADES", message: parsedMessageObj }); + tabs.forEach((tab) => { + chrome.tabs.sendMessage(tab.id, { + type: "RESPONSE_FOR_TRADES", + message: parsedMessageObj, + }); }); }); } catch (error) { - console.error(error) + console.error(error); throw new Error(error.message); } } +function removeDuplicateWindow(popupUrl){ + chrome.windows.getAll( + { populate: true, windowTypes: ["popup"] }, + (windows) => { + // Filter to find popups matching the specific URL + const existingPopupsPending = windows.filter( + (w) => + w.tabs && + w.tabs.some( + (tab) => tab.pendingUrl && tab.pendingUrl.startsWith(popupUrl) + ) + ); + const existingPopups = windows.filter( + (w) => + w.tabs && + w.tabs.some( + (tab) => tab.url && tab.url.startsWith(popupUrl) + ) + ); + + if(existingPopupsPending.length > 1){ + chrome.windows.remove(existingPopupsPending?.[0]?.tabs?.[0]?.windowId, () => { + + }); + } else if(existingPopupsPending.length > 0 && existingPopups.length > 0){ + chrome.windows.remove(existingPopupsPending?.[0]?.tabs?.[0]?.windowId, () => { + + }); + } + } + ); +} + + + +async function setChatHeads(data){ + const wallet = await getSaveWallet(); + const address = wallet.address0; + const dataString = JSON.stringify(data); + return await new Promise((resolve, reject) => { + chrome.storage.local.set({ [`chatheads-${address}`]: dataString }, () => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(true); + } + }); + }); +} + +async function getTempPublish(){ + const wallet = await getSaveWallet(); + const address = wallet.address0; + const key = `tempPublish-${address}` + const res = await chrome.storage.local.get([key]); + const SIX_MINUTES = 6 * 60 * 1000; // 6 minutes in milliseconds + + if (res?.[key]) { + const parsedData = JSON.parse(res[key]); + const currentTime = Date.now(); + + // Filter through each top-level key (e.g., "announcement") and then through its nested entries + const filteredData = Object.fromEntries( + Object.entries(parsedData).map(([category, entries]) => { + // Filter out entries inside each category that are older than 6 minutes + const filteredEntries = Object.fromEntries( + Object.entries(entries).filter(([entryKey, entryValue]) => { + return currentTime - entryValue.timestampSaved < SIX_MINUTES; + }) + ); + return [category, filteredEntries]; + }) + ); + + + if (JSON.stringify(filteredData) !== JSON.stringify(parsedData)) { + const dataString = JSON.stringify(filteredData); + await chrome.storage.local.set({ [key]: dataString }); + } + return filteredData; + } else { + return {}; + } +} + +async function saveTempPublish({data, key}){ + const existingTemp = await getTempPublish() + const wallet = await getSaveWallet(); + const address = wallet.address0; + const newTemp = { + ...existingTemp, + [key]: { + ...(existingTemp[key] || {}), + [data.identifier] : { + data, + timestampSaved: Date.now() + } + } + } + + const dataString = JSON.stringify(newTemp); + + return await new Promise((resolve, reject) => { + chrome.storage.local.set({ [`tempPublish-${address}`]: dataString }, () => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + + resolve(newTemp[key]); + } + }); + }); +} + +async function setChatHeadsDirect(data){ + const wallet = await getSaveWallet(); + const address = wallet.address0; + const dataString = JSON.stringify(data); + return await new Promise((resolve, reject) => { + chrome.storage.local.set({ [`chatheads-direct-${address}`]: dataString }, () => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(true); + } + }); + }); +} + + + +async function getTimestampEnterChat(){ + const wallet = await getSaveWallet(); + const address = wallet.address0; + const key = `enter-chat-timestamp-${address}` + const res = await chrome.storage.local.get([key]); +if (res?.[key]) { + const parsedData = JSON.parse(res[key]) + return parsedData; +} else { + return {} +} +} +async function getTimestampGroupAnnouncement(){ + const wallet = await getSaveWallet(); + const address = wallet.address0; + const key = `group-announcement-${address}` + const res = await chrome.storage.local.get([key]); +if (res?.[key]) { + const parsedData = JSON.parse(res[key]) + return parsedData; +} else { + return {} +} +} + +async function addTimestampGroupAnnouncement({groupId, timestamp, seenTimestamp}){ + const wallet = await getSaveWallet(); + const address = wallet.address0; + const data = await getTimestampGroupAnnouncement() || {} + data[groupId] = { + notification: timestamp, + seentimestamp: seenTimestamp ? true : false + } + const dataString = JSON.stringify(data); + return await new Promise((resolve, reject) => { + chrome.storage.local.set({ [`group-announcement-${address}`]: dataString }, () => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(true); + } + }); + }); +} + + +async function addTimestampEnterChat({groupId, timestamp}){ + const wallet = await getSaveWallet(); + const address = wallet.address0; + const data = await getTimestampEnterChat() + data[groupId] = timestamp + const dataString = JSON.stringify(data); + return await new Promise((resolve, reject) => { + chrome.storage.local.set({ [`enter-chat-timestamp-${address}`]: dataString }, () => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(true); + } + }); + }); +} + +async function notifyAdminRegenerateSecretKey({groupName, adminAddress}){ + const wallet = await getSaveWallet(); + const address = wallet.address0; + const name = await getNameInfo(address) + const nameOrAddress = name || address + await sendChatDirect({ + directTo: adminAddress, + typeMessage: undefined, + chatReference: undefined, + messageText: `

Member ${nameOrAddress} has requested that you regenerate the group's secret key. Group: ${groupName}

` + }) + return true +} + +async function getChatHeads(){ + const wallet = await getSaveWallet(); + const address = wallet.address0; + const key = `chatheads-${address}` + const res = await chrome.storage.local.get([key]); +if (res?.[key]) { + const parsedData = JSON.parse(res[key]) + return parsedData; +} else { + throw new Error("No Chatheads saved"); +} +} + +async function getChatHeadsDirect(){ + const wallet = await getSaveWallet(); + const address = wallet.address0; + const key = `chatheads-direct-${address}` + const res = await chrome.storage.local.get([key]); +if (res?.[key]) { + const parsedData = JSON.parse(res[key]) + return parsedData; +} else { + throw new Error("No Chatheads saved"); +} +} chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request) { switch (request.action) { @@ -708,20 +2323,21 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { }); break; case "getWalletInfo": - - getKeyPair().then(() => { - chrome.storage.local.get(["walletInfo"], (result) => { - if (chrome.runtime.lastError) { - sendResponse({ error: chrome.runtime.lastError.message }); - } else if (result.walletInfo) { - sendResponse({ walletInfo: result.walletInfo }); - } else { - sendResponse({ error: "No wallet info found" }); - } + getKeyPair() + .then(() => { + chrome.storage.local.get(["walletInfo"], (result) => { + if (chrome.runtime.lastError) { + sendResponse({ error: chrome.runtime.lastError.message }); + } else if (result.walletInfo) { + sendResponse({ walletInfo: result.walletInfo }); + } else { + sendResponse({ error: "No wallet info found" }); + } + }); + }) + .catch((error) => { + sendResponse({ error: error.message }); }); - }).catch((error) => { - sendResponse({ error: error.message }); - }) break; case "validApi": @@ -744,6 +2360,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { case "userInfo": getUserInfo() .then((name) => { + sendResponse(name); }) .catch((error) => { @@ -751,20 +2368,23 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { console.error(error.message); }); break; - case "decryptWallet": { - const { password, wallet } = request.payload; + case "decryptWallet": + { + const { password, wallet } = request.payload; - decryptWallet({ - password, wallet, walletVersion - }) - .then((hasDecrypted) => { - sendResponse(hasDecrypted); + decryptWallet({ + password, + wallet, + walletVersion, }) - .catch((error) => { - sendResponse({ error: error?.message }); - console.error(error.message); - }); - } + .then((hasDecrypted) => { + sendResponse(hasDecrypted); + }) + .catch((error) => { + sendResponse({ error: error?.message }); + console.error(error.message); + }); + } break; case "balance": @@ -776,23 +2396,26 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { console.error(error.message); }); break; - case "ltcBalance": { - getLTCBalance() - .then((balance) => { - sendResponse(balance); - }) - .catch((error) => { - console.error(error.message); - }); - - - } - break; + case "ltcBalance": + { + getLTCBalance() + .then((balance) => { + sendResponse(balance); + }) + .catch((error) => { + console.error(error.message); + }); + } + break; case "sendCoin": { const { receiver, password, amount } = request.payload; sendCoin({ receiver, password, amount }) - .then(() => { + .then(({res}) => { + if(!res?.success){ + sendResponse({ error: res?.data?.message }); + return + } sendResponse(true); }) .catch((error) => { @@ -802,11 +2425,202 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { } break; + case "inviteToGroup": + { + const { groupId, qortalAddress, inviteTime } = request.payload; + inviteToGroup({ groupId, qortalAddress, inviteTime }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + break; + case "saveTempPublish": + { + const { data, key } = request.payload; + saveTempPublish({ data, key }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + return true + } + break; + case "getTempPublish": + { + + getTempPublish() + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + + case "createGroup": + { + const { groupName, groupDescription, groupType, groupApprovalThreshold, minBlock, maxBlock } = request.payload; + createGroup({ groupName, groupDescription, groupType, groupApprovalThreshold, minBlock, maxBlock }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "cancelInvitationToGroup": + { + const { groupId, qortalAddress } = request.payload; + cancelInvitationToGroup({ groupId, qortalAddress }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "leaveGroup": + { + const { groupId } = request.payload; + leaveGroup({ groupId }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "joinGroup": + { + const { groupId } = request.payload; + joinGroup({ groupId }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + + case "kickFromGroup": + { + const { groupId, qortalAddress, rBanReason } = request.payload; + kickFromGroup({ groupId, qortalAddress, rBanReason }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "banFromGroup": + { + const { groupId, qortalAddress, rBanReason, rBanTime } = request.payload; + banFromGroup({ groupId, qortalAddress, rBanReason, rBanTime }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "cancelBan": + { + const { groupId, qortalAddress } = request.payload; + cancelBan({ groupId, qortalAddress}) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "registerName": + { + const { name } = request.payload; + registerName({ name}) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "makeAdmin": + { + const { groupId, qortalAddress } = request.payload; + makeAdmin({ groupId, qortalAddress}) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "removeAdmin": + { + const { groupId, qortalAddress } = request.payload; + removeAdmin({ groupId, qortalAddress}) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "oauth": { - const { nodeBaseUrl, senderAddress, senderPublicKey, timestamp } = request.payload; + const { nodeBaseUrl, senderAddress, senderPublicKey, timestamp } = + request.payload; - listenForChatMessage({ nodeBaseUrl, senderAddress, senderPublicKey, timestamp }) + listenForChatMessage({ + nodeBaseUrl, + senderAddress, + senderPublicKey, + timestamp, + }) .then(({ secretCode }) => { sendResponse(secretCode); }) @@ -817,6 +2631,149 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { break; } + case "setChatHeads": { + const { data} = + request.payload; + + setChatHeads({ + data + }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + + break; + } + case "getChatHeads": { + + getChatHeads() + .then((res) => { + + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + + break; + } + case "notification": { + const notificationId = 'chat_notification_' + Date.now(); // Create a unique ID + + const { } = + request.payload; + chrome.notifications.create(notificationId, { + type: 'basic', + iconUrl: 'qort.png', // Add an appropriate icon for chat notifications + title: 'New Group Message!', + message: 'You have received a new message from one of your groups', + priority: 2, // Use the maximum priority to ensure it's noticeable + // buttons: [ + // { title: 'Go to group' } + // ] + }); + // Set a timeout to clear the notification after 'timeout' milliseconds + setTimeout(() => { + chrome.notifications.clear(notificationId); + }, 3000); + sendResponse(true) + break; + } + case "addTimestampEnterChat": { + + const { groupId, timestamp } = + request.payload; + addTimestampEnterChat({groupId, timestamp}) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + break; + } + + case "setApiKey": { + const { payload } = request; + + + // Save the apiKey in chrome.storage.local for persistence + chrome.storage.local.set({ apiKey: payload }, () => { + + sendResponse(true) + }); + return true + break; + } + case "getApiKey": { + getApiKeyFromStorage() + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + return true + break; + } + case "notifyAdminRegenerateSecretKey": { + const { groupName, adminAddress } = + request.payload; + notifyAdminRegenerateSecretKey({groupName, adminAddress}) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + break; + } + + + case "addGroupNotificationTimestamp": { + + const { groupId, timestamp } = + request.payload; + addTimestampGroupAnnouncement({groupId, timestamp, seenTimestamp: true}) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + break; + } + case "getTimestampEnterChat": { + getTimestampEnterChat() + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + break; + } + case "getGroupNotificationTimestamp": { + getTimestampGroupAnnouncement() + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + break; + } case "authentication": { getSaveWallet() @@ -824,7 +2781,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { sendResponse(true); }) .catch((error) => { - const popupUrl = chrome.runtime.getURL("index.html"); + const popupUrl = chrome.runtime.getURL("index.html?secondary=true"); chrome.windows.getAll( { populate: true, windowTypes: ["popup"] }, @@ -860,12 +2817,14 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { (primaryDisplay.bounds.height - windowHeight) / 2; chrome.windows.create({ - url: chrome.runtime.getURL("index.html"), + url: chrome.runtime.getURL("index.html?secondary=true"), type: "popup", width: windowWidth, height: windowHeight, left: leftPosition, top: 0, + } , () => { + removeDuplicateWindow(popupUrl) }); }); } @@ -913,7 +2872,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { error: "User has not authenticated, try again.", }); clearInterval(intervalId); // Stop checking due to timeout - console.log("Timeout exceeded"); + // Handle timeout situation if needed } }; @@ -924,94 +2883,14 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { }); } break; - case "buyOrder": { - const { qortalAtAddress, hostname } = request.payload; - getTradeInfo(qortalAtAddress) - .then((crosschainAtInfo) => { - const popupUrl = chrome.runtime.getURL("index.html"); - - chrome.windows.getAll( - { populate: true, windowTypes: ["popup"] }, - (windows) => { - // Attempt to find an existing popup window that has a tab with the correct URL - const existingPopup = windows.find( - (w) => - w.tabs && - w.tabs.some( - (tab) => tab.url && tab.url.startsWith(popupUrl) - ) - ); - if (existingPopup) { - // If the popup exists but is minimized or not focused, focus it - chrome.windows.update(existingPopup.id, { - focused: true, - state: "normal", - }); - } else { - // No existing popup found, create a new one - chrome.system.display.getInfo((displays) => { - // Assuming the primary display is the first one (adjust logic as needed) - const primaryDisplay = displays[0]; - const screenWidth = primaryDisplay.bounds.width; - const windowHeight = 500; // Your window height - const windowWidth = 400; // Your window width - - // Calculate left position for the window to appear on the right of the screen - const leftPosition = screenWidth - windowWidth; - - // Calculate top position for the window, adjust as desired - const topPosition = - (primaryDisplay.bounds.height - windowHeight) / 2; - - chrome.windows.create({ - url: chrome.runtime.getURL("index.html"), - type: "popup", - width: windowWidth, - height: windowHeight, - left: leftPosition, - top: 0, - }); - }); - } - - const interactionId = Date.now().toString(); // Simple example; consider a better unique ID - - setTimeout(() => { - chrome.runtime.sendMessage({ - action: "SET_COUNTDOWN", - payload: request.timeout ? 0.9 * request.timeout : 20, - }); - chrome.runtime.sendMessage({ - action: "UPDATE_STATE_REQUEST_BUY_ORDER", - payload: { - hostname, - crosschainAtInfo, - interactionId, - }, - }); - }, 500); - - // Store sendResponse callback with the interaction ID - pendingResponses.set(interactionId, sendResponse); - } - ); - - - }) - .catch((error) => { - console.error(error.message); - }); - } - - break; - case "connection": { - const { hostname } = request.payload; - connection(hostname) - .then((isConnected) => { - if (Object.keys(isConnected)?.length > 0 && isConnected[hostname]) { - sendResponse(true); - } else { - const popupUrl = chrome.runtime.getURL("index.html"); + case "buyOrder": + { + + const { qortalAtAddress, hostname } = request.payload; + getTradeInfo(qortalAtAddress) + .then((crosschainAtInfo) => { + + const popupUrl = chrome.runtime.getURL("index.html?secondary=true") chrome.windows.getAll( { populate: true, windowTypes: ["popup"] }, @@ -1047,12 +2926,14 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { (primaryDisplay.bounds.height - windowHeight) / 2; chrome.windows.create({ - url: chrome.runtime.getURL("index.html"), + url: chrome.runtime.getURL("index.html?secondary=true"), type: "popup", width: windowWidth, height: windowHeight, left: leftPosition, top: 0, + }, () => { + removeDuplicateWindow(popupUrl) }); }); } @@ -1065,9 +2946,10 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { payload: request.timeout ? 0.9 * request.timeout : 20, }); chrome.runtime.sendMessage({ - action: "UPDATE_STATE_REQUEST_CONNECTION", + action: "UPDATE_STATE_REQUEST_BUY_ORDER", payload: { hostname, + crosschainAtInfo, interactionId, }, }); @@ -1077,18 +2959,109 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { pendingResponses.set(interactionId, sendResponse); } ); - } - }) - .catch((error) => { - console.error(error.message); - }); - } + }) + .catch((error) => { + console.error(error.message); + }); + } + + break; + case "connection": + { + + const { hostname } = request.payload; + + connection(hostname) + .then((isConnected) => { + if ( + Object.keys(isConnected)?.length > 0 && + isConnected[hostname] + ) { + sendResponse(true); + } else { + const popupUrl = chrome.runtime.getURL("index.html?secondary=true"); + chrome.windows.getAll( + { populate: true, windowTypes: ["popup"] }, + (windows) => { + // Attempt to find an existing popup window that has a tab with the correct URL + const existingPopup = windows.find( + (w) => + w.tabs && + w.tabs.some( + (tab) => tab.url && tab.url.startsWith(popupUrl) + ) + ); + + if (existingPopup) { + // If the popup exists but is minimized or not focused, focus it + chrome.windows.update(existingPopup.id, { + focused: true, + state: "normal", + }); + } else { + + + // No existing popup found, create a new one + chrome.system.display.getInfo((displays) => { + + // Assuming the primary display is the first one (adjust logic as needed) + const primaryDisplay = displays[0]; + const screenWidth = primaryDisplay.bounds.width; + const windowHeight = 500; // Your window height + const windowWidth = 400; // Your window width + + // Calculate left position for the window to appear on the right of the screen + const leftPosition = screenWidth - windowWidth; + + // Calculate top position for the window, adjust as desired + const topPosition = + (primaryDisplay.bounds.height - windowHeight) / 2; + + chrome.windows.create({ + url: popupUrl, + type: "popup", + width: windowWidth, + height: windowHeight, + left: leftPosition, + top: 0, + }, () => { + removeDuplicateWindow(popupUrl) + }); + }); + } + + const interactionId = Date.now().toString(); // Simple example; consider a better unique ID + + setTimeout(() => { + chrome.runtime.sendMessage({ + action: "SET_COUNTDOWN", + payload: request.timeout ? 0.9 * request.timeout : 20, + }); + chrome.runtime.sendMessage({ + action: "UPDATE_STATE_REQUEST_CONNECTION", + payload: { + hostname, + interactionId, + }, + }); + }, 500); + + // Store sendResponse callback with the interaction ID + pendingResponses.set(interactionId, sendResponse); + } + ); + } + }) + .catch((error) => { + console.error(error.message); + }); + } break; case "sendQort": { const { amount, hostname, address, description } = request.payload; - const popupUrl = chrome.runtime.getURL("index.html"); + const popupUrl = chrome.runtime.getURL("index.html?secondary=true"); chrome.windows.getAll( { populate: true, windowTypes: ["popup"] }, @@ -1122,12 +3095,14 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { (primaryDisplay.bounds.height - windowHeight) / 2; chrome.windows.create({ - url: chrome.runtime.getURL("index.html"), + url: chrome.runtime.getURL("index.html?secondary=true"), type: "popup", width: windowWidth, height: windowHeight, left: leftPosition, top: 0, + }, () => { + removeDuplicateWindow(popupUrl) }); }); } @@ -1212,49 +3187,307 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { sendResponse({ error: error.message }); originalSendResponse({ error: error.message }); }); - } break; - case "buyOrderConfirmation": { - const { crosschainAtInfo, isDecline } = request.payload; - const interactionId2 = request.payload.interactionId; - // Retrieve the stored sendResponse callback - const originalSendResponse = pendingResponses.get(interactionId2); + case "buyOrderConfirmation": + { + const { crosschainAtInfo, isDecline } = request.payload; + const interactionId2 = request.payload.interactionId; + // Retrieve the stored sendResponse callback + const originalSendResponse = pendingResponses.get(interactionId2); - if (originalSendResponse) { - if (isDecline) { - originalSendResponse({ error: "User has declined" }); - sendResponse(false); - pendingResponses.delete(interactionId2); - return; - } - createBuyOrderTx({ crosschainAtInfo }) - .then((res) => { - sendResponse(true); - originalSendResponse(res); + if (originalSendResponse) { + if (isDecline) { + originalSendResponse({ error: "User has declined" }); + sendResponse(false); pendingResponses.delete(interactionId2); - }) - .catch((error) => { - console.error(error.message); - sendResponse({ error: error.message }); - // originalSendResponse({ error: error.message }); - }); - + return; + } + createBuyOrderTx({ crosschainAtInfo }) + .then((res) => { + sendResponse(true); + originalSendResponse(res); + pendingResponses.delete(interactionId2); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + // originalSendResponse({ error: error.message }); + }); + } } + + break; + case "encryptAndPublishSymmetricKeyGroupChat": { + const { groupId, previousData, previousNumber } = request.payload; + + encryptAndPublishSymmetricKeyGroupChat({ + groupId, + previousData, + previousNumber, + }) + .then(({data, numberOfMembers}) => { + sendResponse(data); + + if(!previousData){ + // first secret key of the group + sendChatGroup({ + groupId, + typeMessage: undefined, + chatReference: undefined, + messageText: PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY, + }) + .then(() => {}) + .catch((error) => { + console.error('1',error.message); + }); + return + } + sendChatNotification(data, groupId, previousData, numberOfMembers) + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; + } + case "publishGroupEncryptedResource": { + const { encryptedData, identifier } = request.payload; + + publishGroupEncryptedResource({ + encryptedData, identifier + }) + .then((data) => { + sendResponse(data); + + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + return true + break; + } + case "handleActiveGroupDataFromSocket": { + const { groups, directs } = request.payload; + handleActiveGroupDataFromSocket({ + groups, directs + }) + .then((data) => { + sendResponse(true); + + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; + } + case "getThreadActivity": { + checkThreads(true) + .then((data) => { + + sendResponse(data); + + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; } + + case "updateThreadActivity": { + const { threadId, qortalName, groupId, thread } = request.payload; + + updateThreadActivity({threadId, qortalName, groupId, thread}) + .then(() => { + sendResponse(true); + + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); break; + } + case "decryptGroupEncryption": { + const { data } = request.payload; + + decryptGroupEncryption({ data }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; + } + case "encryptSingle": { + const { data, secretKeyObject } = request.payload; + + encryptSingle({ data64: data, secretKeyObject: secretKeyObject }) + .then((res) => { + + sendResponse(res); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; + } + case "decryptSingle": { + const { data, secretKeyObject, skipDecodeBase64 } = request.payload; + + decryptSingleFunc({ messages: data, secretKeyObject, skipDecodeBase64 }) + .then((res) => { + + sendResponse(res); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; + } + case "decryptSingleForPublishes": { + const { data, secretKeyObject, skipDecodeBase64 } = request.payload; + + decryptSingleForPublishes({ messages: data, secretKeyObject, skipDecodeBase64 }) + .then((res) => { + + sendResponse(res); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; + } + + case "decryptDirect": { + const { data, involvingAddress } = request.payload; + + decryptDirectFunc({ messages: data, involvingAddress }) + .then((res) => { + + sendResponse(res); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; + } + + case "sendChatGroup": { + const { + groupId, + typeMessage = undefined, + chatReference = undefined, + messageText, + } = request.payload; + + sendChatGroup({ groupId, typeMessage, chatReference, messageText }) + .then((res) => { + + sendResponse(res); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; + } + case "sendChatDirect": { + const { + directTo, + typeMessage = undefined, + chatReference = undefined, + messageText, + } = request.payload; + + sendChatDirect({ directTo, chatReference, messageText, typeMessage }) + .then((res) => { + + sendResponse(res); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; + } + case "setupGroupWebsocket": { + + checkNewMessages() + checkThreads() + + + // if(socket){ + // if(groups){ + // console.log('hasgroups1') + // chrome.runtime.sendMessage({ + // action: "SET_GROUPS", + // payload: groups, + // }); + // } + // if(directs){ + // console.log('hasgroups1') + // chrome.runtime.sendMessage({ + // action: "SET_DIRECTS", + // payload: directs, + // }); + // } + // sendResponse(true) + // return + // } + // setTimeout(() => { + // // initWebsocketMessageGroup() + // listenForNewGroupAnnouncements() + // listenForThreadUpdates() + // }, 200); + sendResponse(true) + + break; + } + case "logout": { - chrome.storage.local.remove(["keyPair", "walletInfo"], () => { + try { + const logoutFunc = async()=> { + forceCloseWebSocket() + if(interval){ + // for announcement notification + clearInterval(interval) + } + + const wallet = await getSaveWallet(); + const address = wallet.address0; + const key1 = `tempPublish-${address}` + + chrome.storage.local.remove(["keyPair", "walletInfo", "apiKey", "active-groups-directs", key1], () => { if (chrome.runtime.lastError) { // Handle error console.error(chrome.runtime.lastError.message); } else { chrome.tabs.query({}, function (tabs) { - tabs.forEach(tab => { + tabs.forEach((tab) => { chrome.tabs.sendMessage(tab.id, { type: "LOGOUT" }); }); }); @@ -1262,6 +3495,12 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { sendResponse(true); } }); + } + logoutFunc() + } catch (error) { + + } + } break; @@ -1270,16 +3509,42 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { return true; }); +// Function to save window position and size +const saveWindowBounds = (windowId) => { + chrome.windows.get(windowId, (window) => { + const { top, left, width, height } = window; + chrome.storage.local.set({ + windowBounds: { top, left, width, height } + }, () => { + console.log('Window bounds saved:', { top, left, width, height }); + }); + }); +}; + +// Function to restore window position and size +const restoreWindowBounds = (callback) => { + chrome.storage.local.get('windowBounds', (data) => { + if (data.windowBounds) { + callback(data.windowBounds); + } else { + callback(null); // No saved bounds, use default size/position + } + }); +}; + chrome.action.onClicked.addListener((tab) => { - const popupUrl = chrome.runtime.getURL("index.html"); + const popupUrl = chrome.runtime.getURL("index.html?main=true"); chrome.windows.getAll( { populate: true, windowTypes: ["popup"] }, (windows) => { // Attempt to find an existing popup window that has a tab with the correct URL const existingPopup = windows.find( (w) => - w.tabs && - w.tabs.some((tab) => tab.url && tab.url.startsWith(popupUrl)) + { + + return w.tabs && + w.tabs.some((tab) => tab.url && tab.url.startsWith(popupUrl)); + } ); if (existingPopup) { // If the popup exists but is minimized or not focused, focus it @@ -1288,27 +3553,40 @@ chrome.action.onClicked.addListener((tab) => { state: "normal", }); } else { - // No existing popup found, create a new one - chrome.system.display.getInfo((displays) => { - // Assuming the primary display is the first one (adjust logic as needed) - const primaryDisplay = displays[0]; - const screenWidth = primaryDisplay.bounds.width; - const windowHeight = 500; // Your window height - const windowWidth = 400; // Your window width + // No existing popup found, restore the saved bounds or create a new one + restoreWindowBounds((savedBounds) => { + chrome.system.display.getInfo((displays) => { + // Assuming the primary display is the first one (adjust logic as needed) + const primaryDisplay = displays[0]; + const screenWidth = primaryDisplay.bounds.width; + const screenHeight = primaryDisplay.bounds.height; - // Calculate left position for the window to appear on the right of the screen - const leftPosition = screenWidth - windowWidth; + // Create a new window that uses the saved bounds if available + chrome.windows.create({ + url: chrome.runtime.getURL("index.html?main=true"), + type: "popup", + width: savedBounds ? savedBounds.width : screenWidth, + height: savedBounds ? savedBounds.height : screenHeight, + left: savedBounds ? savedBounds.left : 0, + top: savedBounds ? savedBounds.top : 0, + }, (newWindow) => { + - // Calculate top position for the window, adjust as desired - const topPosition = (primaryDisplay.bounds.height - windowHeight) / 2; + // Listen for changes in the window's size or position and save them + chrome.windows.onBoundsChanged.addListener((window) => { + if (window.id === newWindow.id) { + saveWindowBounds(newWindow.id); + } + }); - chrome.windows.create({ - url: chrome.runtime.getURL("index.html"), - type: "popup", - width: windowWidth, - height: windowHeight, - left: leftPosition, - top: 0, + // Save the final window bounds when the window is closed + chrome.windows.onRemoved.addListener((windowId) => { + if (windowId === newWindow.id) { + + saveWindowBounds(windowId); // Save the position/size before it’s closed + } + }); + }); }); }); } @@ -1317,11 +3595,8 @@ chrome.action.onClicked.addListener((tab) => { setTimeout(() => { chrome.runtime.sendMessage({ - action: "UPDATE_STATE_REQUEST_CONNECTION", - payload: { - hostname, - interactionId, - }, + action: "INITIATE_MAIN", + payload: {}, }); }, 500); @@ -1330,3 +3605,240 @@ chrome.action.onClicked.addListener((tab) => { } ); }); + +const checkGroupList = async() => { + try { + + const wallet = await getSaveWallet(); + const address = wallet.address0; + const url = await createEndpoint(`/chat/active/${address}`); + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + const data = await response.json(); + + const filteredGroups = data.groups?.filter(item => item?.groupId !== 0) || []; + const sortedGroups = filteredGroups.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); + const sortedDirects = (data?.direct || []).filter(item => + item?.name !== 'extension-proxy' && item?.address !== 'QSMMGSgysEuqDCuLw3S4cHrQkBrh3vP3VH' + ).sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); + + handleActiveGroupDataFromSocket({ + groups: sortedGroups, + directs: sortedDirects + }) + } catch (error) { + console.error(error) + } finally { + } +} + +const checkActiveChatsForNotifications = async ()=> { + try { + + + const popupUrl = chrome.runtime.getURL("index.html?main=true"); + + chrome.windows.getAll( + { populate: true, windowTypes: ["popup"] }, + (windows) => { + // Attempt to find an existing popup window that has a tab with the correct URL + const existingPopup = windows.find( + (w) => { + + return w.tabs && w.tabs.some((tab) => tab.url && tab.url.startsWith(popupUrl)); + } + ); + + if (existingPopup) { + + } else { + checkGroupList() + + } + + + } + ); + } catch (error) { + + } +} +chrome.notifications.onClicked.addListener( (notificationId) => { + + const popupUrl = chrome.runtime.getURL("index.html?main=true"); + const isDirect = notificationId.includes('_type=direct_'); + const isGroup = notificationId.includes('_type=group_'); + const isGroupAnnouncement = notificationId.includes('_type=group-announcement_'); + const isNewThreadPost = notificationId.includes('_type=thread-post_'); + + let isExisting = false + chrome.windows.getAll( + { populate: true, windowTypes: ["popup"] }, + async (windows) => { + // Attempt to find an existing popup window that has a tab with the correct URL + const existingPopup = windows.find( + (w) => { + + return w.tabs && w.tabs.some((tab) => tab.url && tab.url.startsWith(popupUrl)); + } + ); + + if (existingPopup) { + // If the popup exists but is minimized or not focused, focus it + chrome.windows.update(existingPopup.id, { + focused: true, + state: "normal", + }); + isExisting = true + } else { + // No existing popup found, restore saved bounds or create a new one + restoreWindowBounds((savedBounds) => { + chrome.system.display.getInfo((displays) => { + // Assuming the primary display is the first one (adjust logic as needed) + const primaryDisplay = displays[0]; + const screenWidth = primaryDisplay.bounds.width; + const screenHeight = primaryDisplay.bounds.height; + + // Create a new window that takes up the full screen or uses saved bounds + chrome.windows.create({ + url: chrome.runtime.getURL("index.html?main=true"), + type: "popup", + width: savedBounds ? savedBounds.width : screenWidth, + height: savedBounds ? savedBounds.height : screenHeight, + left: savedBounds ? savedBounds.left : 0, + top: savedBounds ? savedBounds.top : 0, + }, (newWindow) => { + + + // Listen for changes in the window's size or position and save them + chrome.windows.onBoundsChanged.addListener((window) => { + if (window.id === newWindow.id) { + saveWindowBounds(newWindow.id); + } + }); + + // Save the final window bounds when the window is closed + chrome.windows.onRemoved.addListener((windowId) => { + if (windowId === newWindow.id) { + + saveWindowBounds(windowId); // Save the position/size before it’s closed + } + }); + }); + }); + }); + } + const activeData = await getStoredData('active-groups-directs') || { groups: [], directs: [] }; + setTimeout(() => { + + + chrome.runtime.sendMessage({ + action: "SET_GROUPS", + payload: activeData?.groups || [], + }); + chrome.runtime.sendMessage({ + action: "SET_DIRECTS", + payload: activeData?.directs || [], + }); + }, isExisting ? 100 : 1000); + const interactionId = Date.now().toString(); // Simple example; consider a better unique ID + + setTimeout(() => { + chrome.runtime.sendMessage({ + action: "INITIATE_MAIN", + payload: {}, + }); + + // Handle different types of notifications + if (isDirect) { + const fromValue = notificationId.split('_from=')[1]; + chrome.runtime.sendMessage({ + action: "NOTIFICATION_OPEN_DIRECT", + payload: { from: fromValue }, + }); + } else if (isGroup) { + const fromValue = notificationId.split('_from=')[1]; + chrome.runtime.sendMessage({ + action: "NOTIFICATION_OPEN_GROUP", + payload: { from: fromValue }, + }); + } else if (isGroupAnnouncement) { + const fromValue = notificationId.split('_from=')[1]; + chrome.runtime.sendMessage({ + action: "NOTIFICATION_OPEN_ANNOUNCEMENT_GROUP", + payload: { from: fromValue }, + }); + } else if (isNewThreadPost) { + const dataValue = notificationId.split('_data=')[1]; + const dataParsed = JSON.parse(dataValue); + + chrome.runtime.sendMessage({ + action: "NOTIFICATION_OPEN_THREAD_NEW_POST", + payload: { data: dataParsed }, + }); + } + }, isExisting ? 400 : 3000); + + // Store sendResponse callback with the interaction ID + pendingResponses.set(interactionId, sendResponse); + } + ); +}); + +// Reconnect when service worker wakes up +chrome.runtime.onStartup.addListener(() => { + console.log("Service worker started up, reconnecting WebSocket..."); + // initWebsocketMessageGroup(); + // listenForNewGroupAnnouncements() + // listenForThreadUpdates() +}); + +chrome.runtime.onInstalled.addListener((details) => { + if (details.reason === chrome.runtime.OnInstalledReason.INSTALL) { + console.log('Extension Installed'); + // Perform tasks that should only happen on extension installation + // Example: Initialize WebSocket, set default settings, etc. + } else if (details.reason === chrome.runtime.OnInstalledReason.UPDATE) { + console.log('Extension Updated'); + // Handle the update logic here (e.g., migrate settings) + } else if (details.reason === chrome.runtime.OnInstalledReason.CHROME_UPDATE) { + console.log('Chrome updated'); + // Optional: Handle Chrome-specific updates if necessary + } + + // Initialize WebSocket and other required listeners + // initWebsocketMessageGroup(); + // listenForNewGroupAnnouncements(); + // listenForThreadUpdates(); +}); + +// Check if the alarm already exists before creating it +chrome.alarms.get("checkForNotifications", (existingAlarm) => { + if (!existingAlarm) { + // If the alarm does not exist, create it + chrome.alarms.create("checkForNotifications", { periodInMinutes: 4 }); + } +}); + +chrome.alarms.onAlarm.addListener(async (alarm) => { + try { + + if (alarm.name === "checkForNotifications") { + // initWebsocketMessageGroup(address); + const wallet = await getSaveWallet(); + const address = wallet.address0; + if(!address) return + checkActiveChatsForNotifications() + checkNewMessages() + checkThreads() + + } + } catch (error) { + + } +}); + diff --git a/src/backgroundFunctions/encryption.ts b/src/backgroundFunctions/encryption.ts new file mode 100644 index 0000000..d641644 --- /dev/null +++ b/src/backgroundFunctions/encryption.ts @@ -0,0 +1,208 @@ +import { getBaseApi } from "../background"; +import { createSymmetricKeyAndNonce, decryptGroupData, encryptDataGroup, objectToBase64 } from "../qdn/encryption/group-encryption"; +import { publishData } from "../qdn/publish/pubish"; + +const apiEndpoints = [ + "https://api.qortal.org", + "https://api2.qortal.org", + "https://appnode.qortal.org", + "https://apinode.qortalnodes.live", + "https://apinode1.qortalnodes.live", + "https://apinode2.qortalnodes.live", + "https://apinode3.qortalnodes.live", + "https://apinode4.qortalnodes.live", +]; + +async function findUsableApi() { + for (const endpoint of apiEndpoints) { + try { + const response = await fetch(`${endpoint}/admin/status`); + if (!response.ok) throw new Error("Failed to fetch"); + + const data = await response.json(); + if (data.isSynchronizing === false && data.syncPercent === 100) { + console.log(`Usable API found: ${endpoint}`); + return endpoint; + } else { + console.log(`API not ready: ${endpoint}`); + } + } catch (error) { + console.error(`Error checking API ${endpoint}:`, error); + } + } + + throw new Error("No usable API found"); + } + + +async function getSaveWallet() { + const res = await chrome.storage.local.get(["walletInfo"]); + if (res?.walletInfo) { + return res.walletInfo; + } else { + throw new Error("No wallet saved"); + } + } +async function getNameInfo() { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const validApi = await getBaseApi() + const response = await fetch(validApi + "/names/address/" + address); + const nameData = await response.json(); + if (nameData?.length > 0) { + return nameData[0].name; + } else { + return ""; + } + } +async function getKeyPair() { + const res = await chrome.storage.local.get(["keyPair"]); + if (res?.keyPair) { + return res.keyPair; + } else { + throw new Error("Wallet not authenticated"); + } + } +const getPublicKeys = async (groupNumber: number) => { + const validApi = await getBaseApi() + const response = await fetch(`${validApi}/groups/members/${groupNumber}?limit=0`); + const groupData = await response.json(); + + let members: any = []; + if (groupData && Array.isArray(groupData?.members)) { + for (const member of groupData.members) { + if (member.member) { + const resAddress = await fetch(`${validApi}/addresses/${member.member}`); + const resData = await resAddress.json(); + const publicKey = resData.publicKey; + members.push(publicKey) + } + } + } + + return members + } + + + +export const encryptAndPublishSymmetricKeyGroupChat = async ({groupId, previousData}: { + groupId: number, + previousData: Object, +}) => { + try { + + let highestKey = 0 + if(previousData){ + highestKey = Math.max(...Object.keys((previousData || {})).filter(item=> !isNaN(+item)).map(Number)); + + } + + const resKeyPair = await getKeyPair() + const parsedData = JSON.parse(resKeyPair) + const privateKey = parsedData.privateKey + const userPublicKey = parsedData.publicKey + const groupmemberPublicKeys = await getPublicKeys(groupId) + const symmetricKey = createSymmetricKeyAndNonce() + const nextNumber = highestKey + 1 + const objectToSave = { + ...previousData, + [nextNumber]: symmetricKey + } + + const symmetricKeyAndNonceBase64 = await objectToBase64(objectToSave) + + const encryptedData = encryptDataGroup({ + data64: symmetricKeyAndNonceBase64, + publicKeys: groupmemberPublicKeys, + privateKey, + userPublicKey + }) + if(encryptedData){ + const registeredName = await getNameInfo() + const data = await publishData({ + registeredName, file: encryptedData, service: 'DOCUMENT_PRIVATE', identifier: `symmetric-qchat-group-${groupId}`, uploadType: 'file', isBase64: true, withFee: true + }) + return { + data, + numberOfMembers: groupmemberPublicKeys.length + } + + } else { + throw new Error('Cannot encrypt content') + } + } catch (error: any) { + throw new Error(error.message); + } +} +export const publishGroupEncryptedResource = async ({encryptedData, identifier}) => { + try { + + if(encryptedData && identifier){ + const registeredName = await getNameInfo() + if(!registeredName) throw new Error('You need a name to publish') + const data = await publishData({ + registeredName, file: encryptedData, service: 'DOCUMENT', identifier, uploadType: 'file', isBase64: true, withFee: true + }) + return data + + } else { + throw new Error('Cannot encrypt content') + } + } catch (error: any) { + throw new Error(error.message); + } +} + +export function uint8ArrayToBase64(uint8Array: any) { + const length = uint8Array.length + let binaryString = '' + const chunkSize = 1024 * 1024; // Process 1MB at a time + for (let i = 0; i < length; i += chunkSize) { + const chunkEnd = Math.min(i + chunkSize, length) + const chunk = uint8Array.subarray(i, chunkEnd) + + // @ts-ignore + binaryString += Array.from(chunk, byte => String.fromCharCode(byte)).join('') + } + return btoa(binaryString) +} + +export function base64ToUint8Array(base64: string) { + const binaryString = atob(base64) + const len = binaryString.length + const bytes = new Uint8Array(len) + + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i) + } + + return bytes + } + +export const decryptGroupEncryption = async ({data}: { + data: string +}) => { + try { + const resKeyPair = await getKeyPair() + const parsedData = JSON.parse(resKeyPair) + const privateKey = parsedData.privateKey + const encryptedData = decryptGroupData( + data, + privateKey, + ) + return { + data: uint8ArrayToBase64(encryptedData.decryptedData), + count: encryptedData.count + } + } catch (error: any) { + throw new Error(error.message); + } +} + +export function uint8ArrayToObject(uint8Array: any) { + // Decode the byte array using TextDecoder + const decoder = new TextDecoder() + const jsonString = decoder.decode(uint8Array) + // Convert the JSON string back into an object + return JSON.parse(jsonString) +} \ No newline at end of file diff --git a/src/common/CustomLoader.tsx b/src/common/CustomLoader.tsx new file mode 100644 index 0000000..5998f4b --- /dev/null +++ b/src/common/CustomLoader.tsx @@ -0,0 +1,7 @@ +import React from 'react' +import './customloader.css' +export const CustomLoader = () => { + return ( +
+ ) +} diff --git a/src/common/LazyLoad.tsx b/src/common/LazyLoad.tsx new file mode 100644 index 0000000..6369d0d --- /dev/null +++ b/src/common/LazyLoad.tsx @@ -0,0 +1,47 @@ +import React, { useState, useEffect, useRef } from 'react' +import { useInView } from 'react-intersection-observer' +import CircularProgress from '@mui/material/CircularProgress' + +interface Props { + onLoadMore: () => Promise +} + +const LazyLoad: React.FC = ({ onLoadMore }) => { + const [isFetching, setIsFetching] = useState(false) + + const firstLoad = useRef(false) + const [ref, inView] = useInView({ + threshold: 0.7 + }) + + useEffect(() => { + if (inView) { + setIsFetching(true) + onLoadMore().finally(() => { + setIsFetching(false) + firstLoad.current = true + }) + } + }, [inView]) + + return ( +
+
+ +
+
+ ) +} + +export default LazyLoad diff --git a/src/common/customloader.css b/src/common/customloader.css new file mode 100644 index 0000000..ff3dfbc --- /dev/null +++ b/src/common/customloader.css @@ -0,0 +1,64 @@ + +.lds-ellipsis { + color: white + } +.lds-ellipsis, +.lds-ellipsis div { + box-sizing: border-box; +} +.lds-ellipsis { + display: inline-block; + position: relative; + width: 80px; + height: 80px; +} +.lds-ellipsis div { + position: absolute; + top: 33.33333px; + width: 13.33333px; + height: 13.33333px; + border-radius: 50%; + background: currentColor; + animation-timing-function: cubic-bezier(0, 1, 1, 0); +} +.lds-ellipsis div:nth-child(1) { + left: 8px; + animation: lds-ellipsis1 0.6s infinite; +} +.lds-ellipsis div:nth-child(2) { + left: 8px; + animation: lds-ellipsis2 0.6s infinite; +} +.lds-ellipsis div:nth-child(3) { + left: 32px; + animation: lds-ellipsis2 0.6s infinite; +} +.lds-ellipsis div:nth-child(4) { + left: 56px; + animation: lds-ellipsis3 0.6s infinite; +} +@keyframes lds-ellipsis1 { + 0% { + transform: scale(0); + } + 100% { + transform: scale(1); + } +} +@keyframes lds-ellipsis3 { + 0% { + transform: scale(1); + } + 100% { + transform: scale(0); + } +} +@keyframes lds-ellipsis2 { + 0% { + transform: translate(0, 0); + } + 100% { + transform: translate(24px, 0); + } +} + diff --git a/src/common/useModal.tsx b/src/common/useModal.tsx new file mode 100644 index 0000000..b9ab713 --- /dev/null +++ b/src/common/useModal.tsx @@ -0,0 +1,64 @@ +import { useRef, useState } from 'react'; + +interface State { + isShow: boolean; +} +export const useModal = () => { + const [state, setState] = useState({ + isShow: false, + }); + const [message, setMessage] = useState({ + publishFee: "", + message: "" + }); + const promiseConfig = useRef(null); + const show = async (data) => { + setMessage(data) + return new Promise((resolve, reject) => { + promiseConfig.current = { + resolve, + reject, + }; + setState({ + isShow: true, + }); + }); + }; + + const hide = () => { + setState({ + isShow: false, + }); + setMessage({ + publishFee: "", + message: "" + }) + }; + + const onOk = (payload:any) => { + const { resolve } = promiseConfig.current; + setMessage({ + publishFee: "", + message: "" + }) + hide(); + resolve(payload); + }; + + const onCancel = () => { + const { reject } = promiseConfig.current; + hide(); + reject(); + setMessage({ + publishFee: "", + message: "" + }) + }; + return { + show, + onOk, + onCancel, + isShow: state.isShow, + message + }; +}; \ No newline at end of file diff --git a/src/components/Chat/AnnouncementDiscussion.tsx b/src/components/Chat/AnnouncementDiscussion.tsx new file mode 100644 index 0000000..fd61ab2 --- /dev/null +++ b/src/components/Chat/AnnouncementDiscussion.tsx @@ -0,0 +1,344 @@ +import React, { useMemo, useRef, useState } from "react"; +import TipTap from "./TipTap"; +import { AuthenticatedContainerInnerTop, CustomButton } from "../../App-styles"; +import { Box, CircularProgress } from "@mui/material"; +import { objectToBase64 } from "../../qdn/encryption/group-encryption"; +import ShortUniqueId from "short-unique-id"; +import { LoadingSnackbar } from "../Snackbar/LoadingSnackbar"; +import { getBaseApi, getFee } from "../../background"; +import { decryptPublishes, getTempPublish, saveTempPublish } from "./GroupAnnouncements"; +import { AnnouncementList } from "./AnnouncementList"; +import { Spacer } from "../../common/Spacer"; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import { getBaseApiReact } from "../../App"; + +const tempKey = 'accouncement-comment' + +const uid = new ShortUniqueId({ length: 8 }); +export const AnnouncementDiscussion = ({ + getSecretKey, + encryptChatMessage, + selectedAnnouncement, + secretKey, + setSelectedAnnouncement, + show, + myName +}) => { + const [isSending, setIsSending] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [comments, setComments] = useState([]) + const [tempPublishedList, setTempPublishedList] = useState([]) + const firstMountRef = useRef(false) + const [data, setData] = useState({}) + const editorRef = useRef(null); + const setEditorRef = (editorInstance) => { + editorRef.current = editorInstance; + }; + + const clearEditorContent = () => { + if (editorRef.current) { + editorRef.current.chain().focus().clearContent().run(); + } + }; + + const getData = async ({ identifier, name }) => { + try { + + const res = await fetch( + `${getBaseApiReact()}/arbitrary/DOCUMENT/${name}/${identifier}?encoding=base64` + ); + const data = await res.text(); + const response = await decryptPublishes([{ data }], secretKey); + + const messageData = response[0]; + setData((prev) => { + return { + ...prev, + [`${identifier}-${name}`]: messageData, + }; + }); + + } catch (error) {} + }; + + const publishAnc = async ({ encryptedData, identifier }: any) => { + try { + if (!selectedAnnouncement) return; + + return new Promise((res, rej) => { + chrome.runtime.sendMessage( + { + action: "publishGroupEncryptedResource", + payload: { + encryptedData, + identifier, + }, + }, + (response) => { + + if (!response?.error) { + res(response); + } + rej(response.error); + } + ); + }); + } catch (error) {} + }; + + const setTempData = async ()=> { + try { + const getTempAnnouncements = await getTempPublish() + if(getTempAnnouncements[tempKey]){ + let tempData = [] + Object.keys(getTempAnnouncements[tempKey] || {}).map((key)=> { + const value = getTempAnnouncements[tempKey][key] + if(value.data?.announcementId === selectedAnnouncement.identifier){ + tempData.push(value.data) + } + }) + setTempPublishedList(tempData) + } + } catch (error) { + + } + + } + + const publishComment = async () => { + try { + const fee = await getFee('ARBITRARY') + await show({ + message: "Would you like to perform a ARBITRARY transaction?" , + publishFee: fee.fee + ' QORT' + }) + if (isSending) return; + if (editorRef.current) { + const htmlContent = editorRef.current.getHTML(); + + if (!htmlContent?.trim() || htmlContent?.trim() === "

") return; + setIsSending(true); + const message = { + version: 1, + extra: {}, + message: htmlContent, + }; + const secretKeyObject = await getSecretKey(); + const message64: any = await objectToBase64(message); + + const encryptSingle = await encryptChatMessage( + message64, + secretKeyObject + ); + const randomUid = uid.rnd(); + const identifier = `cm-${selectedAnnouncement.identifier}-${randomUid}`; + const res = await publishAnc({ + encryptedData: encryptSingle, + identifier + }); + + const dataToSaveToStorage = { + name: myName, + identifier, + service: 'DOCUMENT', + tempData: message, + created: Date.now(), + announcementId: selectedAnnouncement.identifier + } + await saveTempPublish({data: dataToSaveToStorage, key: tempKey}) + setTempData() + + clearEditorContent(); + } + // send chat message + } catch (error) { + console.error(error); + } finally { + setIsSending(false); + } + }; + + const getComments = React.useCallback( + async (selectedAnnouncement) => { + try { + setIsLoading(true); + + const offset = 0; + + // dispatch(setIsLoadingGlobal(true)) + const identifier = `cm-${selectedAnnouncement.identifier}`; + const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${offset}&reverse=true`; + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + const responseData = await response.json(); + setTempData() + setComments(responseData); + setIsLoading(false); + for (const data of responseData) { + getData({ name: data.name, identifier: data.identifier }); + } + } catch (error) { + } finally { + setIsLoading(false); + + // dispatch(setIsLoadingGlobal(false)) + } + }, + [secretKey] + ); + + const loadMore = async()=> { + try { + setIsLoading(true); + + const offset = comments.length + const identifier = `cm-${selectedAnnouncement.identifier}`; + const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${offset}&reverse=true&prefix=true`; + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + const responseData = await response.json(); + + setComments((prev)=> [...prev, ...responseData]); + setIsLoading(false); + for (const data of responseData) { + getData({ name: data.name, identifier: data.identifier }); + } + } catch (error) { + + } + + } + + const combinedListTempAndReal = useMemo(() => { + // Combine the two lists + const combined = [...tempPublishedList, ...comments]; + + // Remove duplicates based on the "identifier" + const uniqueItems = new Map(); + combined.forEach(item => { + uniqueItems.set(item.identifier, item); // This will overwrite duplicates, keeping the last occurrence + }); + + // Convert the map back to an array and sort by "created" timestamp in descending order + const sortedList = Array.from(uniqueItems.values()).sort((a, b) => b.created - a.created); + + return sortedList; + }, [tempPublishedList, comments]); + + React.useEffect(() => { + if (selectedAnnouncement && secretKey && !firstMountRef.current) { + getComments(selectedAnnouncement); + firstMountRef.current = true + } + }, [selectedAnnouncement, secretKey]); + return ( +
+
+ + + setSelectedAnnouncement(null)} sx={{ + cursor: 'pointer' + }} /> + + + +
+ {}} + disableComment + showLoadMore={comments.length > 0 && comments.length % 20 === 0} + loadMore={loadMore} + + /> +
+
+ +
+ { + if (isSending) return; + publishComment(); + }} + style={{ + marginTop: "auto", + alignSelf: "center", + cursor: isSending ? "default" : "pointer", + background: isSending && "rgba(0, 0, 0, 0.8)", + flexShrink: 0, + }} + > + {isSending && ( + + )} + {` Publish Comment`} + +
+ + +
+ ); +}; diff --git a/src/components/Chat/AnnouncementItem.tsx b/src/components/Chat/AnnouncementItem.tsx new file mode 100644 index 0000000..0ab0e53 --- /dev/null +++ b/src/components/Chat/AnnouncementItem.tsx @@ -0,0 +1,167 @@ +import { Message } from "@chatscope/chat-ui-kit-react"; +import React, { useEffect, useState } from "react"; +import { useInView } from "react-intersection-observer"; +import { MessageDisplay } from "./MessageDisplay"; +import { Avatar, Box, Typography } from "@mui/material"; +import { formatTimestamp } from "../../utils/time"; +import ChatBubbleIcon from '@mui/icons-material/ChatBubble'; +import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; +import { getBaseApi } from "../../background"; +import { requestQueueCommentCount } from "./GroupAnnouncements"; +import { CustomLoader } from "../../common/CustomLoader"; +import { getBaseApiReact } from "../../App"; +export const AnnouncementItem = ({ message, messageData, setSelectedAnnouncement, disableComment }) => { + + const [commentLength, setCommentLength] = useState(0) + const getNumberOfComments = React.useCallback( + async () => { + try { + const offset = 0; + + // dispatch(setIsLoadingGlobal(true)) + const identifier = `cm-${message.identifier}`; + const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=0&includemetadata=false&offset=${offset}&reverse=true`; + + const response = await requestQueueCommentCount.enqueue(() => { + return fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + }) + const responseData = await response.json(); + + setCommentLength(responseData?.length); + + } catch (error) { + } finally { + // dispatch(setIsLoadingGlobal(false)) + } + }, + [] + ); + useEffect(()=> { + if(disableComment) return + getNumberOfComments() + }, []) + return ( +
+ + + {message?.name?.charAt(0)} + + + + {message?.name} + + {!messageData?.decryptedData && ( + + + + )} + {messageData?.decryptedData?.message && ( + <> + {messageData?.type === "notification" ? ( + + ) : ( + + )} + + )} + + + + {formatTimestamp(message.created)} + + + + {!disableComment && ( + setSelectedAnnouncement(message)}> + + + + {commentLength ? ( + {`${commentLength > 1 ? `${commentLength} comments` : `${commentLength} comment`}`} + ) : ( + Leave comment + )} + + + + + )} + +
+ ); +}; diff --git a/src/components/Chat/AnnouncementList.tsx b/src/components/Chat/AnnouncementList.tsx new file mode 100644 index 0000000..f4ed7b2 --- /dev/null +++ b/src/components/Chat/AnnouncementList.tsx @@ -0,0 +1,96 @@ +import React, { useCallback, useState, useEffect, useRef } from "react"; +import { + List, + AutoSizer, + CellMeasurerCache, + CellMeasurer, +} from "react-virtualized"; +import { AnnouncementItem } from "./AnnouncementItem"; +import { Box } from "@mui/material"; +import { CustomButton } from "../../App-styles"; + +const cache = new CellMeasurerCache({ + fixedWidth: true, + defaultHeight: 50, +}); + +export const AnnouncementList = ({ + initialMessages, + announcementData, + setSelectedAnnouncement, + disableComment, + showLoadMore, + loadMore +}) => { + + const listRef = useRef(); + const [messages, setMessages] = useState(initialMessages); + + useEffect(() => { + cache.clearAll(); + }, []); + + useEffect(() => { + setMessages(initialMessages); + }, [initialMessages]); + + + return ( +
+ {messages.map((message) => { + const messageData = message?.tempData ? { + decryptedData: message?.tempData + } : announcementData[`${message.identifier}-${message.name}`]; + + return ( + +
+ +
+ + ); + })} + {/* + {({ height, width }) => ( + + )} + */} + + {showLoadMore && ( + Load older announcements + )} + +
+ ); +}; diff --git a/src/components/Chat/ChatContainer.tsx b/src/components/Chat/ChatContainer.tsx new file mode 100644 index 0000000..399e5a4 --- /dev/null +++ b/src/components/Chat/ChatContainer.tsx @@ -0,0 +1,56 @@ +import React, { useState } from "react"; +import InfiniteScroll from "react-infinite-scroller"; +import { + MainContainer, + ChatContainer, + MessageList, + Message, + MessageInput, + Avatar +} from "@chatscope/chat-ui-kit-react"; +import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css"; + +export const ChatContainerComp = ({messages}) => { + // const [messages, setMessages] = useState([ + // { id: 1, text: "Hello! How are you?", sender: "Joe"}, + // { id: 2, text: "I'm good, thank you!", sender: "Me" } + // ]); + + // const loadMoreMessages = () => { + // // Simulate loading more messages (you could fetch these from an API) + // const moreMessages = [ + // { id: 3, text: "What about you?", sender: "Joe", direction: "incoming" }, + // { id: 4, text: "I'm great, thanks!", sender: "Me", direction: "outgoing" } + // ]; + // setMessages((prevMessages) => [...moreMessages, ...prevMessages]); + // }; + + return ( +
+ + + + {messages.map((msg) => ( + + {msg.direction === "incoming" && } + + ))} + + + + + +
+ ); +}; + + diff --git a/src/components/Chat/ChatDirect.tsx b/src/components/Chat/ChatDirect.tsx new file mode 100644 index 0000000..791ee58 --- /dev/null +++ b/src/components/Chat/ChatDirect.tsx @@ -0,0 +1,305 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' + +import { objectToBase64 } from '../../qdn/encryption/group-encryption' +import { ChatList } from './ChatList' +import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css"; +import Tiptap from './TipTap' +import { CustomButton } from '../../App-styles' +import CircularProgress from '@mui/material/CircularProgress'; +import { Input } from '@mui/material'; +import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar'; +import { getNameInfo } from '../Group/Group'; +import { Spacer } from '../../common/Spacer'; +import { CustomizedSnackbars } from '../Snackbar/Snackbar'; +import { getBaseApiReactSocket } from '../../App'; + + + + + +export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDirect, setNewChat, getTimestampEnterChat, myName}) => { + const [messages, setMessages] = useState([]) + const [isSending, setIsSending] = useState(false) + const [directToValue, setDirectToValue] = useState('') + const hasInitialized = useRef(false) + const [isLoading, setIsLoading] = useState(false) + const [openSnack, setOpenSnack] = React.useState(false); + const [infoSnack, setInfoSnack] = React.useState(null); + const hasInitializedWebsocket = useRef(false) + const editorRef = useRef(null); + const setEditorRef = (editorInstance) => { + editorRef.current = editorInstance; + }; + + + + + + + + const decryptMessages = (encryptedMessages: any[])=> { + try { + return new Promise((res, rej)=> { + chrome.runtime.sendMessage({ action: "decryptDirect", payload: { + data: encryptedMessages, + involvingAddress: selectedDirect?.address + }}, (response) => { + + if (!response?.error) { + res(response) + if(hasInitialized.current){ + + const formatted = response.map((item: any)=> { + return { + ...item, + id: item.signature, + text: item.message, + unread: true + } + } ) + setMessages((prev)=> [...prev, ...formatted]) + } else { + const formatted = response.map((item: any)=> { + return { + ...item, + id: item.signature, + text: item.message, + unread: false + } + } ) + setMessages(formatted) + hasInitialized.current = true + + } + } + rej(response.error) + }); + }) + } catch (error) { + + } + } + + const initWebsocketMessageGroup = () => { + let timeoutId + let groupSocketTimeout + + let socketTimeout: any + let socketLink = `${getBaseApiReactSocket()}/websockets/chat/messages?involving=${selectedDirect?.address}&involving=${myAddress}&encoding=BASE64&limit=100` + const socket = new WebSocket(socketLink) + + const pingGroupSocket = () => { + socket.send('ping') + timeoutId = setTimeout(() => { + socket.close() + clearTimeout(groupSocketTimeout) + }, 5000) // Close the WebSocket connection if no pong message is received within 5 seconds. + } + socket.onopen = () => { + + setTimeout(pingGroupSocket, 50) + } + socket.onmessage = (e) => { + try { + if (e.data === 'pong') { + + clearTimeout(timeoutId) + groupSocketTimeout = setTimeout(pingGroupSocket, 45000) + return + } else { + decryptMessages(JSON.parse(e.data)) + setIsLoading(false) + } + + } catch (error) { + + } + + } + socket.onclose = () => { + console.log('closed') + clearTimeout(socketTimeout) + setTimeout(() => initWebsocketMessageGroup(), 50) + + } + socket.onerror = (e) => { + clearTimeout(groupSocketTimeout) + socket.close() + } + } + + + + useEffect(()=> { + if(hasInitializedWebsocket.current) return + setIsLoading(true) + initWebsocketMessageGroup() + hasInitializedWebsocket.current = true + }, []) + + + + +const sendChatDirect = async ({ chatReference = undefined, messageText}: any)=> { + try { + const directTo = isNewChat ? directToValue : selectedDirect.address + + if(!directTo) return + return new Promise((res, rej)=> { + chrome.runtime.sendMessage({ action: "sendChatDirect", payload: { + directTo, chatReference, messageText + }}, async (response) => { + + if (!response?.error) { + if(isNewChat){ + + let getRecipientName = null + try { + getRecipientName = await getNameInfo(response.recipient) + } catch (error) { + + } + setSelectedDirect({ + "address": response.recipient, + "name": getRecipientName, + "timestamp": Date.now(), + "sender": myAddress, + "senderName": myName + }) + setNewChat(null) + chrome.runtime.sendMessage({ + action: "addTimestampEnterChat", + payload: { + timestamp: Date.now(), + groupId: response.recipient, + }, + }); + setTimeout(() => { + getTimestampEnterChat() + }, 400); + } + res(response) + return + } + rej(response.error) + }); + }) + } catch (error) { + throw new Error(error) + } +} +const clearEditorContent = () => { + if (editorRef.current) { + editorRef.current.chain().focus().clearContent().run(); + } +}; + + + const sendMessage = async ()=> { + try { + if(isSending) return + if (editorRef.current) { + const htmlContent = editorRef.current.getHTML(); + + if(!htmlContent?.trim() || htmlContent?.trim() === '

') return + setIsSending(true) + const message = JSON.stringify(htmlContent) + + const res = await sendChatDirect({ messageText: htmlContent}) + clearEditorContent() + } + // send chat message + } catch (error) { + setInfoSnack({ + type: "error", + message: error, + }); + setOpenSnack(true); + console.error(error) + } finally { + setIsSending(false) + } + } + + + + + return ( +
+ {isNewChat && ( + <> + + setDirectToValue(e.target.value)} /> + + + )} + + + + +
+
+ + + +
+ { + if(isSending) return + sendMessage() + }} + style={{ + marginTop: 'auto', + alignSelf: 'center', + cursor: isSending ? 'default' : 'pointer', + background: isSending && 'rgba(0, 0, 0, 0.8)', + flexShrink: 0 + }} + > + {isSending && ( + + )} + {` Send`} + +
+ + +
+ ) +} diff --git a/src/components/Chat/ChatGroup.tsx b/src/components/Chat/ChatGroup.tsx new file mode 100644 index 0000000..7ccd170 --- /dev/null +++ b/src/components/Chat/ChatGroup.tsx @@ -0,0 +1,377 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { CreateCommonSecret } from './CreateCommonSecret' +import { reusableGet } from '../../qdn/publish/pubish' +import { uint8ArrayToObject } from '../../backgroundFunctions/encryption' +import { base64ToUint8Array, objectToBase64 } from '../../qdn/encryption/group-encryption' +import { ChatContainerComp } from './ChatContainer' +import { ChatList } from './ChatList' +import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css"; +import Tiptap from './TipTap' +import { CustomButton } from '../../App-styles' +import CircularProgress from '@mui/material/CircularProgress'; +import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar' +import { getBaseApiReactSocket } from '../../App' +import { CustomizedSnackbars } from '../Snackbar/Snackbar' +import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from '../../constants/codes' + + + + + +export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, myAddress, handleNewEncryptionNotification, hide, handleSecretKeyCreationInProgress, triedToFetchSecretKey}) => { + const [messages, setMessages] = useState([]) + const [isSending, setIsSending] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const [isMoved, setIsMoved] = useState(false); + const [openSnack, setOpenSnack] = React.useState(false); + const [infoSnack, setInfoSnack] = React.useState(null); + const hasInitialized = useRef(false) + const hasInitializedWebsocket = useRef(false) + const socketRef = useRef(null); // WebSocket reference + const timeoutIdRef = useRef(null); // Timeout ID reference + const groupSocketTimeoutRef = useRef(null); // Group Socket Timeout reference + const editorRef = useRef(null); + + const setEditorRef = (editorInstance) => { + editorRef.current = editorInstance; + }; + + const secretKeyRef = useRef(null) + + useEffect(()=> { + if(secretKey){ + secretKeyRef.current = secretKey + } + }, [secretKey]) + + // const getEncryptedSecretKey = useCallback(()=> { + // const response = getResource() + // const decryptResponse = decryptResource() + // return + // }, []) + + + const checkForFirstSecretKeyNotification = (messages)=> { + messages?.forEach((message)=> { + try { + const decodeMsg = atob(message.data); + + if(decodeMsg === PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY){ + handleSecretKeyCreationInProgress() + return + } + } catch (error) { + + } + }) + } + + + const decryptMessages = (encryptedMessages: any[])=> { + try { + if(!secretKeyRef.current){ + checkForFirstSecretKeyNotification(encryptedMessages) + return + } + return new Promise((res, rej)=> { + chrome.runtime.sendMessage({ action: "decryptSingle", payload: { + data: encryptedMessages, + secretKeyObject: secretKey + }}, (response) => { + + if (!response?.error) { + res(response) + if(hasInitialized.current){ + + const formatted = response.map((item: any)=> { + return { + ...item, + id: item.signature, + text: item.text, + unread: true + } + } ) + setMessages((prev)=> [...prev, ...formatted]) + } else { + const formatted = response.map((item: any)=> { + return { + ...item, + id: item.signature, + text: item.text, + unread: false + } + } ) + setMessages(formatted) + hasInitialized.current = true + + } + } + rej(response.error) + }); + }) + } catch (error) { + + } + } + + + + const forceCloseWebSocket = () => { + if (socketRef.current) { + + clearTimeout(timeoutIdRef.current); + clearTimeout(groupSocketTimeoutRef.current); + socketRef.current.close(1000, 'forced'); + socketRef.current = null; + } + }; + + const pingGroupSocket = () => { + try { + if (socketRef.current?.readyState === WebSocket.OPEN) { + socketRef.current.send('ping'); + timeoutIdRef.current = setTimeout(() => { + if (socketRef.current) { + socketRef.current.close(); + clearTimeout(groupSocketTimeoutRef.current); + } + }, 5000); // Close if no pong in 5 seconds + } + } catch (error) { + console.error('Error during ping:', error); + } + } + const initWebsocketMessageGroup = () => { + + + let socketLink = `${getBaseApiReactSocket()}/websockets/chat/messages?txGroupId=${selectedGroup}&encoding=BASE64&limit=100` + socketRef.current = new WebSocket(socketLink) + + + socketRef.current.onopen = () => { + setTimeout(pingGroupSocket, 50) + } + socketRef.current.onmessage = (e) => { + try { + if (e.data === 'pong') { + clearTimeout(timeoutIdRef.current); + groupSocketTimeoutRef.current = setTimeout(pingGroupSocket, 45000); // Ping every 45 seconds + } else { + decryptMessages(JSON.parse(e.data)) + setIsLoading(false) + } + } catch (error) { + + } + + + } + socketRef.current.onclose = () => { + clearTimeout(groupSocketTimeoutRef.current); + clearTimeout(timeoutIdRef.current); + console.warn(`WebSocket closed: ${event.reason || 'unknown reason'}`); + if (event.reason !== 'forced' && event.code !== 1000) { + setTimeout(() => initWebsocketMessageGroup(), 1000); // Retry after 10 seconds + } + } + socketRef.current.onerror = (e) => { + console.error('WebSocket error:', error); + clearTimeout(groupSocketTimeoutRef.current); + clearTimeout(timeoutIdRef.current); + if (socketRef.current) { + socketRef.current.close(); + } + } + } + + useEffect(()=> { + if(hasInitializedWebsocket.current) return + if(triedToFetchSecretKey && !secretKey){ + forceCloseWebSocket() + setMessages([]) + setIsLoading(true) + initWebsocketMessageGroup() + } + }, [triedToFetchSecretKey, secretKey]) + + useEffect(()=> { + if(!secretKey || hasInitializedWebsocket.current) return + forceCloseWebSocket() + setMessages([]) + setIsLoading(true) + initWebsocketMessageGroup() + hasInitializedWebsocket.current = true + }, [secretKey]) + + + useEffect(()=> { + const notifications = messages.filter((message)=> message?.text?.type === 'notification') + if(notifications.length === 0) return + const latestNotification = notifications.reduce((latest, current) => { + return current.timestamp > latest.timestamp ? current : latest; + }, notifications[0]); + handleNewEncryptionNotification(latestNotification) + + }, [messages]) + + + const encryptChatMessage = async (data: string, secretKeyObject: any)=> { + try { + return new Promise((res, rej)=> { + chrome.runtime.sendMessage({ action: "encryptSingle", payload: { + data, + secretKeyObject + }}, (response) => { + + if (!response?.error) { + res(response) + } + rej(response.error) + }); + }) + } catch (error) { + + } +} + +const sendChatGroup = async ({groupId, typeMessage = undefined, chatReference = undefined, messageText}: any)=> { + try { + return new Promise((res, rej)=> { + chrome.runtime.sendMessage({ action: "sendChatGroup", payload: { + groupId, typeMessage, chatReference, messageText + }}, (response) => { + + if (!response?.error) { + res(response) + return + } + rej(response.error) + }); + }) + } catch (error) { + throw new Error(error) + } +} +const clearEditorContent = () => { + if (editorRef.current) { + editorRef.current.chain().focus().clearContent().run(); + } +}; + + + const sendMessage = async ()=> { + try { + if(isSending) return + if (editorRef.current) { + const htmlContent = editorRef.current.getHTML(); + + if(!htmlContent?.trim() || htmlContent?.trim() === '

') return + setIsSending(true) + const message = htmlContent + const secretKeyObject = await getSecretKey() + const message64: any = await objectToBase64(message) + + const encryptSingle = await encryptChatMessage(message64, secretKeyObject) + const res = await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle}) + + clearEditorContent() + } + // send chat message + } catch (error) { + setInfoSnack({ + type: "error", + message: error, + }); + setOpenSnack(true); + console.error(error) + } finally { + setIsSending(false) + } + } + + useEffect(() => { + if (hide) { + setTimeout(() => setIsMoved(true), 500); // Wait for the fade-out to complete before moving + } else { + setIsMoved(false); // Reset the position immediately when showing + } + }, [hide]); + + + return ( +
+ + + + +
+
+ + + +
+ { + if(isSending) return + sendMessage() + }} + style={{ + marginTop: 'auto', + alignSelf: 'center', + cursor: isSending ? 'default' : 'pointer', + background: isSending && 'rgba(0, 0, 0, 0.8)', + flexShrink: 0 + }} + > + {isSending && ( + + )} + {` Send`} + + {/* */} +
+ {/* */} + + + +
+ ) +} diff --git a/src/components/Chat/ChatList.tsx b/src/components/Chat/ChatList.tsx new file mode 100644 index 0000000..43e5ba6 --- /dev/null +++ b/src/components/Chat/ChatList.tsx @@ -0,0 +1,144 @@ +import React, { useCallback, useState, useEffect, useRef } from 'react'; +import { List, AutoSizer, CellMeasurerCache, CellMeasurer } from 'react-virtualized'; +import { MessageItem } from './MessageItem'; + +const cache = new CellMeasurerCache({ + fixedWidth: true, + defaultHeight: 50, +}); + +export const ChatList = ({ initialMessages, myAddress }) => { + const hasLoadedInitialRef = useRef(false); + const listRef = useRef(); + const [messages, setMessages] = useState(initialMessages); + const [showScrollButton, setShowScrollButton] = useState(false); + + + useEffect(()=> { + cache.clearAll(); + }, []) + const handleMessageSeen = useCallback((messageId) => { + setMessages((prevMessages) => + prevMessages.map((msg) => + msg.id === messageId ? { ...msg, unread: false } : msg + ) + ); + }, []); + + const handleScroll = ({ scrollTop, scrollHeight, clientHeight }) => { + const isAtBottom = scrollTop + clientHeight >= scrollHeight - 50; + const hasUnreadMessages = messages.some((msg) => msg.unread); + + if (!isAtBottom && hasUnreadMessages) { + setShowScrollButton(true); + } else { + setShowScrollButton(false); + } + }; + + const scrollToBottom = () => { + if (listRef.current) { + listRef.current?.recomputeRowHeights(); + listRef.current.scrollToRow(messages.length - 1); + setTimeout(() => { + listRef.current?.recomputeRowHeights(); + listRef.current.scrollToRow(messages.length - 1); + }, 100); + + + setShowScrollButton(false); + } + }; + + const rowRenderer = ({ index, key, parent, style }) => { + const message = messages[index]; + + return ( + + {({ measure }) => ( +
+ +
+ +
+
+ + )} +
+ ); + }; + + useEffect(() => { + setMessages(initialMessages); + setTimeout(() => { + if (listRef.current) { + // Accessing scrollTop, scrollHeight, clientHeight from List's methods + const scrollTop = listRef.current.Grid._scrollingContainer.scrollTop; + const scrollHeight = listRef.current.Grid._scrollingContainer.scrollHeight; + const clientHeight = listRef.current.Grid._scrollingContainer.clientHeight; + + handleScroll({ scrollTop, scrollHeight, clientHeight }); + } + }, 100); + }, [initialMessages]); + + useEffect(() => { + // Scroll to the bottom on initial load or when messages change + if (listRef.current && messages.length > 0 && hasLoadedInitialRef.current === false) { + scrollToBottom(); + hasLoadedInitialRef.current = true; + } else if (messages.length > 0 && messages[messages.length - 1].sender === myAddress) { + scrollToBottom(); + } + }, [messages, myAddress]); + + return ( +
+ + + {({ height, width }) => ( + + )} + + {showScrollButton && ( + + )} +
+ ); +}; diff --git a/src/components/Chat/CreateCommonSecret.tsx b/src/components/Chat/CreateCommonSecret.tsx new file mode 100644 index 0000000..2e46a49 --- /dev/null +++ b/src/components/Chat/CreateCommonSecret.tsx @@ -0,0 +1,79 @@ +import { Box, Button, Typography } from '@mui/material' +import React, { useContext } from 'react' +import { CustomizedSnackbars } from '../Snackbar/Snackbar'; +import { LoadingButton } from '@mui/lab'; +import { MyContext } from '../../App'; +import { getFee } from '../../background'; + +export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, secretKeyDetails, userInfo, noSecretKey}) => { + const { show, setTxList } = useContext(MyContext); + + const [openSnack, setOpenSnack] = React.useState(false); + const [infoSnack, setInfoSnack] = React.useState(null); + const [isLoading, setIsLoading] = React.useState(false) + const createCommonSecret = async ()=> { + try { + const fee = await getFee('ARBITRARY') + await show({ + message: "Would you like to perform an ARBITRARY transaction?" , + publishFee: fee.fee + ' QORT' + }) + + + setIsLoading(true) + chrome.runtime.sendMessage({ action: "encryptAndPublishSymmetricKeyGroupChat", payload: { + groupId: groupId, + previousData: secretKey + } }, (response) => { + if (!response?.error) { + setInfoSnack({ + type: "success", + message: "Successfully re-encrypted secret key. It may take a couple of minutes for the changes to propagate. Refresh the group in 5mins", + }); + setOpenSnack(true); + setTxList((prev)=> [{ + ...response, + type: 'created-common-secret', + label: `Published secret key for group ${groupId}: awaiting confirmation`, + labelDone: `Published secret key for group ${groupId}: success!`, + done: false, + groupId, + + }, ...prev]) + } + setIsLoading(false) + }); + } catch (error) { + + } + } + + + return ( + + Re-encyrpt key + {noSecretKey ? ( + + There is no group secret key. Be the first admin to publish one! + + ) : isOwner && secretKeyDetails && userInfo?.name && userInfo.name !== secretKeyDetails?.name ? ( + + The latest group secret key was published by a non-owner. As the owner of the group please re-encrypt the key as a safeguard + + ): ( + + The group member list has changed. Please re-encrypt the secret key. + + )} + + + + ) +} diff --git a/src/components/Chat/CustomImage.ts b/src/components/Chat/CustomImage.ts new file mode 100644 index 0000000..b1310ae --- /dev/null +++ b/src/components/Chat/CustomImage.ts @@ -0,0 +1,59 @@ +import { Node, mergeAttributes } from '@tiptap/core'; +import { ReactNodeViewRenderer } from '@tiptap/react'; +import ResizableImage from './ResizableImage'; // Import your ResizableImage component + +const CustomImage = Node.create({ + name: 'image', + + inline: false, + group: 'block', + draggable: true, + + addAttributes() { + return { + src: { + default: null, + }, + alt: { + default: null, + }, + title: { + default: null, + }, + width: { + default: 'auto', + }, + }; + }, + + parseHTML() { + return [ + { + tag: 'img[src]', + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return ['img', mergeAttributes(HTMLAttributes)]; + }, + + addNodeView() { + return ReactNodeViewRenderer(ResizableImage); + }, + + addCommands() { + return { + setImage: + (options) => + ({ commands }) => { + return commands.insertContent({ + type: this.name, + attrs: options, + }); + }, + }; + }, +}); + +export default CustomImage; diff --git a/src/components/Chat/GroupAnnouncements.tsx b/src/components/Chat/GroupAnnouncements.tsx new file mode 100644 index 0000000..778e019 --- /dev/null +++ b/src/components/Chat/GroupAnnouncements.tsx @@ -0,0 +1,607 @@ +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { CreateCommonSecret } from "./CreateCommonSecret"; +import { reusableGet } from "../../qdn/publish/pubish"; +import { uint8ArrayToObject } from "../../backgroundFunctions/encryption"; +import { + base64ToUint8Array, + objectToBase64, +} from "../../qdn/encryption/group-encryption"; +import { ChatContainerComp } from "./ChatContainer"; +import { ChatList } from "./ChatList"; +import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css"; +import Tiptap from "./TipTap"; +import { AuthenticatedContainerInnerTop, CustomButton } from "../../App-styles"; +import CircularProgress from "@mui/material/CircularProgress"; +import { getBaseApi, getFee } from "../../background"; +import { LoadingSnackbar } from "../Snackbar/LoadingSnackbar"; +import { Box, Typography } from "@mui/material"; +import { Spacer } from "../../common/Spacer"; +import ShortUniqueId from "short-unique-id"; +import { AnnouncementList } from "./AnnouncementList"; +const uid = new ShortUniqueId({ length: 8 }); +import CampaignIcon from '@mui/icons-material/Campaign'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import { AnnouncementDiscussion } from "./AnnouncementDiscussion"; +import { MyContext, getBaseApiReact } from "../../App"; +import { RequestQueueWithPromise } from "../../utils/queue/queue"; +import { CustomizedSnackbars } from "../Snackbar/Snackbar"; + +export const requestQueueCommentCount = new RequestQueueWithPromise(3) +export const requestQueuePublishedAccouncements = new RequestQueueWithPromise(3) + +export const saveTempPublish = async ({ data, key }: any) => { + + + return new Promise((res, rej) => { + chrome.runtime.sendMessage( + { + action: "saveTempPublish", + payload: { + data, + key, + }, + }, + (response) => { + + if (!response?.error) { + res(response); + } + rej(response.error); + } + ); + }); +}; + +export const getTempPublish = async () => { + + + return new Promise((res, rej) => { + chrome.runtime.sendMessage( + { + action: "getTempPublish", + payload: { + }, + }, + (response) => { + if (!response?.error) { + res(response); + } + rej(response.error); + } + ); + }); +}; + +export const decryptPublishes = async (encryptedMessages: any[], secretKey) => { + try { + return await new Promise((res, rej) => { + chrome.runtime.sendMessage( + { + action: "decryptSingleForPublishes", + payload: { + data: encryptedMessages, + secretKeyObject: secretKey, + skipDecodeBase64: true, + }, + }, + (response) => { + + if (!response?.error) { + res(response); + // if(hasInitialized.current){ + + // setMessages((prev)=> [...prev, ...formatted]) + // } else { + // const formatted = response.map((item: any)=> { + // return { + // ...item, + // id: item.signature, + // text: item.text, + // unread: false + // } + // } ) + // setMessages(formatted) + // hasInitialized.current = true + + // } + } + rej(response.error); + } + ); + }); + } catch (error) {} +}; +export const GroupAnnouncements = ({ + selectedGroup, + secretKey, + setSecretKey, + getSecretKey, + myAddress, + handleNewEncryptionNotification, + isAdmin, + hide, + myName +}) => { + const [messages, setMessages] = useState([]); + const [isSending, setIsSending] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [announcements, setAnnouncements] = useState([]); + const [tempPublishedList, setTempPublishedList] = useState([]) + const [announcementData, setAnnouncementData] = useState({}); + const [selectedAnnouncement, setSelectedAnnouncement] = useState(null); + const { show } = React.useContext(MyContext); + const [openSnack, setOpenSnack] = React.useState(false); + const [infoSnack, setInfoSnack] = React.useState(null); + const hasInitialized = useRef(false); + const hasInitializedWebsocket = useRef(false); + const editorRef = useRef(null); + + const setEditorRef = (editorInstance) => { + editorRef.current = editorInstance; + }; + + const getAnnouncementData = async ({ identifier, name }) => { + try { + + const res = await requestQueuePublishedAccouncements.enqueue(()=> { + return fetch( + `${getBaseApiReact()}/arbitrary/DOCUMENT/${name}/${identifier}?encoding=base64` + ); + }) + const data = await res.text(); + const response = await decryptPublishes([{ data }], secretKey); + + const messageData = response[0]; + setAnnouncementData((prev) => { + return { + ...prev, + [`${identifier}-${name}`]: messageData, + }; + }); + + } catch (error) {} + }; + + + + + + useEffect(() => { + if (!secretKey || hasInitializedWebsocket.current) return; + setIsLoading(true); + // initWebsocketMessageGroup() + hasInitializedWebsocket.current = true; + }, [secretKey]); + + const encryptChatMessage = async (data: string, secretKeyObject: any) => { + try { + return new Promise((res, rej) => { + chrome.runtime.sendMessage( + { + action: "encryptSingle", + payload: { + data, + secretKeyObject, + }, + }, + (response) => { + if (!response?.error) { + res(response); + return; + } + rej(response.error); + } + ); + }); + } catch (error) {} + }; + + const publishAnc = async ({ encryptedData, identifier }: any) => { + + + return new Promise((res, rej) => { + chrome.runtime.sendMessage( + { + action: "publishGroupEncryptedResource", + payload: { + encryptedData, + identifier, + }, + }, + (response) => { + if (!response?.error) { + res(response); + } + rej(response.error); + } + ); + }); + }; + const clearEditorContent = () => { + if (editorRef.current) { + editorRef.current.chain().focus().clearContent().run(); + } + }; + + const setTempData = async ()=> { + try { + const getTempAnnouncements = await getTempPublish() + if(getTempAnnouncements?.announcement){ + let tempData = [] + Object.keys(getTempAnnouncements?.announcement || {}).map((key)=> { + const value = getTempAnnouncements?.announcement[key] + tempData.push(value.data) + }) + setTempPublishedList(tempData) + } + } catch (error) { + + } + + } + + const publishAnnouncement = async () => { + try { + const fee = await getFee('ARBITRARY') + await show({ + message: "Would you like to perform a ARBITRARY transaction?" , + publishFee: fee.fee + ' QORT' + }) + if (isSending) return; + if (editorRef.current) { + const htmlContent = editorRef.current.getHTML(); + if (!htmlContent?.trim() || htmlContent?.trim() === "

") return; + setIsSending(true); + const message = { + version: 1, + extra: {}, + message: htmlContent + } + const secretKeyObject = await getSecretKey(); + const message64: any = await objectToBase64(message); + const encryptSingle = await encryptChatMessage( + message64, + secretKeyObject + ); + const randomUid = uid.rnd(); + const identifier = `grp-${selectedGroup}-anc-${randomUid}`; + const res = await publishAnc({ + encryptedData: encryptSingle, + identifier + }); + + const dataToSaveToStorage = { + name: myName, + identifier, + service: 'DOCUMENT', + tempData: message, + created: Date.now() + } + await saveTempPublish({data: dataToSaveToStorage, key: 'announcement'}) + setTempData() + clearEditorContent(); + } + // send chat message + } catch (error) { + setInfoSnack({ + type: "error", + message: error, + }); + setOpenSnack(true) + } finally { + setIsSending(false); + } + }; + + + + const getAnnouncements = React.useCallback( + async (selectedGroup) => { + try { + const offset = 0; + + // dispatch(setIsLoadingGlobal(true)) + const identifier = `grp-${selectedGroup}-anc-`; + const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${offset}&reverse=true&prefix=true`; + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + const responseData = await response.json(); + + setTempData() + setAnnouncements(responseData); + setIsLoading(false); + for (const data of responseData) { + getAnnouncementData({ name: data.name, identifier: data.identifier }); + } + } catch (error) { + } finally { + // dispatch(setIsLoadingGlobal(false)) + } + }, + [secretKey] + ); + + React.useEffect(() => { + if (selectedGroup && secretKey && !hasInitialized.current) { + getAnnouncements(selectedGroup); + hasInitialized.current = true + } + }, [selectedGroup, secretKey]); + + + const loadMore = async()=> { + try { + setIsLoading(true); + + const offset = announcements.length + const identifier = `grp-${selectedGroup}-anc-`; + const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${offset}&reverse=true&prefix=true`; + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + const responseData = await response.json(); + + setAnnouncements((prev)=> [...prev, ...responseData]); + setIsLoading(false); + for (const data of responseData) { + getAnnouncementData({ name: data.name, identifier: data.identifier }); + } + } catch (error) { + + } + + } + + const interval = useRef(null) + + const checkNewMessages = React.useCallback( + async () => { + try { + + const identifier = `grp-${selectedGroup}-anc-`; + const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${0}&reverse=true&prefix=true`; + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }) + const responseData = await response.json() + const latestMessage = announcements[0] + if (!latestMessage) { + for (const data of responseData) { + try { + + getAnnouncementData({ name: data.name, identifier: data.identifier }); + + } catch (error) {} + } + setAnnouncements(responseData) + return + } + const findMessage = responseData?.findIndex( + (item: any) => item?.identifier === latestMessage?.identifier + ) + + if(findMessage === -1) return + const newArray = responseData.slice(0, findMessage) + + for (const data of newArray) { + try { + + getAnnouncementData({ name: data.name, identifier: data.identifier }); + + } catch (error) {} + } + setAnnouncements((prev)=> [...newArray, ...prev]) + } catch (error) { + } finally { + } + }, + [announcements, secretKey, selectedGroup] + ) + + const checkNewMessagesFunc = useCallback(() => { + let isCalling = false + interval.current = setInterval(async () => { + if (isCalling) return + isCalling = true + const res = await checkNewMessages() + isCalling = false + }, 20000) + }, [checkNewMessages]) + + useEffect(() => { + if(!secretKey) return + checkNewMessagesFunc() + return () => { + if (interval?.current) { + clearInterval(interval.current) + } + } + }, [checkNewMessagesFunc]) + + + + const combinedListTempAndReal = useMemo(() => { + // Combine the two lists + const combined = [...tempPublishedList, ...announcements]; + + // Remove duplicates based on the "identifier" + const uniqueItems = new Map(); + combined.forEach(item => { + uniqueItems.set(item.identifier, item); // This will overwrite duplicates, keeping the last occurrence + }); + + // Convert the map back to an array and sort by "created" timestamp in descending order + const sortedList = Array.from(uniqueItems.values()).sort((a, b) => b.created - a.created); + + return sortedList; + }, [tempPublishedList, announcements]); + + + if(selectedAnnouncement){ + return ( +
+ +
+ ) + } + + + + return ( +
+
+ + + Group Announcements + + + +
+ {!isLoading && combinedListTempAndReal?.length === 0 && ( + + No announcements + + )} + 0 && announcements.length % 20 === 0} + loadMore={loadMore} + /> + + +{isAdmin && ( +
+
+ +
+ { + if (isSending) return; + publishAnnouncement(); + }} + style={{ + marginTop: "auto", + alignSelf: "center", + cursor: isSending ? "default" : "pointer", + background: isSending && "rgba(0, 0, 0, 0.8)", + flexShrink: 0, + }} + > + {isSending && ( + + )} + {` Publish Announcement`} + +
+)} + + + + +
+ ); +}; diff --git a/src/components/Chat/GroupForum.tsx b/src/components/Chat/GroupForum.tsx new file mode 100644 index 0000000..85f8bbb --- /dev/null +++ b/src/components/Chat/GroupForum.tsx @@ -0,0 +1,52 @@ +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { GroupMail } from "../Group/Forum/GroupMail"; + + + + + + +export const GroupForum = ({ + selectedGroup, + userInfo, + secretKey, + getSecretKey, + isAdmin, + myAddress, + hide, + defaultThread, + setDefaultThread +}) => { + const [isMoved, setIsMoved] = useState(false); + useEffect(() => { + if (hide) { + setTimeout(() => setIsMoved(true), 300); // Wait for the fade-out to complete before moving + } else { + setIsMoved(false); // Reset the position immediately when showing + } + }, [hide]); + + return ( +
+ + +
+ ); +}; diff --git a/src/components/Chat/MessageDisplay.tsx b/src/components/Chat/MessageDisplay.tsx new file mode 100644 index 0000000..b52a5be --- /dev/null +++ b/src/components/Chat/MessageDisplay.tsx @@ -0,0 +1,66 @@ +import React, { useEffect } from 'react'; +import DOMPurify from 'dompurify'; +import './styles.css'; // Ensure this CSS file is imported + +export const MessageDisplay = ({ htmlContent }) => { + const linkify = (text) => { + // Regular expression to find URLs starting with https://, http://, or www. + const urlPattern = /(\bhttps?:\/\/[^\s<]+|\bwww\.[^\s<]+)/g; + + // Replace plain text URLs with anchor tags + return text.replace(urlPattern, (url) => { + const href = url.startsWith('http') ? url : `https://${url}`; + return `${DOMPurify.sanitize(url)}`; + }); + }; + + // Sanitize and linkify the content + const sanitizedContent = DOMPurify.sanitize(linkify(htmlContent), { + ALLOWED_TAGS: [ + 'a', 'b', '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: [ + 'href', 'target', 'rel', 'class', 'src', 'alt', 'title', + 'width', 'height', 'style', 'align', 'valign', 'colspan', 'rowspan', 'border', 'cellpadding', 'cellspacing' + ], + }); + + // Function to handle link clicks + const handleClick = (e) => { + e.preventDefault(); + + // Ensure we are targeting an element + const target = e.target.closest('a'); + if (target) { + const href = target.getAttribute('href'); + + if (chrome && chrome.tabs) { + chrome.tabs.create({ url: href }, (tab) => { + if (chrome.runtime.lastError) { + console.error('Error opening tab:', chrome.runtime.lastError); + } else { + console.log('Tab opened successfully:', tab); + } + }); + } else { + console.error('chrome.tabs API is not available.'); + } + } else { + console.error('No tag found or href is null.'); + } + }; + return ( +
{ + // Delegate click handling to the parent div + if (e.target.tagName === 'A') { + handleClick(e); + } + }} + /> + ); +}; + diff --git a/src/components/Chat/MessageItem.tsx b/src/components/Chat/MessageItem.tsx new file mode 100644 index 0000000..e7e2b87 --- /dev/null +++ b/src/components/Chat/MessageItem.tsx @@ -0,0 +1,93 @@ +import { Message } from "@chatscope/chat-ui-kit-react"; +import React, { useEffect } from "react"; +import { useInView } from "react-intersection-observer"; +import { MessageDisplay } from "./MessageDisplay"; +import { Avatar, Box, Typography } from "@mui/material"; +import { formatTimestamp } from "../../utils/time"; +import { getBaseApi } from "../../background"; +import { getBaseApiReact } from "../../App"; + +export const MessageItem = ({ message, onSeen }) => { + + const { ref, inView } = useInView({ + threshold: 1.0, // Fully visible + triggerOnce: true, // Only trigger once when it becomes visible + }); + + useEffect(() => { + if (inView && message.unread) { + onSeen(message.id); + } + }, [inView, message.id, message.unread, onSeen]); + + return ( +
+ + {message?.senderName?.charAt(0)} + + + + {message?.senderName || message?.sender} + + {message?.text?.type === "notification" ? ( + + ) : ( + + )} + + {formatTimestamp(message.timestamp)} + + + + {/* */} + {/* {!message.unread && Seen} */} +
+ ); +}; diff --git a/src/components/Chat/ResizableImage.tsx b/src/components/Chat/ResizableImage.tsx new file mode 100644 index 0000000..8c29292 --- /dev/null +++ b/src/components/Chat/ResizableImage.tsx @@ -0,0 +1,63 @@ +import React, { useRef } from 'react'; +import { NodeViewWrapper } from '@tiptap/react'; + +const ResizableImage = ({ node, updateAttributes, selected }) => { + const imgRef = useRef(null); + + const startResizing = (e) => { + e.preventDefault(); + e.stopPropagation(); + + const startX = e.clientX; + const startWidth = imgRef.current.offsetWidth; + + const onMouseMove = (e) => { + const newWidth = startWidth + e.clientX - startX; + updateAttributes({ width: `${newWidth}px` }); + }; + + const onMouseUp = () => { + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + }; + + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + }; + + return ( + + {node.attrs.alt +
+
+ ); +}; + +export default ResizableImage; diff --git a/src/components/Chat/TipTap.tsx b/src/components/Chat/TipTap.tsx new file mode 100644 index 0000000..c7c078d --- /dev/null +++ b/src/components/Chat/TipTap.tsx @@ -0,0 +1,292 @@ +import React, { useEffect, useRef } from 'react'; +import { EditorProvider, useCurrentEditor } from '@tiptap/react'; +import StarterKit from '@tiptap/starter-kit'; +import { Color } from '@tiptap/extension-color'; +import ListItem from '@tiptap/extension-list-item'; +import TextStyle from '@tiptap/extension-text-style'; +import Placeholder from '@tiptap/extension-placeholder' +import Image from '@tiptap/extension-image'; +import IconButton from '@mui/material/IconButton'; +import FormatBoldIcon from '@mui/icons-material/FormatBold'; +import FormatItalicIcon from '@mui/icons-material/FormatItalic'; +import StrikethroughSIcon from '@mui/icons-material/StrikethroughS'; +import FormatClearIcon from '@mui/icons-material/FormatClear'; +import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted'; +import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; +import CodeIcon from '@mui/icons-material/Code'; +import ImageIcon from '@mui/icons-material/Image'; // Import Image icon +import FormatQuoteIcon from '@mui/icons-material/FormatQuote'; +import HorizontalRuleIcon from '@mui/icons-material/HorizontalRule'; +import UndoIcon from '@mui/icons-material/Undo'; +import RedoIcon from '@mui/icons-material/Redo'; +import FormatHeadingIcon from '@mui/icons-material/FormatSize'; +import DeveloperModeIcon from '@mui/icons-material/DeveloperMode'; +import CustomImage from './CustomImage'; +import Compressor from 'compressorjs' + +import ImageResize from 'tiptap-extension-resize-image'; // Import the ResizeImage extension +const MenuBar = ({ setEditorRef }) => { + const { editor } = useCurrentEditor(); + const fileInputRef = useRef(null); + if (!editor) { + return null; + } + + useEffect(() => { + if (editor && setEditorRef) { + setEditorRef(editor); + } + }, [editor, setEditorRef]); + + const handleImageUpload = async (event) => { + + const file = event.target.files[0]; + + let compressedFile + await new Promise((resolve) => { + new Compressor(file, { + quality: 0.6, + maxWidth: 1200, + mimeType: 'image/webp', + success(result) { + const file = new File([result], 'name', { + type: 'image/webp' + }) + compressedFile = file + resolve() + }, + error(err) {} + }) + }) + + if (compressedFile) { + const reader = new FileReader(); + reader.onload = () => { + const url = reader.result; + editor.chain().focus().setImage({ src: url , style: "width: auto"}).run(); + fileInputRef.current.value = ''; + }; + reader.readAsDataURL(compressedFile); + } + }; + + const triggerImageUpload = () => { + fileInputRef.current.click(); // Trigger the file input click + }; + + return ( +
+
+ editor.chain().focus().toggleBold().run()} + disabled={ + !editor.can() + .chain() + .focus() + .toggleBold() + .run() + } + // color={editor.isActive('bold') ? 'white' : 'gray'} + sx={{ + color: editor.isActive('bold') ? 'white' : 'gray' + }} + > + + + editor.chain().focus().toggleItalic().run()} + disabled={ + !editor.can() + .chain() + .focus() + .toggleItalic() + .run() + } + // color={editor.isActive('italic') ? 'white' : 'gray'} + sx={{ + color: editor.isActive('italic') ? 'white' : 'gray' + }} + > + + + editor.chain().focus().toggleStrike().run()} + disabled={ + !editor.can() + .chain() + .focus() + .toggleStrike() + .run() + } + // color={editor.isActive('strike') ? 'white' : 'gray'} + sx={{ + color: editor.isActive('strike') ? 'white' : 'gray' + }} + > + + + editor.chain().focus().toggleCode().run()} + disabled={ + !editor.can() + .chain() + .focus() + .toggleCode() + .run() + } + // color={editor.isActive('code') ? 'white' : 'gray'} + sx={{ + color: editor.isActive('code') ? 'white' : 'gray' + }} + > + + + editor.chain().focus().unsetAllMarks().run()}> + + + editor.chain().focus().toggleBulletList().run()} + // color={editor.isActive('bulletList') ? 'white' : 'gray'} + sx={{ + color: editor.isActive('bulletList') ? 'white' : 'gray' + }} + > + + + editor.chain().focus().toggleOrderedList().run()} + // color={editor.isActive('orderedList') ? 'white' : 'gray'} + sx={{ + color: editor.isActive('orderedList') ? 'white' : 'gray' + }} + > + + + editor.chain().focus().toggleCodeBlock().run()} + // color={editor.isActive('codeBlock') ? 'white' : 'gray'} + sx={{ + color: editor.isActive('codeBlock') ? 'white' : 'gray' + }} + > + + + editor.chain().focus().toggleBlockquote().run()} + // color={editor.isActive('blockquote') ? 'white' : 'gray'} + sx={{ + color: editor.isActive('blockquote') ? 'white' : 'gray' + }} + > + + + editor.chain().focus().setHorizontalRule().run()}> + + + editor.chain().focus().toggleHeading({ level: 1 }).run()} + // color={editor.isActive('heading', { level: 1 }) ? 'white' : 'gray'} + sx={{ + color: editor.isActive('heading', { level: 1 }) ? 'white' : 'gray' + }} + > + + + editor.chain().focus().undo().run()} + disabled={ + !editor.can() + .chain() + .focus() + .undo() + .run() + } + sx={{ + color: 'gray' + }} + > + + + editor.chain().focus().redo().run()} + disabled={ + !editor.can() + .chain() + .focus() + .redo() + .run() + } + > + + + + + + +
+
+ ); +}; + +const extensions = [ + Color.configure({ types: [TextStyle.name, ListItem.name] }), + TextStyle.configure({ types: [ListItem.name] }), + StarterKit.configure({ + bulletList: { + keepMarks: true, + keepAttributes: false, + }, + orderedList: { + keepMarks: true, + keepAttributes: false, + }, + }), + Placeholder.configure({ + placeholder: 'Start typing here...', // Add your placeholder text here + }), + ImageResize, +]; + +const content = ``; + +export default ({ setEditorRef, onEnter, disableEnter }) => { + return ( + } + extensions={extensions} + content={content} + editorProps={{ + handleKeyDown(view, event) { + if (!disableEnter && event.key === 'Enter') { + if (event.shiftKey) { + // Shift+Enter: Insert a hard break + view.dispatch(view.state.tr.replaceSelectionWith(view.state.schema.nodes.hardBreak.create())); + return true; + } else { + // Enter: Call the callback function + if (typeof onEnter === 'function') { + onEnter(); + } + return true; // Prevent the default action of adding a new line + } + } + return false; // Allow default handling for other keys + }, + }} + /> + ); +}; diff --git a/src/components/Chat/styles.css b/src/components/Chat/styles.css new file mode 100644 index 0000000..f7b28ea --- /dev/null +++ b/src/components/Chat/styles.css @@ -0,0 +1,121 @@ +.tiptap { + margin-top: 0; + color: white; /* Set default font color to white */ + width: 100%; +} + +.tiptap ul, +.tiptap ol { + padding: 0 1rem; + margin: 1.25rem 1rem 1.25rem 0.4rem; +} + +.tiptap ul li p, +.tiptap ol li p { + margin-top: 0.25em; + margin-bottom: 0.25em; +} + +/* Heading styles */ +.tiptap h1, +.tiptap h2, +.tiptap h3, +.tiptap h4, +.tiptap h5, +.tiptap h6 { + line-height: 1.1; + margin-top: 2.5rem; + text-wrap: pretty; + color: white; /* Ensure heading font color is white */ +} + +.tiptap h1, +.tiptap h2 { + margin-top: 3.5rem; + margin-bottom: 1.5rem; +} + +.tiptap h1 { + font-size: 1.4rem; +} + +.tiptap h2 { + font-size: 1.2rem; +} + +.tiptap h3 { + font-size: 1.1rem; +} + +.tiptap h4, +.tiptap h5, +.tiptap h6 { + font-size: 1rem; +} + +/* Code and preformatted text styles */ +.tiptap code { + background-color: #27282c; /* Set code background color to #27282c */ + border-radius: 0.4rem; + color: white; /* Ensure inline code text color is white */ + font-size: 0.85rem; + padding: 0.25em 0.3em; + text-wrap: pretty; +} + +.tiptap pre { + background: #27282c; /* Set code block background color to #27282c */ + border-radius: 0.5rem; + color: white; /* Ensure code block text color is white */ + font-family: 'JetBrainsMono', monospace; + margin: 1.5rem 0; + padding: 0.75rem 1rem; + outline: none; +} + +.tiptap pre code { + background: none; + color: inherit; /* Inherit text color from the parent pre block */ + font-size: 0.8rem; + padding: 0; + text-wrap: pretty; +} + +.tiptap blockquote { + border-left: 3px solid var(--gray-3); + margin: 1.5rem 0; + padding-left: 1rem; + color: white; /* Ensure blockquote text color is white */ + text-wrap: pretty; +} + +.tiptap hr { + border: none; + border-top: 1px solid var(--gray-2); + margin: 2rem 0; +} + +.ProseMirror:focus-visible { + outline: none !important; +} + +.tiptap p { + font-size: 16px; + color: white; /* Ensure paragraph text color is white */ +} + .tiptap p.is-editor-empty:first-child::before { + color: #adb5bd; + content: attr(data-placeholder); + float: left; + height: 0; + pointer-events: none; + } + +.tiptap a { + color: cadetblue +} + +.tiptap img { + display: block; + max-width: 100%; +} \ No newline at end of file diff --git a/src/components/Group/AddGroup.tsx b/src/components/Group/AddGroup.tsx new file mode 100644 index 0000000..47bbf0e --- /dev/null +++ b/src/components/Group/AddGroup.tsx @@ -0,0 +1,474 @@ +import * as React from "react"; +import Button from "@mui/material/Button"; +import Dialog from "@mui/material/Dialog"; +import ListItemText from "@mui/material/ListItemText"; +import ListItemButton from "@mui/material/ListItemButton"; +import List from "@mui/material/List"; +import Divider from "@mui/material/Divider"; +import AppBar from "@mui/material/AppBar"; +import Toolbar from "@mui/material/Toolbar"; +import IconButton from "@mui/material/IconButton"; +import Typography from "@mui/material/Typography"; +import CloseIcon from "@mui/icons-material/Close"; +import ExpandLess from "@mui/icons-material/ExpandLess"; +import ExpandMore from "@mui/icons-material/ExpandMore"; +import Slide from "@mui/material/Slide"; +import { TransitionProps } from "@mui/material/transitions"; +import { + Box, + Collapse, + Input, + MenuItem, + Select, + SelectChangeEvent, + Tab, + Tabs, + styled, +} from "@mui/material"; +import { AddGroupList } from "./AddGroupList"; +import { UserListOfInvites } from "./UserListOfInvites"; +import { CustomizedSnackbars } from "../Snackbar/Snackbar"; +import { getFee } from "../../background"; +import { MyContext } from "../../App"; +import { subscribeToEvent, unsubscribeFromEvent } from "../../utils/events"; + +export const Label = styled("label")( + ({ theme }) => ` + font-family: 'IBM Plex Sans', sans-serif; + font-size: 14px; + display: block; + margin-bottom: 4px; + font-weight: 400; + ` +); +const Transition = React.forwardRef(function Transition( + props: TransitionProps & { + children: React.ReactElement; + }, + ref: React.Ref +) { + return ; +}); + +export const AddGroup = ({ address, open, setOpen }) => { + const {show, setTxList} = React.useContext(MyContext) + + const [tab, setTab] = React.useState("create"); + const [openAdvance, setOpenAdvance] = React.useState(false); + + const [name, setName] = React.useState(""); + const [description, setDescription] = React.useState(""); + const [groupType, setGroupType] = React.useState("1"); + const [approvalThreshold, setApprovalThreshold] = React.useState("40"); + const [minBlock, setMinBlock] = React.useState("5"); + const [maxBlock, setMaxBlock] = React.useState("21600"); + const [value, setValue] = React.useState(0); + const [openSnack, setOpenSnack] = React.useState(false); + const [infoSnack, setInfoSnack] = React.useState(null); + + const handleChange = (event: React.SyntheticEvent, newValue: number) => { + setValue(newValue); + }; + const handleClose = () => { + setOpen(false); + }; + + const handleChangeGroupType = (event: SelectChangeEvent) => { + setGroupType(event.target.value as string); + }; + + const handleChangeApprovalThreshold = (event: SelectChangeEvent) => { + setGroupType(event.target.value as string); + }; + + const handleChangeMinBlock = (event: SelectChangeEvent) => { + setMinBlock(event.target.value as string); + }; + + const handleChangeMaxBlock = (event: SelectChangeEvent) => { + setMaxBlock(event.target.value as string); + }; + + + + const handleCreateGroup = async () => { + try { + if(!name) throw new Error('Please provide a name') + if(!description) throw new Error('Please provide a description') + + const fee = await getFee('CREATE_GROUP') + await show({ + message: "Would you like to perform an CREATE_GROUP transaction?" , + publishFee: fee.fee + ' QORT' + }) + + await new Promise((res, rej) => { + chrome.runtime.sendMessage( + { + action: "createGroup", + payload: { + groupName: name, + groupDescription: description, + groupType: +groupType, + groupApprovalThreshold: +approvalThreshold, + minBlock: +minBlock, + maxBlock: +maxBlock, + }, + }, + (response) => { + + if (!response?.error) { + setInfoSnack({ + type: "success", + message: "Successfully created group. It may take a couple of minutes for the changes to propagate", + }); + setOpenSnack(true); + setTxList((prev)=> [{ + ...response, + type: 'created-group', + label: `Created group ${name}: awaiting confirmation`, + labelDone: `Created group ${name}: success !`, + done: false + }, ...prev]) + res(response); + return + } + rej({message: response.error}); + + } + ); + }); + } catch (error) { + setInfoSnack({ + type: "error", + message: error?.message, + }); + setOpenSnack(true); + } + }; + + function CustomTabPanel(props: TabPanelProps) { + const { children, value, index, ...other } = props; + + return ( + + ); + } + + function a11yProps(index: number) { + return { + id: `simple-tab-${index}`, + "aria-controls": `simple-tabpanel-${index}`, + }; + } + + + const openGroupInvitesRequestFunc = ()=> { + setValue(2) + } + + React.useEffect(() => { + subscribeToEvent("openGroupInvitesRequest", openGroupInvitesRequestFunc); + + return () => { + unsubscribeFromEvent("openGroupInvitesRequest", openGroupInvitesRequestFunc); + }; + }, []); + + return ( + + + + + + Add Group + + + + + + + {/* */} + + + + + + + + + + + + {value === 0 && ( + + + + + setName(e.target.value)} + /> + + + + + setDescription(e.target.value)} + /> + + + + + + setOpenAdvance((prev) => !prev)} + > + Advanced options + + {openAdvance ? : } + + + + + + + + + + + + + + + + + + + + + )} + {value === 1 && ( + + + + + + )} + + {value === 2 && ( + + + + )} + + + + + + + ); +}; diff --git a/src/components/Group/AddGroupList.tsx b/src/components/Group/AddGroupList.tsx new file mode 100644 index 0000000..d0b8772 --- /dev/null +++ b/src/components/Group/AddGroupList.tsx @@ -0,0 +1,272 @@ +import { + Box, + Button, + ListItem, + ListItemButton, + ListItemText, + Popover, + TextField, + Typography, +} from "@mui/material"; +import React, { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { + AutoSizer, + CellMeasurer, + CellMeasurerCache, + List, +} from "react-virtualized"; +import _ from "lodash"; +import { MyContext, getBaseApiReact } from "../../App"; +import { LoadingButton } from "@mui/lab"; +import { getBaseApi, getFee } from "../../background"; + +const cache = new CellMeasurerCache({ + fixedWidth: true, + defaultHeight: 50, +}); + +export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => { + const { memberGroups, show, setTxList } = useContext(MyContext); + + const [groups, setGroups] = useState([]); + const [popoverAnchor, setPopoverAnchor] = useState(null); // Track which list item the popover is anchored to + const [openPopoverIndex, setOpenPopoverIndex] = useState(null); // Track which list item has the popover open + const listRef = useRef(); + const [inputValue, setInputValue] = useState(""); + const [filteredItems, setFilteredItems] = useState(groups); + const [isLoading, setIsLoading] = useState(false); + + const handleFilter = useCallback( + (query) => { + if (query) { + setFilteredItems( + groups.filter((item) => + item.groupName.toLowerCase().includes(query.toLowerCase()) + ) + ); + } else { + setFilteredItems(groups); + } + }, + [groups] + ); + const debouncedFilter = useMemo( + () => _.debounce(handleFilter, 500), + [handleFilter] + ); + + const handleChange = (event) => { + const value = event.target.value; + setInputValue(value); + debouncedFilter(value); + }; + + const getGroups = async () => { + try { + const response = await fetch( + `${getBaseApiReact()}/groups/?limit=0` + ); + const groupData = await response.json(); + const filteredGroup = groupData.filter( + (item) => !memberGroups.find((group) => group.groupId === item.groupId) + ); + setGroups(filteredGroup); + setFilteredItems(filteredGroup); + } catch (error) { + console.error(error); + } + }; + + useEffect(() => { + getGroups(); + }, [memberGroups]); + + const handlePopoverOpen = (event, index) => { + setPopoverAnchor(event.currentTarget); + setOpenPopoverIndex(index); + }; + + const handlePopoverClose = () => { + setPopoverAnchor(null); + setOpenPopoverIndex(null); + }; + + const handleJoinGroup = async (group, isOpen) => { + try { + const groupId = group.groupId; + const fee = await getFee('JOIN_GROUP') + await show({ + message: "Would you like to perform an JOIN_GROUP transaction?" , + publishFee: fee.fee + ' QORT' + }) + setIsLoading(true); + await new Promise((res, rej) => { + chrome.runtime.sendMessage( + { + action: "joinGroup", + payload: { + groupId, + }, + }, + (response) => { + + if (!response?.error) { + setInfoSnack({ + type: "success", + message: "Successfully requested to join group. It may take a couple of minutes for the changes to propagate", + }); + if(isOpen){ + setTxList((prev)=> [{ + ...response, + type: 'joined-group', + label: `Joined Group ${group?.groupName}: awaiting confirmation`, + labelDone: `Joined Group ${group?.groupName}: success !`, + done: false, + groupId, + }, ...prev]) + } else { + setTxList((prev)=> [{ + ...response, + type: 'joined-group-request', + label: `Requested to join Group ${group?.groupName}: awaiting confirmation`, + labelDone: `Requested to join Group ${group?.groupName}: success !`, + done: false, + groupId, + }, ...prev]) + } + setOpenSnack(true); + handlePopoverClose(); + res(response); + return; + } else { + setInfoSnack({ + type: "error", + message: response?.error, + }); + setOpenSnack(true); + rej(response.error); + } + } + ); + }); + setIsLoading(false); + } catch (error) {} finally { + setIsLoading(false); + + } + }; + + const rowRenderer = ({ index, key, parent, style }) => { + const group = filteredItems[index]; + + return ( + + {({ measure }) => ( +
+ + + + Join {group?.groupName} + + {group?.isOpen === false && + "This is a closed/private group, so you will need to wait until an admin accepts your request"} + + handleJoinGroup(group, group?.isOpen)} + > + Join group + + + + handlePopoverOpen(event, index)} + > + + + + +
+ )} +
+ ); + }; + + return ( +
+

Groups list

+ +
+ + {({ height, width }) => ( + + )} + +
+
+ ); +}; diff --git a/src/components/Group/Forum/DisplayHtml.tsx b/src/components/Group/Forum/DisplayHtml.tsx new file mode 100644 index 0000000..5bae4a7 --- /dev/null +++ b/src/components/Group/Forum/DisplayHtml.tsx @@ -0,0 +1,45 @@ +import { useMemo } from "react"; +import DOMPurify from "dompurify"; +import "react-quill/dist/quill.snow.css"; +import "react-quill/dist/quill.core.css"; +import "react-quill/dist/quill.bubble.css"; +import { Box, styled } from "@mui/material"; +import { convertQortalLinks } from "../../../utils/qortalLink"; + + +const CrowdfundInlineContent = styled(Box)(({ theme }) => ({ + display: "flex", + fontFamily: "Mulish", + fontSize: "19px", + fontWeight: 400, + letterSpacing: 0, + color: theme.palette.text.primary, + width: '100%' + })); + +export const DisplayHtml = ({ html, textColor }: any) => { + const cleanContent = useMemo(() => { + if (!html) return null; + + const sanitize: string = DOMPurify.sanitize(html, { + USE_PROFILES: { html: true }, + }); + const anchorQortal = convertQortalLinks(sanitize); + return anchorQortal; + }, [html]); + + if (!cleanContent) return null; + return ( + +
+ + ); +}; diff --git a/src/components/Group/Forum/GroupMail.tsx b/src/components/Group/Forum/GroupMail.tsx new file mode 100644 index 0000000..d962347 --- /dev/null +++ b/src/components/Group/Forum/GroupMail.tsx @@ -0,0 +1,777 @@ +import React, { + FC, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { Avatar, Box, Popover, Typography } from "@mui/material"; +// import { MAIL_SERVICE_TYPE, THREAD_SERVICE_TYPE } from "../../constants/mail"; +import { Thread } from "./Thread"; +import { + AllThreadP, + ArrowDownIcon, + ComposeContainer, + ComposeContainerBlank, + ComposeIcon, + ComposeP, + GroupContainer, + GroupNameP, + InstanceFooter, + InstanceListContainer, + InstanceListContainerRow, + InstanceListContainerRowCheck, + InstanceListContainerRowCheckIcon, + InstanceListContainerRowMain, + InstanceListContainerRowMainP, + InstanceListHeader, + InstanceListParent, + SelectInstanceContainerFilterInner, + SingleThreadParent, + ThreadContainer, + ThreadContainerFullWidth, + ThreadInfoColumn, + ThreadInfoColumnNameP, + ThreadInfoColumnTime, + ThreadInfoColumnbyP, + ThreadSingleLastMessageP, + ThreadSingleLastMessageSpanP, + ThreadSingleTitle, +} from "./Mail-styles"; +import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; +import { Spacer } from "../../../common/Spacer"; +import { formatDate, formatTimestamp } from "../../../utils/time"; +import LazyLoad from "../../../common/LazyLoad"; +import { delay } from "../../../utils/helpers"; +import { NewThread } from "./NewThread"; +import { getBaseApi } from "../../../background"; +import { decryptPublishes, getTempPublish } from "../../Chat/GroupAnnouncements"; +import CheckSVG from "../../../assets/svgs/Check.svg"; +import SortSVG from "../../../assets/svgs/Sort.svg"; +import ArrowDownSVG from "../../../assets/svgs/ArrowDown.svg"; +import { LoadingSnackbar } from "../../Snackbar/LoadingSnackbar"; +import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "../../../utils/events"; +import RefreshIcon from '@mui/icons-material/Refresh'; +import { getBaseApiReact } from "../../../App"; +const filterOptions = ["Recently active", "Newest", "Oldest"]; + +export const threadIdentifier = "DOCUMENT"; +export const GroupMail = ({ + selectedGroup, + userInfo, + getSecretKey, + secretKey, + defaultThread, + setDefaultThread +}) => { + const [viewedThreads, setViewedThreads] = React.useState({}); + const [filterMode, setFilterMode] = useState("Recently active"); + const [currentThread, setCurrentThread] = React.useState(null); + const [recentThreads, setRecentThreads] = useState([]); + const [allThreads, setAllThreads] = useState([]); + const [members, setMembers] = useState(null); + const [isOpenFilterList, setIsOpenFilterList] = useState(false); + const anchorElInstanceFilter = useRef(null); + const [tempPublishedList, setTempPublishedList] = useState([]) + + const [isLoading, setIsLoading] = useState(false) + const groupIdRef = useRef(null); + const groupId = useMemo(() => { + return selectedGroup?.groupId; + }, [selectedGroup]); + + useEffect(() => { + if (groupId !== groupIdRef?.current) { + setCurrentThread(null); + setRecentThreads([]); + setAllThreads([]); + groupIdRef.current = groupId; + } + }, [groupId]); + + const setTempData = async ()=> { + try { + const getTempAnnouncements = await getTempPublish() + + if(getTempAnnouncements?.thread){ + let tempData = [] + Object.keys(getTempAnnouncements?.thread || {}).map((key)=> { + const value = getTempAnnouncements?.thread[key] + tempData.push(value.data) + }) + setTempPublishedList(tempData) + } + } catch (error) { + + } + + } + + const getEncryptedResource = async ({ name, identifier }) => { + + const res = await fetch( + `${getBaseApiReact()}/arbitrary/DOCUMENT/${name}/${identifier}?encoding=base64` + ); + const data = await res.text(); + const response = await decryptPublishes([{ data }], secretKey); + + const messageData = response[0]; + return messageData.decryptedData; + }; + + const updateThreadActivity = async ({threadId, qortalName, groupId, thread}) => { + try { + await new Promise((res, rej) => { + chrome.runtime.sendMessage( + { + action: "updateThreadActivity", + payload: { + threadId, qortalName, groupId, thread + }, + }, + (response) => { + + if (!response?.error) { + res(response); + return + } + rej(response.error); + } + ); + }); + + } catch (error) { + + } finally { + } + }; + + const getAllThreads = React.useCallback( + async (groupId: string, mode: string, isInitial?: boolean) => { + try { + setIsLoading(true) + const offset = isInitial ? 0 : allThreads.length; + const isReverse = mode === "Newest" ? true : false; + if (isInitial) { + // dispatch(setIsLoadingCustom("Loading threads")); + } + const identifier = `grp-${groupId}-thread-`; + + const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=${20}&includemetadata=false&offset=${offset}&reverse=${isReverse}&prefix=true`; + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + const responseData = await response.json(); + + let fullArrayMsg = isInitial ? [] : [...allThreads]; + const getMessageForThreads = responseData.map(async (message: any) => { + let fullObject: any = null; + if (message?.metadata?.description) { + fullObject = { + ...message, + threadData: { + title: message?.metadata?.description, + groupId: groupId, + createdAt: message?.created, + name: message?.name, + }, + threadOwner: message?.name, + }; + } else { + let threadRes = null; + try { + threadRes = await Promise.race([ + getEncryptedResource({ + name: message.name, + identifier: message.identifier, + }), + delay(5000), + ]); + } catch (error) {} + + if (threadRes?.title) { + fullObject = { + ...message, + threadData: threadRes, + threadOwner: message?.name, + threadId: message.identifier + }; + } + } + if (fullObject?.identifier) { + const index = fullArrayMsg.findIndex( + (p) => p.identifier === fullObject.identifier + ); + if (index !== -1) { + fullArrayMsg[index] = fullObject; + } else { + fullArrayMsg.push(fullObject); + } + } + }); + await Promise.all(getMessageForThreads); + let sorted = fullArrayMsg; + if (isReverse) { + sorted = fullArrayMsg.sort((a: any, b: any) => b.created - a.created); + } else { + sorted = fullArrayMsg.sort((a: any, b: any) => a.created - b.created); + } + + setAllThreads(sorted); + } catch (error) { + console.log({ error }); + } finally { + if (isInitial) { + setIsLoading(false) + // dispatch(setIsLoadingCustom(null)); + } + } + }, + [allThreads] + ); + const getMailMessages = React.useCallback( + async (groupId: string, members: any) => { + try { + setIsLoading(true) + // const memberNames = Object.keys(members); + // const queryString = memberNames + // .map(name => `&name=${encodeURIComponent(name)}`) + // .join(""); + + // dispatch(setIsLoadingCustom("Loading recent threads")); + const identifier = `thmsg-grp-${groupId}-thread-`; + const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=100&includemetadata=false&offset=${0}&reverse=true&prefix=true`; + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + const responseData = await response.json(); + const messagesForThread: any = {}; + for (const message of responseData) { + let str = message.identifier; + const parts = str.split("-"); + + // Get the second last element + const secondLastId = parts[parts.length - 2]; + const result = `grp-${groupId}-thread-${secondLastId}`; + const checkMessage = messagesForThread[result]; + if (!checkMessage) { + messagesForThread[result] = message; + } + } + + const newArray = Object.keys(messagesForThread) + .map((key) => { + return { + ...messagesForThread[key], + threadId: key, + }; + }) + .sort((a, b) => b.created - a.created) + .slice(0, 10); + + let fullThreadArray: any = []; + const getMessageForThreads = newArray.map(async (message: any) => { + try { + const identifierQuery = message.threadId; + const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=${threadIdentifier}&identifier=${identifierQuery}&limit=1&includemetadata=false&offset=${0}&reverse=true&prefix=true`; + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + const responseData = await response.json(); + if (responseData.length > 0) { + const thread = responseData[0]; + if (thread?.metadata?.description) { + const fullObject = { + ...message, + threadData: { + title: thread?.metadata?.description, + groupId: groupId, + createdAt: thread?.created, + name: thread?.name, + }, + threadOwner: thread?.name, + }; + fullThreadArray.push(fullObject); + } else { + let threadRes = await Promise.race([ + getEncryptedResource({ + name: thread.name, + identifier: message.threadId, + }), + delay(10000), + ]); + if (threadRes?.title) { + const fullObject = { + ...message, + threadData: threadRes, + threadOwner: thread?.name, + }; + fullThreadArray.push(fullObject); + } + } + } + } catch (error) { + console.log(error); + } + return null; + }); + await Promise.all(getMessageForThreads); + const sorted = fullThreadArray.sort( + (a: any, b: any) => b.created - a.created + ); + setRecentThreads(sorted); + } catch (error) { + } finally { + setIsLoading(false) + // dispatch(setIsLoadingCustom(null)); + } + }, + [secretKey] + ); + + const getMessages = React.useCallback(async () => { + + // if ( !groupId || members?.length === 0) return; + if (!groupId) return; + + await getMailMessages(groupId, members); + }, [getMailMessages, groupId, members, secretKey]); + + const interval = useRef(null); + + const firstMount = useRef(false); + const filterModeRef = useRef(""); + + useEffect(() => { + if (filterModeRef.current !== filterMode) { + firstMount.current = false; + } + // if (groupId && !firstMount.current && members.length > 0) { + if (groupId && !firstMount.current) { + if (filterMode === "Recently active") { + getMessages(); + } else if (filterMode === "Newest") { + getAllThreads(groupId, "Newest", true); + } else if (filterMode === "Oldest") { + getAllThreads(groupId, "Oldest", true); + } + setTempData() + firstMount.current = true; + } + }, [groupId, members, filterMode]); + + const closeThread = useCallback(() => { + setCurrentThread(null); + }, []); + + const getGroupMembers = useCallback(async (groupNumber: string) => { + try { + const response = await fetch(`/groups/members/${groupNumber}?limit=0`); + const groupData = await response.json(); + + let members: any = {}; + if (groupData && Array.isArray(groupData?.members)) { + for (const member of groupData.members) { + if (member.member) { + // const res = await getNameInfo(member.member); + // const resAddress = await qortalRequest({ + // action: "GET_ACCOUNT_DATA", + // address: member.member, + // }); + const name = res; + const publicKey = resAddress.publicKey; + if (name) { + members[name] = { + publicKey, + address: member.member, + }; + } + } + } + } + + setMembers(members); + } catch (error) { + console.log({ error }); + } + }, []); + + // useEffect(() => { + // if(groupId){ + // getGroupMembers(groupId); + // interval.current = setInterval(async () => { + // getGroupMembers(groupId); + // }, 180000) + // } + // return () => { + // if (interval?.current) { + // clearInterval(interval.current) + // } + // } + // }, [getGroupMembers, groupId]); + + + let listOfThreadsToDisplay = recentThreads; + if (filterMode === "Newest" || filterMode === "Oldest") { + listOfThreadsToDisplay = allThreads; + } + + const onSubmitNewThread = useCallback( + (val: any) => { + if (filterMode === "Recently active") { + setRecentThreads((prev) => [val, ...prev]); + } else if (filterMode === "Newest") { + setAllThreads((prev) => [val, ...prev]); + } + }, + [filterMode] + ); + + // useEffect(()=> { + // if(user?.name){ + // const threads = JSON.parse( + // localStorage.getItem(`qmail_threads_viewedtimestamp_${user.name}`) || "{}" + // ); + // setViewedThreads(threads) + + // } + // }, [user?.name, currentThread]) + + const handleCloseThreadFilterList = () => { + setIsOpenFilterList(false); + }; + + const refetchThreadsLists = useCallback(()=> { + if (filterMode === "Recently active") { + getMessages(); + } else if (filterMode === "Newest") { + getAllThreads(groupId, "Newest", true); + } else if (filterMode === "Oldest") { + getAllThreads(groupId, "Oldest", true); + } + }, [filterMode]) + + const updateThreadActivityCurrentThread = ()=> { + if(!currentThread) return + const thread = currentThread + updateThreadActivity({ + threadId: thread?.threadId, qortalName: thread?.threadData?.name, groupId: groupId, thread: thread + }) + } + + const setThreadFunc = (data)=> { + const thread = data + setCurrentThread(thread); + if(thread?.threadId && thread?.threadData?.name){ + updateThreadActivity({ + threadId: thread?.threadId, qortalName: thread?.threadData?.name, groupId: groupId, thread: thread + }) + } + setTimeout(() => { + executeEvent("threadFetchMode", { + mode: "last-page" + }); + }, 300); + } + + + useEffect(()=> { + if(defaultThread){ + setThreadFunc(defaultThread) + setDefaultThread(null) + } + }, [defaultThread]) + + const combinedListTempAndReal = useMemo(() => { + // Combine the two lists + const transformTempPublishedList = tempPublishedList.map((item)=> { + return { + ...item, + threadData: item.tempData, + threadOwner: item?.name, + threadId: item.identifier + } + }) + const combined = [...transformTempPublishedList, ...listOfThreadsToDisplay]; + + // Remove duplicates based on the "identifier" + const uniqueItems = new Map(); + combined.forEach(item => { + uniqueItems.set(item.threadId, item); // This will overwrite duplicates, keeping the last occurrence + }); + + // Convert the map back to an array and sort by "created" timestamp in descending order + const sortedList = Array.from(uniqueItems.values()).sort((a, b) => b.threadData?.createdAt - a.threadData?.createdAt); + + return sortedList; + }, [tempPublishedList, listOfThreadsToDisplay]); + + if (currentThread) + return ( + + ); + + return ( + + + + + + {filterOptions?.map((filter) => { + return ( + { + setFilterMode(filter); + }} + sx={{ + backgroundColor: + filterMode === filter ? "rgba(74, 158, 244, 1)" : "unset", + }} + key={filter} + > + + {filter === filterMode && ( + + )} + + + + {filter} + + + + ); + })} + + + + + + + + + + + {selectedGroup && !currentThread && ( + { + setIsOpenFilterList(true); + }} + ref={anchorElInstanceFilter} + > + + + + Sort by + + + + )} + + + + + {filterMode} + + + + + + {combinedListTempAndReal.map((thread) => { + const hasViewedRecent = + viewedThreads[ + `qmail_threads_${thread?.threadData?.groupId}_${thread?.threadId}` + ]; + const shouldAppearLighter = + hasViewedRecent && + filterMode === "Recently active" && + thread?.threadData?.createdAt < hasViewedRecent?.timestamp; + return ( + { + setCurrentThread(thread); + if(thread?.threadId && thread?.threadData?.name){ + updateThreadActivity({ + threadId: thread?.threadId, qortalName: thread?.threadData?.name, groupId: groupId, thread: thread + }) + } + }} + > + + {thread?.threadData?.name?.charAt(0)} + + + + by + {thread?.threadData?.name} + + + {formatTimestamp(thread?.threadData?.createdAt)} + + +
+ + {thread?.threadData?.title} + + {filterMode === "Recently active" && ( +
+ + + last message:{" "} + + {formatDate(thread?.created)} + +
+ )} + +
+ { + setTimeout(() => { + executeEvent("threadFetchMode", { + mode: "last-page" + }); + }, 300); + + + }} sx={{ + position: 'absolute', + bottom: '2px', + right: '2px', + borderRadius: '5px', + backgroundColor: '#27282c', + display: 'flex', + gap: '10px', + alignItems: 'center', + padding: '5px', + cursor: 'pointer', + '&:hover': { + background: 'rgba(255, 255, 255, 0.60)' + } + }}> + Last page + + +
+ ); + })} + + + {listOfThreadsToDisplay.length >= 20 && + filterMode !== "Recently active" && ( + getAllThreads(groupId, filterMode, false)} + > + )} + +
+
+ +
+ ); +}; diff --git a/src/components/Group/Forum/Mail-styles.ts b/src/components/Group/Forum/Mail-styles.ts new file mode 100644 index 0000000..2d39cee --- /dev/null +++ b/src/components/Group/Forum/Mail-styles.ts @@ -0,0 +1,799 @@ +import { + AppBar, + Button, + Toolbar, + Typography, + Box, + TextField, +} from "@mui/material"; +import { styled } from "@mui/system"; + +export const InstanceContainer = styled(Box)(({ theme }) => ({ + display: "flex", + alignItems: "center", + width: "100%", + backgroundColor: "var(--color-instance)", + height: "59px", + flexShrink: 0, + justifyContent: "space-between", +})); +export const MailContainer = styled(Box)(({ theme }) => ({ + display: "flex", + flexDirection: "column", + width: "100%", + height: "calc(100vh - 78px)", + overflow: "hidden", +})); + +export const MailBody = styled(Box)(({ theme }) => ({ + display: "flex", + flexDirection: "row", + width: "100%", + height: "calc(100% - 59px)", + // overflow: 'auto !important' +})); +export const MailBodyInner = styled(Box)(({ theme }) => ({ + display: "flex", + flexDirection: "column", + width: "50%", + height: "100%", +})); +export const MailBodyInnerHeader = styled(Box)(({ theme }) => ({ + display: "flex", + width: "100%", + height: "25px", + marginTop: "50px", + marginBottom: "35px", + justifyContent: "center", + alignItems: "center", + gap: "11px", +})); + +export const MailBodyInnerScroll = styled(Box)` + display: flex; + flex-direction: column; + overflow: auto !important; + transition: background-color 0.3s; + height: calc(100% - 110px); + &::-webkit-scrollbar { + width: 8px; + height: 8px; + background-color: transparent; /* Initially transparent */ + transition: background-color 0.3s; /* Transition for background color */ + } + + &::-webkit-scrollbar-thumb { + background-color: transparent; /* Initially transparent */ + border-radius: 3px; /* Scrollbar thumb radius */ + transition: background-color 0.3s; /* Transition for thumb color */ + } + + &:hover { + &::-webkit-scrollbar { + background-color: #494747; /* Scrollbar background color on hover */ + } + + &::-webkit-scrollbar-thumb { + background-color: #ffffff3d; /* Scrollbar thumb color on hover */ + } + + &::-webkit-scrollbar-thumb:hover { + background-color: #ffffff3d; /* Color when hovering over the thumb */ + } + } +`; + +export const ComposeContainer = styled(Box)(({ theme }) => ({ + display: "flex", + width: "150px", + alignItems: "center", + gap: "7px", + height: "100%", + cursor: "pointer", + transition: "0.2s background-color", + justifyContent: "center", + "&:hover": { + backgroundColor: "rgba(67, 68, 72, 1)", + }, +})); +export const ComposeContainerBlank = styled(Box)(({ theme }) => ({ + display: "flex", + width: "150px", + alignItems: "center", + gap: "7px", + height: "100%", +})); +export const ComposeP = styled(Typography)(({ theme }) => ({ + fontSize: "15px", + fontWeight: 500, +})); + +export const ComposeIcon = styled("img")({ + width: "auto", + height: "auto", + userSelect: "none", + objectFit: "contain", + cursor: "pointer", +}); +export const ArrowDownIcon = styled("img")({ + width: "auto", + height: "auto", + userSelect: "none", + objectFit: "contain", + cursor: "pointer", +}); +export const MailIconImg = styled("img")({ + width: "auto", + height: "auto", + userSelect: "none", + objectFit: "contain", +}); + +export const MailMessageRowInfoImg = styled("img")({ + width: "auto", + height: "auto", + userSelect: "none", + objectFit: "contain", +}); + +export const SelectInstanceContainer = styled(Box)(({ theme }) => ({ + display: "flex", + alignItems: "center", + gap: "17px", +})); +export const SelectInstanceContainerInner = styled(Box)(({ theme }) => ({ + display: "flex", + alignItems: "center", + gap: "3px", + cursor: "pointer", + padding: "8px", + transition: "all 0.2s", + "&:hover": { + borderRadius: "8px", + background: "#434448", + }, +})); +export const SelectInstanceContainerFilterInner = styled(Box)(({ theme }) => ({ + display: "flex", + alignItems: "center", + gap: "3px", + cursor: "pointer", + padding: "8px", + transition: "all 0.2s" +})); + + +export const InstanceLabel = styled(Typography)(({ theme }) => ({ + fontSize: "16px", + fontWeight: 500, + color: "#FFFFFF33", +})); + +export const InstanceP = styled(Typography)(({ theme }) => ({ + fontSize: "16px", + fontWeight: 500, +})); + +export const MailMessageRowContainer = styled(Box)(({ theme }) => ({ + display: "flex", + alignItems: "center", + cursor: "pointer", + justifyContent: "space-between", + borderRadius: "56px 5px 10px 56px", + paddingRight: "15px", + transition: "background 0.2s", + gap: "10px", + "&:hover": { + background: "#434448", + }, +})); +export const MailMessageRowProfile = styled(Box)(({ theme }) => ({ + display: "flex", + alignItems: "center", + cursor: "pointer", + justifyContent: "flex-start", + gap: "10px", + width: "50%", + overflow: "hidden", +})); +export const MailMessageRowInfo = styled(Box)(({ theme }) => ({ + display: "flex", + alignItems: "center", + cursor: "pointer", + justifyContent: "flex-start", + gap: "7px", + width: "50%", +})); +export const MailMessageRowInfoStatusNotDecrypted = styled(Typography)( + ({ theme }) => ({ + fontSize: "16px", + fontWeight: 900, + textTransform: "uppercase", + paddingTop: "2px", + }) +); +export const MailMessageRowInfoStatusRead = styled(Typography)(({ theme }) => ({ + fontSize: "16px", + fontWeight: 300, +})); + +export const MessageExtraInfo = styled(Box)(({ theme }) => ({ + display: "flex", + flexDirection: "column", + gap: "2px", + overflow: "hidden", +})); +export const MessageExtraName = styled(Typography)(({ theme }) => ({ + fontSize: "16px", + fontWeight: 900, + whiteSpace: "nowrap", + textOverflow: "ellipsis", + overflow: "hidden", +})); +export const MessageExtraDate = styled(Typography)(({ theme }) => ({ + fontSize: "15px", + fontWeight: 500, +})); + +export const MessagesContainer = styled(Box)(({ theme }) => ({ + width: "460px", + maxWidth: "90%", + display: "flex", + flexDirection: "column", + gap: "12px", +})); + +export const InstanceListParent = styled(Box)` + display: flex; + flex-direction: column; + width: 100%; + min-height: 246px; + max-height: 325px; + width: 425px; + padding: 10px 0px 7px 0px; + background-color: var(--color-instance-popover-bg); + border: 1px solid rgba(0, 0, 0, 0.1); +`; +export const InstanceListHeader = styled(Box)` + display: flex; + flex-direction: column; + width: 100%; + background-color: var(--color-instance-popover-bg); +`; +export const InstanceFooter = styled(Box)` + display: flex; + flex-direction: column; + width: 100%; + flex-shrink: 0; +`; +export const InstanceListContainer = styled(Box)` + width: 100%; + display: flex; + flex-direction: column; + flex-grow: 1; + + overflow: auto !important; + transition: background-color 0.3s; + &::-webkit-scrollbar { + width: 8px; + height: 8px; + background-color: transparent; /* Initially transparent */ + transition: background-color 0.3s; /* Transition for background color */ + } + + &::-webkit-scrollbar-thumb { + background-color: transparent; /* Initially transparent */ + border-radius: 3px; /* Scrollbar thumb radius */ + transition: background-color 0.3s; /* Transition for thumb color */ + } + + &:hover { + &::-webkit-scrollbar { + background-color: #494747; /* Scrollbar background color on hover */ + } + + &::-webkit-scrollbar-thumb { + background-color: #ffffff3d; /* Scrollbar thumb color on hover */ + } + + &::-webkit-scrollbar-thumb:hover { + background-color: #ffffff3d; /* Color when hovering over the thumb */ + } + } +`; +export const InstanceListContainerRowLabelContainer = styled(Box)( + ({ theme }) => ({ + width: "100%", + display: "flex", + alignItems: "center", + gap: "10px", + height: "50px", + }) +); +export const InstanceListContainerRow = styled(Box)(({ theme }) => ({ + width: "100%", + display: "flex", + alignItems: "center", + gap: "10px", + height: "50px", + cursor: "pointer", + transition: "0.2s background", + "&:hover": { + background: "rgba(67, 68, 72, 1)", + }, + flexShrink: 0, +})); +export const InstanceListContainerRowCheck = styled(Box)(({ theme }) => ({ + width: "47px", + display: "flex", + alignItems: "center", + justifyContent: "center", +})); +export const InstanceListContainerRowMain = styled(Box)(({ theme }) => ({ + display: "flex", + justifyContent: "space-between", + width: "100%", + alignItems: "center", + paddingRight: "30px", + overflow: "hidden", +})); +export const CloseParent = styled(Box)(({ theme }) => ({ + display: "flex", + alignItems: "center", + gap: "20px", +})); +export const InstanceListContainerRowMainP = styled(Typography)( + ({ theme }) => ({ + fontWeight: 500, + fontSize: "16px", + textOverflow: "ellipsis", + overflow: "hidden", + }) +); + +export const InstanceListContainerRowCheckIcon = styled("img")({ + width: "auto", + height: "auto", + userSelect: "none", + objectFit: "contain", +}); +export const InstanceListContainerRowGroupIcon = styled("img")({ + width: "auto", + height: "auto", + userSelect: "none", + objectFit: "contain", +}); +export const TypeInAliasTextfield = styled(TextField)({ + width: "340px", // Adjust the width as needed + borderRadius: "5px", + backgroundColor: "rgba(30, 30, 32, 1)", + border: "none", + outline: "none", + input: { + fontSize: 16, + color: "white", + "&::placeholder": { + fontSize: 16, + color: "rgba(255, 255, 255, 0.2)", + }, + border: "none", + outline: "none", + padding: "10px", + }, + "& .MuiOutlinedInput-root": { + "& fieldset": { + border: "none", + }, + "&:hover fieldset": { + border: "none", + }, + "&.Mui-focused fieldset": { + border: "none", + }, + }, + "& .MuiInput-underline:before": { + borderBottom: "none", + }, + "& .MuiInput-underline:hover:not(.Mui-disabled):before": { + borderBottom: "none", + }, + "& .MuiInput-underline:after": { + borderBottom: "none", + }, +}); + +export const NewMessageCloseImg = styled("img")({ + width: "auto", + height: "auto", + userSelect: "none", + objectFit: "contain", + cursor: "pointer", +}); +export const NewMessageHeaderP = styled(Typography)(({ theme }) => ({ + fontSize: "18px", + fontWeight: 600, +})); + +export const NewMessageInputRow = styled(Box)(({ theme }) => ({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + borderBottom: "3px solid rgba(237, 239, 241, 1)", + width: "100%", + paddingBottom: "6px", +})); +export const NewMessageInputLabelP = styled(Typography)` + color: rgba(84, 84, 84, 0.7); + font-size: 20px; + font-style: normal; + font-weight: 400; + line-height: 120%; /* 24px */ + letter-spacing: 0.15px; +`; +export const AliasLabelP = styled(Typography)` + color: rgba(84, 84, 84, 0.7); + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 120%; /* 24px */ + letter-spacing: 0.15px; + transition: color 0.2s; + cursor: pointer; + &:hover { + color: rgba(43, 43, 43, 1); + } +`; +export const NewMessageAliasContainer = styled(Box)(({ theme }) => ({ + display: "flex", + alignItems: "center", + gap: "12px", +})); +export const AttachmentContainer = styled(Box)(({ theme }) => ({ + height: "36px", + width: "100%", + display: "flex", + alignItems: "center", +})); + +export const NewMessageAttachmentImg = styled("img")({ + width: "auto", + height: "auto", + userSelect: "none", + objectFit: "contain", + cursor: "pointer", + padding: "10px", + border: "1px dashed #646464", +}); + +export const NewMessageSendButton = styled(Box)` + border-radius: 4px; + border: 1px solid rgba(0, 0, 0, 0.9); + display: inline-flex; + padding: 8px 16px 8px 12px; + justify-content: center; + align-items: center; + gap: 8px; + width: fit-content; + transition: all 0.2s; + color: black; + min-width: 120px; + gap: 8px; + position: relative; + cursor: pointer; + &:hover { + background-color: rgba(41, 41, 43, 1); + color: white; + svg path { + fill: white; // Fill color changes to white on hover + } + } +`; + +export const NewMessageSendP = styled(Typography)` + font-family: Roboto; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 120%; /* 19.2px */ + letter-spacing: -0.16px; +`; + +export const ShowMessageNameP = styled(Typography)` + font-family: Roboto; + font-size: 16px; + font-weight: 900; + line-height: 19px; + letter-spacing: 0em; + text-align: left; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +`; +export const ShowMessageTimeP = styled(Typography)` + color: rgba(255, 255, 255, 0.5); + font-family: Roboto; + font-size: 15px; + font-style: normal; + font-weight: 500; + line-height: normal; +`; +export const ShowMessageSubjectP = styled(Typography)` + font-family: Roboto; + font-size: 16px; + font-weight: 500; + line-height: 19px; + letter-spacing: 0.0075em; + text-align: left; +`; + +export const ShowMessageButton = styled(Box)` +display: inline-flex; +padding: 8px 16px 8px 16px; +align-items: center; +justify-content: center; +gap: 8px; +width: fit-content; +transition: all 0.2s; +color: white; +background-color: rgba(41, 41, 43, 1) +min-width: 120px; +gap: 8px; +border-radius: 4px; +border: 0.5px solid rgba(255, 255, 255, 0.70); +font-family: Roboto; + +min-width: 120px; +cursor: pointer; +&:hover { + border-radius: 4px; +border: 0.5px solid rgba(255, 255, 255, 0.70); +background: #434448; +} +`; +export const ShowMessageReturnButton = styled(Box)` +display: inline-flex; +padding: 8px 16px 8px 16px; +align-items: center; +justify-content: center; +gap: 8px; +width: fit-content; +transition: all 0.2s; +color: white; +background-color: rgba(41, 41, 43, 1) +min-width: 120px; +gap: 8px; +border-radius: 4px; +font-family: Roboto; + +min-width: 120px; +cursor: pointer; +&:hover { + border-radius: 4px; +background: #434448; +} +`; + +export const ShowMessageButtonP = styled(Typography)` + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 120%; /* 19.2px */ + letter-spacing: -0.16px; + color: white; +`; + +export const ShowMessageButtonImg = styled("img")({ + width: "auto", + height: "auto", + userSelect: "none", + objectFit: "contain", + cursor: "pointer", +}); + +export const MailAttachmentImg = styled("img")({ + width: "auto", + height: "auto", + userSelect: "none", + objectFit: "contain", +}); +export const AliasAvatarImg = styled("img")({ + width: "auto", + height: "auto", + userSelect: "none", + objectFit: "contain", +}); +export const MoreImg = styled("img")({ + width: "auto", + height: "auto", + userSelect: "none", + objectFit: "contain", + transition: "0.2s all", + "&:hover": { + transform: "scale(1.3)", + }, +}); + +export const MoreP = styled(Typography)` + color: rgba(255, 255, 255, 0.5); + + /* Attachments */ + font-family: Roboto; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 120%; /* 19.2px */ + letter-spacing: -0.16px; + white-space: nowrap; +`; +export const ThreadContainerFullWidth = styled(Box)(({ theme }) => ({ + display: "flex", + flexDirection: "column", + width: "100%", + alignItems: "center", +})); +export const ThreadContainer = styled(Box)(({ theme }) => ({ + display: "flex", + flexDirection: "column", + width: "100%", + maxWidth: "95%", +})); + +export const GroupNameP = styled(Typography)` + color: #fff; + font-size: 25px; + font-style: normal; + font-weight: 700; + line-height: 120%; /* 30px */ + letter-spacing: 0.188px; +`; + +export const AllThreadP = styled(Typography)` + color: #FFF; +font-size: 20px; +font-style: normal; +font-weight: 400; +line-height: 120%; /* 24px */ +letter-spacing: 0.15px; +`; + +export const SingleThreadParent = styled(Box)` +border-radius: 35px 4px 4px 35px; +position: relative; +background: #434448; +display: flex; +padding: 13px; +cursor: pointer; +margin-bottom: 5px; +height: 76px; +align-items:center; +transition: 0.2s all; +&:hover { +background: rgba(255, 255, 255, 0.20) +} +`; +export const SingleTheadMessageParent = styled(Box)` +border-radius: 35px 4px 4px 35px; +background: #434448; +display: flex; +padding: 13px; +cursor: pointer; +margin-bottom: 5px; +height: 76px; +align-items:center; + +`; + +export const ThreadInfoColumn = styled(Box)(({ theme }) => ({ + display: "flex", + flexDirection: "column", + width: "170px", + gap: '2px', + marginLeft: '10px', + height: '100%', + justifyContent: 'center' +})); + + +export const ThreadInfoColumnNameP = styled(Typography)` +color: #FFF; +font-family: Roboto; +font-size: 16px; +font-style: normal; +font-weight: 900; +line-height: normal; +white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +`; +export const ThreadInfoColumnbyP = styled('span')` +color: rgba(255, 255, 255, 0.80); +font-family: Roboto; +font-size: 16px; +font-style: normal; +font-weight: 500; +line-height: normal; +`; + +export const ThreadInfoColumnTime = styled(Typography)` +color: rgba(255, 255, 255, 0.80); +font-family: Roboto; +font-size: 15px; +font-style: normal; +font-weight: 500; +line-height: normal; +` +export const ThreadSingleTitle = styled(Typography)` +color: #FFF; +font-family: Roboto; +font-size: 23px; +font-style: normal; +font-weight: 700; +line-height: normal; +white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +` +export const ThreadSingleLastMessageP = styled(Typography)` +color: #FFF; +font-family: Roboto; +font-size: 12px; +font-style: normal; +font-weight: 600; +line-height: normal; +` +export const ThreadSingleLastMessageSpanP = styled('span')` +color: #FFF; +font-family: Roboto; +font-size: 12px; +font-style: normal; +font-weight: 400; +line-height: normal; +`; + +export const GroupContainer = styled(Box)` +position: relative; + overflow: auto; + width: 100%; +&::-webkit-scrollbar-track { + background-color: transparent; +} +&::-webkit-scrollbar-track:hover { + background-color: transparent; +} + +&::-webkit-scrollbar { + width: 16px; + height: 10px; + background-color: white; +} + +&::-webkit-scrollbar-thumb { + background-color: #838eee; + border-radius: 8px; + background-clip: content-box; + border: 4px solid transparent; +} + +&::-webkit-scrollbar-thumb:hover { + background-color: #6270f0; +} + +` + +export const CloseContainer = styled(Box)(({ theme }) => ({ + display: "flex", + width: "50px", + overflow: "hidden", + alignItems: "center", + cursor: "pointer", + transition: "0.2s background-color", + justifyContent: "center", + position: 'absolute', + top: '0px', + right: '0px', + height: '50px', + borderRadius: '0px 12px 0px 0px', + "&:hover": { + backgroundColor: "rgba(162, 31, 31, 1)", + }, +})); \ No newline at end of file diff --git a/src/components/Group/Forum/NewThread.tsx b/src/components/Group/Forum/NewThread.tsx new file mode 100644 index 0000000..7e165e5 --- /dev/null +++ b/src/components/Group/Forum/NewThread.tsx @@ -0,0 +1,554 @@ +import React, { useEffect, useRef, useState } from "react"; +import { Box, Button, CircularProgress, Input, Typography } from "@mui/material"; +import ShortUniqueId from "short-unique-id"; +import CloseIcon from "@mui/icons-material/Close"; + +import ModalCloseSVG from "../../../assets/svgs/ModalClose.svg"; + +import ComposeIconSVG from "../../../assets/svgs/ComposeIcon.svg"; + +import { + AttachmentContainer, + CloseContainer, + ComposeContainer, + ComposeIcon, + ComposeP, + InstanceFooter, + InstanceListContainer, + InstanceListHeader, + NewMessageAttachmentImg, + NewMessageCloseImg, + NewMessageHeaderP, + NewMessageInputRow, + NewMessageSendButton, + NewMessageSendP, +} from "./Mail-styles"; + +import { ReusableModal } from "./ReusableModal"; +import { Spacer } from "../../../common/Spacer"; +import { formatBytes } from "../../../utils/Size"; +import { CreateThreadIcon } from "../../../assets/svgs/CreateThreadIcon"; +import { SendNewMessage } from "../../../assets/svgs/SendNewMessage"; +import { TextEditor } from "./TextEditor"; +import { MyContext, pauseAllQueues, resumeAllQueues } from "../../../App"; +import { getFee } from "../../../background"; +import TipTap from "../../Chat/TipTap"; +import { MessageDisplay } from "../../Chat/MessageDisplay"; +import { CustomizedSnackbars } from "../../Snackbar/Snackbar"; +import { saveTempPublish } from "../../Chat/GroupAnnouncements"; + +const uid = new ShortUniqueId({ length: 8 }); + +export const toBase64 = (file: File): Promise => + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => resolve(reader.result); + reader.onerror = (error) => { + reject(error); + }; + }); + +export function objectToBase64(obj: any) { + // Step 1: Convert the object to a JSON string + const jsonString = JSON.stringify(obj); + + // Step 2: Create a Blob from the JSON string + const blob = new Blob([jsonString], { type: "application/json" }); + + // Step 3: Create a FileReader to read the Blob as a base64-encoded string + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => { + if (typeof reader.result === "string") { + // Remove 'data:application/json;base64,' prefix + const base64 = reader.result.replace( + "data:application/json;base64,", + "" + ); + resolve(base64); + } else { + reject(new Error("Failed to read the Blob as a base64-encoded string")); + } + }; + reader.onerror = () => { + reject(reader.error); + }; + reader.readAsDataURL(blob); + }); +} + +interface NewMessageProps { + hideButton?: boolean; + groupInfo: any; + currentThread?: any; + isMessage?: boolean; + messageCallback?: (val: any) => void; + publishCallback?: () => void; + refreshLatestThreads?: () => void; + members: any; +} + +export const publishGroupEncryptedResource = async ({ + encryptedData, + identifier, +}) => { + return new Promise((res, rej) => { + chrome.runtime.sendMessage( + { + action: "publishGroupEncryptedResource", + payload: { + encryptedData, + identifier, + }, + }, + (response) => { + + if (!response?.error) { + res(response); + return + } + rej(response.error); + } + ); + }); +}; + +export const encryptSingleFunc = async (data: string, secretKeyObject: any) => { + try { + return new Promise((res, rej) => { + chrome.runtime.sendMessage( + { + action: "encryptSingle", + payload: { + data, + secretKeyObject, + }, + }, + (response) => { + + if (!response?.error) { + res(response); + return; + } + rej(response.error); + } + ); + }); + } catch (error) {} +}; +export const NewThread = ({ + groupInfo, + members, + currentThread, + isMessage = false, + publishCallback, + userInfo, + getSecretKey, + closeCallback, + postReply, + myName +}: NewMessageProps) => { + const { show } = React.useContext(MyContext); + + const [isOpen, setIsOpen] = useState(false); + const [value, setValue] = useState(""); + const [isSending, setIsSending] = useState(false); + const [threadTitle, setThreadTitle] = useState(""); + const [openSnack, setOpenSnack] = React.useState(false); + const [infoSnack, setInfoSnack] = React.useState(null); + const editorRef = useRef(null); + const setEditorRef = (editorInstance) => { + editorRef.current = editorInstance; + }; + + useEffect(() => { + if (postReply) { + setIsOpen(true); + } + }, [postReply]); + + const closeModal = () => { + setIsOpen(false); + setValue(""); + }; + + async function publishQDNResource() { + try { + pauseAllQueues() + if(isSending) return + setIsSending(true) + let name: string = ""; + let errorMsg = ""; + + name = userInfo?.name || ""; + + const missingFields: string[] = []; + + if (!isMessage && !threadTitle) { + errorMsg = "Please provide a thread title"; + } + + if (!name) { + errorMsg = "Cannot send a message without a access to your name"; + } + if (!groupInfo) { + errorMsg = "Cannot access group information"; + } + + // if (!description) missingFields.push('subject') + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(", "); + const errMsg = `Missing: ${missingFieldsString}`; + errorMsg = errMsg; + } + + + if (errorMsg) { + // dispatch( + // setNotification({ + // msg: errorMsg, + // alertType: "error", + // }) + // ); + throw new Error(errorMsg); + } + + const htmlContent = editorRef.current.getHTML(); + + if (!htmlContent?.trim() || htmlContent?.trim() === "

") + throw new Error("Please provide a first message to the thread"); + const fee = await getFee("ARBITRARY"); + let feeToShow = fee.fee; + if (!isMessage) { + feeToShow = +feeToShow * 2; + } + await show({ + message: "Would you like to perform a ARBITRARY transaction?", + publishFee: feeToShow + " QORT", + }); + + let reply = null; + if (postReply) { + reply = { ...postReply }; + if (reply.reply) { + delete reply.reply; + } + } + const mailObject: any = { + createdAt: Date.now(), + version: 1, + textContentV2: htmlContent, + name, + threadOwner: currentThread?.threadData?.name || name, + reply, + }; + + const secretKey = await getSecretKey(); + if (!secretKey) { + throw new Error("Cannot get group secret key"); + } + + if (!isMessage) { + const idThread = uid.rnd(); + const idMsg = uid.rnd(); + const messageToBase64 = await objectToBase64(mailObject); + const encryptSingleFirstPost = await encryptSingleFunc( + messageToBase64, + secretKey + ); + const threadObject = { + title: threadTitle, + groupId: groupInfo.id, + createdAt: Date.now(), + name, + }; + const threadToBase64 = await objectToBase64(threadObject); + + const encryptSingleThread = await encryptSingleFunc( + threadToBase64, + secretKey + ); + let identifierThread = `grp-${groupInfo.groupId}-thread-${idThread}`; + await publishGroupEncryptedResource({ + identifier: identifierThread, + encryptedData: encryptSingleThread, + }); + + let identifierPost = `thmsg-${identifierThread}-${idMsg}`; + await publishGroupEncryptedResource({ + identifier: identifierPost, + encryptedData: encryptSingleFirstPost, + }); + const dataToSaveToStorage = { + name: myName, + identifier: identifierThread, + service: 'DOCUMENT', + tempData: threadObject, + created: Date.now(), + } + const dataToSaveToStoragePost = { + name: myName, + identifier: identifierPost, + service: 'DOCUMENT', + tempData: mailObject, + created: Date.now(), + threadId: identifierThread + } + await saveTempPublish({data: dataToSaveToStorage, key: 'thread'}) + await saveTempPublish({data: dataToSaveToStoragePost, key: 'thread-post'}) + setInfoSnack({ + type: "success", + message: "Successfully created thread. It may take some time for the publish to propagate", + }); + setOpenSnack(true) + + // dispatch( + // setNotification({ + // msg: "Message sent", + // alertType: "success", + // }) + // ); + if (publishCallback) { + publishCallback() + // threadCallback({ + // threadData: threadObject, + // threadOwner: name, + // name, + // threadId: identifierThread, + // created: Date.now(), + // service: 'MAIL_PRIVATE', + // identifier: identifier + // }) + } + closeModal(); + } else { + + if (!currentThread) throw new Error("unable to locate thread Id"); + const idThread = currentThread.threadId; + const messageToBase64 = await objectToBase64(mailObject); + const encryptSinglePost = await encryptSingleFunc( + messageToBase64, + secretKey + ); + const idMsg = uid.rnd(); + let identifier = `thmsg-${idThread}-${idMsg}`; + const res = await publishGroupEncryptedResource({ + identifier: identifier, + encryptedData: encryptSinglePost, + }); + + const dataToSaveToStoragePost = { + threadId: idThread, + name: myName, + identifier: identifier, + service: 'DOCUMENT', + tempData: mailObject, + created: Date.now() + } + await saveTempPublish({data: dataToSaveToStoragePost, key: 'thread-post'}) + // await qortalRequest(multiplePublishMsg); + // dispatch( + // setNotification({ + // msg: "Message sent", + // alertType: "success", + // }) + // ); + setInfoSnack({ + type: "success", + message: "Successfully created post. It may take some time for the publish to propagate", + }); + setOpenSnack(true) + if(publishCallback){ + publishCallback() + } + // messageCallback({ + // identifier, + // id: identifier, + // name, + // service: MAIL_SERVICE_TYPE, + // created: Date.now(), + // ...mailObject, + // }); + } + + closeModal(); + } catch (error: any) { + if(error?.message){ + setInfoSnack({ + type: "error", + message: error?.message, + }); + setOpenSnack(true) + } + + } finally { + setIsSending(false); + resumeAllQueues() + } + } + + const sendMail = () => { + publishQDNResource(); + }; + return ( + + setIsOpen(true)} + > + + {currentThread ? "New Post" : "New Thread"} + + + + + + {isMessage ? "Post Message" : "New Thread"} + + + + + + + {!isMessage && ( + <> + + + { + setThreadTitle(e.target.value); + }} + placeholder="Thread Title" + disableUnderline + autoComplete="off" + autoCorrect="off" + sx={{ + width: "100%", + color: "white", + "& .MuiInput-input::placeholder": { + color: "rgba(255,255,255, 0.70) !important", + fontSize: "20px", + fontStyle: "normal", + fontWeight: 400, + lineHeight: "120%", // 24px + letterSpacing: "0.15px", + opacity: 1, + }, + "&:focus": { + outline: "none", + }, + // Add any additional styles for the input here + }} + /> + + + )} + + {postReply && postReply.textContentV2 && ( + + + + )} + + + + {/* { + setValue(val); + }} + /> */} + + + + + {isSending && ( + + + + )} + + {isMessage ? "Post" : "Create Thread"} + + {isMessage ? ( + + ) : ( + + )} + + + + + + + ); +}; diff --git a/src/components/Group/Forum/ReadOnlySlate.tsx b/src/components/Group/Forum/ReadOnlySlate.tsx new file mode 100644 index 0000000..d64cf9e --- /dev/null +++ b/src/components/Group/Forum/ReadOnlySlate.tsx @@ -0,0 +1,129 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { createEditor} from 'slate'; +import { withReact, Slate, Editable, RenderElementProps, RenderLeafProps } from 'slate-react'; + +type ExtendedRenderElementProps = RenderElementProps & { mode?: string } + +export const renderElement = ({ + attributes, + children, + element, + mode +}: ExtendedRenderElementProps) => { + switch (element.type) { + case 'block-quote': + return
{children}
+ case 'heading-2': + return ( +

+ {children} +

+ ) + case 'heading-3': + return ( +

+ {children} +

+ ) + case 'code-block': + return ( +
+          {children}
+        
+ ) + case 'code-line': + return
{children}
+ case 'link': + return ( +
+ {children} + + ) + default: + return ( +

+ {children} +

+ ) + } +} + + +export const renderLeaf = ({ attributes, children, leaf }: RenderLeafProps) => { + let el = children + + if (leaf.bold) { + el = {el} + } + + if (leaf.italic) { + el = {el} + } + + if (leaf.underline) { + el = {el} + } + + if (leaf.link) { + el = ( + + {el} + + ) + } + + return {el} +} + +interface ReadOnlySlateProps { + content: any + mode?: string +} +const ReadOnlySlate: React.FC = ({ content, mode }) => { + const [load, setLoad] = useState(false) + const editor = useMemo(() => withReact(createEditor()), []) + const value = useMemo(() => content, [content]) + + const performUpdate = useCallback(async()=> { + setLoad(true) + await new Promise((res)=> { + setTimeout(() => { + res() + }, 250); + }) + setLoad(false) + }, []) + useEffect(()=> { + + + + + performUpdate() + }, [value]) + + if(load) return null + + return ( + {}}> + renderElement({ ...props, mode })} + renderLeaf={renderLeaf} + /> + + ) +} + +export default ReadOnlySlate; \ No newline at end of file diff --git a/src/components/Group/Forum/ReusableModal.tsx b/src/components/Group/Forum/ReusableModal.tsx new file mode 100644 index 0000000..b61e83e --- /dev/null +++ b/src/components/Group/Forum/ReusableModal.tsx @@ -0,0 +1,57 @@ +import React from 'react' +import { Box, Modal, useTheme } from '@mui/material' + +interface MyModalProps { + open: boolean + onClose?: () => void + onSubmit?: (obj: any) => Promise + children: any + customStyles?: any +} + +export const ReusableModal: React.FC = ({ + open, + onClose, + onSubmit, + children, + customStyles = {} +}) => { + const theme = useTheme() + return ( + + + {children} + + + ) +} diff --git a/src/components/Group/Forum/ShowMessageWithoutModal.tsx b/src/components/Group/Forum/ShowMessageWithoutModal.tsx new file mode 100644 index 0000000..c8fb5cb --- /dev/null +++ b/src/components/Group/Forum/ShowMessageWithoutModal.tsx @@ -0,0 +1,224 @@ +import React, { useState } from "react"; +import { Avatar, Box, IconButton } from "@mui/material"; +import DOMPurify from "dompurify"; +import FormatQuoteIcon from '@mui/icons-material/FormatQuote'; +import MoreSVG from '../../../assets/svgs/More.svg' + +import { + MoreImg, + MoreP, + SingleTheadMessageParent, + ThreadInfoColumn, + ThreadInfoColumnNameP, + ThreadInfoColumnTime, +} from "./Mail-styles"; +import { Spacer } from "../../../common/Spacer"; +import { DisplayHtml } from "./DisplayHtml"; +import { formatTimestampForum } from "../../../utils/time"; +import ReadOnlySlate from "./ReadOnlySlate"; +import { MessageDisplay } from "../../Chat/MessageDisplay"; +import { getBaseApi } from "../../../background"; +import { getBaseApiReact } from "../../../App"; + +export const ShowMessage = ({ message, openNewPostWithQuote }: any) => { + const [expandAttachments, setExpandAttachments] = useState(false); + + let cleanHTML = ""; + if (message?.htmlContent) { + cleanHTML = DOMPurify.sanitize(message.htmlContent); + } + + return ( + + + + + {message?.name?.charAt(0)} + + {message?.name} + + {formatTimestampForum(message?.created)} + + +
+ {message?.attachments?.length > 0 && ( + + {message?.attachments + .map((file: any, index: number) => { + const isFirst = index === 0 + return ( + + + {/* + + + + {file?.originalFilename || file?.filename} + + */} + {message?.attachments?.length > 1 && isFirst && ( + { + setExpandAttachments(prev => !prev); + }} + > + + + {expandAttachments ? 'hide' : `(${message?.attachments?.length - 1} more)`} + + + + )} + + + ); + }) + } + + )} + +
+
+ + {message?.reply?.textContentV2 && ( + <> + + + + {message?.reply?.name?.charAt(0)} + + {message?.reply?.name} + + + + + + + + + )} + + {message?.textContent && ( + + )} + {message?.textContentV2 && ( + + )} + {message?.htmlContent && ( +
+ )} + + openNewPostWithQuote(message)} + + > + + + + + + + + + ); +}; diff --git a/src/components/Group/Forum/TextEditor.tsx b/src/components/Group/Forum/TextEditor.tsx new file mode 100644 index 0000000..874f2e3 --- /dev/null +++ b/src/components/Group/Forum/TextEditor.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import ReactQuill, { Quill } from "react-quill"; +import "react-quill/dist/quill.snow.css"; +import ImageResize from "quill-image-resize-module-react"; +import './texteditor.css' +Quill.register("modules/imageResize", ImageResize); + +const modules = { + imageResize: { + parchment: Quill.import("parchment"), + modules: ["Resize", "DisplaySize"], + }, + toolbar: [ + ["bold", "italic", "underline", "strike"], // styled text + ["blockquote", "code-block"], // blocks + [{ header: 1 }, { header: 2 }], // custom button values + [{ list: "ordered" }, { list: "bullet" }], // lists + [{ script: "sub" }, { script: "super" }], // superscript/subscript + [{ indent: "-1" }, { indent: "+1" }], // outdent/indent + [{ direction: "rtl" }], // text direction + [{ size: ["small", false, "large", "huge"] }], // custom dropdown + [{ header: [1, 2, 3, 4, 5, 6, false] }], // custom button values + [{ color: [] }, { background: [] }], // dropdown with defaults + [{ font: [] }], // font family + [{ align: [] }], // text align + ["clean"], // remove formatting + // ["image"], // image + ], +}; +export const TextEditor = ({ inlineContent, setInlineContent }: any) => { + return ( + + ); +}; diff --git a/src/components/Group/Forum/Thread copy.tsx b/src/components/Group/Forum/Thread copy.tsx new file mode 100644 index 0000000..7aadabd --- /dev/null +++ b/src/components/Group/Forum/Thread copy.tsx @@ -0,0 +1,329 @@ +import React, { + FC, + useCallback, + useEffect, + useRef, + useState +} from 'react' + +import { + Box, + + Skeleton, + +} from '@mui/material' +import { ShowMessage } from './ShowMessageWithoutModal' +// import { +// setIsLoadingCustom, +// } from '../../state/features/globalSlice' +import { ComposeP, GroupContainer, GroupNameP, MailIconImg, ShowMessageReturnButton, SingleThreadParent, ThreadContainer, ThreadContainerFullWidth } from './Mail-styles' +import { Spacer } from '../../../common/Spacer' +import { threadIdentifier } from './GroupMail' +import LazyLoad from '../../../common/LazyLoad' +import ReturnSVG from '../../../assets/svgs/Return.svg' +import { NewThread } from './NewThread' +import { decryptPublishes } from '../../Chat/GroupAnnouncements' +import { getBaseApi } from '../../../background' +import { getBaseApiReact } from '../../../App' +interface ThreadProps { + currentThread: any + groupInfo: any + closeThread: () => void + members: any +} + +const getEncryptedResource = async ({name, identifier, secretKey})=> { + + const res = await fetch( + `${getBaseApiReact()}/arbitrary/DOCUMENT/${name}/${identifier}?encoding=base64` + ); + const data = await res.text(); + const response = await decryptPublishes([{ data }], secretKey); + const messageData = response[0]; + return messageData.decryptedData + +} + +export const Thread = ({ + currentThread, + groupInfo, + closeThread, + members, + userInfo, + secretKey, + getSecretKey +}: ThreadProps) => { + const [messages, setMessages] = useState([]) + const [hashMapMailMessages, setHashMapMailMessages] = useState({}) + const secretKeyRef = useRef(null) + + + useEffect(() => { + secretKeyRef.current = secretKey; + }, [secretKey]); + const getIndividualMsg = async (message: any) => { + try { + const responseDataMessage = await getEncryptedResource({identifier: message.identifier, name: message.name, secretKey}) + + + const fullObject = { + ...message, + ...(responseDataMessage || {}), + id: message.identifier + } + setHashMapMailMessages((prev)=> { + return { + ...prev, + [message.identifier]: fullObject + } + }) + } catch (error) {} + } + + const getMailMessages = React.useCallback( + async (groupInfo: any, reset?: boolean, hideAlert?: boolean) => { + try { + if(!hideAlert){ + // dispatch(setIsLoadingCustom('Loading messages')) + + } + let threadId = groupInfo.threadId + + const offset = messages.length + const identifier = `thmsg-${threadId}` + const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=20&includemetadata=false&offset=${offset}&reverse=true&prefix=true` + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }) + const responseData = await response.json() + let fullArrayMsg = reset ? [] : [...messages] + let newMessages: any[] = [] + for (const message of responseData) { + const index = fullArrayMsg.findIndex( + (p) => p.identifier === message.identifier + ) + if (index !== -1) { + fullArrayMsg[index] = message + } else { + fullArrayMsg.push(message) + getIndividualMsg(message) + } + } + setMessages(fullArrayMsg) + } catch (error) { + } finally { + if(!hideAlert){ + // dispatch(setIsLoadingCustom(null)) + } + } + }, + [messages, secretKey] + ) + const getMessages = React.useCallback(async () => { + if (!currentThread || !secretKey) return + await getMailMessages(currentThread, true) + }, [getMailMessages, currentThread, secretKey]) + const firstMount = useRef(false) + + const saveTimestamp = useCallback((currentThread: any, username?: string)=> { + if(!currentThread?.threadData?.groupId || !currentThread?.threadId || !username) return + const threadIdForLocalStorage = `qmail_threads_${currentThread?.threadData?.groupId}_${currentThread?.threadId}` + const threads = JSON.parse( + localStorage.getItem(`qmail_threads_viewedtimestamp_${username}`) || "{}" + ); + // Convert to an array of objects with identifier and all fields + let dataArray = Object.entries(threads).map(([identifier, value]) => ({ + identifier, + ...(value as any), + })); + + // Sort the array based on timestamp in descending order + dataArray.sort((a, b) => b.timestamp - a.timestamp); + + // Slice the array to keep only the first 500 elements + let latest500 = dataArray.slice(0, 500); + + // Convert back to the original object format + let latest500Data: any = {}; + latest500.forEach(item => { + const { identifier, ...rest } = item; + latest500Data[identifier] = rest; + }); + latest500Data[threadIdForLocalStorage] = { + timestamp: Date.now(), + } + localStorage.setItem( + `qmail_threads_viewedtimestamp_${username}`, + JSON.stringify(latest500Data) + ); + }, []) + useEffect(() => { + if (currentThread && secretKey) { + getMessages() + firstMount.current = true + // saveTimestamp(currentThread, user.name) + } + }, [ currentThread, secretKey]) + const messageCallback = useCallback((msg: any) => { + // dispatch(addToHashMapMail(msg)) + setMessages((prev) => [msg, ...prev]) + }, []) + + const interval = useRef(null) + + const checkNewMessages = React.useCallback( + async (groupInfo: any) => { + try { + let threadId = groupInfo.threadId + + const identifier = `thmsg-${threadId}` + const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=20&includemetadata=false&offset=${0}&reverse=true&prefix=true` + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }) + const responseData = await response.json() + const latestMessage = messages[0] + if (!latestMessage) return + const findMessage = responseData?.findIndex( + (item: any) => item?.identifier === latestMessage?.identifier + ) + let sliceLength = responseData.length + if (findMessage !== -1) { + sliceLength = findMessage + } + const newArray = responseData.slice(0, findMessage).reverse() + let fullArrayMsg = [...messages] + for (const message of newArray) { + try { + + const responseDataMessage = await getEncryptedResource({identifier: message.identifier, name: message.name, secretKey: secretKeyRef.current}) + + const fullObject = { + ...message, + ...(responseDataMessage || {}), + id: message.identifier + } + setHashMapMailMessages((prev)=> { + return { + ...prev, + [message.identifier]: fullObject + } + }) + const index = messages.findIndex( + (p) => p.identifier === fullObject.identifier + ) + if (index !== -1) { + fullArrayMsg[index] = fullObject + } else { + fullArrayMsg.unshift(fullObject) + } + } catch (error) {} + } + setMessages(fullArrayMsg) + } catch (error) { + } finally { + } + }, + [messages] + ) + + const checkNewMessagesFunc = useCallback(() => { + let isCalling = false + interval.current = setInterval(async () => { + if (isCalling) return + isCalling = true + const res = await checkNewMessages(currentThread) + isCalling = false + }, 8000) + }, [checkNewMessages, currentThread]) + + useEffect(() => { + checkNewMessagesFunc() + return () => { + if (interval?.current) { + clearInterval(interval.current) + } + } + }, [checkNewMessagesFunc]) + + + + if (!currentThread) return null + return ( + + + + + + + + {currentThread?.threadData?.title} + + { + setMessages([]) + closeThread() + }}> + + Return to Threads + + + + {messages.map((message) => { + let fullMessage = message + + if (hashMapMailMessages[message?.identifier]) { + fullMessage = hashMapMailMessages[message.identifier] + return + } + + return ( + + + + + + ) + })} + + + {messages.length >= 20 && ( + getMailMessages(currentThread, false, true)}> + + )} + + + ) +} diff --git a/src/components/Group/Forum/Thread.tsx b/src/components/Group/Forum/Thread.tsx new file mode 100644 index 0000000..6dd0dbb --- /dev/null +++ b/src/components/Group/Forum/Thread.tsx @@ -0,0 +1,663 @@ +import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from "react"; + +import { Box, Button, IconButton, Skeleton } from "@mui/material"; +import { ShowMessage } from "./ShowMessageWithoutModal"; +// import { +// setIsLoadingCustom, +// } from '../../state/features/globalSlice' +import { + ComposeP, + GroupContainer, + GroupNameP, + MailIconImg, + ShowMessageReturnButton, + SingleThreadParent, + ThreadContainer, + ThreadContainerFullWidth, +} from "./Mail-styles"; +import { Spacer } from "../../../common/Spacer"; +import { threadIdentifier } from "./GroupMail"; +import LazyLoad from "../../../common/LazyLoad"; +import ReturnSVG from "../../../assets/svgs/Return.svg"; +import { NewThread } from "./NewThread"; +import { decryptPublishes, getTempPublish } from "../../Chat/GroupAnnouncements"; +import { LoadingSnackbar } from "../../Snackbar/LoadingSnackbar"; +import { subscribeToEvent, unsubscribeFromEvent } from "../../../utils/events"; +import RefreshIcon from "@mui/icons-material/Refresh"; +import { getBaseApi } from "../../../background"; +import { getBaseApiReact } from "../../../App"; + +interface ThreadProps { + currentThread: any; + groupInfo: any; + closeThread: () => void; + members: any; +} + +const getEncryptedResource = async ({ name, identifier, secretKey }) => { + const res = await fetch( + `${getBaseApiReact()}/arbitrary/DOCUMENT/${name}/${identifier}?encoding=base64` + ); + const data = await res.text(); + const response = await decryptPublishes([{ data }], secretKey); + + const messageData = response[0]; + return messageData.decryptedData; +}; + +export const Thread = ({ + currentThread, + groupInfo, + closeThread, + members, + userInfo, + secretKey, + getSecretKey, + updateThreadActivityCurrentThread +}: ThreadProps) => { + const [tempPublishedList, setTempPublishedList] = useState([]) + + const [messages, setMessages] = useState([]); + const [hashMapMailMessages, setHashMapMailMessages] = useState({}); + const [hasFirstPage, setHasFirstPage] = useState(false); + const [hasPreviousPage, setHasPreviousPage] = useState(false); + const [hasNextPage, setHasNextPage] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [postReply, setPostReply] = useState(null); + const [hasLastPage, setHasLastPage] = useState(false); + + const secretKeyRef = useRef(null); + const currentThreadRef = useRef(null); + const containerRef = useRef(null); + useEffect(() => { + currentThreadRef.current = currentThread; + }, [currentThread]); + + useEffect(() => { + secretKeyRef.current = secretKey; + }, [secretKey]); + + const getIndividualMsg = async (message: any) => { + try { + const responseDataMessage = await getEncryptedResource({ + identifier: message.identifier, + name: message.name, + secretKey, + }); + + + const fullObject = { + ...message, + ...(responseDataMessage || {}), + id: message.identifier, + }; + setHashMapMailMessages((prev) => { + return { + ...prev, + [message.identifier]: fullObject, + }; + }); + } catch (error) {} + }; + + const setTempData = async ()=> { + try { + let threadId = currentThread.threadId; + + const keyTemp = 'thread-post' + const getTempAnnouncements = await getTempPublish() + + if(getTempAnnouncements?.[keyTemp]){ + + let tempData = [] + Object.keys(getTempAnnouncements?.[keyTemp] || {}).map((key)=> { + const value = getTempAnnouncements?.[keyTemp][key] + + if(value.data?.threadId === threadId){ + tempData.push(value.data) + } + + }) + setTempPublishedList(tempData) + } + } catch (error) { + + } + + } + + + const getMailMessages = React.useCallback( + async (groupInfo: any, before, after, isReverse) => { + try { + setTempPublishedList([]) + setIsLoading(true); + setHasFirstPage(false); + setHasPreviousPage(false); + setHasLastPage(false); + setHasNextPage(false); + let threadId = groupInfo.threadId; + + const identifier = `thmsg-${threadId}`; + let url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=20&includemetadata=false&prefix=true`; + if (!isReverse) { + url = url + "&reverse=false"; + } + if (isReverse) { + url = url + "&reverse=true"; + } + if (after) { + url = url + `&after=${after}`; + } + if (before) { + url = url + `&before=${before}`; + } + + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + const responseData = await response.json(); + + + let fullArrayMsg = [...responseData]; + if (isReverse) { + fullArrayMsg = fullArrayMsg.reverse(); + } + // let newMessages: any[] = [] + for (const message of responseData) { + getIndividualMsg(message); + } + setMessages(fullArrayMsg); + if (before === null && after === null && isReverse) { + setTimeout(() => { + containerRef.current.scrollIntoView({ behavior: "smooth" }); + }, 300); + + } + + if (fullArrayMsg.length === 0){ + setTempData() + return; + } + // check if there are newer posts + const urlNewer = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=1&includemetadata=false&reverse=false&prefix=true&before=${fullArrayMsg[0].created}`; + const responseNewer = await fetch(urlNewer, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + const responseDataNewer = await responseNewer.json(); + if (responseDataNewer.length > 0) { + setHasFirstPage(true); + setHasPreviousPage(true); + } else { + setHasFirstPage(false); + setHasPreviousPage(false); + } + // check if there are older posts + const urlOlder = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=1&includemetadata=false&reverse=false&prefix=true&after=${ + fullArrayMsg[fullArrayMsg.length - 1].created + }`; + const responseOlder = await fetch(urlOlder, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + const responseDataOlder = await responseOlder.json(); + if (responseDataOlder.length > 0) { + setHasLastPage(true); + setHasNextPage(true); + } else { + setHasLastPage(false); + setHasNextPage(false); + setTempData() + updateThreadActivityCurrentThread() + } + } catch (error) { + } finally { + setIsLoading(false); + + } + }, + [messages, secretKey] + ); + const getMessages = React.useCallback(async () => { + + if (!currentThread || !secretKey) return; + await getMailMessages(currentThread, null, null, false); + }, [getMailMessages, currentThread, secretKey]); + const firstMount = useRef(false); + + const saveTimestamp = useCallback((currentThread: any, username?: string) => { + if ( + !currentThread?.threadData?.groupId || + !currentThread?.threadId || + !username + ) + return; + const threadIdForLocalStorage = `qmail_threads_${currentThread?.threadData?.groupId}_${currentThread?.threadId}`; + const threads = JSON.parse( + localStorage.getItem(`qmail_threads_viewedtimestamp_${username}`) || "{}" + ); + // Convert to an array of objects with identifier and all fields + let dataArray = Object.entries(threads).map(([identifier, value]) => ({ + identifier, + ...(value as any), + })); + + // Sort the array based on timestamp in descending order + dataArray.sort((a, b) => b.timestamp - a.timestamp); + + // Slice the array to keep only the first 500 elements + let latest500 = dataArray.slice(0, 500); + + // Convert back to the original object format + let latest500Data: any = {}; + latest500.forEach((item) => { + const { identifier, ...rest } = item; + latest500Data[identifier] = rest; + }); + latest500Data[threadIdForLocalStorage] = { + timestamp: Date.now(), + }; + localStorage.setItem( + `qmail_threads_viewedtimestamp_${username}`, + JSON.stringify(latest500Data) + ); + }, []); + + const getMessagesMiddleware = async () => { + await new Promise((res) => { + setTimeout(() => { + res(null); + }, 400); + }); + if (firstMount.current) return; + getMessages(); + firstMount.current = true; + }; + useEffect(() => { + if (currentThreadRef.current?.threadId !== currentThread?.threadId) { + firstMount.current = false; + } + if (currentThread && secretKey && !firstMount.current) { + getMessagesMiddleware(); + + // saveTimestamp(currentThread, user.name) + } + }, [currentThread, secretKey]); + const messageCallback = useCallback((msg: any) => { + // dispatch(addToHashMapMail(msg)) + // setMessages((prev) => [msg, ...prev]) + }, []); + + const interval = useRef(null); + + const checkNewMessages = React.useCallback( + async (groupInfo: any) => { + try { + let threadId = groupInfo.threadId; + + const identifier = `thmsg-${threadId}`; + const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=20&includemetadata=false&offset=${0}&reverse=true&prefix=true`; + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + const responseData = await response.json(); + const latestMessage = messages[0]; + if (!latestMessage) return; + const findMessage = responseData?.findIndex( + (item: any) => item?.identifier === latestMessage?.identifier + ); + let sliceLength = responseData.length; + if (findMessage !== -1) { + sliceLength = findMessage; + } + const newArray = responseData.slice(0, findMessage).reverse(); + let fullArrayMsg = [...messages]; + for (const message of newArray) { + try { + const responseDataMessage = await getEncryptedResource({ + identifier: message.identifier, + name: message.name, + secretKey: secretKeyRef.current, + }); + + const fullObject = { + ...message, + ...(responseDataMessage || {}), + id: message.identifier, + }; + setHashMapMailMessages((prev) => { + return { + ...prev, + [message.identifier]: fullObject, + }; + }); + const index = messages.findIndex( + (p) => p.identifier === fullObject.identifier + ); + if (index !== -1) { + fullArrayMsg[index] = fullObject; + } else { + fullArrayMsg.unshift(fullObject); + } + } catch (error) {} + } + setMessages(fullArrayMsg); + } catch (error) { + } finally { + } + }, + [messages] + ); + + // const checkNewMessagesFunc = useCallback(() => { + // let isCalling = false + // interval.current = setInterval(async () => { + // if (isCalling) return + // isCalling = true + // const res = await checkNewMessages(currentThread) + // isCalling = false + // }, 8000) + // }, [checkNewMessages, currentThrefirstMount.current = truead]) + + // useEffect(() => { + // checkNewMessagesFunc() + // return () => { + // if (interval?.current) { + // clearInterval(interval.current) + // } + // } + // }, [checkNewMessagesFunc]) + + const openNewPostWithQuote = useCallback((reply) => { + setPostReply(reply); + }, []); + + const closeCallback = useCallback(() => { + setPostReply(null); + }, []); + + const threadFetchModeFunc = (e) => { + const mode = e.detail?.mode; + if (mode === "last-page") { + getMailMessages(currentThread, null, null, true); + } + firstMount.current = true; + }; + + React.useEffect(() => { + subscribeToEvent("threadFetchMode", threadFetchModeFunc); + + return () => { + unsubscribeFromEvent("threadFetchMode", threadFetchModeFunc); + }; + }, []); + + const combinedListTempAndReal = useMemo(() => { + // Combine the two lists + const combined = [...tempPublishedList, ...messages]; + + // Remove duplicates based on the "identifier" + const uniqueItems = new Map(); + combined.forEach(item => { + uniqueItems.set(item.identifier, item); // This will overwrite duplicates, keeping the last occurrence + }); + + // Convert the map back to an array and sort by "created" timestamp in descending order + const sortedList = Array.from(uniqueItems.values()).sort((a, b) => a.created - b.created); + + return sortedList; + }, [tempPublishedList, messages]); + + if (!currentThread) return null; + return ( + + + + + + + {currentThread?.threadData?.title} + + { + setMessages([]); + closeThread(); + }} + > + + Return to Threads + + + + + + + + + + {combinedListTempAndReal.map((message) => { + let fullMessage = message; + + if (hashMapMailMessages[message?.identifier]) { + fullMessage = hashMapMailMessages[message.identifier]; + return ( + + ); + } else if(message?.tempData){ + return ( + + ); + } + + return ( + + + + ); + })} +
+ {!hasLastPage && !isLoading && ( + <> + + + + + + )} + + {messages?.length > 4 && ( + <> + + + + + + + + + + )} + + + {/* {messages.length >= 20 && ( + getMailMessages(currentThread, false, true)}> + + )} */} + + + ); +}; diff --git a/src/components/Group/Forum/texteditor.css b/src/components/Group/Forum/texteditor.css new file mode 100644 index 0000000..e3bbd50 --- /dev/null +++ b/src/components/Group/Forum/texteditor.css @@ -0,0 +1,71 @@ +.ql-editor { + min-height: 200px; + width: 100%; + color: black; + font-size: 16px; + font-family: Roboto; + max-height: 225px; + overflow-y: scroll; + padding: 0px !important; + } + + .ql-editor::-webkit-scrollbar-track { + background-color: transparent; + cursor: default; + } + .ql-editor::-webkit-scrollbar-track:hover { + background-color: transparent; + } + + .ql-editor::-webkit-scrollbar { + width: 16px; + height: 10px; + background-color: rgba(229, 229, 229, 0.70); + } + + .ql-editor::-webkit-scrollbar-thumb { + background-color: #B0B0B0; + border-radius: 8px; + background-clip: content-box; + border: 4px solid transparent; + } + + + .ql-editor img { + cursor: default; + } + + .ql-editor-display { + min-height: 20px; + width: 100%; + color: black; + font-size: 16px; + font-family: Roboto; + padding: 0px !important; + } + + .ql-editor-display img { + cursor: default; + } + + .ql-container { + font-size: 16px + } + + .ql-toolbar .ql-stroke { + fill: none !important; + stroke: black !important; +} + +.ql-toolbar .ql-fill { + fill: black !important; + stroke: none !important; +} + +.ql-toolbar .ql-picker { + color: black !important; +} + +.ql-toolbar .ql-picker-options { + background-color: white !important; +} \ No newline at end of file diff --git a/src/components/Group/Group.tsx b/src/components/Group/Group.tsx new file mode 100644 index 0000000..51973e2 --- /dev/null +++ b/src/components/Group/Group.tsx @@ -0,0 +1,1943 @@ +import { + Avatar, + Box, + Button, + IconButton, + List, + ListItem, + ListItemAvatar, + ListItemText, + Typography, +} from "@mui/material"; +import React, { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import SettingsIcon from "@mui/icons-material/Settings"; +import { ChatGroup } from "../Chat/ChatGroup"; +import { CreateCommonSecret } from "../Chat/CreateCommonSecret"; +import { base64ToUint8Array } from "../../qdn/encryption/group-encryption"; +import { uint8ArrayToObject } from "../../backgroundFunctions/encryption"; +import ChatIcon from "@mui/icons-material/Chat"; +import CampaignIcon from "@mui/icons-material/Campaign"; +import { AddGroup } from "./AddGroup"; +import MarkUnreadChatAltIcon from "@mui/icons-material/MarkUnreadChatAlt"; +import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline"; +import CreateIcon from "@mui/icons-material/Create"; +import RefreshIcon from "@mui/icons-material/Refresh"; + +import { + AuthenticatedContainerInnerRight, + CustomButton, +} from "../../App-styles"; +import ForumIcon from "@mui/icons-material/Forum"; +import { Spacer } from "../../common/Spacer"; +import PeopleIcon from "@mui/icons-material/People"; +import { ManageMembers } from "./ManageMembers"; +import MarkChatUnreadIcon from "@mui/icons-material/MarkChatUnread"; +import { MyContext, clearAllQueues, getBaseApiReact } from "../../App"; +import { ChatDirect } from "../Chat/ChatDirect"; +import { CustomizedSnackbars } from "../Snackbar/Snackbar"; +import { LoadingButton } from "@mui/lab"; +import { LoadingSnackbar } from "../Snackbar/LoadingSnackbar"; +import { GroupAnnouncements } from "../Chat/GroupAnnouncements"; +import HomeIcon from "@mui/icons-material/Home"; + +import { ThingsToDoInitial } from "./ThingsToDoInitial"; +import { GroupJoinRequests } from "./GroupJoinRequests"; +import { GroupForum } from "../Chat/GroupForum"; +import { GroupInvites } from "./GroupInvites"; +import { + executeEvent, + subscribeToEvent, + unsubscribeFromEvent, +} from "../../utils/events"; +import { ListOfThreadPostsWatched } from "./ListOfThreadPostsWatched"; +import { RequestQueueWithPromise } from "../../utils/queue/queue"; +import { WebSocketActive } from "./WebsocketActive"; +import { flushSync } from "react-dom"; + +interface GroupProps { + myAddress: string; + isFocused: boolean; + isMain: boolean; + userInfo: any; + balance: number; +} + +const timeDifferenceForNotificationChats = 900000; + +export const requestQueueMemberNames = new RequestQueueWithPromise(5); +export const requestQueueAdminMemberNames = new RequestQueueWithPromise(5); + +const audio = new Audio(chrome.runtime.getURL("msg-not1.wav")); + +export const getGroupAdimnsAddress = async (groupNumber: number) => { + // const validApi = await findUsableApi(); + + const response = await fetch( + `${getBaseApiReact()}/groups/members/${groupNumber}?limit=0&onlyAdmins=true` + ); + const groupData = await response.json(); + let members: any = []; + if (groupData && Array.isArray(groupData?.members)) { + for (const member of groupData.members) { + if (member.member) { + members.push(member?.member); + } + } + + return members; + } +}; + +export 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 and nonce properties + if (!value.hasOwnProperty("messageKey") || !value.hasOwnProperty("nonce")) { + return false; + } + + // Ensure messageKey and nonce are non-empty strings + if ( + typeof value.messageKey !== "string" || + value.messageKey.trim() === "" + ) { + return false; + } + if (typeof value.nonce !== "string" || value.nonce.trim() === "") { + return false; + } + } + + // If all checks passed, return true + return true; +} + +export const getGroupMembers = async (groupNumber: number) => { + // const validApi = await findUsableApi(); + + const response = await fetch( + `${getBaseApiReact()}/groups/members/${groupNumber}?limit=0` + ); + const groupData = await response.json(); + return groupData; +}; + +const decryptResource = async (data: string) => { + try { + return new Promise((res, rej) => { + chrome.runtime.sendMessage( + { + action: "decryptGroupEncryption", + payload: { + data, + }, + }, + (response) => { + + if (!response?.error) { + res(response); + } + rej(response.error); + } + ); + }); + } catch (error) {} +}; + +export async function getNameInfo(address: string) { + const response = await fetch(`${getBaseApiReact()}/names/address/` + address); + const nameData = await response.json(); + + if (nameData?.length > 0) { + return nameData[0]?.name; + } else { + return ""; + } +} + +export const getGroupAdimns = async (groupNumber: number) => { + // const validApi = await findUsableApi(); + + const response = await fetch( + `${getBaseApiReact()}/groups/members/${groupNumber}?limit=0&onlyAdmins=true` + ); + const groupData = await response.json(); + let members: any = []; + // if (groupData && Array.isArray(groupData?.members)) { + // for (const member of groupData.members) { + // if (member.member) { + // const name = await getNameInfo(member.member); + // if (name) { + // members.push(name); + // } + // } + // } + // } + + const getMemNames = groupData?.members?.map(async (member) => { + if (member?.member) { + const name = await requestQueueAdminMemberNames.enqueue(() => { + return getNameInfo(member.member); + }); + if (name) { + members.push(name); + } + } + + return true; + }); + await Promise.all(getMemNames); + + return members; +}; + +export const getNames = async (listOfMembers) => { + // const validApi = await findUsableApi(); + + let members: any = []; + + const getMemNames = listOfMembers.map(async (member) => { + if (member.member) { + const name = await requestQueueMemberNames.enqueue(() => { + return getNameInfo(member.member); + }); + if (name) { + members.push({ ...member, name }); + } else { + members.push({ ...member, name: "" }); + } + } + + return true; + }); + + await Promise.all(getMemNames); + + return members; +}; +export const getNamesForAdmins = async (admins) => { + // const validApi = await findUsableApi(); + + let members: any = []; + // if (admins && Array.isArray(admins)) { + // for (const admin of admins) { + // const name = await getNameInfo(admin); + // if (name) { + // members.push({ address: admin, name }); + // } + // } + // } + const getMemNames = admins?.map(async (admin) => { + if (admin) { + const name = await requestQueueAdminMemberNames.enqueue(() => { + return getNameInfo(admin); + }); + if (name) { + members.push({ address: admin, name }); + } + } + + return true; + }); + await Promise.all(getMemNames); + + return members; +}; + +export const Group = ({ + myAddress, + isFocused, + isMain, + userInfo, + balance, +}: GroupProps) => { + const [secretKey, setSecretKey] = useState(null); + const [secretKeyPublishDate, setSecretKeyPublishDate] = useState(null); + const [secretKeyDetails, setSecretKeyDetails] = useState(null); + const [newEncryptionNotification, setNewEncryptionNotification] = + useState(null); + const [memberCountFromSecretKeyData, setMemberCountFromSecretKeyData] = + useState(null); + const [selectedGroup, setSelectedGroup] = useState(null); + const [selectedDirect, setSelectedDirect] = useState(null); + const hasInitialized = useRef(false); + const hasInitializedWebsocket = useRef(false); + const [groups, setGroups] = useState([]); + const [directs, setDirects] = useState([]); + const [admins, setAdmins] = useState([]); + const [adminsWithNames, setAdminsWithNames] = useState([]); + + const [members, setMembers] = useState([]); + const [groupOwner, setGroupOwner] = useState(null); + const [triedToFetchSecretKey, setTriedToFetchSecretKey] = useState(false); + const [openAddGroup, setOpenAddGroup] = useState(false); + const [isInitialGroups, setIsInitialGroups] = useState(false); + const [openManageMembers, setOpenManageMembers] = useState(false); + const { setMemberGroups, memberGroups } = useContext(MyContext); + const lastGroupNotification = useRef(null); + const [timestampEnterData, setTimestampEnterData] = useState({}); + const [chatMode, setChatMode] = useState("groups"); + const [newChat, setNewChat] = useState(false); + const [openSnack, setOpenSnack] = React.useState(false); + const [infoSnack, setInfoSnack] = React.useState(null); + const [isLoadingNotifyAdmin, setIsLoadingNotifyAdmin] = React.useState(false); + const [isLoadingGroups, setIsLoadingGroups] = React.useState(false); + const [isLoadingGroup, setIsLoadingGroup] = React.useState(false); + const [firstSecretKeyInCreation, setFirstSecretKeyInCreation] = React.useState(false) + const [groupSection, setGroupSection] = React.useState("home"); + const [groupAnnouncements, setGroupAnnouncements] = React.useState({}); + const [defaultThread, setDefaultThread] = React.useState(null); + + const isFocusedRef = useRef(true); + const selectedGroupRef = useRef(null); + const selectedDirectRef = useRef(null); + const groupSectionRef = useRef(null); + const checkGroupInterval = useRef(null); + const isLoadingOpenSectionFromNotification = useRef(false); + const setupGroupWebsocketInterval = useRef(null); + const settimeoutForRefetchSecretKey = useRef(null); + + + useEffect(() => { + isFocusedRef.current = isFocused; + }, [isFocused]); + useEffect(() => { + groupSectionRef.current = groupSection; + }, [groupSection]); + + useEffect(() => { + selectedGroupRef.current = selectedGroup; + }, [selectedGroup]); + + useEffect(() => { + selectedDirectRef.current = selectedDirect; + }, [selectedDirect]); + + + + const getTimestampEnterChat = async () => { + try { + return new Promise((res, rej) => { + chrome.runtime.sendMessage( + { + action: "getTimestampEnterChat", + }, + (response) => { + if (!response?.error) { + setTimestampEnterData(response); + res(response); + } + rej(response.error); + } + ); + }); + } catch (error) {} + }; + const refreshHomeDataFunc = () => { + setGroupSection("default"); + setTimeout(() => { + setGroupSection("home"); + }, 300); + }; + + const getGroupAnnouncements = async () => { + try { + return new Promise((res, rej) => { + chrome.runtime.sendMessage( + { + action: "getGroupNotificationTimestamp", + }, + (response) => { + + if (!response?.error) { + setGroupAnnouncements(response); + res(response); + } + rej(response.error); + } + ); + }); + } catch (error) {} + }; + + const getGroupOwner = async (groupId) => { + try { + + const url = `${getBaseApiReact()}/groups/${groupId}`; + const response = await fetch(url); + let data = await response.json(); + + const name = await getNameInfo(data?.owner); + if (name) { + data.name = name; + } + setGroupOwner(data); + } catch (error) {} + }; + + const checkGroupList = React.useCallback(async (address) => { + try { + const url = `${getBaseApiReact()}/chat/active/${address}`; + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + const responseData = await response.json(); + if (!Array.isArray(responseData?.groups)) return; + const filterOutGeneral = responseData.groups?.filter( + (item) => item?.groupId !== 0 + ); + const sortedGroups = filterOutGeneral.sort((a, b) => { + // If a has no timestamp, move it down + if (!a.timestamp) return 1; + // If b has no timestamp, move it up + if (!b.timestamp) return -1; + // Otherwise, sort by timestamp in descending order (most recent first) + return b.timestamp - a.timestamp; + }); + setGroups(sortedGroups); + setMemberGroups(sortedGroups); + } catch (error) { + } finally { + } + }, []); + // const checkGroupListFunc = useCallback((myAddress) => { + // let isCalling = false; + // checkGroupInterval.current = setInterval(async () => { + // if (isCalling) return; + // isCalling = true; + // const res = await checkGroupList(myAddress); + // isCalling = false; + // }, 120000); + // }, []); + + const directChatHasUnread = useMemo(() => { + let hasUnread = false; + directs.forEach((direct) => { + if ( + direct?.sender !== myAddress && + direct?.timestamp && + ((!timestampEnterData[direct?.address] && + Date.now() - direct?.timestamp < + timeDifferenceForNotificationChats) || + timestampEnterData[direct?.address] < direct?.timestamp) + ) { + hasUnread = true; + } + }); + return hasUnread; + }, [timestampEnterData, directs, myAddress]); + + // useEffect(() => { + // if (!myAddress) return; + // checkGroupListFunc(myAddress); + // return () => { + // if (checkGroupInterval?.current) { + // clearInterval(checkGroupInterval.current); + // } + // }; + // }, [checkGroupListFunc, myAddress]); + + const getPublishesFromAdmins = async (admins: string[]) => { + // const validApi = await findUsableApi(); + const queryString = admins.map((name) => `name=${name}`).join("&"); + const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=DOCUMENT_PRIVATE&identifier=symmetric-qchat-group-${ + selectedGroup?.groupId + }&exactmatchnames=true&limit=10&reverse=true&${queryString}`; + 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-${selectedGroup?.groupId}` + ); + if (filterId?.length === 0) { + return false; + } + return filterId[0]; + }; + const getSecretKey = async (loadingGroupParam?: boolean) => { + try { + if (loadingGroupParam) { + setIsLoadingGroup(true); + } + if (selectedGroup?.groupId !== selectedGroupRef.current.groupId) { + if (settimeoutForRefetchSecretKey.current) { + clearTimeout(settimeoutForRefetchSecretKey.current); + } + return; + } + const prevGroupId = selectedGroupRef.current.groupId; + // const validApi = await findUsableApi(); + const groupAdmins = await getGroupAdimns(selectedGroup?.groupId); + if(!groupAdmins.length){ + throw new Error('Network error') + } + const publish = await getPublishesFromAdmins(groupAdmins); + + if (prevGroupId !== selectedGroupRef.current.groupId) { + if (settimeoutForRefetchSecretKey.current) { + clearTimeout(settimeoutForRefetchSecretKey.current); + } + return; + } + if (publish === false) { + setTriedToFetchSecretKey(true); + settimeoutForRefetchSecretKey.current = setTimeout(() => { + getSecretKey(); + }, 120000); + return; + } + setSecretKeyPublishDate(publish?.updated || publish?.created); + + const res = await fetch( + `${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${ + publish.identifier + }?encoding=base64` + ); + const data = await res.text(); + + const decryptedKey: any = await decryptResource(data); + + const dataint8Array = base64ToUint8Array(decryptedKey.data); + const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); + + if (!validateSecretKey(decryptedKeyToObject)) + throw new Error("SecretKey is not valid"); + setSecretKeyDetails(publish); + setSecretKey(decryptedKeyToObject); + + setMemberCountFromSecretKeyData(decryptedKey.count); + if (decryptedKeyToObject) { + setTriedToFetchSecretKey(true); + setFirstSecretKeyInCreation(false) + return decryptedKeyToObject; + } else { + setTriedToFetchSecretKey(true); + } + + } catch (error) { + if(error === 'Unable to decrypt data'){ + setTriedToFetchSecretKey(true); + settimeoutForRefetchSecretKey.current = setTimeout(() => { + getSecretKey(); + }, 120000); + } + + } finally { + setIsLoadingGroup(false); + } + }; + + useEffect(() => { + if (selectedGroup) { + setTriedToFetchSecretKey(false); + getSecretKey(true); + getGroupOwner(selectedGroup?.groupId); + } + }, [selectedGroup]); + + // const handleNotification = async (data)=> { + // try { + // if(isFocusedRef.current){ + // throw new Error('isFocused') + // } + // const newActiveChats= data + // const oldActiveChats = await new Promise((res, rej) => { + // chrome.runtime.sendMessage( + // { + // action: "getChatHeads", + // }, + // (response) => { + // console.log({ response }); + // if (!response?.error) { + // res(response); + // } + // rej(response.error); + // } + // ); + // }); + + // let results = [] + // newActiveChats?.groups?.forEach(newChat => { + // let isNewer = true; + // oldActiveChats?.data?.groups?.forEach(oldChat => { + // if (newChat?.timestamp <= oldChat?.timestamp) { + // isNewer = false; + // } + // }); + // if (isNewer) { + // results.push(newChat) + // console.log('This newChat is newer than all oldChats:', newChat); + // } + // }); + + // if(results?.length > 0){ + // if (!lastGroupNotification.current || (Date.now() - lastGroupNotification.current >= 60000)) { + // console.log((Date.now() - lastGroupNotification.current >= 60000), lastGroupNotification.current) + // chrome.runtime.sendMessage( + // { + // action: "notification", + // payload: { + // }, + // }, + // (response) => { + // console.log({ response }); + // if (!response?.error) { + + // } + + // } + // ); + // audio.play(); + // lastGroupNotification.current = Date.now() + + // } + // } + + // } catch (error) { + // console.log('error not', error) + // if(!isFocusedRef.current){ + // chrome.runtime.sendMessage( + // { + // action: "notification", + // payload: { + // }, + // }, + // (response) => { + // console.log({ response }); + // if (!response?.error) { + + // } + + // } + // ); + // audio.play(); + // lastGroupNotification.current = Date.now() + // } + + // } finally { + + // chrome.runtime.sendMessage( + // { + // action: "setChatHeads", + // payload: { + // data, + // }, + // } + // ); + + // } + // } + + const getAdmins = async (groupId) => { + try { + const res = await getGroupAdimnsAddress(groupId); + setAdmins(res); + const adminsWithNames = await getNamesForAdmins(res); + setAdminsWithNames(adminsWithNames); + } catch (error) {} + }; + + useEffect(() => { + // Listen for messages from the background script + chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + + if (message.action === "SET_GROUPS") { + // Update the component state with the received 'sendqort' state + setGroups(message.payload); + + setMemberGroups(message.payload); + + if (selectedGroupRef.current && groupSectionRef.current === "chat") { + chrome.runtime.sendMessage({ + action: "addTimestampEnterChat", + payload: { + timestamp: Date.now(), + groupId: selectedGroupRef.current.groupId, + }, + }); + } + if (selectedDirectRef.current) { + chrome.runtime.sendMessage({ + action: "addTimestampEnterChat", + payload: { + timestamp: Date.now(), + groupId: selectedDirectRef.current.address, + }, + }); + } + setTimeout(() => { + getTimestampEnterChat(); + }, 200); + } + if (message.action === "SET_GROUP_ANNOUNCEMENTS") { + // Update the component state with the received 'sendqort' state + setGroupAnnouncements(message.payload); + + if ( + selectedGroupRef.current && + groupSectionRef.current === "announcement" + ) { + chrome.runtime.sendMessage({ + action: "addGroupNotificationTimestamp", + payload: { + timestamp: Date.now(), + groupId: selectedGroupRef.current.groupId, + }, + }); + setTimeout(() => { + getGroupAnnouncements(); + }, 200); + } + } + if (message.action === "SET_DIRECTS") { + // Update the component state with the received 'sendqort' state + setDirects(message.payload); + + // if (selectedGroupRef.current) { + // chrome.runtime.sendMessage({ + // action: "addTimestampEnterChat", + // payload: { + // timestamp: Date.now(), + // groupId: selectedGroupRef.current.groupId, + // }, + // }); + // } + // setTimeout(() => { + // getTimestampEnterChat(); + // }, 200); + } else if (message.action === "PLAY_NOTIFICATION_SOUND") { + audio.play(); + } + }); + }, []); + + useEffect(() => { + if ( + !myAddress || + hasInitializedWebsocket.current || + !window?.location?.href?.includes("?main=true") || + !groups || + groups?.length === 0 + ) + return; + + chrome.runtime.sendMessage({ action: "setupGroupWebsocket" }); + + hasInitializedWebsocket.current = true; + }, [myAddress, groups]); + + + const getMembers = async (groupId) => { + try { + const res = await getGroupMembers(groupId); + setMembers(res); + } catch (error) {} + }; + useEffect(() => { + if (selectedGroup?.groupId) { + getAdmins(selectedGroup?.groupId); + getMembers(selectedGroup?.groupId); + } + }, [selectedGroup?.groupId]); + + const shouldReEncrypt = useMemo(() => { + + if (triedToFetchSecretKey && !secretKeyPublishDate) return true; + if ( + !secretKeyPublishDate || + !memberCountFromSecretKeyData || + members.length === 0 + ) + return false; + const isDiffMemberNumber = + memberCountFromSecretKeyData !== members?.memberCount && + newEncryptionNotification?.text?.data?.numberOfMembers !== + members?.memberCount; + + if (isDiffMemberNumber) return true; + + const latestJoined = members?.members.reduce((maxJoined, current) => { + return current.joined > maxJoined ? current.joined : maxJoined; + }, members?.members[0].joined); + + if ( + secretKeyPublishDate < latestJoined && + newEncryptionNotification?.data?.timestamp < latestJoined + ) { + return true; + } + return false; + }, [ + memberCountFromSecretKeyData, + members, + secretKeyPublishDate, + newEncryptionNotification, + triedToFetchSecretKey, + ]); + + const notifyAdmin = async (admin) => { + try { + setIsLoadingNotifyAdmin(true); + await new Promise((res, rej) => { + chrome.runtime.sendMessage( + { + action: "notifyAdminRegenerateSecretKey", + payload: { + adminAddress: admin.address, + groupName: selectedGroup?.groupName, + }, + }, + (response) => { + + if (!response?.error) { + res(response); + } + rej(response.error); + } + ); + }); + setInfoSnack({ + type: "success", + message: "Successfully sent notification.", + }); + setOpenSnack(true); + } catch (error) { + setInfoSnack({ + type: "error", + message: "Unable to send notification", + }); + } finally { + setIsLoadingNotifyAdmin(false); + } + }; + + const isUnreadChat = useMemo(() => { + const findGroup = groups + .filter((group) => group?.sender !== myAddress) + .find((gr) => gr?.groupId === selectedGroup?.groupId); + if (!findGroup) return false; + return ( + findGroup?.timestamp && + ((!timestampEnterData[selectedGroup?.groupId] && + Date.now() - findGroup?.timestamp < + timeDifferenceForNotificationChats) || + timestampEnterData?.[selectedGroup?.groupId] < findGroup?.timestamp) + ); + }, [timestampEnterData, selectedGroup]); + + const isUnread = useMemo(() => { + + if (!selectedGroup) return false; + return ( + groupAnnouncements?.[selectedGroup?.groupId]?.seentimestamp === false + ); + }, [groupAnnouncements, selectedGroup, myAddress]); + + const openDirectChatFromNotification = (e) => { + if (isLoadingOpenSectionFromNotification.current) return; + isLoadingOpenSectionFromNotification.current = true; + const directAddress = e.detail?.from; + + const findDirect = directs?.find( + (direct) => direct?.address === directAddress + ); + if (findDirect?.address === selectedDirect?.address) { + isLoadingOpenSectionFromNotification.current = false; + return; + } + if (findDirect) { + setChatMode("directs"); + setSelectedDirect(null); + setSelectedGroup(null); + + setNewChat(false); + + chrome.runtime.sendMessage({ + action: "addTimestampEnterChat", + payload: { + timestamp: Date.now(), + groupId: findDirect.address, + }, + }); + + setTimeout(() => { + setSelectedDirect(findDirect); + getTimestampEnterChat(); + isLoadingOpenSectionFromNotification.current = false; + }, 200); + } else { + isLoadingOpenSectionFromNotification.current = false; + } + }; + + useEffect(() => { + subscribeToEvent("openDirectMessage", openDirectChatFromNotification); + + return () => { + unsubscribeFromEvent("openDirectMessage", openDirectChatFromNotification); + }; + }, [directs, selectedDirect]); + const openGroupChatFromNotification = (e) => { + if (isLoadingOpenSectionFromNotification.current) return; + + const groupId = e.detail?.from; + + const findGroup = groups?.find((group) => +group?.groupId === +groupId); + if (findGroup?.groupId === selectedGroup?.groupId) { + isLoadingOpenSectionFromNotification.current = false; + + return; + } + if (findGroup) { + setChatMode("groups"); + setSelectedGroup(null); + setSelectedDirect(null); + + setNewChat(false); + setSecretKey(null); + setSecretKeyPublishDate(null); + setAdmins([]); + setSecretKeyDetails(null); + setAdminsWithNames([]); + setMembers([]); + setMemberCountFromSecretKeyData(null); + setTriedToFetchSecretKey(false); + setFirstSecretKeyInCreation(false) + setGroupSection("chat"); + + chrome.runtime.sendMessage({ + action: "addTimestampEnterChat", + payload: { + timestamp: Date.now(), + groupId: findGroup.groupId, + }, + }); + + setTimeout(() => { + setSelectedGroup(findGroup); + + getTimestampEnterChat(); + isLoadingOpenSectionFromNotification.current = false; + }, 200); + } else { + isLoadingOpenSectionFromNotification.current = false; + } + }; + + useEffect(() => { + subscribeToEvent("openGroupMessage", openGroupChatFromNotification); + + return () => { + unsubscribeFromEvent("openGroupMessage", openGroupChatFromNotification); + }; + }, [groups, selectedGroup]); + + const openGroupAnnouncementFromNotification = (e) => { + + const groupId = e.detail?.from; + + const findGroup = groups?.find((group) => +group?.groupId === +groupId); + if (findGroup?.groupId === selectedGroup?.groupId) return; + if (findGroup) { + setChatMode("groups"); + setSelectedGroup(null); + setSecretKey(null); + setSecretKeyPublishDate(null); + setAdmins([]); + setSecretKeyDetails(null); + setAdminsWithNames([]); + setMembers([]); + setMemberCountFromSecretKeyData(null); + setTriedToFetchSecretKey(false); + setFirstSecretKeyInCreation(false) + setGroupSection("announcement"); + chrome.runtime.sendMessage({ + action: "addGroupNotificationTimestamp", + payload: { + timestamp: Date.now(), + groupId: findGroup.groupId, + }, + }); + setTimeout(() => { + setSelectedGroup(findGroup); + + getGroupAnnouncements(); + }, 200); + } + }; + + useEffect(() => { + subscribeToEvent( + "openGroupAnnouncement", + openGroupAnnouncementFromNotification + ); + + return () => { + unsubscribeFromEvent( + "openGroupAnnouncement", + openGroupAnnouncementFromNotification + ); + }; + }, [groups, selectedGroup]); + + const openThreadNewPostFunc = (e) => { + const data = e.detail?.data; + const { groupId } = data; + const findGroup = groups?.find((group) => +group?.groupId === +groupId); + if (findGroup?.groupId === selectedGroup?.groupId) { + setGroupSection("forum"); + setDefaultThread(data); + // setTimeout(() => { + // executeEvent("setThreadByEvent", { + // data: data + // }); + // }, 400); + return; + } + if (findGroup) { + setChatMode("groups"); + setSelectedGroup(null); + setSecretKey(null); + setSecretKeyPublishDate(null); + setAdmins([]); + setSecretKeyDetails(null); + setAdminsWithNames([]); + setMembers([]); + setMemberCountFromSecretKeyData(null); + setTriedToFetchSecretKey(false); + setFirstSecretKeyInCreation(false) + setGroupSection("forum"); + setDefaultThread(data); + + setTimeout(() => { + setSelectedGroup(findGroup); + + getGroupAnnouncements(); + }, 200); + } + }; + + useEffect(() => { + subscribeToEvent("openThreadNewPost", openThreadNewPostFunc); + + return () => { + unsubscribeFromEvent("openThreadNewPost", openThreadNewPostFunc); + }; + }, [groups, selectedGroup]); + + const handleSecretKeyCreationInProgress = ()=> { + setFirstSecretKeyInCreation(true) + } + + + + return ( + <> + + + +
+
+
+ { + setChatMode((prev) => + prev === "directs" ? "groups" : "directs" + ); + setNewChat(false); + setSelectedDirect(null); + setSelectedGroup(null); + setGroupSection("default"); + }} + > + {chatMode === "groups" && ( + <> + + + )} + {chatMode === "directs" ? "Switch to groups" : "Direct msgs"} + +
+
+ {directs.map((direct: any) => ( + + + // + // + // } + onClick={() => { + setSelectedDirect(null); + setNewChat(false); + setSelectedGroup(null); + chrome.runtime.sendMessage({ + action: "addTimestampEnterChat", + payload: { + timestamp: Date.now(), + groupId: direct.address, + }, + }); + setTimeout(() => { + setSelectedDirect(direct); + + getTimestampEnterChat(); + }, 200); + }} + sx={{ + display: "flex", + width: "100%", + flexDirection: "column", + cursor: "pointer", + border: "1px #232428 solid", + padding: "2px", + borderRadius: "2px", + background: + direct?.address === selectedDirect?.address && "white", + }} + > + + + + {(direct?.name || direct?.address)?.charAt(0)} + + + + {direct?.sender !== myAddress && + direct?.timestamp && + ((!timestampEnterData[direct?.address] && + Date.now() - direct?.timestamp < + timeDifferenceForNotificationChats) || + timestampEnterData[direct?.address] < + direct?.timestamp) && ( + + )} + + + + ))} +
+
+ {groups.map((group: any) => ( + + + // + // + // } + onClick={() => { + clearAllQueues() + setSelectedDirect(null); + + setNewChat(false); + setSelectedGroup(null); + setSecretKey(null); + setSecretKeyPublishDate(null); + setAdmins([]); + setSecretKeyDetails(null); + setAdminsWithNames([]); + setMembers([]); + setMemberCountFromSecretKeyData(null); + setTriedToFetchSecretKey(false); + setFirstSecretKeyInCreation(false) + setGroupSection("announcement"); + + setTimeout(() => { + setSelectedGroup(group); + + getTimestampEnterChat(); + }, 200); + + if (groupSectionRef.current === "announcement") { + chrome.runtime.sendMessage({ + action: "addGroupNotificationTimestamp", + payload: { + timestamp: Date.now(), + groupId: group.groupId, + }, + }); + } + + setTimeout(() => { + getGroupAnnouncements(); + }, 600); + }} + sx={{ + display: "flex", + width: "100%", + flexDirection: "column", + cursor: "pointer", + border: "1px #232428 solid", + padding: "2px", + borderRadius: "2px", + background: + group?.groupId === selectedGroup?.groupId && "white", + }} + > + + + + {group.groupName?.charAt(0)} + + + + {groupAnnouncements[group?.groupId] && + !groupAnnouncements[group?.groupId]?.seentimestamp && ( + + )} + {group?.sender !== myAddress && + group?.timestamp && + ((!timestampEnterData[group?.groupId] && + Date.now() - group?.timestamp < + timeDifferenceForNotificationChats) || + timestampEnterData[group?.groupId] < + group?.timestamp) && ( + + )} + + + + ))} +
+
+ {chatMode === "groups" && ( + { + setOpenAddGroup(true); + }} + > + + Add Group + + )} + {chatMode === "directs" && ( + { + setNewChat(true); + setSelectedDirect(null); + setSelectedGroup(null); + }} + > + + New Chat + + )} +
+
+ + {newChat && ( + <> + + + )} + {selectedGroup && !newChat && ( + <> + + {triedToFetchSecretKey && ( + + )} + {firstSecretKeyInCreation && triedToFetchSecretKey && !secretKeyPublishDate && ( +
+ {" "} + + The group's first common encryption key is in the process of creation. Please wait a few minutes for it to be retrieved by the network. Checking every 2 minutes... + +
+ )} + {!admins.includes(myAddress) && + !secretKey && + triedToFetchSecretKey ? ( + <> + {(secretKeyPublishDate || !secretKeyPublishDate && !firstSecretKeyInCreation) ? ( +
+ {" "} + + You are not part of the encrypted group of members. Wait + until an admin re-encrypts the keys. + + + + Try notifying an admin from the list of admins below: + + + {adminsWithNames.map((admin) => { + + return ( + + {admin?.name} + notifyAdmin(admin)} + > + Notify + + + ); + })} +
+ ) : null} + + + ) : admins.includes(myAddress) && + !secretKey && + triedToFetchSecretKey ? null : !triedToFetchSecretKey ? null : ( + <> + + + + + + )} + + + {admins.includes(myAddress) && + shouldReEncrypt && + triedToFetchSecretKey && !firstSecretKeyInCreation && ( + + )} + +
+ {openManageMembers && ( + + )} + + + + { + setGroupSection("default"); + clearAllQueues() + await new Promise((res) => { + setTimeout(() => { + res(null); + }, 200); + }); + setGroupSection("home"); + setSelectedGroup(null); + setNewChat(false); + setSelectedDirect(null); + setSecretKey(null); + setSecretKeyPublishDate(null); + setAdmins([]); + setSecretKeyDetails(null); + setAdminsWithNames([]); + setMembers([]); + setMemberCountFromSecretKeyData(null); + setTriedToFetchSecretKey(false); + setFirstSecretKeyInCreation(false) + }} + > + + + Home + + + + + { + setGroupSection("default"); + await new Promise((res) => { + setTimeout(() => { + res(null); + }, 200); + }); + setGroupSection("announcement"); + chrome.runtime.sendMessage({ + action: "addGroupNotificationTimestamp", + payload: { + timestamp: Date.now(), + groupId: selectedGroupRef.current.groupId, + }, + }); + setTimeout(() => { + getGroupAnnouncements(); + }, 200); + }} + > + + + Announcements + + + + + { + setGroupSection("default"); + await new Promise((res) => { + setTimeout(() => { + res(null); + }, 200); + }); + setGroupSection("chat"); + if (selectedGroupRef.current) { + chrome.runtime.sendMessage({ + action: "addTimestampEnterChat", + payload: { + timestamp: Date.now(), + groupId: selectedGroupRef.current.groupId, + }, + }); + + setTimeout(() => { + getTimestampEnterChat(); + }, 200); + } + }} + > + + + Chat + + + + + { + setGroupSection("forum"); + }} + > + + + Forum + + + + setOpenManageMembers(true)} + sx={{ + display: "flex", + gap: "3px", + alignItems: "center", + justifyContent: "flex-start", + width: "100%", + cursor: 'pointer' + }} + > + + + Members + + + + {/* */} + + + )} + + {selectedDirect && !newChat && ( + <> + + + + + )} + {!selectedDirect && + !selectedGroup && + !newChat && + groupSection === "home" && ( + + + + + + + + + + + + + )} + + +
+ + ); +}; diff --git a/src/components/Group/GroupInvites.tsx b/src/components/Group/GroupInvites.tsx new file mode 100644 index 0000000..06e176d --- /dev/null +++ b/src/components/Group/GroupInvites.tsx @@ -0,0 +1,114 @@ +import * as React from "react"; +import List from "@mui/material/List"; +import ListItem from "@mui/material/ListItem"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import ListItemText from "@mui/material/ListItemText"; +import Checkbox from "@mui/material/Checkbox"; +import IconButton from "@mui/material/IconButton"; +import CommentIcon from "@mui/icons-material/Comment"; +import InfoIcon from "@mui/icons-material/Info"; +import GroupAddIcon from '@mui/icons-material/GroupAdd'; +import { executeEvent } from "../../utils/events"; +import { Box, Typography } from "@mui/material"; +import { Spacer } from "../../common/Spacer"; +import { getGroupNames } from "./UserListOfInvites"; +import { CustomLoader } from "../../common/CustomLoader"; +import { getBaseApiReact } from "../../App"; + +export const GroupInvites = ({ myAddress, setOpenAddGroup }) => { + const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState([]) + const [loading, setLoading] = React.useState(true) + + const getJoinRequests = async ()=> { + try { + setLoading(true) + const response = await fetch(`${getBaseApiReact()}/groups/invites/${myAddress}/?limit=0`); + const data = await response.json(); + const resMoreData = await getGroupNames(data) + + setGroupsWithJoinRequests(resMoreData) + } catch (error) { + + } finally { + setLoading(false) + } + } + + React.useEffect(() => { + if (myAddress) { + getJoinRequests() + } + }, [myAddress]); + + + return ( + + Group Invites + + {loading && groupsWithJoinRequests.length === 0 && ( + + + + )} + {!loading && groupsWithJoinRequests.length === 0 && ( + + No invites + + )} + + {groupsWithJoinRequests?.map((group)=> { + return ( + { + setOpenAddGroup(true) + setTimeout(() => { + executeEvent("openGroupInvitesRequest", {}); + + }, 300); + }} + disablePadding + secondaryAction={ + + + + } + > + + + + + + ) + + })} + + + + + + ); +}; diff --git a/src/components/Group/GroupJoinRequests.tsx b/src/components/Group/GroupJoinRequests.tsx new file mode 100644 index 0000000..1123d57 --- /dev/null +++ b/src/components/Group/GroupJoinRequests.tsx @@ -0,0 +1,170 @@ +import * as React from "react"; +import List from "@mui/material/List"; +import ListItem from "@mui/material/ListItem"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import ListItemText from "@mui/material/ListItemText"; +import Checkbox from "@mui/material/Checkbox"; +import IconButton from "@mui/material/IconButton"; +import CommentIcon from "@mui/icons-material/Comment"; +import InfoIcon from "@mui/icons-material/Info"; +import { RequestQueueWithPromise } from "../../utils/queue/queue"; +import GroupAddIcon from '@mui/icons-material/GroupAdd'; +import { executeEvent } from "../../utils/events"; +import { Box, Typography } from "@mui/material"; +import { Spacer } from "../../common/Spacer"; +import { CustomLoader } from "../../common/CustomLoader"; +import { getBaseApi } from "../../background"; +import { getBaseApiReact } from "../../App"; +export const requestQueueGroupJoinRequests = new RequestQueueWithPromise(3) + +export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, getTimestampEnterChat, setSelectedGroup, setGroupSection }) => { + const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState([]) + const [loading, setLoading] = React.useState(true) + + + + const getJoinRequests = async ()=> { + try { + setLoading(true) + + let groupsAsAdmin = [] + const getAllGroupsAsAdmin = groups.map(async (group)=> { + + const isAdminResponse = await requestQueueGroupJoinRequests.enqueue(()=> { + return fetch( + `${getBaseApiReact()}/groups/members/${group.groupId}?limit=0&onlyAdmins=true` + ); + }) + const isAdminData = await isAdminResponse.json() + + + const findMyself = isAdminData?.members?.find((member)=> member.member === myAddress) + + if(findMyself){ + groupsAsAdmin.push(group) + } + return true + }) + + // const getJoinGroupRequests = groupsAsAdmin.map(async (group)=> { + // console.log('getJoinGroupRequests', group) + // const joinRequestResponse = await requestQueueGroupJoinRequests.enqueue(()=> { + // return fetch( + // `${getBaseApiReact()}/groups/joinrequests/${group.groupId}` + // ); + // }) + + // const joinRequestData = await joinRequestResponse.json() + // return { + // group, + // data: joinRequestData + // } + // }) + await Promise.all(getAllGroupsAsAdmin) + const res = await Promise.all(groupsAsAdmin.map(async (group)=> { + + const joinRequestResponse = await requestQueueGroupJoinRequests.enqueue(()=> { + return fetch( + `${getBaseApiReact()}/groups/joinrequests/${group.groupId}` + ); + }) + + const joinRequestData = await joinRequestResponse.json() + return { + group, + data: joinRequestData + } + })) + setGroupsWithJoinRequests(res) + } catch (error) { + + } finally { + setLoading(false) + } + } + + React.useEffect(() => { + if (myAddress && groups.length > 0) { + getJoinRequests() + } else { + setLoading(false) + } + }, [myAddress, groups]); + + + + return ( + + Join Requests + + {loading && groupsWithJoinRequests.length === 0 && ( + + + + )} + {!loading && groupsWithJoinRequests.length === 0 && ( + + No join requests + + )} + + {groupsWithJoinRequests?.map((group)=> { + if(group?.data?.length === 0) return null + return ( + { + setSelectedGroup(group?.group) + getTimestampEnterChat() + setGroupSection("announcement") + setOpenManageMembers(true) + setTimeout(() => { + executeEvent("openGroupJoinRequest", {}); + + }, 300); + }} + disablePadding + secondaryAction={ + + + + } + > + + + + + + ) + + })} + + + + + + ); +}; diff --git a/src/components/Group/InviteMember.tsx b/src/components/Group/InviteMember.tsx new file mode 100644 index 0000000..5a22678 --- /dev/null +++ b/src/components/Group/InviteMember.tsx @@ -0,0 +1,108 @@ +import { LoadingButton } from "@mui/lab"; +import { + Box, + Button, + Input, + MenuItem, + Select, + SelectChangeEvent, +} from "@mui/material"; +import React, { useState } from "react"; +import { Spacer } from "../../common/Spacer"; +import { Label } from "./AddGroup"; +import { getFee } from "../../background"; + +export const InviteMember = ({ groupId, setInfoSnack, setOpenSnack, show }) => { + const [value, setValue] = useState(""); + const [expiryTime, setExpiryTime] = useState('259200'); + const [isLoadingInvite, setIsLoadingInvite] = useState(false) + const inviteMember = async () => { + try { + const fee = await getFee('GROUP_INVITE') + await show({ + message: "Would you like to perform a GROUP_INVITE transaction?" , + publishFee: fee.fee + ' QORT' + }) + setIsLoadingInvite(true) + if (!expiryTime || !value) return; + new Promise((res, rej) => { + chrome.runtime.sendMessage( + { + action: "inviteToGroup", + payload: { + groupId, + qortalAddress: value, + inviteTime: +expiryTime, + }, + }, + (response) => { + + if (!response?.error) { + setInfoSnack({ + type: "success", + message: `Successfully invited ${value}. It may take a couple of minutes for the changes to propagate`, + }); + setOpenSnack(true); + res(response); + + setValue(""); + return + } + setInfoSnack({ + type: "error", + message: response?.error, + }); + setOpenSnack(true); + rej(response.error); + } + ); + }); + } catch (error) {} finally { + setIsLoadingInvite(false) + } + }; + + const handleChange = (event: SelectChangeEvent) => { + setExpiryTime(event.target.value as string); + }; + + return ( + + Invite member + + setValue(e.target.value)} + /> + + + + + + Invite + + ); +}; diff --git a/src/components/Group/ListOfBans.tsx b/src/components/Group/ListOfBans.tsx new file mode 100644 index 0000000..6cd1236 --- /dev/null +++ b/src/components/Group/ListOfBans.tsx @@ -0,0 +1,188 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { Avatar, Box, Button, ListItem, ListItemAvatar, ListItemButton, ListItemText, Popover } from '@mui/material'; +import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from 'react-virtualized'; +import { getNameInfo } from './Group'; +import { getBaseApi, getFee } from '../../background'; +import { LoadingButton } from '@mui/lab'; +import { getBaseApiReact } from '../../App'; + +export const getMemberInvites = async (groupNumber) => { + const response = await fetch(`${getBaseApiReact()}/groups/bans/${groupNumber}?limit=0`); + const groupData = await response.json(); + return groupData; +} + +const getNames = async (listOfMembers) => { + let members = []; + if (listOfMembers && Array.isArray(listOfMembers)) { + for (const member of listOfMembers) { + if (member.offender) { + const name = await getNameInfo(member.offender); + if (name) { + members.push({ ...member, name }); + } + } + } + } + return members; +} + +const cache = new CellMeasurerCache({ + fixedWidth: true, + defaultHeight: 50, +}); + +export const ListOfBans = ({ groupId, setInfoSnack, setOpenSnack, show }) => { + const [bans, setBans] = useState([]); + const [popoverAnchor, setPopoverAnchor] = useState(null); // Track which list item the popover is anchored to + const [openPopoverIndex, setOpenPopoverIndex] = useState(null); // Track which list item has the popover open + const listRef = useRef(); + const [isLoadingUnban, setIsLoadingUnban] = useState(false); + + const getInvites = async (groupId) => { + try { + const res = await getMemberInvites(groupId); + const resWithNames = await getNames(res); + setBans(resWithNames); + } catch (error) { + console.error(error); + } + } + + useEffect(() => { + if (groupId) { + getInvites(groupId); + } + }, [groupId]); + + const handlePopoverOpen = (event, index) => { + setPopoverAnchor(event.currentTarget); + setOpenPopoverIndex(index); + }; + + const handlePopoverClose = () => { + setPopoverAnchor(null); + setOpenPopoverIndex(null); + }; + + const handleCancelBan = async (address)=> { + try { + const fee = await getFee('CANCEL_GROUP_BAN') + await show({ + message: "Would you like to perform a CANCEL_GROUP_BAN transaction?" , + publishFee: fee.fee + ' QORT' + }) + setIsLoadingUnban(true) + new Promise((res, rej)=> { + chrome.runtime.sendMessage({ action: "cancelBan", payload: { + groupId, + qortalAddress: address, + }}, (response) => { + + if (!response?.error) { + res(response) + setIsLoadingUnban(false) + setInfoSnack({ + type: "success", + message: "Successfully unbanned user. It may take a couple of minutes for the changes to propagate", + }); + handlePopoverClose(); + setOpenSnack(true); + return + } + setInfoSnack({ + type: "error", + message: response?.error, + }); + setOpenSnack(true); + rej(response.error) + }); + }) + } catch (error) { + + } finally { + setIsLoadingUnban(false) + } + } + + const rowRenderer = ({ index, key, parent, style }) => { + const member = bans[index]; + + return ( + + {({ measure }) => ( +
+ + + + handleCancelBan(member?.offender)}>Cancel Ban + + + handlePopoverOpen(event, index)}> + + + + + + +
+ )} +
+ ); + }; + + return ( +
+

Ban list

+
+ + {({ height, width }) => ( + + )} + +
+
+ ); +} diff --git a/src/components/Group/ListOfInvites.tsx b/src/components/Group/ListOfInvites.tsx new file mode 100644 index 0000000..b6a9734 --- /dev/null +++ b/src/components/Group/ListOfInvites.tsx @@ -0,0 +1,189 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { Avatar, Box, Button, ListItem, ListItemAvatar, ListItemButton, ListItemText, Popover } from '@mui/material'; +import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from 'react-virtualized'; +import { getNameInfo } from './Group'; +import { getBaseApi, getFee } from '../../background'; +import { LoadingButton } from '@mui/lab'; +import { getBaseApiReact } from '../../App'; + +export const getMemberInvites = async (groupNumber) => { + const response = await fetch(`${getBaseApiReact()}/groups/invites/group/${groupNumber}?limit=0`); + const groupData = await response.json(); + return groupData; +} + +const getNames = async (listOfMembers) => { + let members = []; + if (listOfMembers && Array.isArray(listOfMembers)) { + for (const member of listOfMembers) { + if (member.invitee) { + const name = await getNameInfo(member.invitee); + if (name) { + members.push({ ...member, name }); + } + } + } + } + return members; +} + +const cache = new CellMeasurerCache({ + fixedWidth: true, + defaultHeight: 50, +}); + +export const ListOfInvites = ({ groupId, setInfoSnack, setOpenSnack, show }) => { + const [invites, setInvites] = useState([]); + const [popoverAnchor, setPopoverAnchor] = useState(null); // Track which list item the popover is anchored to + const [openPopoverIndex, setOpenPopoverIndex] = useState(null); // Track which list item has the popover open + const [isLoadingCancelInvite, setIsLoadingCancelInvite] = useState(false); + + const listRef = useRef(); + + const getInvites = async (groupId) => { + try { + const res = await getMemberInvites(groupId); + const resWithNames = await getNames(res); + setInvites(resWithNames); + } catch (error) { + console.error(error); + } + } + + useEffect(() => { + if (groupId) { + getInvites(groupId); + } + }, [groupId]); + + const handlePopoverOpen = (event, index) => { + setPopoverAnchor(event.currentTarget); + setOpenPopoverIndex(index); + }; + + const handlePopoverClose = () => { + setPopoverAnchor(null); + setOpenPopoverIndex(null); + }; + + 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)=> { + chrome.runtime.sendMessage({ action: "cancelInvitationToGroup", payload: { + groupId, + qortalAddress: address, + }}, (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) { + + } finally { + setIsLoadingCancelInvite(false) + } + } + + const rowRenderer = ({ index, key, parent, style }) => { + const member = invites[index]; + + return ( + + {({ measure }) => ( +
+ + + + handleCancelInvitation(member?.invitee)}>Cancel Invitation + + + handlePopoverOpen(event, index)}> + + + + + + +
+ )} +
+ ); + }; + + return ( +
+

Invitees list

+
+ + {({ height, width }) => ( + + )} + +
+
+ ); +} diff --git a/src/components/Group/ListOfJoinRequests.tsx b/src/components/Group/ListOfJoinRequests.tsx new file mode 100644 index 0000000..160a7ca --- /dev/null +++ b/src/components/Group/ListOfJoinRequests.tsx @@ -0,0 +1,189 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { Avatar, Box, Button, ListItem, ListItemAvatar, ListItemButton, ListItemText, Popover } from '@mui/material'; +import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from 'react-virtualized'; +import { getNameInfo } from './Group'; +import { getBaseApi, getFee } from '../../background'; +import { LoadingButton } from '@mui/lab'; +import { getBaseApiReact } from '../../App'; + +export const getMemberInvites = async (groupNumber) => { + const response = await fetch(`${getBaseApiReact()}/groups/joinrequests/${groupNumber}?limit=0`); + const groupData = await response.json(); + return groupData; +} + +const getNames = async (listOfMembers) => { + let members = []; + if (listOfMembers && Array.isArray(listOfMembers)) { + for (const member of listOfMembers) { + if (member.joiner) { + const name = await getNameInfo(member.joiner); + if (name) { + members.push({ ...member, name }); + } + } + } + } + return members; +} + +const cache = new CellMeasurerCache({ + fixedWidth: true, + defaultHeight: 50, +}); + +export const ListOfJoinRequests = ({ groupId, setInfoSnack, setOpenSnack, show }) => { + const [invites, setInvites] = useState([]); + const [popoverAnchor, setPopoverAnchor] = useState(null); // Track which list item the popover is anchored to + const [openPopoverIndex, setOpenPopoverIndex] = useState(null); // Track which list item has the popover open + const listRef = useRef(); + const [isLoadingAccept, setIsLoadingAccept] = useState(false); + + const getInvites = async (groupId) => { + try { + const res = await getMemberInvites(groupId); + const resWithNames = await getNames(res); + setInvites(resWithNames); + } catch (error) { + console.error(error); + } + } + + useEffect(() => { + if (groupId) { + getInvites(groupId); + } + }, [groupId]); + + const handlePopoverOpen = (event, index) => { + setPopoverAnchor(event.currentTarget); + setOpenPopoverIndex(index); + }; + + const handlePopoverClose = () => { + setPopoverAnchor(null); + setOpenPopoverIndex(null); + }; + + const handleAcceptJoinRequest = async (address)=> { + try { + const fee = await getFee('GROUP_INVITE') + await show({ + message: "Would you like to perform a GROUP_INVITE transaction?" , + publishFee: fee.fee + ' QORT' + }) + setIsLoadingAccept(true) + await new Promise((res, rej)=> { + chrome.runtime.sendMessage({ action: "inviteToGroup", payload: { + groupId, + qortalAddress: address, + inviteTime: 10800, + }}, (response) => { + + if (!response?.error) { + setIsLoadingAccept(false) + setInfoSnack({ + type: "success", + message: "Successfully accepted join request. It may take a couple of minutes for the changes to propagate", + }); + setOpenSnack(true); + handlePopoverClose(); + res(response) + return + } + setInfoSnack({ + type: "error", + message: response?.error, + }); + setOpenSnack(true); + rej(response.error) + }); + }) + } catch (error) { + + } finally { + setIsLoadingAccept(false) + } + } + + const rowRenderer = ({ index, key, parent, style }) => { + const member = invites[index]; + + return ( + + {({ measure }) => ( +
+ + + + handleAcceptJoinRequest(member?.joiner)}>Accept + + + handlePopoverOpen(event, index)}> + + + + + + +
+ )} +
+ ); + }; + + return ( +
+

Join request list

+
+ + {({ height, width }) => ( + + )} + +
+
+ ); +} diff --git a/src/components/Group/ListOfMembers.tsx b/src/components/Group/ListOfMembers.tsx new file mode 100644 index 0000000..3662a91 --- /dev/null +++ b/src/components/Group/ListOfMembers.tsx @@ -0,0 +1,385 @@ +import { + Avatar, + Box, + Button, + ListItem, + ListItemAvatar, + ListItemButton, + ListItemText, + Popover, + Typography, +} from "@mui/material"; +import React, { useRef, useState } from "react"; +import { + AutoSizer, + CellMeasurer, + CellMeasurerCache, + List, +} from "react-virtualized"; +import { LoadingButton } from "@mui/lab"; +import { getBaseApi, getFee } from "../../background"; +import { getBaseApiReact } from "../../App"; + +const cache = new CellMeasurerCache({ + fixedWidth: true, + defaultHeight: 50, +}); +const ListOfMembers = ({ + members, + groupId, + setInfoSnack, + setOpenSnack, + isAdmin, + isOwner, + show, +}) => { + const [popoverAnchor, setPopoverAnchor] = useState(null); // Track which list item the popover is anchored to + const [openPopoverIndex, setOpenPopoverIndex] = useState(null); // Track which list item has the popover open + const [isLoadingKick, setIsLoadingKick] = useState(false); + const [isLoadingBan, setIsLoadingBan] = useState(false); + const [isLoadingMakeAdmin, setIsLoadingMakeAdmin] = useState(false); + const [isLoadingRemoveAdmin, setIsLoadingRemoveAdmin] = useState(false); + + + const listRef = useRef(); + + const handlePopoverOpen = (event, index) => { + setPopoverAnchor(event.currentTarget); + setOpenPopoverIndex(index); + }; + + const handlePopoverClose = () => { + setPopoverAnchor(null); + setOpenPopoverIndex(null); + }; + + const handleKick = async (address) => { + try { + const fee = await getFee("GROUP_KICK"); + await show({ + message: "Would you like to perform a GROUP_KICK transaction?", + publishFee: fee.fee + " QORT", + }); + + setIsLoadingKick(true); + new Promise((res, rej) => { + chrome.runtime.sendMessage( + { + action: "kickFromGroup", + payload: { + groupId, + qortalAddress: address, + }, + }, + (response) => { + + if (!response?.error) { + setInfoSnack({ + type: "success", + message: + "Successfully kicked member from group. It may take a couple of minutes for the changes to propagate", + }); + setOpenSnack(true); + handlePopoverClose(); + res(response); + return; + } + setInfoSnack({ + type: "error", + message: response?.error, + }); + setOpenSnack(true); + rej(response.error); + } + ); + }); + } catch (error) { + } finally { + setIsLoadingKick(false); + } + }; + const handleBan = async (address) => { + try { + const fee = await getFee("GROUP_BAN"); + await show({ + message: "Would you like to perform a GROUP_BAN transaction?", + publishFee: fee.fee + " QORT", + }); + setIsLoadingBan(true); + await new Promise((res, rej) => { + chrome.runtime.sendMessage( + { + action: "banFromGroup", + payload: { + groupId, + qortalAddress: address, + rBanTime: 0, + }, + }, + (response) => { + + if (!response?.error) { + setInfoSnack({ + type: "success", + message: + "Successfully banned member from group. It may take a couple of minutes for the changes to propagate", + }); + setOpenSnack(true); + handlePopoverClose(); + res(response); + return; + } + setInfoSnack({ + type: "error", + message: response?.error, + }); + setOpenSnack(true); + rej(response.error); + } + ); + }); + } catch (error) { + } finally { + setIsLoadingBan(false); + } + }; + + const makeAdmin = async (address) => { + try { + const fee = await getFee("ADD_GROUP_ADMIN"); + await show({ + message: "Would you like to perform a ADD_GROUP_ADMIN transaction?", + publishFee: fee.fee + " QORT", + }); + setIsLoadingMakeAdmin(true); + await new Promise((res, rej) => { + chrome.runtime.sendMessage( + { + action: "makeAdmin", + payload: { + groupId, + qortalAddress: address, + }, + }, + (response) => { + + if (!response?.error) { + setInfoSnack({ + type: "success", + message: + "Successfully made member an admin. It may take a couple of minutes for the changes to propagate", + }); + setOpenSnack(true); + handlePopoverClose(); + res(response); + return; + } + setInfoSnack({ + type: "error", + message: response?.error, + }); + setOpenSnack(true); + rej(response.error); + } + ); + }); + } catch (error) { + } finally { + setIsLoadingMakeAdmin(false); + } + }; + + const removeAdmin = async (address) => { + try { + const fee = await getFee("REMOVE_GROUP_ADMIN"); + await show({ + message: "Would you like to perform a REMOVE_GROUP_ADMIN transaction?", + publishFee: fee.fee + " QORT", + }); + setIsLoadingRemoveAdmin(true); + await new Promise((res, rej) => { + chrome.runtime.sendMessage( + { + action: "removeAdmin", + payload: { + groupId, + qortalAddress: address, + }, + }, + (response) => { + + if (!response?.error) { + setInfoSnack({ + type: "success", + message: + "Successfully removed member as an admin. It may take a couple of minutes for the changes to propagate", + }); + setOpenSnack(true); + handlePopoverClose(); + res(response); + return; + } + setInfoSnack({ + type: "error", + message: response?.error, + }); + setOpenSnack(true); + rej(response.error); + } + ); + }); + } catch (error) { + } finally { + setIsLoadingRemoveAdmin(false); + } + }; + + const rowRenderer = ({ index, key, parent, style }) => { + const member = members[index]; + + return ( + + {({ measure }) => ( +
+ + + {isOwner && ( + <> + handleKick(member?.member)} + > + Kick member from group + + handleBan(member?.member)} + > + Ban member from group + + makeAdmin(member?.member)} + > + Make an admin + + removeAdmin(member?.member)} + > + Remove as admin + + + )} + + + + // } + disablePadding + > + handlePopoverOpen(event, index)} + > + + + + + {member?.isAdmin && ( + Admin + )} + + + +
+ )} +
+ ); + }; + + return ( +
+

Member list

+
+ + {({ height, width }) => ( + + )} + +
+
+ ); +}; + +export default ListOfMembers; diff --git a/src/components/Group/ListOfThreadPostsWatched.tsx b/src/components/Group/ListOfThreadPostsWatched.tsx new file mode 100644 index 0000000..ed8e7d9 --- /dev/null +++ b/src/components/Group/ListOfThreadPostsWatched.tsx @@ -0,0 +1,138 @@ +import * as React from "react"; +import List from "@mui/material/List"; +import ListItem from "@mui/material/ListItem"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import ListItemText from "@mui/material/ListItemText"; +import Checkbox from "@mui/material/Checkbox"; +import IconButton from "@mui/material/IconButton"; +import CommentIcon from "@mui/icons-material/Comment"; +import InfoIcon from "@mui/icons-material/Info"; +import GroupAddIcon from '@mui/icons-material/GroupAdd'; +import { executeEvent } from "../../utils/events"; +import { Box, Typography } from "@mui/material"; +import { Spacer } from "../../common/Spacer"; +import { getGroupNames } from "./UserListOfInvites"; +import { CustomLoader } from "../../common/CustomLoader"; +import VisibilityIcon from '@mui/icons-material/Visibility'; + +export const ListOfThreadPostsWatched = () => { + const [posts, setPosts] = React.useState([]) + const [loading, setLoading] = React.useState(true) + + const getPosts = async ()=> { + try { + await new Promise((res, rej) => { + chrome.runtime.sendMessage( + { + action: "getThreadActivity", + payload: { + + }, + }, + (response) => { + + if (!response?.error) { + if(!response) { + res(null) + return + } + const uniquePosts = response.reduce((acc, current) => { + const x = acc.find(item => item?.thread?.threadId === current?.thread?.threadId); + if (!x) { + return acc.concat([current]); + } else { + return acc; + } + }, []); + setPosts(uniquePosts) + res(uniquePosts); + return + } + rej(response.error); + } + ); + }); + } catch (error) { + + } finally { + setLoading(false) + } + } + + React.useEffect(() => { + + getPosts() + + }, []); + + + + return ( + + New Thread Posts + + {loading && posts.length === 0 && ( + + + + )} + {!loading && posts.length === 0 && ( + + No thread post notifications + + )} + + {posts?.map((post)=> { + return ( + { + executeEvent("openThreadNewPost", { + data: post + }); + }} + disablePadding + secondaryAction={ + + + + } + > + + + + + + ) + + })} + + + + + + ); +}; diff --git a/src/components/Group/ManageMembers.tsx b/src/components/Group/ManageMembers.tsx new file mode 100644 index 0000000..aebb3fb --- /dev/null +++ b/src/components/Group/ManageMembers.tsx @@ -0,0 +1,316 @@ +import * as React from "react"; +import Button from "@mui/material/Button"; +import Dialog from "@mui/material/Dialog"; +import ListItemText from "@mui/material/ListItemText"; +import ListItemButton from "@mui/material/ListItemButton"; +import List from "@mui/material/List"; +import Divider from "@mui/material/Divider"; +import AppBar from "@mui/material/AppBar"; +import Toolbar from "@mui/material/Toolbar"; +import IconButton from "@mui/material/IconButton"; +import Typography from "@mui/material/Typography"; +import CloseIcon from "@mui/icons-material/Close"; +import Slide from "@mui/material/Slide"; +import { TransitionProps } from "@mui/material/transitions"; +import ListOfMembers from "./ListOfMembers"; +import { InviteMember } from "./InviteMember"; +import { ListOfInvites } from "./ListOfInvites"; +import { ListOfBans } from "./ListOfBans"; +import { ListOfJoinRequests } from "./ListOfJoinRequests"; +import { Box, Tab, Tabs } from "@mui/material"; +import { CustomizedSnackbars } from "../Snackbar/Snackbar"; +import { MyContext } from "../../App"; +import { getGroupMembers, getNames } from "./Group"; +import { LoadingSnackbar } from "../Snackbar/LoadingSnackbar"; +import { getFee } from "../../background"; +import { LoadingButton } from "@mui/lab"; +import { subscribeToEvent, unsubscribeFromEvent } from "../../utils/events"; + +function a11yProps(index: number) { + return { + id: `simple-tab-${index}`, + "aria-controls": `simple-tabpanel-${index}`, + }; +} + +const Transition = React.forwardRef(function Transition( + props: TransitionProps & { + children: React.ReactElement; + }, + ref: React.Ref +) { + return ; +}); + +export const ManageMembers = ({ + address, + open, + setOpen, + selectedGroup, + + isAdmin, + isOwner +}) => { + const [membersWithNames, setMembersWithNames] = React.useState([]); + const [tab, setTab] = React.useState("create"); + const [value, setValue] = React.useState(0); + const [openSnack, setOpenSnack] = React.useState(false); + const [infoSnack, setInfoSnack] = React.useState(null); + const [isLoadingMembers, setIsLoadingMembers] = React.useState(false) + const [isLoadingLeave, setIsLoadingLeave] = React.useState(false) + const handleChange = (event: React.SyntheticEvent, newValue: number) => { + setValue(newValue); + }; + const { show, setTxList } = React.useContext(MyContext); + + const handleClose = () => { + setOpen(false); + }; + + const handleLeaveGroup = async () => { + try { + setIsLoadingLeave(true) + const fee = await getFee('LEAVE_GROUP') + await show({ + message: "Would you like to perform an LEAVE_GROUP transaction?" , + publishFee: fee.fee + ' QORT' + }) + + await new Promise((res, rej) => { + chrome.runtime.sendMessage( + { + action: "leaveGroup", + payload: { + groupId: selectedGroup?.groupId, + }, + }, + (response) => { + + if (!response?.error) { + setTxList((prev)=> [{ + ...response, + type: 'leave-group', + label: `Left Group ${selectedGroup?.groupName}: awaiting confirmation`, + labelDone: `Left Group ${selectedGroup?.groupName}: success !`, + done: false, + groupId: selectedGroup?.groupId, + + }, ...prev]) + res(response); + setInfoSnack({ + type: "success", + message: "Successfully requested to leave group. It may take a couple of minutes for the changes to propagate", + }); + setOpenSnack(true); + return + } + rej(response.error); + } + ); + }); + } catch (error) {} finally { + setIsLoadingLeave(false) + } + }; + + const getMembers = async (groupId) => { + try { + setIsLoadingMembers(true) + const res = await getGroupMembers(groupId); + const resWithNames = await getNames(res.members); + setMembersWithNames(resWithNames); + setIsLoadingMembers(false) + } catch (error) {} + }; + + React.useEffect(()=> { + if(selectedGroup?.groupId){ + getMembers(selectedGroup?.groupId) + } + }, [selectedGroup?.groupId]) + + const openGroupJoinRequestFunc = ()=> { + setValue(4) + } + + React.useEffect(() => { + subscribeToEvent("openGroupJoinRequest", openGroupJoinRequestFunc); + + return () => { + unsubscribeFromEvent("openGroupJoinRequest", openGroupJoinRequestFunc); + }; + }, []); + + return ( + + + + + + Manage Members + + + + + + + + + + + + + + + + + + + {selectedGroup?.groupId && !isOwner && ( + + Leave Group + + )} + + {value === 0 && ( + + + + )} + {value === 1 && ( + + + + )} + + {value === 2 && ( + + + + + )} + + {value === 3 && ( + + + + )} + + {value === 4 && ( + + + + )} + + + + + + + ); +}; diff --git a/src/components/Group/ThingsToDoInitial.tsx b/src/components/Group/ThingsToDoInitial.tsx new file mode 100644 index 0000000..18f75b5 --- /dev/null +++ b/src/components/Group/ThingsToDoInitial.tsx @@ -0,0 +1,166 @@ +import * as React from "react"; +import List from "@mui/material/List"; +import ListItem from "@mui/material/ListItem"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import ListItemText from "@mui/material/ListItemText"; +import Checkbox from "@mui/material/Checkbox"; +import IconButton from "@mui/material/IconButton"; +import CommentIcon from "@mui/icons-material/Comment"; +import InfoIcon from "@mui/icons-material/Info"; +import { Box, Typography } from "@mui/material"; +import { Spacer } from "../../common/Spacer"; + +export const ThingsToDoInitial = ({ myAddress, name, hasGroups, balance }) => { + const [checked1, setChecked1] = React.useState(false); + const [checked2, setChecked2] = React.useState(false); + const [checked3, setChecked3] = React.useState(false); + +// const getAddressInfo = async (address) => { +// const response = await fetch(getBaseApiReact() + "/addresses/" + address); +// const data = await response.json(); +// if (data.error && data.error === 124) { +// setChecked1(false); +// } else if (data.address) { +// setChecked1(true); +// } +// }; + +// const checkInfo = async () => { +// try { +// getAddressInfo(myAddress); +// } catch (error) {} +// }; + + + + React.useEffect(() => { + if (balance && +balance >= 6) { + setChecked1(true) + } + }, [balance]); + + React.useEffect(()=> { + if(hasGroups) setChecked3(true) + }, [hasGroups]) + + React.useEffect(()=> { + if(name) setChecked2(true) + }, [name]) + + return ( + + Suggestion: Complete the following + + + + // + // + // } + disablePadding + > + + + + + + + + + // + // + // } + disablePadding + > + + + + + + + + + // + // + // } + disablePadding + > + + + + + + + + + + ); +}; diff --git a/src/components/Group/UserListOfInvites.tsx b/src/components/Group/UserListOfInvites.tsx new file mode 100644 index 0000000..d126024 --- /dev/null +++ b/src/components/Group/UserListOfInvites.tsx @@ -0,0 +1,206 @@ +import { Box, Button, ListItem, ListItemButton, ListItemText, Popover, Typography } from '@mui/material'; +import React, { useContext, useEffect, useRef, useState } from 'react' +import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from 'react-virtualized'; +import { MyContext, getBaseApiReact } from '../../App'; +import { LoadingButton } from '@mui/lab'; +import { getBaseApi, getFee } from '../../background'; + + +const cache = new CellMeasurerCache({ + fixedWidth: true, + defaultHeight: 50, + }); + + + +const getGroupInfo = async (groupId)=> { + const response = await fetch(`${getBaseApiReact()}/groups/` + groupId); + const groupData = await response.json(); + + if (groupData) { + return groupData + } +} + export const getGroupNames = async (listOfGroups) => { + let groups = []; + if (listOfGroups && Array.isArray(listOfGroups)) { + for (const group of listOfGroups) { + + const groupInfo = await getGroupInfo(group.groupId); + if (groupInfo) { + groups.push({ ...group, ...groupInfo }); + + } + } + } + return groups; + } + +export const UserListOfInvites = ({myAddress, setInfoSnack, setOpenSnack}) => { + const {txList, setTxList, show} = useContext(MyContext) + const [invites, setInvites] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + const [popoverAnchor, setPopoverAnchor] = useState(null); // Track which list item the popover is anchored to + const [openPopoverIndex, setOpenPopoverIndex] = useState(null); // Track which list item has the popover open + const listRef = useRef(); + + const getRequests = async () => { + try { + const response = await fetch(`${getBaseApiReact()}/groups/invites/${myAddress}/?limit=0`); + const inviteData = await response.json(); + + const resMoreData = await getGroupNames(inviteData) + setInvites(resMoreData); + } catch (error) { + console.error(error); + } + } + + useEffect(() => { + + getRequests(); + + }, []); + + const handlePopoverOpen = (event, index) => { + setPopoverAnchor(event.currentTarget); + setOpenPopoverIndex(index); + }; + + const handlePopoverClose = () => { + setPopoverAnchor(null); + setOpenPopoverIndex(null); + }; + + const handleJoinGroup = async (groupId, groupName)=> { + try { + + const fee = await getFee('JOIN_GROUP') + await show({ + message: "Would you like to perform an JOIN_GROUP transaction?" , + publishFee: fee.fee + ' QORT' + }) + + setIsLoading(true); + + await new Promise((res, rej)=> { + chrome.runtime.sendMessage({ action: "joinGroup", payload: { + groupId, + }}, (response) => { + + if (!response?.error) { + setTxList((prev)=> [{ + ...response, + type: 'joined-group', + label: `Joined Group ${groupName}: awaiting confirmation`, + labelDone: `Joined Group ${groupName}: success !`, + done: false, + groupId, + + }, ...prev]) + res(response) + setInfoSnack({ + type: "success", + message: "Successfully requested to join group. It may take a couple of minutes for the changes to propagate", + }); + setOpenSnack(true); + handlePopoverClose(); + return + } + setInfoSnack({ + type: "error", + message: response?.error, + }); + setOpenSnack(true); + rej(response.error) + + }); + }) + + } catch (error) { + + } finally { + setIsLoading(false); + + } + } + + const rowRenderer = ({ index, key, parent, style }) => { + const invite = invites[index]; + + return ( + + {({ measure }) => ( +
+ + + + Join {invite?.groupName} + handleJoinGroup(invite?.groupId, invite?.groupName)}>Join group + + + handlePopoverOpen(event, index)}> + + + + +
+ )} +
+ ); + }; + + return ( +
+

Invite list

+
+ + {({ height, width }) => ( + + )} + +
+
+ ); +} diff --git a/src/components/Group/WebsocketActive.tsx b/src/components/Group/WebsocketActive.tsx new file mode 100644 index 0000000..b09ff9f --- /dev/null +++ b/src/components/Group/WebsocketActive.tsx @@ -0,0 +1,109 @@ +import React, { useEffect, useRef } from 'react'; +import { getBaseApiReactSocket } from '../../App'; + +export const WebSocketActive = ({ myAddress }) => { + const socketRef = useRef(null); // WebSocket reference + const timeoutIdRef = useRef(null); // Timeout ID reference + const groupSocketTimeoutRef = useRef(null); // Group Socket Timeout reference + + const forceCloseWebSocket = () => { + if (socketRef.current) { + console.log('Force closing the WebSocket'); + clearTimeout(timeoutIdRef.current); + clearTimeout(groupSocketTimeoutRef.current); + socketRef.current.close(1000, 'forced'); + socketRef.current = null; + } + }; + + useEffect(() => { + if (!myAddress) return; // Only proceed if myAddress is set + if (!window?.location?.href?.includes("?main=true")) return; + + const pingHeads = () => { + try { + if (socketRef.current?.readyState === WebSocket.OPEN) { + socketRef.current.send('ping'); + timeoutIdRef.current = setTimeout(() => { + if (socketRef.current) { + socketRef.current.close(); + clearTimeout(groupSocketTimeoutRef.current); + } + }, 5000); // Close if no pong in 5 seconds + } + } catch (error) { + console.error('Error during ping:', error); + } + }; + + const initWebsocketMessageGroup = async () => { + forceCloseWebSocket(); // Ensure we close any existing connection + const currentAddress = myAddress; + + try { + const socketLink = `${getBaseApiReactSocket()}/websockets/chat/active/${currentAddress}?encoding=BASE64`; + socketRef.current = new WebSocket(socketLink); + + socketRef.current.onopen = () => { + console.log('WebSocket connection opened'); + setTimeout(pingHeads, 50); // Initial ping + }; + + socketRef.current.onmessage = (e) => { + try { + if (e.data === 'pong') { + clearTimeout(timeoutIdRef.current); + groupSocketTimeoutRef.current = setTimeout(pingHeads, 45000); // Ping every 45 seconds + } else { + const data = JSON.parse(e.data); + const filteredGroups = data.groups?.filter(item => item?.groupId !== 0) || []; + const sortedGroups = filteredGroups.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); + const sortedDirects = (data?.direct || []).filter(item => + item?.name !== 'extension-proxy' && item?.address !== 'QSMMGSgysEuqDCuLw3S4cHrQkBrh3vP3VH' + ).sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); + + + chrome.runtime.sendMessage({ + action: 'handleActiveGroupDataFromSocket', + payload: { + groups: sortedGroups, + directs: sortedDirects, + }, + }); + } + } catch (error) { + console.error('Error parsing onmessage data:', error); + } + }; + + socketRef.current.onclose = (event) => { + clearTimeout(groupSocketTimeoutRef.current); + clearTimeout(timeoutIdRef.current); + console.warn(`WebSocket closed: ${event.reason || 'unknown reason'}`); + if (event.reason !== 'forced' && event.code !== 1000) { + setTimeout(() => initWebsocketMessageGroup(), 10000); // Retry after 10 seconds + } + }; + + socketRef.current.onerror = (error) => { + console.error('WebSocket error:', error); + clearTimeout(groupSocketTimeoutRef.current); + clearTimeout(timeoutIdRef.current); + if (socketRef.current) { + socketRef.current.close(); + } + }; + } catch (error) { + console.error('Error initializing WebSocket:', error); + } + }; + + initWebsocketMessageGroup(); // Initialize WebSocket on component mount + + return () => { + forceCloseWebSocket(); // Clean up WebSocket on component unmount + }; + }, [myAddress]); + + return null; +}; diff --git a/src/components/Snackbar/LoadingSnackbar.tsx b/src/components/Snackbar/LoadingSnackbar.tsx new file mode 100644 index 0000000..c1e43a9 --- /dev/null +++ b/src/components/Snackbar/LoadingSnackbar.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; +import Button from '@mui/material/Button'; +import Snackbar, { SnackbarCloseReason } from '@mui/material/Snackbar'; +import Alert from '@mui/material/Alert'; + +export const LoadingSnackbar = ({open, info}) => { + + return ( +
+ + + {info?.message} + + +
+ ); +} \ No newline at end of file diff --git a/src/components/Snackbar/Snackbar.tsx b/src/components/Snackbar/Snackbar.tsx new file mode 100644 index 0000000..a909487 --- /dev/null +++ b/src/components/Snackbar/Snackbar.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import Button from '@mui/material/Button'; +import Snackbar, { SnackbarCloseReason } from '@mui/material/Snackbar'; +import Alert from '@mui/material/Alert'; + +export const CustomizedSnackbars = ({open, setOpen, info, setInfo}) => { + + + + const handleClose = ( + event?: React.SyntheticEvent | Event, + reason?: SnackbarCloseReason, + ) => { + if (reason === 'clickaway') { + return; + } + + setOpen(false); + setInfo(null) + }; + + return ( +
+ + + {info?.message} + + +
+ ); +} \ No newline at end of file diff --git a/src/components/TaskManager/TaskManger.tsx b/src/components/TaskManager/TaskManger.tsx new file mode 100644 index 0000000..e735fc1 --- /dev/null +++ b/src/components/TaskManager/TaskManger.tsx @@ -0,0 +1,175 @@ +import { List, ListItemButton, ListItemIcon } from "@mui/material"; +import React, { useContext, useEffect, useRef } from "react"; + +import ListItemText from "@mui/material/ListItemText"; +import Collapse from "@mui/material/Collapse"; +import InboxIcon from "@mui/icons-material/MoveToInbox"; + +import ExpandLess from "@mui/icons-material/ExpandLess"; +import ExpandMore from "@mui/icons-material/ExpandMore"; +import StarBorder from "@mui/icons-material/StarBorder"; +import PendingIcon from "@mui/icons-material/Pending"; +import TaskAltIcon from "@mui/icons-material/TaskAlt"; +import { MyContext, getBaseApiReact } from "../../App"; +import { getBaseApi } from "../../background"; + + + +export const TaskManger = ({getUserInfo}) => { + const { txList, setTxList, memberGroups } = useContext(MyContext); + const [open, setOpen] = React.useState(true); + + const handleClick = () => { + setOpen(!open); + }; + + const intervals = useRef({}) + + const getStatus = ({signature}, callback?: any) =>{ + + let stop = false + + const getAnswer = async () => { + const getTx = async () => { + const url = `${getBaseApiReact()}/transactions/signature/${signature}` + const res = await fetch(url) + + return await res.json() + } + + if (!stop) { + stop = true + + try { + const txTransaction = await getTx() + + if (!txTransaction.error && txTransaction.signature) { + await new Promise((res)=> { + setTimeout(() => { + res(null) + }, 300000); + }) + setTxList((prev)=> { + let previousData = [...prev]; + const findTxWithSignature = previousData.findIndex((tx)=> tx.signature === signature) + if(findTxWithSignature !== -1){ + previousData[findTxWithSignature].done = true; + return previousData + } + return previousData + }) + if(callback){ + callback(true) + } + clearInterval(intervals.current[signature]) + + } + } catch (error) { } + + stop = false + } + } + + intervals.current[signature] = setInterval(getAnswer, 120000) + } + + useEffect(() => { + setTxList((prev) => { + let previousData = [...prev]; + memberGroups.forEach((group) => { + const findGroup = txList.findIndex( + (tx) => tx?.type === "joined-group" && tx?.groupId === group.groupId + ); + if (findGroup !== -1 && !previousData[findGroup]?.done ) { + // add notification + previousData[findGroup].done = true; + } + + }); + memberGroups.forEach((group) => { + const findGroup = txList.findIndex( + (tx) => tx?.type === "created-group" && tx?.groupName === group.groupName + ); + if (findGroup !== -1 && !previousData[findGroup]?.done ) { + // add notification + previousData[findGroup].done = true; + } + + }); + prev.forEach((tx, index)=> { + if(tx?.type === "leave-group" && memberGroups.findIndex( + (group) => tx?.groupId === group.groupId + ) === -1){ + previousData[index].done = true; + } + + }) + prev.forEach((tx, index)=> { + + if(tx?.type === "created-common-secret" && tx?.signature && !tx.done){ + if(intervals.current[tx.signature]) return + + getStatus({signature: tx.signature}) + } + + }) + prev.forEach((tx, index)=> { + + if(tx?.type === "joined-group-request" && tx?.signature && !tx.done){ + if(intervals.current[tx.signature]) return + + getStatus({signature: tx.signature}) + } + + }) + prev.forEach((tx, index)=> { + + if(tx?.type === "register-name" && tx?.signature && !tx.done){ + if(intervals.current[tx.signature]) return + + getStatus({signature: tx.signature}, getUserInfo) + } + + }) + + return previousData; + }); + }, [memberGroups, getUserInfo]); + + if (txList?.length === 0 || txList.filter((item) => !item?.done).length === 0) return null; + return ( + + + + {txList.find((item) => !item.done) ? ( + + ) : ( + + )} + + + {open ? : } + + + + {txList.map((item) => { + return ( + + + + + ); + })} + + + + ); +}; diff --git a/src/constants/codes.ts b/src/constants/codes.ts new file mode 100644 index 0000000..9b9224b --- /dev/null +++ b/src/constants/codes.ts @@ -0,0 +1 @@ +export const PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY = "4001" \ No newline at end of file diff --git a/src/constants/forum.ts b/src/constants/forum.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/index.css b/src/index.css index 9f2ef96..853afa4 100644 --- a/src/index.css +++ b/src/index.css @@ -25,6 +25,10 @@ padding: 0px; margin: 0px; box-sizing: border-box !important; + --color-instance : #1E1E20; + --color-instance-popover-bg: #222222; + --Mail-Background: rgba(49, 51, 56, 1); + --new-message-text: black; } body { @@ -52,4 +56,28 @@ body { .image-container:hover .base-image { opacity: 0; +} + +::-webkit-scrollbar-track { + background-color: transparent; +} +::-webkit-scrollbar-track:hover { + background-color: transparent; +} + +::-webkit-scrollbar { + width: 16px; + height: 10px; + background-color: #232428; +} + +::-webkit-scrollbar-thumb { + background-color: white; + border-radius: 8px; + background-clip: content-box; + border: 4px solid transparent; +} + +.group-list::-webkit-scrollbar-thumb:hover { + background-color: whitesmoke; } \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index 3d7150d..e763d2b 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,8 +3,53 @@ import ReactDOM from 'react-dom/client' import App from './App.tsx' import './index.css' +import { ThemeProvider, createTheme } from '@mui/material/styles'; +import { CssBaseline } from '@mui/material'; + +const theme = createTheme({ + palette: { + primary: { + main: '#232428', // Primary color (e.g., used for buttons, headers, etc.) + }, + secondary: { + main: '#232428', // Secondary color + }, + background: { + default: '#27282c', // Default background color + paper: '#1d1d1d', // Paper component background (for dropdowns, dialogs, etc.) + }, + text: { + primary: '#ffffff', // White as the primary text color + secondary: '#b0b0b0', // Light gray for secondary text + disabled: '#808080', // Gray for disabled text + }, + }, + typography: { + fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif', // Font family + h1: { + color: '#ffffff', // White color for h1 elements + }, + h2: { + color: '#ffffff', // White color for h2 elements + }, + body1: { + color: '#ffffff', // Default body text color + }, + body2: { + color: '#b0b0b0', // Lighter text for body2, often used for secondary text + }, + }, +}); + +export default theme; + + ReactDOM.createRoot(document.getElementById('root')!).render( + + + + , ) diff --git a/src/qdn/encryption/group-encryption.ts b/src/qdn/encryption/group-encryption.ts new file mode 100644 index 0000000..a93938f --- /dev/null +++ b/src/qdn/encryption/group-encryption.ts @@ -0,0 +1,266 @@ +// @ts-nocheck + +import Base58 from "../../deps/Base58" +import ed2curve from "../../deps/ed2curve" +import nacl from "../../deps/nacl-fast" + + +export function base64ToUint8Array(base64: string) { + const binaryString = atob(base64) + const len = binaryString.length + const bytes = new Uint8Array(len) + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i) + } + return bytes +} + +export function uint8ArrayToBase64(uint8Array: any) { + const length = uint8Array.length + let binaryString = '' + const chunkSize = 1024 * 1024; // Process 1MB at a time + for (let i = 0; i < length; i += chunkSize) { + const chunkEnd = Math.min(i + chunkSize, length) + const chunk = uint8Array.subarray(i, chunkEnd) + binaryString += Array.from(chunk, byte => String.fromCharCode(byte)).join('') + } + return btoa(binaryString) +} + +export function objectToBase64(obj: Object) { + // Step 1: Convert the object to a JSON string + const jsonString = JSON.stringify(obj) + // Step 2: Create a Blob from the JSON string + const blob = new Blob([jsonString], { type: 'application/json' }) + // Step 3: Create a FileReader to read the Blob as a base64-encoded string + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onloadend = () => { + if (typeof reader.result === 'string') { + // Remove 'data:application/json;base64,' prefix + const base64 = reader.result.replace( + 'data:application/json;base64,', + '' + ) + resolve(base64) + } else { + reject(new Error('Failed to read the Blob as a base64-encoded string')) + } + } + reader.onerror = () => { + reject(reader.error) + } + reader.readAsDataURL(blob) + }) +} + +// Function to create a symmetric key and nonce +export const createSymmetricKeyAndNonce = () => { + const messageKey = new Uint8Array(32); // 32 bytes for the symmetric key + crypto.getRandomValues(messageKey); + + const nonce = new Uint8Array(24); // 24 bytes for the nonce + crypto.getRandomValues(nonce); + + return { messageKey: uint8ArrayToBase64(messageKey), nonce: uint8ArrayToBase64(nonce) }; +}; + + +export const encryptDataGroup = ({ data64, publicKeys, privateKey, userPublicKey }: any) => { + + let combinedPublicKeys = publicKeys + const decodedPrivateKey = Base58.decode(privateKey) + const publicKeysDuplicateFree = [...new Set(combinedPublicKeys)] + + const Uint8ArrayData = base64ToUint8Array(data64) + if (!(Uint8ArrayData instanceof Uint8Array)) { + throw new Error("The Uint8ArrayData you've submitted is invalid") + } + try { + // Generate a random symmetric key for the message. + const messageKey = new Uint8Array(32) + crypto.getRandomValues(messageKey) + const nonce = new Uint8Array(24) + crypto.getRandomValues(nonce) + // Encrypt the data with the symmetric key. + const encryptedData = nacl.secretbox(Uint8ArrayData, nonce, messageKey) + // Generate a keyNonce outside of the loop. + const keyNonce = new Uint8Array(24) + crypto.getRandomValues(keyNonce) + // Encrypt the symmetric key for each recipient. + let encryptedKeys = [] + publicKeysDuplicateFree.forEach((recipientPublicKey) => { + const publicKeyUnit8Array = Base58.decode(recipientPublicKey) + const convertedPrivateKey = ed2curve.convertSecretKey(decodedPrivateKey) + const convertedPublicKey = ed2curve.convertPublicKey(publicKeyUnit8Array) + const sharedSecret = new Uint8Array(32) + + // the length of the sharedSecret will be 32 + 16 + // When you're encrypting data using nacl.secretbox, it's adding an authentication tag to the result, which is 16 bytes long. This tag is used for verifying the integrity and authenticity of the data when it is decrypted + nacl.lowlevel.crypto_scalarmult(sharedSecret, convertedPrivateKey, convertedPublicKey) + + // Encrypt the symmetric key with the shared secret. + const encryptedKey = nacl.secretbox(messageKey, keyNonce, sharedSecret) + + encryptedKeys.push(encryptedKey) + }) + const str = "qortalGroupEncryptedData" + const strEncoder = new TextEncoder() + const strUint8Array = strEncoder.encode(str) + // Convert sender's public key to Uint8Array and add to the message + const senderPublicKeyUint8Array = Base58.decode(userPublicKey) + // Combine all data into a single Uint8Array. + // Calculate size of combinedData + let combinedDataSize = strUint8Array.length + nonce.length + keyNonce.length + senderPublicKeyUint8Array.length + encryptedData.length + 4 + let encryptedKeysSize = 0 + encryptedKeys.forEach((key) => { + encryptedKeysSize += key.length + }) + combinedDataSize += encryptedKeysSize + let combinedData = new Uint8Array(combinedDataSize) + combinedData.set(strUint8Array) + combinedData.set(nonce, strUint8Array.length) + combinedData.set(keyNonce, strUint8Array.length + nonce.length) + combinedData.set(senderPublicKeyUint8Array, strUint8Array.length + nonce.length + keyNonce.length) + combinedData.set(encryptedData, strUint8Array.length + nonce.length + keyNonce.length + senderPublicKeyUint8Array.length) + // Initialize offset for encryptedKeys + let encryptedKeysOffset = strUint8Array.length + nonce.length + keyNonce.length + senderPublicKeyUint8Array.length + encryptedData.length + encryptedKeys.forEach((key) => { + combinedData.set(key, encryptedKeysOffset) + encryptedKeysOffset += key.length + }) + const countArray = new Uint8Array(new Uint32Array([publicKeysDuplicateFree.length]).buffer) + combinedData.set(countArray, combinedData.length - 4) + return uint8ArrayToBase64(combinedData) + } catch (error) { + + throw new Error("Error in encrypting data") + } +} + +export const encryptSingle = async ({ data64,secretKeyObject }: any) => { + + const highestKey = Math.max(...Object.keys(secretKeyObject).filter(item=> !isNaN(+item)).map(Number)); + + + const highestKeyObject = secretKeyObject[highestKey]; + + const Uint8ArrayData = base64ToUint8Array(data64) + const nonce = base64ToUint8Array(highestKeyObject.nonce) + const messageKey = base64ToUint8Array(highestKeyObject.messageKey) + + if (!(Uint8ArrayData instanceof Uint8Array)) { + throw new Error("The Uint8ArrayData you've submitted is invalid") + } + try { + const encryptedData = nacl.secretbox(Uint8ArrayData, nonce, messageKey); + + const encryptedDataBase64 = uint8ArrayToBase64(encryptedData) + const highestKeyStr = highestKey.toString().padStart(10, '0'); // Fixed length of 10 digits + const concatenatedData = highestKeyStr + encryptedDataBase64; + const finalEncryptedData = btoa(concatenatedData); + + + return finalEncryptedData; + } catch (error) { + + throw new Error("Error in encrypting data") + } +} + +export const decryptSingle = async ({ data64, secretKeyObject, skipDecodeBase64 }: any) => { + + + const decodedData = skipDecodeBase64 ? data64 : atob(data64); + + // Extract the key (assuming it's 10 characters long) + const decodeForNumber = atob(decodedData) + const keyStr = decodeForNumber.slice(0, 10); + + // Convert the key string back to a number + const highestKey = parseInt(keyStr, 10); + + // Extract the remaining part as the Base64-encoded encrypted data + const encryptedDataBase64 = decodeForNumber.slice(10); + let _encryptedMessage = encryptedDataBase64 + if(!secretKeyObject[highestKey]) throw new Error('Cannot find correct secretKey') + const nonce64 = secretKeyObject[highestKey].nonce + const messageKey64 = secretKeyObject[highestKey].messageKey + + const Uint8ArrayData = base64ToUint8Array(_encryptedMessage) + const nonce = base64ToUint8Array(nonce64) + const messageKey = base64ToUint8Array(messageKey64) + + if (!(Uint8ArrayData instanceof Uint8Array)) { + throw new Error("The Uint8ArrayData you've submitted is invalid") + } + + + // Decrypt the data using the nonce and messageKey + const decryptedData = nacl.secretbox.open(Uint8ArrayData, nonce, messageKey); + + // Check if decryption was successful + if (!decryptedData) { + throw new Error("Decryption failed"); + } + + // Convert the decrypted Uint8Array back to a UTF-8 string + return uint8ArrayToBase64(decryptedData) + +} + + +export function decryptGroupData(data64EncryptedData: string, privateKey: string) { + + 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 + const senderPublicKey = allCombined.slice(senderPublicKeyStartPosition, senderPublicKeyEndPosition) + // 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) + // Extract the encrypted keys + // 32+16 = 48 + const combinedKeys = allCombined.slice(encryptedDataEndPosition, encryptedDataEndPosition + (count * 48)) + if (!privateKey) { + throw new Error("Unable to retrieve keys") + } + const decodedPrivateKey = Base58.decode(privateKey) + const convertedPrivateKey = ed2curve.convertSecretKey(decodedPrivateKey) + const convertedSenderPublicKey = ed2curve.convertPublicKey(senderPublicKey) + const sharedSecret = new Uint8Array(32) + nacl.lowlevel.crypto_scalarmult(sharedSecret, convertedPrivateKey, convertedSenderPublicKey) + for (let i = 0; i < count; i++) { + const encryptedKey = combinedKeys.slice(i * 48, (i + 1) * 48) + // Decrypt the symmetric key. + const decryptedKey = nacl.secretbox.open(encryptedKey, keyNonce, sharedSecret) + + // If decryption was successful, decryptedKey will not be null. + if (decryptedKey) { + // Decrypt the data using the symmetric key. + const decryptedData = nacl.secretbox.open(encryptedData, nonce, decryptedKey) + // If decryption was successful, decryptedData will not be null. + if (decryptedData) { + return {decryptedData, count} + } + } + } + throw new Error("Unable to decrypt data") +} \ No newline at end of file diff --git a/src/qdn/publish/pubish.ts b/src/qdn/publish/pubish.ts new file mode 100644 index 0000000..f359bba --- /dev/null +++ b/src/qdn/publish/pubish.ts @@ -0,0 +1,267 @@ +// @ts-nocheck + +import { Buffer } from "buffer" +import Base58 from "../../deps/Base58" +import nacl from "../../deps/nacl-fast" +import utils from "../../utils/utils" +import { createEndpoint, getBaseApi } from "../../background"; + +export async function reusableGet(endpoint){ + const validApi = await getBaseApi(); + + const response = await fetch(validApi + endpoint); + const data = await response.json(); + return data + } + + async function reusablePost(endpoint, _body){ + // const validApi = await findUsableApi(); + const url = await createEndpoint(endpoint) + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: _body + }); + let data + try { + data = await response.clone().json() + } catch (e) { + data = await response.text() + } + return data + } + +async function getKeyPair() { + const res = await chrome.storage.local.get(["keyPair"]); + if (res?.keyPair) { + return res.keyPair; + } else { + throw new Error("Wallet not authenticated"); + } + } + +export const publishData = async ({ + registeredName, + file, + service, + identifier, + uploadType, + isBase64, + filename, + withFee, + title, + description, + category, + tag1, + tag2, + tag3, + tag4, + tag5, + feeAmount +}: any) => { + + const validateName = async (receiverName: string) => { + return await reusableGet(`/names/${receiverName}`) + } + + const convertBytesForSigning = async (transactionBytesBase58: string) => { + return await reusablePost('/transactions/convert', transactionBytesBase58) + } + + const getArbitraryFee = async () => { + const timestamp = Date.now() + + let fee = await reusableGet(`/transactions/unitfee?txType=ARBITRARY×tamp=${timestamp}`) + + return { + timestamp, + fee: Number(fee), + feeToShow: (Number(fee) / 1e8).toFixed(8) + } + } + + const signArbitraryWithFee = (arbitraryBytesBase58, arbitraryBytesForSigningBase58, keyPair) => { + if (!arbitraryBytesBase58) { + throw new Error('ArbitraryBytesBase58 not defined') + } + + if (!keyPair) { + throw new Error('keyPair not defined') + } + + const arbitraryBytes = Base58.decode(arbitraryBytesBase58) + const _arbitraryBytesBuffer = Object.keys(arbitraryBytes).map(function (key) { return arbitraryBytes[key]; }) + const arbitraryBytesBuffer = new Uint8Array(_arbitraryBytesBuffer) + const arbitraryBytesForSigning = Base58.decode(arbitraryBytesForSigningBase58) + const _arbitraryBytesForSigningBuffer = Object.keys(arbitraryBytesForSigning).map(function (key) { return arbitraryBytesForSigning[key]; }) + const arbitraryBytesForSigningBuffer = new Uint8Array(_arbitraryBytesForSigningBuffer) + const signature = nacl.sign.detached(arbitraryBytesForSigningBuffer, keyPair.privateKey) + + return utils.appendBuffer(arbitraryBytesBuffer, signature) + } + + const processTransactionVersion2 = async (bytes) => { + + return await reusablePost('/transactions/process?apiVersion=2', Base58.encode(bytes)) + } + + const signAndProcessWithFee = async (transactionBytesBase58: string) => { + let convertedBytesBase58 = await convertBytesForSigning( + transactionBytesBase58 + ) + + if (convertedBytesBase58.error) { + throw new Error('Error when signing') + } + + + const resKeyPair = await getKeyPair() + const parsedData = JSON.parse(resKeyPair) + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey + }; + + let signedArbitraryBytes = signArbitraryWithFee(transactionBytesBase58, convertedBytesBase58, keyPair) + const response = await processTransactionVersion2(signedArbitraryBytes) + + let myResponse = { error: '' } + + if (response === false) { + throw new Error('Error when signing') + } else { + myResponse = response + } + + return myResponse + } + + const validate = async () => { + let validNameRes = await validateName(registeredName) + + if (validNameRes.error) { + throw new Error('Name not found') + } + + let fee = null + + if (withFee && feeAmount) { + fee = feeAmount + } else if (withFee) { + const res = await getArbitraryFee() + + if (res.fee) { + fee = res.fee + } else { + throw new Error('unable to get fee') + } + } + + let transactionBytes = await uploadData(registeredName, file, fee) + + if (transactionBytes.error) { + throw new Error(transactionBytes.message || 'Error when uploading') + } else if (transactionBytes.includes('Error 500 Internal Server Error')) { + throw new Error('Error when uploading') + } + + let signAndProcessRes + + if (withFee) { + signAndProcessRes = await signAndProcessWithFee(transactionBytes) + } + + if (signAndProcessRes?.error) { + throw new Error('Error when signing') + } + + return signAndProcessRes + } + + const uploadData = async (registeredName: string, file:any, fee: number) => { + if (identifier != null && identifier.trim().length > 0) { + let postBody = '' + let urlSuffix = '' + + if (file != null) { + // If we're sending zipped data, make sure to use the /zip version of the POST /arbitrary/* API + if (uploadType === 'zip') { + urlSuffix = '/zip' + } + + // If we're sending file data, use the /base64 version of the POST /arbitrary/* API + else if (uploadType === 'file') { + urlSuffix = '/base64' + } + + // Base64 encode the file to work around compatibility issues between javascript and java byte arrays + if (isBase64) { + postBody = file + } + + if (!isBase64) { + let fileBuffer = new Uint8Array(await file.arrayBuffer()) + postBody = Buffer.from(fileBuffer).toString("base64") + } + + } + + let uploadDataUrl = `/arbitrary/${service}/${registeredName}${urlSuffix}` + + if (identifier.trim().length > 0) { + uploadDataUrl = `/arbitrary/${service}/${registeredName}/${identifier}${urlSuffix}` + } + + uploadDataUrl = uploadDataUrl + `?fee=${fee}` + + + if (filename != null && filename != 'undefined') { + uploadDataUrl = uploadDataUrl + '&filename=' + encodeURIComponent(filename) + } + + if (title != null && title != 'undefined') { + uploadDataUrl = uploadDataUrl + '&title=' + encodeURIComponent(title) + } + + if (description != null && description != 'undefined') { + uploadDataUrl = uploadDataUrl + '&description=' + encodeURIComponent(description) + } + + if (category != null && category != 'undefined') { + uploadDataUrl = uploadDataUrl + '&category=' + encodeURIComponent(category) + } + + if (tag1 != null && tag1 != 'undefined') { + uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag1) + } + + if (tag2 != null && tag2 != 'undefined') { + uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag2) + } + + if (tag3 != null && tag3 != 'undefined') { + uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag3) + } + + if (tag4 != null && tag4 != 'undefined') { + uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag4) + } + + if (tag5 != null && tag5 != 'undefined') { + uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag5) + } + + return await reusablePost(uploadDataUrl, postBody) + } + } + + try { + return await validate() + } catch (error: any) { + throw new Error(error?.message) + } +} \ No newline at end of file diff --git a/src/transactions/AddGroupAdminTransaction.ts b/src/transactions/AddGroupAdminTransaction.ts new file mode 100644 index 0000000..f66f730 --- /dev/null +++ b/src/transactions/AddGroupAdminTransaction.ts @@ -0,0 +1,37 @@ +// @ts-nocheck + +import { QORT_DECIMALS } from "../constants/constants" +import TransactionBase from "./TransactionBase" + + +export default class AddGroupAdminTransaction extends TransactionBase { + constructor() { + super() + this.type = 24 + } + + set rGroupId(rGroupId) { + this._rGroupId = rGroupId + this._rGroupIdBytes = this.constructor.utils.int32ToBytes(this._rGroupId) + } + + set recipient(recipient) { + this._recipient = recipient instanceof Uint8Array ? recipient : this.constructor.Base58.decode(recipient) + this.theRecipient = recipient + } + + set fee(fee) { + this._fee = fee * QORT_DECIMALS + this._feeBytes = this.constructor.utils.int64ToBytes(this._fee) + } + + get params() { + const params = super.params + params.push( + this._rGroupIdBytes, + this._recipient, + this._feeBytes + ) + return params + } +} diff --git a/src/transactions/CancelGroupBanTransaction.ts b/src/transactions/CancelGroupBanTransaction.ts new file mode 100644 index 0000000..d1bbef7 --- /dev/null +++ b/src/transactions/CancelGroupBanTransaction.ts @@ -0,0 +1,37 @@ +// @ts-nocheck + +import { QORT_DECIMALS } from "../constants/constants" +import TransactionBase from "./TransactionBase" + + +export default class CancelGroupBanTransaction extends TransactionBase { + constructor() { + super() + this.type = 27 + } + + set rGroupId(rGroupId) { + this._rGroupId = rGroupId + this._rGroupIdBytes = this.constructor.utils.int32ToBytes(this._rGroupId) + } + + set recipient(recipient) { + this._recipient = recipient instanceof Uint8Array ? recipient : this.constructor.Base58.decode(recipient) + this.theRecipient = recipient + } + + set fee(fee) { + this._fee = fee * QORT_DECIMALS + this._feeBytes = this.constructor.utils.int64ToBytes(this._fee) + } + + get params() { + const params = super.params + params.push( + this._rGroupIdBytes, + this._recipient, + this._feeBytes + ) + return params + } +} diff --git a/src/transactions/CancelGroupInviteTransaction.ts b/src/transactions/CancelGroupInviteTransaction.ts new file mode 100644 index 0000000..b575f3e --- /dev/null +++ b/src/transactions/CancelGroupInviteTransaction.ts @@ -0,0 +1,36 @@ +// @ts-nocheck +import { QORT_DECIMALS } from "../constants/constants" +import TransactionBase from "./TransactionBase" + + +export default class CancelGroupInviteTransaction extends TransactionBase { + constructor() { + super() + this.type = 30 + } + + set rGroupId(rGroupId) { + this._rGroupId = rGroupId + this._rGroupIdBytes = this.constructor.utils.int32ToBytes(this._rGroupId) + } + + set recipient(recipient) { + this._recipient = recipient instanceof Uint8Array ? recipient : this.constructor.Base58.decode(recipient) + this.theRecipient = recipient + } + + set fee(fee) { + this._fee = fee * QORT_DECIMALS + this._feeBytes = this.constructor.utils.int64ToBytes(this._fee) + } + + get params() { + const params = super.params + params.push( + this._rGroupIdBytes, + this._recipient, + this._feeBytes + ) + return params + } +} diff --git a/src/transactions/ChatBase.ts b/src/transactions/ChatBase.ts index 85cbdd9..cdfacaf 100644 --- a/src/transactions/ChatBase.ts +++ b/src/transactions/ChatBase.ts @@ -19,6 +19,7 @@ export default class ChatBase { } constructor() { + this.fee = 0 this.groupID = 0 this.tests = [ @@ -47,6 +48,7 @@ export default class ChatBase { return true }, () => { + if (!(this._lastReference instanceof Uint8Array && this._lastReference.byteLength == 64)) { return 'Invalid last reference: ' + this._lastReference } @@ -93,6 +95,7 @@ export default class ChatBase { } set lastReference(lastReference) { + this._lastReference = lastReference instanceof Uint8Array ? lastReference : this.constructor.Base58.decode(lastReference) } diff --git a/src/transactions/CreateGroupTransaction.ts b/src/transactions/CreateGroupTransaction.ts new file mode 100644 index 0000000..a2eb7ea --- /dev/null +++ b/src/transactions/CreateGroupTransaction.ts @@ -0,0 +1,64 @@ +// @ts-nocheck + +import { QORT_DECIMALS } from "../constants/constants" +import TransactionBase from "./TransactionBase" + + +export default class CreateGroupTransaction extends TransactionBase { + constructor() { + super() + this.type = 22 + } + + + set fee(fee) { + this._fee = fee * QORT_DECIMALS + this._feeBytes = this.constructor.utils.int64ToBytes(this._fee) + } + + set rGroupName(rGroupName) { + this._rGroupName = rGroupName + this._rGroupNameBytes = this.constructor.utils.stringtoUTF8Array(this._rGroupName) + this._rGroupNameLength = this.constructor.utils.int32ToBytes(this._rGroupNameBytes.length) + } + + set rGroupDesc(rGroupDesc) { + this._rGroupDesc = rGroupDesc + this._rGroupDescBytes = this.constructor.utils.stringtoUTF8Array(this._rGroupDesc) + this._rGroupDescLength = this.constructor.utils.int32ToBytes(this._rGroupDescBytes.length) + } + + set rGroupType(rGroupType) { + this._rGroupType = new Uint8Array(1) + this._rGroupType[0] = rGroupType + } + + set rGroupApprovalThreshold(rGroupApprovalThreshold) { + this._rGroupApprovalThreshold = new Uint8Array(1) + this._rGroupApprovalThreshold[0] = rGroupApprovalThreshold + } + + set rGroupMinimumBlockDelay(rGroupMinimumBlockDelay) { + this._rGroupMinimumBlockDelayBytes = this.constructor.utils.int32ToBytes(rGroupMinimumBlockDelay) + } + + set rGroupMaximumBlockDelay(rGroupMaximumBlockDelay) { + this._rGroupMaximumBlockDelayBytes = this.constructor.utils.int32ToBytes(rGroupMaximumBlockDelay) + } + + get params() { + const params = super.params + params.push( + this._rGroupNameLength, + this._rGroupNameBytes, + this._rGroupDescLength, + this._rGroupDescBytes, + this._rGroupType, + this._rGroupApprovalThreshold, + this._rGroupMinimumBlockDelayBytes, + this._rGroupMaximumBlockDelayBytes, + this._feeBytes + ) + return params + } +} diff --git a/src/transactions/GroupBanTransaction.ts b/src/transactions/GroupBanTransaction.ts new file mode 100644 index 0000000..3eb3210 --- /dev/null +++ b/src/transactions/GroupBanTransaction.ts @@ -0,0 +1,50 @@ +// @ts-nocheck + +import { QORT_DECIMALS } from "../constants/constants" +import TransactionBase from "./TransactionBase" + +export default class GroupBanTransaction extends TransactionBase { + constructor() { + super() + this.type = 26 + } + + set rGroupId(rGroupId) { + this._rGroupId = rGroupId + this._rGroupIdBytes = this.constructor.utils.int32ToBytes(this._rGroupId) + } + + set rBanReason(rBanReason) { + this._rBanReason = rBanReason + this._rBanReasonBytes = this.constructor.utils.stringtoUTF8Array(this._rBanReason) + this._rBanReasonLength = this.constructor.utils.int32ToBytes(this._rBanReasonBytes.length) + } + + set rBanTime(rBanTime) { + this._rBanTime = rBanTime + this._rBanTimeBytes = this.constructor.utils.int32ToBytes(this._rBanTime) + } + + set recipient(recipient) { + this._recipient = recipient instanceof Uint8Array ? recipient : this.constructor.Base58.decode(recipient) + this.theRecipient = recipient + } + + set fee(fee) { + this._fee = fee * QORT_DECIMALS + this._feeBytes = this.constructor.utils.int64ToBytes(this._fee) + } + + get params() { + const params = super.params + params.push( + this._rGroupIdBytes, + this._recipient, + this._rBanReasonLength, + this._rBanReasonBytes, + this._rBanTimeBytes, + this._feeBytes + ) + return params + } +} diff --git a/src/transactions/GroupChatTransaction.ts b/src/transactions/GroupChatTransaction.ts new file mode 100644 index 0000000..1cd9d9b --- /dev/null +++ b/src/transactions/GroupChatTransaction.ts @@ -0,0 +1,72 @@ +// @ts-nocheck + + +import ChatBase from './ChatBase' +import { CHAT_REFERENCE_FEATURE_TRIGGER_TIMESTAMP } from '../constants/constants' + +export default class GroupChatTransaction extends ChatBase { + constructor() { + super(); + this.type = 18 + this.fee = 0 + } + + set proofOfWorkNonce(proofOfWorkNonce) { + this._proofOfWorkNonce = this.constructor.utils.int32ToBytes(proofOfWorkNonce) + } + + set hasReceipient(hasReceipient) { + this._hasReceipient = new Uint8Array(1) + this._hasReceipient[0] = hasReceipient + } + + set message(message) { + this.messageText = message + + this._message = this.constructor.utils.stringtoUTF8Array(message) + this._messageLength = this.constructor.utils.int32ToBytes(this._message.length) + } + + set hasChatReference(hasChatReference) { + this._hasChatReference = new Uint8Array(1) + this._hasChatReference[0] = hasChatReference + } + + set chatReference(chatReference) { + this._chatReference = chatReference instanceof Uint8Array ? chatReference : this.constructor.Base58.decode(chatReference) + } + + set isEncrypted(isEncrypted) { + this._isEncrypted = new Uint8Array(1) + this._isEncrypted[0] = isEncrypted + } + + set isText(isText) { + this._isText = new Uint8Array(1) + this._isText[0] = isText + } + + get params() { + const params = super.params + params.push( + this._proofOfWorkNonce, + this._hasReceipient, + this._messageLength, + this._message, + this._isEncrypted, + this._isText, + this._feeBytes + ) + + // After the feature trigger timestamp we need to include chat reference + if (new Date(this._timestamp).getTime() >= CHAT_REFERENCE_FEATURE_TRIGGER_TIMESTAMP) { + params.push(this._hasChatReference) + + if (this._hasChatReference[0] == 1) { + params.push(this._chatReference) + } + } + + return params + } +} diff --git a/src/transactions/GroupInviteTransaction.ts b/src/transactions/GroupInviteTransaction.ts new file mode 100644 index 0000000..3cbf978 --- /dev/null +++ b/src/transactions/GroupInviteTransaction.ts @@ -0,0 +1,42 @@ +// @ts-nocheck + +import { QORT_DECIMALS } from "../constants/constants" +import TransactionBase from "./TransactionBase" + +export default class GroupInviteTransaction extends TransactionBase { + constructor() { + super() + this.type = 29 + } + + set rGroupId(rGroupId) { + this._rGroupId = rGroupId + this._rGroupIdBytes = this.constructor.utils.int32ToBytes(this._rGroupId) + } + + set rInviteTime(rInviteTime) { + this._rInviteTime = rInviteTime + this._rInviteTimeBytes = this.constructor.utils.int32ToBytes(this._rInviteTime) + } + + set recipient(recipient) { + this._recipient = recipient instanceof Uint8Array ? recipient : this.constructor.Base58.decode(recipient) + this.theRecipient = recipient + } + + set fee(fee) { + this._fee = fee * QORT_DECIMALS + this._feeBytes = this.constructor.utils.int64ToBytes(this._fee) + } + + get params() { + const params = super.params + params.push( + this._rGroupIdBytes, + this._recipient, + this._rInviteTimeBytes, + this._feeBytes + ) + return params + } +} diff --git a/src/transactions/GroupKickTransaction.ts b/src/transactions/GroupKickTransaction.ts new file mode 100644 index 0000000..969380f --- /dev/null +++ b/src/transactions/GroupKickTransaction.ts @@ -0,0 +1,46 @@ +// @ts-nocheck + + +import { QORT_DECIMALS } from "../constants/constants" +import TransactionBase from "./TransactionBase" + + +export default class GroupKickTransaction extends TransactionBase { + constructor() { + super() + this.type = 28 + } + + set rGroupId(rGroupId) { + this._rGroupId = rGroupId + this._rGroupIdBytes = this.constructor.utils.int32ToBytes(this._rGroupId) + } + + set rBanReason(rBanReason) { + this._rBanReason = rBanReason + this._rBanReasonBytes = this.constructor.utils.stringtoUTF8Array(this._rBanReason) + this._rBanReasonLength = this.constructor.utils.int32ToBytes(this._rBanReasonBytes.length) + } + + set recipient(recipient) { + this._recipient = recipient instanceof Uint8Array ? recipient : this.constructor.Base58.decode(recipient) + this.theRecipient = recipient + } + + set fee(fee) { + this._fee = fee * QORT_DECIMALS + this._feeBytes = this.constructor.utils.int64ToBytes(this._fee) + } + + get params() { + const params = super.params + params.push( + this._rGroupIdBytes, + this._recipient, + this._rBanReasonLength, + this._rBanReasonBytes, + this._feeBytes + ) + return params + } +} diff --git a/src/transactions/JoinGroupTransaction.ts b/src/transactions/JoinGroupTransaction.ts new file mode 100644 index 0000000..0aae8f7 --- /dev/null +++ b/src/transactions/JoinGroupTransaction.ts @@ -0,0 +1,38 @@ +// @ts-nocheck + +import { QORT_DECIMALS } from "../constants/constants" +import TransactionBase from "./TransactionBase" + + +export default class JoinGroupTransaction extends TransactionBase { + constructor() { + super() + this.type = 31 + } + + + + set fee(fee) { + this._fee = fee * QORT_DECIMALS + this._feeBytes = this.constructor.utils.int64ToBytes(this._fee) + } + + set registrantAddress(registrantAddress) { + this._registrantAddress = registrantAddress instanceof Uint8Array ? registrantAddress : this.constructor.Base58.decode(registrantAddress) + } + + set rGroupId(rGroupId) { + this._rGroupId = rGroupId + this._rGroupIdBytes = this.constructor.utils.int32ToBytes(this._rGroupId) + } + + + get params() { + const params = super.params + params.push( + this._rGroupIdBytes, + this._feeBytes + ) + return params + } +} diff --git a/src/transactions/LeaveGroupTransaction.ts b/src/transactions/LeaveGroupTransaction.ts new file mode 100644 index 0000000..222f250 --- /dev/null +++ b/src/transactions/LeaveGroupTransaction.ts @@ -0,0 +1,35 @@ +// @ts-nocheck + +import { QORT_DECIMALS } from "../constants/constants" +import TransactionBase from "./TransactionBase" + + +export default class LeaveGroupTransaction extends TransactionBase { + constructor() { + super() + this.type = 32 + } + + set fee(fee) { + this._fee = fee * QORT_DECIMALS + this._feeBytes = this.constructor.utils.int64ToBytes(this._fee) + } + + set registrantAddress(registrantAddress) { + this._registrantAddress = registrantAddress instanceof Uint8Array ? registrantAddress : this.constructor.Base58.decode(registrantAddress) + } + + set rGroupId(rGroupId) { + this._rGroupId = rGroupId + this._rGroupIdBytes = this.constructor.utils.int32ToBytes(this._rGroupId) + } + + get params() { + const params = super.params + params.push( + this._rGroupIdBytes, + this._feeBytes + ) + return params + } +} diff --git a/src/transactions/RegisterNameTransaction.ts b/src/transactions/RegisterNameTransaction.ts new file mode 100644 index 0000000..53046ee --- /dev/null +++ b/src/transactions/RegisterNameTransaction.ts @@ -0,0 +1,42 @@ +// @ts-nocheck + +import { QORT_DECIMALS } from "../constants/constants" +import TransactionBase from "./TransactionBase" + + +export default class RegisterNameTransaction extends TransactionBase { + constructor() { + super() + this.type = 3 + } + + + set fee(fee) { + this._fee = fee * QORT_DECIMALS + this._feeBytes = this.constructor.utils.int64ToBytes(this._fee) + } + + set name(name) { + this.nameText = name + this._nameBytes = this.constructor.utils.stringtoUTF8Array(name) + this._nameLength = this.constructor.utils.int32ToBytes(this._nameBytes.length) + } + + set value(value) { + this.valueText = value.length === 0 ? "Registered Name on the Qortal Chain" : value + this._valueBytes = this.constructor.utils.stringtoUTF8Array(this.valueText) + this._valueLength = this.constructor.utils.int32ToBytes(this._valueBytes.length) + } + + get params() { + const params = super.params + params.push( + this._nameLength, + this._nameBytes, + this._valueLength, + this._valueBytes, + this._feeBytes + ) + return params + } +} diff --git a/src/transactions/RemoveGroupAdminTransaction.ts b/src/transactions/RemoveGroupAdminTransaction.ts new file mode 100644 index 0000000..3392f79 --- /dev/null +++ b/src/transactions/RemoveGroupAdminTransaction.ts @@ -0,0 +1,38 @@ +// @ts-nocheck + +import { QORT_DECIMALS } from "../constants/constants" +import TransactionBase from "./TransactionBase" + +export default class RemoveGroupAdminTransaction extends TransactionBase { + constructor() { + super() + this.type = 25 + } + + + + set rGroupId(rGroupId) { + this._rGroupId = rGroupId + this._rGroupIdBytes = this.constructor.utils.int32ToBytes(this._rGroupId) + } + + set recipient(recipient) { + this._recipient = recipient instanceof Uint8Array ? recipient : this.constructor.Base58.decode(recipient) + this.theRecipient = recipient + } + + set fee(fee) { + this._fee = fee * QORT_DECIMALS + this._feeBytes = this.constructor.utils.int64ToBytes(this._fee) + } + + get params() { + const params = super.params + params.push( + this._rGroupIdBytes, + this._recipient, + this._feeBytes + ) + return params + } +} diff --git a/src/transactions/transactions.ts b/src/transactions/transactions.ts index b2af068..25ddbf5 100644 --- a/src/transactions/transactions.ts +++ b/src/transactions/transactions.ts @@ -2,18 +2,45 @@ import PaymentTransaction from './PaymentTransaction.js' import ChatTransaction from './ChatTransaction.js' +import GroupChatTransaction from './GroupChatTransaction.js' +import GroupInviteTransaction from './GroupInviteTransaction.js' +import CancelGroupInviteTransaction from './CancelGroupInviteTransaction.js' +import GroupKickTransaction from './GroupKickTransaction.js' +import GroupBanTransaction from './GroupBanTransaction.js' +import CancelGroupBanTransaction from './CancelGroupBanTransaction.js' +import CreateGroupTransaction from './CreateGroupTransaction.js' +import LeaveGroupTransaction from './LeaveGroupTransaction.js' +import JoinGroupTransaction from './JoinGroupTransaction.js' +import AddGroupAdminTransaction from './AddGroupAdminTransaction.js' +import RemoveGroupAdminTransaction from './RemoveGroupAdminTransaction.js' +import RegisterNameTransaction from './RegisterNameTransaction.js' export const transactionTypes = { + 3: RegisterNameTransaction, 2: PaymentTransaction, - 18: ChatTransaction + 18: ChatTransaction, + 181: GroupChatTransaction, + 22: CreateGroupTransaction, + 24: AddGroupAdminTransaction, + 25: RemoveGroupAdminTransaction, + 26: GroupBanTransaction, + 27: CancelGroupBanTransaction, + 28: GroupKickTransaction, + 29: GroupInviteTransaction, + 30: CancelGroupInviteTransaction, + 31: JoinGroupTransaction, + 32: LeaveGroupTransaction } export const createTransaction = (type, keyPair, params) => { + const tx = new transactionTypes[type]() + tx.keyPair = keyPair Object.keys(params).forEach(param => { + tx[param] = params[param] }) diff --git a/src/utils/Size/index.ts b/src/utils/Size/index.ts new file mode 100644 index 0000000..ac6cb39 --- /dev/null +++ b/src/utils/Size/index.ts @@ -0,0 +1,11 @@ +export function formatBytes(bytes: number, decimals = 2) { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; + } \ No newline at end of file diff --git a/src/utils/decryptChatMessage.ts b/src/utils/decryptChatMessage.ts index bb8a104..c50bb45 100644 --- a/src/utils/decryptChatMessage.ts +++ b/src/utils/decryptChatMessage.ts @@ -8,7 +8,15 @@ import {Sha256} from 'asmcrypto.js' export const decryptChatMessage = (encryptedMessage, privateKey, recipientPublicKey, lastReference) => { const test = encryptedMessage - let _encryptedMessage = Base58.decode(encryptedMessage) + let _encryptedMessage = atob(encryptedMessage) + const binaryLength = _encryptedMessage.length + const bytes = new Uint8Array(binaryLength) + + for (let i = 0; i < binaryLength; i++) { + bytes[i] = _encryptedMessage.charCodeAt(i) + } + + const _base58RecipientPublicKey = recipientPublicKey instanceof Uint8Array ? Base58.encode(recipientPublicKey) : recipientPublicKey const _recipientPublicKey = Base58.decode(_base58RecipientPublicKey) @@ -20,10 +28,11 @@ export const decryptChatMessage = (encryptedMessage, privateKey, recipientPublic nacl.lowlevel.crypto_scalarmult(sharedSecret, convertedPrivateKey, convertedPublicKey) const _chatEncryptionSeed = new Sha256().process(sharedSecret).finish().result - const _decryptedMessage = nacl.secretbox.open(_encryptedMessage, _lastReference.slice(0, 24), _chatEncryptionSeed) + const _decryptedMessage = nacl.secretbox.open(bytes, _lastReference.slice(0, 24), _chatEncryptionSeed) let decryptedMessage = '' _decryptedMessage === false ? decryptedMessage : decryptedMessage = new TextDecoder('utf-8').decode(_decryptedMessage) + return decryptedMessage } \ No newline at end of file diff --git a/src/utils/events.ts b/src/utils/events.ts new file mode 100644 index 0000000..94d1c31 --- /dev/null +++ b/src/utils/events.ts @@ -0,0 +1,11 @@ +export const executeEvent = (eventName: string, data: any)=> { + const event = new CustomEvent(eventName, {detail: data}) + document.dispatchEvent(event) +} +export const subscribeToEvent = (eventName: string, listener: any)=> { + document.addEventListener(eventName, listener) +} + +export const unsubscribeFromEvent = (eventName: string, listener: any)=> { + document.removeEventListener(eventName, listener) +} \ No newline at end of file diff --git a/src/utils/generateWallet/generateWallet.ts b/src/utils/generateWallet/generateWallet.ts index 678b983..7182d99 100644 --- a/src/utils/generateWallet/generateWallet.ts +++ b/src/utils/generateWallet/generateWallet.ts @@ -92,7 +92,7 @@ export const createAccount = async()=> { saveAs(blob, fileName); } catch (error) { - console.log({ error }); + if (error.name === 'AbortError') { return; } diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts new file mode 100644 index 0000000..ff7997c --- /dev/null +++ b/src/utils/helpers.ts @@ -0,0 +1,34 @@ +import moment from "moment"; + +export const delay = (time: number) => new Promise((_, reject) => + setTimeout(() => reject(new Error('Request timed out')), time) +); + +// const originalHtml = `

---------- Forwarded message ---------

From: Alex

Date: Mon, Jun 9 2014 9:32 PM

Subject: Batteries

To: Jessica



`; + + +// export function updateMessageDetails(newFrom: string, newDateMillis: number, newTo: string) { +// let htmlString = originalHtml +// // Use Moment.js to format the date from milliseconds +// const formattedDate = moment(newDateMillis).format('ddd, MMM D YYYY h:mm A'); + +// // Replace the From, Date, and To fields in the HTML string +// htmlString = htmlString.replace(/

From:.*?<\/p>/, `

From: ${newFrom}

`); +// htmlString = htmlString.replace(/

Date:.*?<\/p>/, `

Date: ${formattedDate}

`); +// htmlString = htmlString.replace(/

To:.*?<\/p>/, `

To: ${newTo}

`); + +// return htmlString; +// } + +const originalHtml = `

---------- Forwarded message ---------

From: Alex

Subject: Batteries

To: Jessica



`; + + +export function updateMessageDetails(newFrom: string, newSubject: string, newTo: string) { + let htmlString = originalHtml + + htmlString = htmlString.replace(/

From:.*?<\/p>/, `

From: ${newFrom}

`); + htmlString = htmlString.replace(/

Subject:.*?<\/p>/, `

Subject: ${newSubject}

`); + htmlString = htmlString.replace(/

To:.*?<\/p>/, `

To: ${newTo}

`); + + return htmlString; +} \ No newline at end of file diff --git a/src/utils/qortalLink/index.ts b/src/utils/qortalLink/index.ts new file mode 100644 index 0000000..bc2f132 --- /dev/null +++ b/src/utils/qortalLink/index.ts @@ -0,0 +1,12 @@ +export function convertQortalLinks(inputHtml: string) { + // Regular expression to match 'qortal://...' URLs. + // This will stop at the first whitespace, comma, or HTML tag + var regex = /(qortal:\/\/[^\s,<]+)/g; + + // Replace matches in inputHtml with formatted anchor tag + var outputHtml = inputHtml.replace(regex, function (match) { + return `${match}`; + }); + + return outputHtml; +} \ No newline at end of file diff --git a/src/utils/queue/queue.ts b/src/utils/queue/queue.ts new file mode 100644 index 0000000..fcbd544 --- /dev/null +++ b/src/utils/queue/queue.ts @@ -0,0 +1,56 @@ +export class RequestQueueWithPromise { + constructor(maxConcurrent = 5) { + this.queue = []; + this.maxConcurrent = maxConcurrent; + this.currentlyProcessing = 0; + this.isPaused = false; // Flag to track whether the queue is paused + } + + // Add a request to the queue and return a promise + enqueue(request) { + return new Promise((resolve, reject) => { + // Push the request and its resolve and reject callbacks to the queue + this.queue.push({ request, resolve, reject }); + this.process(); + }); + } + + // Process requests in the queue + async process() { + // Process requests only if the queue is not paused + if (this.isPaused) return; + + while (this.queue.length > 0 && this.currentlyProcessing < this.maxConcurrent) { + this.currentlyProcessing++; + + const { request, resolve, reject } = this.queue.shift(); + + try { + const response = await request(); + resolve(response); + } catch (error) { + reject(error); + } finally { + this.currentlyProcessing--; + await this.process(); + } + } + } + + // Pause the queue processing + pause() { + this.isPaused = true; + } + + // Resume the queue processing + resume() { + this.isPaused = false; + this.process(); // Continue processing when resumed + } + + // Clear pending requests in the queue + clear() { + this.queue.length = 0; + } + } + \ No newline at end of file diff --git a/src/utils/time.ts b/src/utils/time.ts new file mode 100644 index 0000000..77a0daf --- /dev/null +++ b/src/utils/time.ts @@ -0,0 +1,38 @@ +import moment from "moment" + +export function formatTimestamp(timestamp: number): string { + const now = moment() + const timestampMoment = moment(timestamp) + const elapsedTime = now.diff(timestampMoment, 'minutes') + + if (elapsedTime < 1) { + return 'Just now' + } else if (elapsedTime < 60) { + return `${elapsedTime}m ago` + } else if (elapsedTime < 1440) { + return `${Math.floor(elapsedTime / 60)}h ago` + } else { + return timestampMoment.format('MMM D') + } + } + export function formatTimestampForum(timestamp: number): string { + const now = moment(); + const timestampMoment = moment(timestamp); + const elapsedTime = now.diff(timestampMoment, 'minutes'); + + if (elapsedTime < 1) { + return `Just now - ${timestampMoment.format('h:mm A')}`; + } else if (elapsedTime < 60) { + return `${elapsedTime}m ago - ${timestampMoment.format('h:mm A')}`; + } else if (elapsedTime < 1440) { + return `${Math.floor(elapsedTime / 60)}h ago - ${timestampMoment.format('h:mm A')}`; + } else { + return timestampMoment.format('MMM D, YYYY - h:mm A'); + } +} + + export const formatDate = (unixTimestamp: number): string => { + const date = moment(unixTimestamp, 'x').fromNow() + + return date + } \ No newline at end of file