diff --git a/.eslintrc.cjs b/.eslintrc.cjs index d6c9537..92d403a 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -11,7 +11,7 @@ module.exports = { plugins: ['react-refresh'], rules: { 'react-refresh/only-export-components': [ - 'warn', + 'off', { allowConstantExport: true }, ], }, diff --git a/.gitignore b/.gitignore index e7e5b37..e494b90 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ dist-ssr *.sln *.sw? release-builds/ +.env \ No newline at end of file diff --git a/README.md b/README.md index 0d6babe..25f4e90 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,21 @@ -# React + TypeScript + Vite +# Qortal Hub - Desktop Interface for Qortal -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +Qortal Hub is the newest interface for Qortal, part of the 'Qortal Trifecta' series of new User Interfaces for the platform/network. -Currently, two official plugins are available: +It is likely that Qortal Hub will become the new 'primary interface' for Qortal, and that the primary development focus surrounding Qortal Interface development, will be focused here instead of the previous 'qortal-ui' repo. -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh +## Qortal Hub - Next-Level Secure Communications and More -## Expanding the ESLint configuration +Qortal Hub came along with the new Group Encryption methodologies applied, which provide **encrypted chat in Q-Chat for private groups.** Qortal Hub was the first to implement the new method of group encryption, which allows new users to see previously published data, unlike the previous group encryption methodology of things like 'threads' in Q-Mail. -If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: +Allowing new users to view older messages also comes along with a massive boost to the usability of the group encryption, and as such has been leveraged in multiple places inside Qortal Hub, Qortal Extension, and Qortal Go. -- Configure the top-level `parserOptions` property like this: +## Ease of Use Expanded + +Qortal Hub has a focus on ease of use for new users. Providing both the ability to utlilize Qortal without needing to run a local node (though running a local node is still the recommended method to access Qortal), and multiple built-in (QDN-published) walk-thru videos (by Qortal Justin) that explain the various basics of any given section of the application. This allows new users to 'jump right in' to utilizing Qortal Hub, and Qortal overall, in a much more streamlined fashion than that which was previously required by the 'legacy UI' (qortal-ui). + +Leveraging a redundant set of publicly accessible nodes provided by crowetic, Qortal Hub, Qortal Go, and Qortal Extension, all allow the use of Qortal without running a node, making it very simple to 'install and go' and start making use of the extensive functionality provided within the Qortal Ecosystem. + +Many additional details and a fully featured wiki will be created over time. Reach out on the chat on https://qortal.dev or in any of the community locations for Qortal, if you have any issues. Thank you! -```js -export default { - // other rules... - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - project: ['./tsconfig.json', './tsconfig.node.json'], - tsconfigRootDir: __dirname, - }, -} -``` -- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` -- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` -- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/capacitor.config.ts b/capacitor.config.ts index d64b8cd..c7f641f 100644 --- a/capacitor.config.ts +++ b/capacitor.config.ts @@ -1,8 +1,8 @@ import type { CapacitorConfig } from '@capacitor/cli'; const config: CapacitorConfig = { - appId: 'com.example.app', - appName: 'Qortal ', + appId: 'org.Qortal.Qortal-Hub', + appName: 'Qortal-Hub', webDir: 'dist', "plugins": { "LocalNotifications": { diff --git a/electron/assets/mac/appIcon.icns b/electron/assets/mac/appIcon.icns new file mode 100644 index 0000000..5c1296e Binary files /dev/null and b/electron/assets/mac/appIcon.icns differ diff --git a/electron/assets/png/1024x1024.png b/electron/assets/png/1024x1024.png new file mode 100644 index 0000000..94e2fa3 Binary files /dev/null and b/electron/assets/png/1024x1024.png differ diff --git a/electron/assets/png/128x128.png b/electron/assets/png/128x128.png new file mode 100644 index 0000000..ff31fee Binary files /dev/null and b/electron/assets/png/128x128.png differ diff --git a/electron/assets/png/16x16.png b/electron/assets/png/16x16.png new file mode 100644 index 0000000..34dbb9b Binary files /dev/null and b/electron/assets/png/16x16.png differ diff --git a/electron/assets/png/24x24.png b/electron/assets/png/24x24.png new file mode 100644 index 0000000..f4870a9 Binary files /dev/null and b/electron/assets/png/24x24.png differ diff --git a/electron/assets/png/256x256.png b/electron/assets/png/256x256.png new file mode 100644 index 0000000..11e8b5c Binary files /dev/null and b/electron/assets/png/256x256.png differ diff --git a/electron/assets/png/32x32.png b/electron/assets/png/32x32.png new file mode 100644 index 0000000..50256a2 Binary files /dev/null and b/electron/assets/png/32x32.png differ diff --git a/electron/assets/png/48x48.png b/electron/assets/png/48x48.png new file mode 100644 index 0000000..f7d98a4 Binary files /dev/null and b/electron/assets/png/48x48.png differ diff --git a/electron/assets/png/512x512.png b/electron/assets/png/512x512.png new file mode 100644 index 0000000..9ad9dcb Binary files /dev/null and b/electron/assets/png/512x512.png differ diff --git a/electron/assets/png/64x64.png b/electron/assets/png/64x64.png new file mode 100644 index 0000000..96c9fdf Binary files /dev/null and b/electron/assets/png/64x64.png differ diff --git a/electron/assets/png/96x96.png b/electron/assets/png/96x96.png new file mode 100644 index 0000000..8776f6e Binary files /dev/null and b/electron/assets/png/96x96.png differ diff --git a/electron/buildmac/entitlements.mac.plist b/electron/buildmac/entitlements.mac.plist new file mode 100644 index 0000000..d6b93bc --- /dev/null +++ b/electron/buildmac/entitlements.mac.plist @@ -0,0 +1,8 @@ + + + + + com.apple.security.cs.allow-unsigned-executable-memory + + + diff --git a/electron/buildmac/logo-hub.png b/electron/buildmac/logo-hub.png new file mode 100644 index 0000000..c9e096c Binary files /dev/null and b/electron/buildmac/logo-hub.png differ diff --git a/electron/capacitor.config.ts b/electron/capacitor.config.ts index d64b8cd..c7f641f 100644 --- a/electron/capacitor.config.ts +++ b/electron/capacitor.config.ts @@ -1,8 +1,8 @@ import type { CapacitorConfig } from '@capacitor/cli'; const config: CapacitorConfig = { - appId: 'com.example.app', - appName: 'Qortal ', + appId: 'org.Qortal.Qortal-Hub', + appName: 'Qortal-Hub', webDir: 'dist', "plugins": { "LocalNotifications": { diff --git a/electron/electron-builder.config.arm.json b/electron/electron-builder.config.arm.json new file mode 100644 index 0000000..1490123 --- /dev/null +++ b/electron/electron-builder.config.arm.json @@ -0,0 +1,47 @@ +{ + "appId": "org.qortal.Qortal-Hub", + "productName": "Qortal Hub", + "copyright": "Copyright © 2021 - 2025 Qortal", + "compression": "normal", + "asar": "true", + "afterPack": "scripts/afterPack.js", + "files": [ + "assets/**/*", + "build/**/*", + "capacitor.config.*", + "app/**/*", + "scripts/**/*" + ], + "linux": { + "target": [ + "AppImage", + "deb" + ], + "category": "Network", + "packageCategory": "Network", + "desktop": { + "StartupWMClass": "qortal-hub" + }, + "executableName": "Qortal Hub", + "icon": "assets/png" + }, + "appImage": { + "artifactName": "Qortal-Hub-arm64_${version}.${ext}" + }, + "deb": { + "artifactName": "Qortal-Hub-Setup-arm64_${version}.${ext}", + "synopsis": "Qortal Hub for Linux", + }, + "directories": { + "output": "dist", + "buildResources": "resources" + }, + "publish": [ + { + "provider": "github", + "owner": "Qortal", + "repo": "Qortal-Hub", + "releaseType": "draft" + } + ] +} diff --git a/electron/electron-builder.config.json b/electron/electron-builder.config.json index 055d483..6f3bf7c 100644 --- a/electron/electron-builder.config.json +++ b/electron/electron-builder.config.json @@ -1,14 +1,9 @@ { - "appId": "com.github.Qortal.Qortal-Hub", + "appId": "org.Qortal.Qortal-Hub", "directories": { "buildResources": "resources" }, - "files": [ - "assets/**/*", - "build/**/*", - "capacitor.config.*", - "app/**/*" - ], + "files": ["assets/**/*", "build/**/*", "capacitor.config.*", "app/**/*"], "nsis": { "allowElevation": true, "oneClick": false, @@ -17,23 +12,43 @@ "publish": [ { "provider": "github", - "owner": "Qortal", - "repo": "Qortal-Hub", + "owner": "Qortal", + "repo": "Qortal-Hub", "releaseType": "draft" } ], "win": { - "target": "nsis", - "icon": "assets/appIcon.ico" + "target": ["nsis", "portable"], + "icon": "assets/appIcon.ico", + "artifactName": "Qortal-Hub-Setup_${version}.exe", }, "linux": { "target": ["AppImage"], - "category": "Utility", - "executableName": "Qortal", - "icon": "assets/qortal.png" + "category": "Network", + "packageCategory": "Network", + "desktop": { + "StartupWMClass": "qortal-hub" + }, + "executableName": "Qortal-Hub", + "icon": "assets/png", + "asar": true + }, + "deb": { + "artifactName": "Qortal-Hub-Setup_${version}.${ext}", + "synopsis": "Qortal Hub for Linux" + }, + "appImage": { + "artifactName": "Qortal-Hub.${ext}" + }, + + "snap": { + "artifactName": "Qortal-Hub-Setup_${version}.${ext}", + "synopsis": "Qortal Hub for Linux" }, "mac": { - "category": "your.app.category.type", - "target": "dmg" - } + "icon": "assets/mac/appIcon.icns", + "category": "public.app-category.utilities", + "target": ["dmg"] + }, + "productName": "Qortal Hub" } diff --git a/electron/electron-builder.config.lin.json b/electron/electron-builder.config.lin.json new file mode 100644 index 0000000..863557d --- /dev/null +++ b/electron/electron-builder.config.lin.json @@ -0,0 +1,58 @@ +{ + "appId": "org.qortal.Qortal-Hub", + "productName": "Qortal Hub", + "copyright": "Copyright © 2021 - 2025 Qortal", + "compression": "normal", + "asar": "true", + "afterPack": "scripts/afterPack.js", + "files": [ + "assets/**/*", + "build/**/*", + "capacitor.config.*", + "app/**/*", + "scripts/**/*" + ], + "linux": { + "target": [ + "AppImage", + "deb", + "snap", + "rpm" + ], + "category": "Network", + "packageCategory": "Network", + "desktop": { + "StartupWMClass": "qortal-hub" + }, + "executableName": "Qortal Hub", + "icon": "assets/png" + }, + "appImage": { + "artifactName": "Qortal-Hub_${version}.${ext}" + }, + "deb": { + "artifactName": "Qortal-Hub-Setup_${version}.${ext}", + "synopsis": "Qortal Hub for Linux", + "afterInstall": "scripts/add-debian-apt-repo.sh" + }, + "snap": { + "artifactName": "Qortal-Hub-Setup_${version}.${ext}", + "synopsis": "Qortal Hub for Linux" + }, + "rpm": { + "artifactName": "Qortal-Hub-Setup_${version}.${ext}", + "synopsis": "Qortal Hub for Linux" + }, + "directories": { + "output": "dist", + "buildResources": "resources" + }, + "publish": [ + { + "provider": "github", + "owner": "Qortal", + "repo": "Qortal-Hub", + "releaseType": "draft" + } + ] +} diff --git a/electron/electron-builder.config.mac.json b/electron/electron-builder.config.mac.json new file mode 100644 index 0000000..056babc --- /dev/null +++ b/electron/electron-builder.config.mac.json @@ -0,0 +1,81 @@ +{ + "appId": "org.Qortal.Qortal-Hub", + "productName": "Qortal Hub", + "copyright": "Copyright © 2021 - 2025 Qortal", + "artifactName": "Qortal-Hub-Setup-macOS_${version}.${ext}", + + "compression": "normal", + + "asar": true, + + "afterPack": "scripts/afterPack.js", + + "afterSign": "scripts/notarize.js", + + "files": [ + "assets/**/*", + "build/**/*", + "capacitor.config.*", + "app/**/*", + "scripts/**/*" + ], + + "mac": { + "icon": "assets/mac/appIcon.icns", + "hardenedRuntime": true, + "gatekeeperAssess": false, + "entitlements": "buildmac/entitlements.mac.plist", + "entitlementsInherit": "buildmac/entitlements.mac.plist", + "category": "public.app-category.utilities", + "asarUnpack": ["**/*.node"], + "target": ["dmg", "pkg"] + }, + + "dmg": { + "sign": false, + "artifactName": "Qortal-Hub-Setup-macOS_${version}.${ext}", + "icon": "assets/mac/appIcon.icns", + "iconSize": 100, + "contents": [ + { + "x": 130, + "y": 220 + }, + { + "x": 410, + "y": 220, + "type": "link", + "path": "/Applications" + } + ] + }, + + "pkg": { + "artifactName": "Qortal-Hub-Setup-macOS_${version}.${ext}", + "installLocation": "/Applications", + "background": { + "file": "buildmac/logo-hub.png", + "alignment": "bottomleft", + "scaling": "none" + }, + "allowAnywhere": true, + "allowCurrentUserHome": true, + "allowRootDirectory": true, + "isVersionChecked": true, + "isRelocatable": false, + "overwriteAction": "upgrade" + }, + + "directories": { + "buildResources": "resources" + }, + + "publish": [ + { + "provider": "github", + "owner": "Qortal", + "repo": "Qortal-Hub", + "releaseType": "draft" + } + ] +} \ No newline at end of file diff --git a/electron/electron-builder.config.win.json b/electron/electron-builder.config.win.json new file mode 100644 index 0000000..4097af6 --- /dev/null +++ b/electron/electron-builder.config.win.json @@ -0,0 +1,44 @@ +{ + "appId": "org.qortal.Qortal-Hub", + "productName": "Qortal Hub", + "copyright": "Copyright © 2021 - 2025 Qortal", + "compression": "normal", + "asar": "true", + "files": [ + "assets/**/*", + "build/**/*", + "capacitor.config.*", + "app/**/*" + ], + "win": { + "legalTrademarks": "QORTAL.ORG", + "icon": "assets/appIcon.ico", + "target": [ + "nsis", + "portable" + ] + }, + "nsis": { + "artifactName": "Qortal-Hub-Setup-win64_${version}.${ext}", + "allowElevation": true, + "oneClick": false, + "allowToChangeInstallationDirectory": true, + "perMachine": true, + "runAfterFinish": true, + "deleteAppDataOnUninstall": true, + "createDesktopShortcut": true, + "createStartMenuShortcut": true + }, + "directories": { + "output": "dist", + "buildResources": "resources" + }, + "publish": [ + { + "provider": "github", + "owner": "Qortal", + "repo": "Qortal-Hub", + "releaseType": "draft" + } + ] +} diff --git a/electron/package-lock.json b/electron/package-lock.json index 6642e13..b3b1a6b 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "qortal-hub", - "version": "0.3.3", + "version": "0.5.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "qortal-hub", - "version": "0.3.3", + "version": "0.5.2", "license": "MIT", "dependencies": { "@capacitor-community/electron": "^5.0.0", @@ -19,9 +19,10 @@ "electron-window-state": "^5.0.3" }, "devDependencies": { - "electron": "^26.2.2", - "electron-builder": "~23.6.0", - "electron-rebuild": "^3.2.9", + "@electron/notarize": "^2.5.0", + "electron": "^32.3.1", + "electron-builder": "^25.1.8", + "shelljs": "^0.8.5", "typescript": "^5.0.4" } }, @@ -44,14 +45,13 @@ } }, "node_modules/@capacitor/cli": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-6.1.2.tgz", - "integrity": "sha512-HKCNGE0RP8U7aiEF2vg5wTivJROS8BVfu8a3yYJb1mRQvzv+czpmtHNsTWS/WukvwoxUjyjRmsNQSAACHfMTmQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-6.2.0.tgz", + "integrity": "sha512-EWcXG39mZh35zrHhOqzN1ILeSyMRyEqWVtQDXqMGjCXYRH6b6p5TvyvLDN8ZNy26tbhI3i79gfrgirt+mNwuuw==", "license": "MIT", "dependencies": { "@ionic/cli-framework-output": "^2.2.5", "@ionic/utils-fs": "^3.1.6", - "@ionic/utils-process": "^2.1.11", "@ionic/utils-subprocess": "2.1.11", "@ionic/utils-terminal": "^2.3.3", "commander": "^9.3.0", @@ -77,9 +77,9 @@ } }, "node_modules/@capacitor/core": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-6.1.2.tgz", - "integrity": "sha512-xFy1/4qLFLp5WCIzIhtwUuVNNoz36+V7/BzHmLqgVJcvotc4MMjswW/TshnPQaLLujEOaLkA4h8ZJ0uoK3ImGg==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-6.2.0.tgz", + "integrity": "sha512-B9IlJtDpUqhhYb+T8+cp2Db/3RETX36STgjeU2kQZBs/SLAcFiMama227o+msRjLeo3DO+7HJjWVA1+XlyyPEg==", "license": "MIT", "dependencies": { "tslib": "^2.1.0" @@ -103,6 +103,56 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/@electron/asar": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.3.1.tgz", + "integrity": "sha512-WtpC/+34p0skWZiarRjLAyqaAX78DofhDxnREy/V5XHfu1XEXbFCSSMcDQ6hNCPJFaPy8/NnUgYuf9uiCkvKPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/@electron/asar/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@electron/get": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", @@ -160,26 +210,22 @@ "node": ">= 4.0.0" } }, - "node_modules/@electron/universal": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.2.1.tgz", - "integrity": "sha512-7323HyMh7KBAl/nPDppdLsC87G6RwRU02dy5FPeGB1eS7rUePh55+WNWiDPLhFQqqVPHzh77M69uhmoT8XnwMQ==", + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", "dev": true, "license": "MIT", "dependencies": { - "@malept/cross-spawn-promise": "^1.1.0", - "asar": "^3.1.0", - "debug": "^4.3.1", - "dir-compare": "^2.4.0", + "debug": "^4.1.1", "fs-extra": "^9.0.1", - "minimatch": "^3.0.4", - "plist": "^3.0.4" + "promise-retry": "^2.0.1" }, "engines": { - "node": ">=8.6" + "node": ">= 10.0.0" } }, - "node_modules/@electron/universal/node_modules/fs-extra": { + "node_modules/@electron/notarize/node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", @@ -195,7 +241,7 @@ "node": ">=10" } }, - "node_modules/@electron/universal/node_modules/jsonfile": { + "node_modules/@electron/notarize/node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", @@ -208,6 +254,114 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/@electron/osx-sign": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.1.tgz", + "integrity": "sha512-BAfviURMHpmb1Yb50YbCxnOY0wfwaLXH5KJ4+80zS0gUkzDX3ec23naTlEqKsN+PwYn+a1cCzM7BJ4Wcd3sGzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/osx-sign/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz", + "integrity": "sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.2.7", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -285,33 +439,53 @@ } }, "node_modules/@ionic/utils-object": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.6.tgz", - "integrity": "sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.5.tgz", + "integrity": "sha512-XnYNSwfewUqxq+yjER1hxTKggftpNjFLJH0s37jcrNDwbzmbpFTQTVAp4ikNK4rd9DOebX/jbeZb8jfD86IYxw==", "license": "MIT", "dependencies": { "debug": "^4.0.0", "tslib": "^2.0.1" }, "engines": { - "node": ">=16.0.0" + "node": ">=10.3.0" } }, "node_modules/@ionic/utils-process": { - "version": "2.1.12", - "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.12.tgz", - "integrity": "sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==", + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.10.tgz", + "integrity": "sha512-mZ7JEowcuGQK+SKsJXi0liYTcXd2bNMR3nE0CyTROpMECUpJeAvvaBaPGZf5ERQUPeWBVuwqAqjUmIdxhz5bxw==", "license": "MIT", "dependencies": { - "@ionic/utils-object": "2.1.6", - "@ionic/utils-terminal": "2.3.5", + "@ionic/utils-object": "2.1.5", + "@ionic/utils-terminal": "2.3.3", "debug": "^4.0.0", "signal-exit": "^3.0.3", "tree-kill": "^1.2.2", "tslib": "^2.0.1" }, "engines": { - "node": ">=16.0.0" + "node": ">=10.3.0" + } + }, + "node_modules/@ionic/utils-process/node_modules/@ionic/utils-terminal": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.3.tgz", + "integrity": "sha512-RnuSfNZ5fLEyX3R5mtcMY97cGD1A0NVBbarsSQ6yMMfRJ5YHU7hHVyUfvZeClbqkBC/pAqI/rYJuXKCT9YeMCQ==", + "license": "MIT", + "dependencies": { + "@types/slice-ansi": "^4.0.0", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "slice-ansi": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "tslib": "^2.0.1", + "untildify": "^4.0.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10.3.0" } }, "node_modules/@ionic/utils-stream": { @@ -361,36 +535,6 @@ "node": ">=10.3.0" } }, - "node_modules/@ionic/utils-subprocess/node_modules/@ionic/utils-object": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.5.tgz", - "integrity": "sha512-XnYNSwfewUqxq+yjER1hxTKggftpNjFLJH0s37jcrNDwbzmbpFTQTVAp4ikNK4rd9DOebX/jbeZb8jfD86IYxw==", - "license": "MIT", - "dependencies": { - "debug": "^4.0.0", - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=10.3.0" - } - }, - "node_modules/@ionic/utils-subprocess/node_modules/@ionic/utils-process": { - "version": "2.1.10", - "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.10.tgz", - "integrity": "sha512-mZ7JEowcuGQK+SKsJXi0liYTcXd2bNMR3nE0CyTROpMECUpJeAvvaBaPGZf5ERQUPeWBVuwqAqjUmIdxhz5bxw==", - "license": "MIT", - "dependencies": { - "@ionic/utils-object": "2.1.5", - "@ionic/utils-terminal": "2.3.3", - "debug": "^4.0.0", - "signal-exit": "^3.0.3", - "tree-kill": "^1.2.2", - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=10.3.0" - } - }, "node_modules/@ionic/utils-subprocess/node_modules/@ionic/utils-terminal": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.3.tgz", @@ -458,10 +602,113 @@ "node": ">=16.0.0" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@malept/cross-spawn-promise": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", - "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", "dev": true, "funding": [ { @@ -478,7 +725,7 @@ "cross-spawn": "^7.0.1" }, "engines": { - "node": ">= 10" + "node": ">= 12.13.0" } }, "node_modules/@malept/flatpak-bundler": { @@ -555,6 +802,28 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/@npmcli/move-file/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@npmcli/move-file/node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -585,6 +854,17 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -653,18 +933,6 @@ "@types/node": "*" } }, - "node_modules/@types/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/minimatch": "*", - "@types/node": "*" - } - }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", @@ -682,28 +950,20 @@ "@types/node": "*" } }, - "node_modules/@types/minimatch": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/@types/ms": { - "version": "0.7.34", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", - "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "dev": true, "license": "MIT" }, "node_modules/@types/node": { - "version": "22.8.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.5.tgz", - "integrity": "sha512-5iYk6AMPtsMbkZqCO1UGF9W5L38twq11S2pYWkybGHH2ogPUvXWNlQqJBzuEZWKj/WRH+QTeiv6ySWqJtvIEgA==", + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", "license": "MIT", "dependencies": { - "undici-types": "~6.19.8" + "undici-types": "~6.20.0" } }, "node_modules/@types/plist": { @@ -748,23 +1008,6 @@ "license": "MIT", "optional": true }, - "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/yauzl": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", @@ -786,9 +1029,9 @@ } }, "node_modules/7zip-bin": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.1.1.tgz", - "integrity": "sha512-sAP4LldeWNz0lNzmTird3uWfFDWWTeg6V/MsmyyLR9X1idwKBWIgt/ZvinqQldJm3LecKEs1emkbquO6PCiLVQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", "dev": true, "license": "MIT" }, @@ -803,6 +1046,7 @@ "version": "0.5.16", "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "license": "MIT", "engines": { "node": ">=12.0" } @@ -912,48 +1156,121 @@ } }, "node_modules/app-builder-bin": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-4.0.0.tgz", - "integrity": "sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==", + "version": "5.0.0-alpha.10", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.10.tgz", + "integrity": "sha512-Ev4jj3D7Bo+O0GPD2NMvJl+PGiBAfS7pUGawntBNpCbxtpncfUixqFj9z9Jme7V7s3LBGqsWZZP54fxBX3JKJw==", "dev": true, "license": "MIT" }, "node_modules/app-builder-lib": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-23.6.0.tgz", - "integrity": "sha512-dQYDuqm/rmy8GSCE6Xl/3ShJg6Ab4bZJMT8KaTKGzT436gl1DN4REP3FCWfXoh75qGTJ+u+WsdnnpO9Jl8nyMA==", + "version": "25.1.8", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-25.1.8.tgz", + "integrity": "sha512-pCqe7dfsQFBABC1jeKZXQWhGcCPF3rPCXDdfqVKjIeWBcXzyC1iOWZdfFhGl+S9MyE/k//DFmC6FzuGAUudNDg==", "dev": true, "license": "MIT", "dependencies": { "@develar/schema-utils": "~2.6.5", - "@electron/universal": "1.2.1", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.1", + "@electron/rebuild": "3.6.1", + "@electron/universal": "2.0.1", "@malept/flatpak-bundler": "^0.4.0", - "7zip-bin": "~5.1.1", + "@types/fs-extra": "9.0.13", "async-exit-hook": "^2.0.1", "bluebird-lst": "^1.0.9", - "builder-util": "23.6.0", - "builder-util-runtime": "9.1.1", + "builder-util": "25.1.7", + "builder-util-runtime": "9.2.10", "chromium-pickle-js": "^0.2.0", + "config-file-ts": "0.2.8-rc1", "debug": "^4.3.4", - "ejs": "^3.1.7", - "electron-osx-sign": "^0.6.0", - "electron-publish": "23.6.0", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "25.1.7", "form-data": "^4.0.0", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "is-ci": "^3.0.0", - "isbinaryfile": "^4.0.10", + "isbinaryfile": "^5.0.0", "js-yaml": "^4.1.0", + "json5": "^2.2.3", "lazy-val": "^1.0.5", - "minimatch": "^3.1.2", - "read-config-file": "6.2.0", + "minimatch": "^10.0.0", + "resedit": "^1.7.0", "sanitize-filename": "^1.6.3", - "semver": "^7.3.7", - "tar": "^6.1.11", + "semver": "^7.3.8", + "tar": "^6.1.12", "temp-file": "^3.4.0" }, "engines": { "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "25.1.8", + "electron-builder-squirrel-windows": "25.1.8" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/rebuild": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.6.1.tgz", + "integrity": "sha512-f6596ZHpEq/YskUd8emYvOUne89ij8mQgjYFA5ru25QwbrRO+t1SImofdDv7kKOuWCmVOuU5tvfkbgGxIl3E/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "chalk": "^4.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "fs-extra": "^10.0.0", + "got": "^11.7.0", + "node-abi": "^3.45.0", + "node-api-version": "^0.2.0", + "node-gyp": "^9.0.0", + "ora": "^5.1.0", + "read-binary-file-arch": "^1.0.6", + "semver": "^7.3.5", + "tar": "^6.0.5", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/app-builder-lib/node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/app-builder-lib/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/builder-util-runtime": { + "version": "9.2.10", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz", + "integrity": "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" } }, "node_modules/app-builder-lib/node_modules/fs-extra": { @@ -984,6 +1301,22 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/app-builder-lib/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -991,6 +1324,108 @@ "dev": true, "license": "ISC" }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/are-we-there-yet": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", @@ -1012,39 +1447,6 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, - "node_modules/asar": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/asar/-/asar-3.2.0.tgz", - "integrity": "sha512-COdw2ZQvKdFGFxXwX3oYh2/sOsJWJegrdJCGxnN4MZ7IULgRBp9P6665aqj9z1v9VwP4oP1hRBojRDQ//IGgAg==", - "deprecated": "Please use @electron/asar moving forward. There is no API change, just a package name change", - "dev": true, - "license": "MIT", - "dependencies": { - "chromium-pickle-js": "^0.2.0", - "commander": "^5.0.0", - "glob": "^7.1.6", - "minimatch": "^3.0.4" - }, - "bin": { - "asar": "bin/asar.js" - }, - "engines": { - "node": ">=10.12.0" - }, - "optionalDependencies": { - "@types/glob": "^7.1.1" - } - }, - "node_modules/asar/node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -1241,24 +1643,6 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" - } - }, - "node_modules/buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", - "dev": true, - "license": "MIT" - }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -1268,23 +1652,6 @@ "node": "*" } }, - "node_modules/buffer-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", - "integrity": "sha512-tcBWO2Dl4e7Asr9hTGcpVrCe+F7DubpmqWCTbj4FHLmjqO2hIaC383acQubWtRJhdceqs5uBHs6Es+Sk//RKiQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", - "dev": true, - "license": "MIT" - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -1293,24 +1660,23 @@ "license": "MIT" }, "node_modules/builder-util": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-23.6.0.tgz", - "integrity": "sha512-QiQHweYsh8o+U/KNCZFSvISRnvRctb8m/2rB2I1JdByzvNKxPeFLlHFRPQRXab6aYeXc18j9LpsDLJ3sGQmWTQ==", + "version": "25.1.7", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-25.1.7.tgz", + "integrity": "sha512-7jPjzBwEGRbwNcep0gGNpLXG9P94VA3CPAZQCzxkFXiV2GMQKlziMbY//rXPI7WKfhsvGgFXjTcXdBEwgXw9ww==", "dev": true, "license": "MIT", "dependencies": { "@types/debug": "^4.1.6", - "@types/fs-extra": "^9.0.11", - "7zip-bin": "~5.1.1", - "app-builder-bin": "4.0.0", + "7zip-bin": "~5.2.0", + "app-builder-bin": "5.0.0-alpha.10", "bluebird-lst": "^1.0.9", - "builder-util-runtime": "9.1.1", - "chalk": "^4.1.1", + "builder-util-runtime": "9.2.10", + "chalk": "^4.1.2", "cross-spawn": "^7.0.3", "debug": "^4.3.4", - "fs-extra": "^10.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", "is-ci": "^3.0.0", "js-yaml": "^4.1.0", "source-map-support": "^0.5.19", @@ -1331,14 +1697,28 @@ "node": ">=12.0.0" } }, - "node_modules/builder-util/node_modules/@types/fs-extra": { - "version": "9.0.13", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", - "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "node_modules/builder-util/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/builder-util/node_modules/builder-util-runtime": { + "version": "9.2.10", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz", + "integrity": "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*" + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" } }, "node_modules/builder-util/node_modules/fs-extra": { @@ -1356,6 +1736,34 @@ "node": ">=12" } }, + "node_modules/builder-util/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/builder-util/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/builder-util/node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -1399,37 +1807,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/cacache/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/cacache/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/cacache/node_modules/lru-cache": { "version": "7.18.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", @@ -1440,19 +1817,6 @@ "node": ">=12" } }, - "node_modules/cacache/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/cacache/node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -1483,17 +1847,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/cacache/node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/cacache/node_modules/rimraf/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -1516,19 +1869,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/cacache/node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/cacheable-lookup": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", @@ -1558,6 +1898,20 @@ "node": ">=8" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1578,6 +1932,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -1761,16 +2116,6 @@ "color-support": "bin.js" } }, - "node_modules/colors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", - "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1803,6 +2148,23 @@ "node": ">=0.10.0" } }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1810,6 +2172,74 @@ "dev": true, "license": "MIT" }, + "node_modules/config-file-ts": { + "version": "0.2.8-rc1", + "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.8-rc1.tgz", + "integrity": "sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.3.12", + "typescript": "^5.4.3" + } + }, + "node_modules/config-file-ts/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/config-file-ts/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -1822,8 +2252,7 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/crc": { "version": "3.8.0", @@ -1836,10 +2265,39 @@ "buffer": "^5.1.0" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -1851,9 +2309,9 @@ } }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2001,58 +2459,27 @@ "optional": true }, "node_modules/dir-compare": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-2.4.0.tgz", - "integrity": "sha512-l9hmu8x/rjVC9Z2zmGzkhOEowZvW7pmYws5CWHutg8u1JgvsKWMx7Q/UODeu4djLZ4FgW5besw5yvMQnBHzuCA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", "dev": true, "license": "MIT", "dependencies": { - "buffer-equal": "1.0.0", - "colors": "1.0.3", - "commander": "2.9.0", - "minimatch": "3.0.4" - }, - "bin": { - "dircompare": "src/cli/dircompare.js" - } - }, - "node_modules/dir-compare/node_modules/commander": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", - "integrity": "sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-readlink": ">= 1.0.0" - }, - "engines": { - "node": ">= 0.6.x" - } - }, - "node_modules/dir-compare/node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " } }, "node_modules/dmg-builder": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-23.6.0.tgz", - "integrity": "sha512-jFZvY1JohyHarIAlTbfQOk+HnceGjjAdFjVn3n8xlDWKsYNqbO4muca6qXEZTfGXeQMG7TYim6CeS5XKSfSsGA==", + "version": "25.1.8", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-25.1.8.tgz", + "integrity": "sha512-NoXo6Liy2heSklTI5OIZbCgXC1RzrDQsZkeEwXhdOro3FT1VBOvbubvscdPnjVuQ4AMwwv61oaH96AbiYg9EnQ==", "dev": true, "license": "MIT", "dependencies": { - "app-builder-lib": "23.6.0", - "builder-util": "23.6.0", - "builder-util-runtime": "9.1.1", - "fs-extra": "^10.0.0", + "app-builder-lib": "25.1.8", + "builder-util": "25.1.7", + "builder-util-runtime": "9.2.10", + "fs-extra": "^10.1.0", "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" }, @@ -2060,6 +2487,20 @@ "dmg-license": "^1.0.11" } }, + "node_modules/dmg-builder/node_modules/builder-util-runtime": { + "version": "9.2.10", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz", + "integrity": "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/dmg-builder/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -2116,21 +2557,55 @@ } }, "node_modules/dotenv": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", - "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==", + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", "dev": true, "license": "BSD-2-Clause", "engines": { - "node": ">=10" + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } }, "node_modules/dotenv-expand": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", - "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", "dev": true, - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" }, "node_modules/ejs": { "version": "3.1.10", @@ -2149,15 +2624,14 @@ } }, "node_modules/electron": { - "version": "26.6.10", - "resolved": "https://registry.npmjs.org/electron/-/electron-26.6.10.tgz", - "integrity": "sha512-pV2SD0RXzAiNRb/2yZrsVmVkBOMrf+DVsPulIgRjlL0+My9BL5spFuhHVMQO9yHl9tFpWtuRpQv0ofM/i9P8xg==", + "version": "32.3.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-32.3.1.tgz", + "integrity": "sha512-gjHN9NagNajvllKrTZVwGuCqX1hPF7OI6hkCDFRCbqT5Zr05d17qjDhjyTNBmTca2DpvZIYQMRbqieNCcglVYQ==", "dev": true, "hasInstallScript": true, - "license": "MIT", "dependencies": { "@electron/get": "^2.0.0", - "@types/node": "^18.11.18", + "@types/node": "^20.9.0", "extract-zip": "^2.0.1" }, "bin": { @@ -2168,24 +2642,22 @@ } }, "node_modules/electron-builder": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-23.6.0.tgz", - "integrity": "sha512-y8D4zO+HXGCNxFBV/JlyhFnoQ0Y0K7/sFH+XwIbj47pqaW8S6PGYQbjoObolKBR1ddQFPt4rwp4CnwMJrW3HAw==", + "version": "25.1.8", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-25.1.8.tgz", + "integrity": "sha512-poRgAtUHHOnlzZnc9PK4nzG53xh74wj2Jy7jkTrqZ0MWPoHGh1M2+C//hGeYdA+4K8w4yiVCNYoLXF7ySj2Wig==", "dev": true, "license": "MIT", "dependencies": { - "@types/yargs": "^17.0.1", - "app-builder-lib": "23.6.0", - "builder-util": "23.6.0", - "builder-util-runtime": "9.1.1", - "chalk": "^4.1.1", - "dmg-builder": "23.6.0", - "fs-extra": "^10.0.0", + "app-builder-lib": "25.1.8", + "builder-util": "25.1.7", + "builder-util-runtime": "9.2.10", + "chalk": "^4.1.2", + "dmg-builder": "25.1.8", + "fs-extra": "^10.1.0", "is-ci": "^3.0.0", "lazy-val": "^1.0.5", - "read-config-file": "6.2.0", - "simple-update-notifier": "^1.0.7", - "yargs": "^17.5.1" + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" }, "bin": { "electron-builder": "cli.js", @@ -2195,6 +2667,64 @@ "node": ">=14.0.0" } }, + "node_modules/electron-builder-squirrel-windows": { + "version": "25.1.8", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-25.1.8.tgz", + "integrity": "sha512-2ntkJ+9+0GFP6nAISiMabKt6eqBB0kX1QqHNWFWAXgi0VULKGisM46luRFpIBiU3u/TDmhZMM8tzvo2Abn3ayg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "25.1.8", + "archiver": "^5.3.1", + "builder-util": "25.1.7", + "fs-extra": "^10.1.0" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder/node_modules/builder-util-runtime": { + "version": "9.2.10", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz", + "integrity": "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/electron-builder/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -2232,71 +2762,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/electron-osx-sign": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.6.0.tgz", - "integrity": "sha512-+hiIEb2Xxk6eDKJ2FFlpofCnemCbjbT5jz+BKGpVBrRNT3kWTGs4DfNX6IzGwgi33hUcXF+kFs9JW+r6Wc1LRg==", - "deprecated": "Please use @electron/osx-sign moving forward. Be aware the API is slightly different", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "bluebird": "^3.5.0", - "compare-version": "^0.1.2", - "debug": "^2.6.8", - "isbinaryfile": "^3.0.2", - "minimist": "^1.2.0", - "plist": "^3.0.1" - }, - "bin": { - "electron-osx-flat": "bin/electron-osx-flat.js", - "electron-osx-sign": "bin/electron-osx-sign.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/electron-osx-sign/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/electron-osx-sign/node_modules/isbinaryfile": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.3.tgz", - "integrity": "sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-alloc": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/electron-osx-sign/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, "node_modules/electron-publish": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-23.6.0.tgz", - "integrity": "sha512-jPj3y+eIZQJF/+t5SLvsI5eS4mazCbNYqatv5JihbqOstIM13k0d1Z3vAWntvtt13Itl61SO6seicWdioOU5dg==", + "version": "25.1.7", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-25.1.7.tgz", + "integrity": "sha512-+jbTkR9m39eDBMP4gfbqglDd6UvBC7RLh5Y0MhFSsc6UkGHj9Vj9TWobxevHYMMqmoujL11ZLjfPpMX+Pt6YEg==", "dev": true, "license": "MIT", "dependencies": { "@types/fs-extra": "^9.0.11", - "builder-util": "23.6.0", - "builder-util-runtime": "9.1.1", - "chalk": "^4.1.1", - "fs-extra": "^10.0.0", + "builder-util": "25.1.7", + "builder-util-runtime": "9.2.10", + "chalk": "^4.1.2", + "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } @@ -2311,6 +2788,20 @@ "@types/node": "*" } }, + "node_modules/electron-publish/node_modules/builder-util-runtime": { + "version": "9.2.10", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz", + "integrity": "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/electron-publish/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -2339,87 +2830,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/electron-rebuild": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/electron-rebuild/-/electron-rebuild-3.2.9.tgz", - "integrity": "sha512-FkEZNFViUem3P0RLYbZkUjC8LUFIK+wKq09GHoOITSJjfDAVQv964hwaNseTTWt58sITQX3/5fHNYcTefqaCWw==", - "deprecated": "Please use @electron/rebuild moving forward. There is no API change, just a package name change", - "dev": true, - "license": "MIT", - "dependencies": { - "@malept/cross-spawn-promise": "^2.0.0", - "chalk": "^4.0.0", - "debug": "^4.1.1", - "detect-libc": "^2.0.1", - "fs-extra": "^10.0.0", - "got": "^11.7.0", - "lzma-native": "^8.0.5", - "node-abi": "^3.0.0", - "node-api-version": "^0.1.4", - "node-gyp": "^9.0.0", - "ora": "^5.1.0", - "semver": "^7.3.5", - "tar": "^6.0.5", - "yargs": "^17.0.1" - }, - "bin": { - "electron-rebuild": "lib/src/cli.js" - }, - "engines": { - "node": ">=12.13.0" - } - }, - "node_modules/electron-rebuild/node_modules/@malept/cross-spawn-promise": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", - "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/malept" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" - } - ], - "license": "Apache-2.0", - "dependencies": { - "cross-spawn": "^7.0.1" - }, - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/electron-rebuild/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-rebuild/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/electron-serve": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/electron-serve/-/electron-serve-1.1.0.tgz", @@ -2499,21 +2909,19 @@ } }, "node_modules/electron/node_modules/@types/node": { - "version": "18.19.62", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.62.tgz", - "integrity": "sha512-UOGhw+yZV/icyM0qohQVh3ktpY40Sp7tdTW7HxG3pTd7AiMrlFlAJNUrGK9t5mdW0+ViQcFV74zCSIx9ZJpncA==", + "version": "20.17.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.19.tgz", + "integrity": "sha512-LEwC7o1ifqg/6r2gn9Dns0f1rhK+fPFDoMiceTJ6kWmVk6bgXBI/9IOWfVan4WiAavK9pIVWdX0/e3J+eEUh5A==", "dev": true, - "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/electron/node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true, - "license": "MIT" + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true }, "node_modules/elementtree": { "version": "0.1.7", @@ -2586,15 +2994,11 @@ "license": "MIT" }, "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==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, "license": "MIT", - "optional": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, "engines": { "node": ">= 0.4" } @@ -2605,7 +3009,35 @@ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, "license": "MIT", - "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, "engines": { "node": ">= 0.4" } @@ -2758,21 +3190,60 @@ "node": ">=8" } }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" }, "engines": { "node": ">= 6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/fs-extra": { "version": "11.1.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", @@ -2837,7 +3308,6 @@ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, "license": "MIT", - "optional": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2874,18 +3344,22 @@ } }, "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==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -2894,6 +3368,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", @@ -2911,9 +3399,9 @@ } }, "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", @@ -2921,12 +3409,11 @@ "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "minimatch": "^5.0.1", + "once": "^1.3.0" }, "engines": { - "node": "*" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -2944,6 +3431,29 @@ "node": ">= 6" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/global-agent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", @@ -3013,14 +3523,13 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, "license": "MIT", - "optional": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3058,13 +3567,6 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, - "node_modules/graceful-readlink": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==", - "dev": true, - "license": "MIT" - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3088,13 +3590,12 @@ "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==", + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">= 0.4" }, @@ -3102,13 +3603,15 @@ "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==", + "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==", "dev": true, "license": "MIT", - "optional": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, "engines": { "node": ">= 0.4" }, @@ -3129,7 +3632,6 @@ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -3315,6 +3817,16 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", @@ -3354,6 +3866,22 @@ "is-ci": "bin.js" } }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-docker": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", @@ -3448,14 +3976,22 @@ "node": ">=8" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/isbinaryfile": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", - "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.4.tgz", + "integrity": "sha512-YKBKVkKhty7s8rxddb40oOkuP0NbaeXrQvLin6QMHL7Ypiy2RW9LwOVrVgZRyOrhQlayMd9t+D8yDy8MKFTSDQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 8.0.0" + "node": ">= 18.0.0" }, "funding": { "url": "https://github.com/sponsors/gjtorikian/" @@ -3467,6 +4003,22 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jake": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", @@ -3572,6 +4124,56 @@ "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", "license": "MIT" }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -3585,18 +4187,58 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "license": "MIT" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/lodash.escaperegexp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", "license": "MIT" }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "license": "MIT" }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -3636,32 +4278,6 @@ "node": ">=10" } }, - "node_modules/lzma-native": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/lzma-native/-/lzma-native-8.0.6.tgz", - "integrity": "sha512-09xfg67mkL2Lz20PrrDeNYZxzeW7ADtpYFbwSQh9U8+76RIzx5QsJBMy8qikv3hbUPfpy6hqwxt6FcGK81g9AA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^3.1.0", - "node-gyp-build": "^4.2.1", - "readable-stream": "^3.6.0" - }, - "bin": { - "lzmajs": "bin/lzmajs" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/lzma-native/node_modules/node-addon-api": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", - "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", - "dev": true, - "license": "MIT" - }, "node_modules/make-fetch-happen": { "version": "10.2.1", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", @@ -3714,6 +4330,16 @@ "node": ">=10" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mime": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", @@ -3959,9 +4585,9 @@ "optional": true }, "node_modules/node-api-version": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.1.4.tgz", - "integrity": "sha512-KGXihXdUChwJAOHO53bv9/vXcLmdUsZ6jIptbvYvkpKfth+r7jw44JkVxQFA3kX5nQjzjmGu1uAu/xNNLNlI5g==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.0.tgz", + "integrity": "sha512-fthTTsi8CxaBXMaBAD7ST2uylwvsnYxh2PfaScwpMhos6KlSFajXQPcM4ogNE1q2s3Lbz9GCGqeIHC+C6OZnKg==", "dev": true, "license": "MIT", "dependencies": { @@ -3994,16 +4620,26 @@ "node": "^12.13 || ^14.13 || >=16" } }, - "node_modules/node-gyp-build": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.2.tgz", - "integrity": "sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw==", + "node_modules/node-gyp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, - "license": "MIT", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/node-gyp/node_modules/rimraf": { @@ -4164,6 +4800,22 @@ "node": ">=8" } }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -4180,6 +4832,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -4199,6 +4858,13 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", @@ -4230,6 +4896,21 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -4262,6 +4943,14 @@ "node": ">=10.4.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -4349,21 +5038,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/read-config-file": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.2.0.tgz", - "integrity": "sha512-gx7Pgr5I56JtYz+WuqEbQHj/xWo+5Vwua2jhb1VwM4Wid5PqYmZ4i00ZB0YEGIfkVBsCv9UrjgyqCiQfS/Oosg==", + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", "dev": true, "license": "MIT", "dependencies": { - "dotenv": "^9.0.2", - "dotenv-expand": "^5.1.0", - "js-yaml": "^4.1.0", - "json5": "^2.2.0", - "lazy-val": "^1.0.4" + "debug": "^4.3.4" }, - "engines": { - "node": ">=12.0.0" + "bin": { + "read-binary-file-arch": "cli.js" } }, "node_modules/readable-stream": { @@ -4380,6 +5065,42 @@ "node": ">= 6" } }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -4392,6 +5113,18 @@ "node": ">=8.10.0" } }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "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", @@ -4402,6 +5135,45 @@ "node": ">=0.10.0" } }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-alpn": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", @@ -4649,6 +5421,46 @@ "node": ">=8" } }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shelljs/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -4656,26 +5468,16 @@ "license": "ISC" }, "node_modules/simple-update-notifier": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", - "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", "dev": true, "license": "MIT", "dependencies": { - "semver": "~7.0.0" + "semver": "^7.5.3" }, "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/simple-update-notifier/node_modules/semver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", - "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node": ">=10" } }, "node_modules/sisteransi": { @@ -4825,6 +5627,22 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "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, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -4837,6 +5655,20 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", @@ -4862,6 +5694,19 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", @@ -4879,6 +5724,24 @@ "node": ">=10" } }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tar/node_modules/minipass": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", @@ -5000,9 +5863,9 @@ } }, "node_modules/tslib": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", - "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/type-fest": { @@ -5027,9 +5890,9 @@ } }, "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5041,9 +5904,9 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "license": "MIT" }, "node_modules/unique-filename": { @@ -5180,6 +6043,25 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -5272,6 +6154,81 @@ "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } } } } diff --git a/electron/package.json b/electron/package.json index 78de618..b520190 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,16 +1,18 @@ { "name": "qortal-hub", - "version": "0.3.7", + "version": "0.5.2", "description": "A desktop app that gives you access to the Qortal network", "author": { - "name": "" + "name": "", + "email": "qortalblockchain@gmail.com" }, + "homepage": "https://qortal.dev", "repository": { "type": "git", - "url": "" + "url": "git+https://github.com/Qortal/Qortal-Hub.git" }, "build": { - "appId": "com.github.Qortal.Qortal-Hub", + "appId": "org.Qortal.Qortal-Hub", "publish": [ { "provider": "github", @@ -27,7 +29,12 @@ "electron:start": "npm run build && electron --inspect=5858 ./", "electron:pack": "npm run build && electron-builder build --dir -c ./electron-builder.config.json", "electron:make": "npm run build && electron-builder build -c ./electron-builder.config.json -p always", - "electron:make-local": "npm run build && electron-builder build -c ./electron-builder.config.json --publish=never" + "electron:make-local": "npm run build && electron-builder build -c ./electron-builder.config.json --publish=never", + "electron:make-lin": "npm run build && electron-builder build -c ./electron-builder.config.lin.json --publish=never -l", + "electron:make-mac": "npm run build && electron-builder build -c ./electron-builder.config.mac.json --publish=never --mac dmg && electron-builder build -c ./electron-builder.config.mac.json --publish=never --mac pkg && electron-builder build -c ./electron-builder.config.mac.json --publish=never --mac zip", + "electron:make-win": "npm run build && electron-builder build -c ./electron-builder.config.win.json --publish=never -w", + "electron:make-arm": "npm run build && electron-builder build -c ./electron-builder.config.arm.json --publish=never --linux --arm64", + "electron:make-all": "npm run build && electron-builder build -c ./electron-builder.config.win.json --publish=never -w && electron-builder build -c ./electron-builder.config.lin.json --publish=never -l && electron-builder build -c ./electron-builder.config.arm.json --publish=never --linux --arm64" }, "dependencies": { "@capacitor-community/electron": "^5.0.0", @@ -40,13 +47,14 @@ "electron-window-state": "^5.0.3" }, "devDependencies": { - "electron": "^26.2.2", - "electron-builder": "~23.6.0", - "electron-rebuild": "^3.2.9", - "typescript": "^5.0.4" + "electron": "^32.3.1", + "electron-builder": "^25.1.8", + "@electron/notarize": "^2.5.0", + "typescript": "^5.0.4", + "shelljs": "^0.8.5" }, "keywords": [ "capacitor", "electron" ] -} +} \ No newline at end of file diff --git a/electron/scripts/add-debian-apt-repo.sh b/electron/scripts/add-debian-apt-repo.sh new file mode 100755 index 0000000..ca56444 --- /dev/null +++ b/electron/scripts/add-debian-apt-repo.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# Make necessary config and add Qortal Hub apt repo + +# SCript to run HUB without sandbox +echo \'/opt/${productFilename}/qortal-hub\' --no-sandbox > '/opt/${productFilename}/run-hub' +chmod +x '/opt/${productFilename}/run-hub' + +# Link to run-ui +ln -sf '/opt/${productFilename}/run-hub' '/usr/bin/${executable}' + +# SUID chrome-sandbox for Electron 5+ +sudo chown root '/opt/${productFilename}/chrome-sandbox' || true +sudo chmod 4755 '/opt/${productFilename}/chrome-sandbox' || true + +update-mime-database /usr/share/mime || true +update-desktop-database /usr/share/applications || true + +# Install curl if not installed on the system +if ! which curl; then sudo apt-get --yes install curl; fi + +# Install apt repository source list if it does not exist +if ! grep ^ /etc/apt/sources.list /etc/apt/sources.list.d/* | grep qortal-hub.list; then + curl -sS https://update.qortal-hub.org/qortal-hub.gpg | sudo apt-key add - + sudo rm -rf /usr/share/keyrings/qortal-hub.gpg + sudo apt-key export E191E7C3 | sudo gpg --dearmour -o /usr/share/keyrings/qortal-hub.gpg + sudo rm -rf /etc/apt/sources.list.d/qortal-hub.list + echo 'deb [arch=amd64,arm64 signed-by=/usr/share/keyrings/qortal-hub.gpg] https://update.qortal-hub.org/ ./ ' | sudo tee /etc/apt/sources.list.d/qortal-hub.list +fi diff --git a/electron/scripts/afterPack.js b/electron/scripts/afterPack.js new file mode 100644 index 0000000..f7084bd --- /dev/null +++ b/electron/scripts/afterPack.js @@ -0,0 +1,39 @@ +const path = require('path') +const shell = require("shelljs") + +const runShellCommand = (appOutDir) => { + shell.exec( + `chmod 4755 ${path.join(appOutDir, "chrome-sandbox")}`, + + function (code, stdout, stderr) { + console.log('runShellCommand ==> Exit code:', code) + if (stderr) { + console.log('runShellCommand ==> Program stderr:', stderr) + } + } + ) +} + +async function doLinux(context) { + console.log("Running doLinux ==> ") + + const { targets, appOutDir } = context + + targets.forEach(async target => { + if (!["appimage", "snap"].includes(target.name.toLowerCase())) { + await runShellCommand(appOutDir) + } + }) +} + +async function afterPack(context) { + console.log("Running AfterPack") + + const electronPlatformName = context.electronPlatformName.toLowerCase() + + if (electronPlatformName.includes("linux")) { + await doLinux(context) + } +} + +module.exports = afterPack \ No newline at end of file diff --git a/electron/scripts/notarize.js b/electron/scripts/notarize.js new file mode 100644 index 0000000..d09de36 --- /dev/null +++ b/electron/scripts/notarize.js @@ -0,0 +1,21 @@ +require('dotenv').config() +const { notarize } = require('@electron/notarize') + +exports.default = async function notarizing(context) { + const { electronPlatformName, appOutDir } = context + + if (electronPlatformName !== 'darwin') { + return + } + + const appName = context.packager.appInfo.productFilename + + return await notarize({ + appBundleId: 'org.qortal.Qortal-Hub', + appPath: `${appOutDir}/${appName}.app`, + tool: "notarytool", + teamId: process.env.APPLETEAMID, + appleId: process.env.APPLEID, + appleIdPassword: process.env.APPLEIDPASS + }) +} \ No newline at end of file diff --git a/electron/scripts/uninstall-debian-conf.sh b/electron/scripts/uninstall-debian-conf.sh new file mode 100644 index 0000000..9b3ff50 --- /dev/null +++ b/electron/scripts/uninstall-debian-conf.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# Remove all conf made by Qortal Hub + +# Remove apt repository source list when user uninstalls app +if grep ^ /etc/apt/sources.list /etc/apt/sources.list.d/* | grep qortal-hub.list; then + sudo rm /etc/apt/sources.list.d/qortal-hub.list; +fi + +# Get the root user +if [ $SUDO_USER ]; + then getSudoUser=$SUDO_USER; + else getSudoUser=`whoami`; +fi + +getDesktopEntry=/home/$getSudoUser/.config/autostart/qortal-hub.desktop; + +# Remove desktop entry if exists +if [ -f $getDesktopEntry ]; then + sudo rm $getDesktopEntry; +fi + +# App directory which contains all the config and settings files +appDirectory=/home/$getSudoUser/.config/qortal-hub/; + +if [ -d $appDirectory ]; then + sudo rm -rf $appDirectory; +fi + +# Delete the link to the binary +rm -f '/usr/bin/${executable}' + +# Delete run-hub +rm -f '/opt/${productFilename}/run-hub' diff --git a/electron/src/index.ts b/electron/src/index.ts index 9b729b8..31c8981 100644 --- a/electron/src/index.ts +++ b/electron/src/index.ts @@ -16,6 +16,7 @@ const trayMenuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [new MenuIte const appMenuBarMenuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [ { role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' }, { role: 'viewMenu' }, + { role: 'editMenu' }, ]; // Get Config options from capacitor.config diff --git a/electron/src/setup.ts b/electron/src/setup.ts index 2e85c55..6fab164 100644 --- a/electron/src/setup.ts +++ b/electron/src/setup.ts @@ -80,6 +80,7 @@ export class ElectronCapacitorApp { private AppMenuBarMenuTemplate: (MenuItem | MenuItemConstructorOptions)[] = [ { role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' }, { role: 'viewMenu' }, + { role: 'editMenu' }, ]; private mainWindowState; private loadWebApp; @@ -324,7 +325,9 @@ export function setupContentSecurityPolicy(customScheme: string): void { // IPC listener for updating allowed domains ipcMain.on('set-allowed-domains', (event, domains: string[]) => { - + if (!Array.isArray(domains)) { + return; + } // Validate and transform user-provided domains const validatedUserDomains = domains .flatMap((domain) => { diff --git a/package-lock.json b/package-lock.json index 9cd9a63..e8c249f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,6 +72,7 @@ "react-infinite-scroller": "^1.2.6", "react-intersection-observer": "^9.13.0", "react-json-view-lite": "^2.0.1", + "react-loader-spinner": "^6.1.6", "react-qr-code": "^2.0.15", "react-quill": "^2.0.0", "react-redux": "^9.1.2", @@ -83,6 +84,7 @@ "slate-react": "^0.109.0", "tippy.js": "^6.3.7", "tiptap-extension-resize-image": "^1.1.8", + "ts-key-enum": "^2.0.12", "vite-plugin-top-level-await": "^1.4.4", "vite-plugin-wasm": "^3.3.0" }, @@ -4449,6 +4451,11 @@ "resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz", "integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==" }, + "node_modules/@types/stylis": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==" + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -5898,6 +5905,14 @@ "node": ">=6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001674", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001674.tgz", @@ -6677,6 +6692,24 @@ "node": ">= 8" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -13881,9 +13914,9 @@ } }, "node_modules/postcss": { - "version": "8.4.37", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.37.tgz", - "integrity": "sha512-7iB/v/r7Woof0glKLH8b1SPHrsX7uhdO+Geb41QpF/+mWZHU3uxxSlN+UXGVit1PawOYDToO+AbZzhBzWRDwbQ==", + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "funding": [ { "type": "opencollective", @@ -13907,6 +13940,11 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, "node_modules/postject": { "version": "1.0.0-alpha.6", "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", @@ -14483,6 +14521,27 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "node_modules/react-loader-spinner": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/react-loader-spinner/-/react-loader-spinner-6.1.6.tgz", + "integrity": "sha512-x5h1Jcit7Qn03MuKlrWcMG9o12cp9SNDVHVJTNRi9TgtGPKcjKiXkou4NRfLAtXaFB3+Z8yZsVzONmPzhv2ErA==", + "dependencies": { + "react-is": "^18.2.0", + "styled-components": "^6.1.2" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-loader-spinner/node_modules/react-is": { + "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/react-qr-code": { "version": "2.0.15", "resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.15.tgz", @@ -15373,6 +15432,11 @@ "node": ">= 0.4" } }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -15854,6 +15918,38 @@ "node": ">=0.10.0" } }, + "node_modules/styled-components": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.13.tgz", + "integrity": "sha512-M0+N2xSnAtwcVAQeFEsGWFFxXDftHUD7XrKla06QbpUMmbmtFBMMTcKWvFXtWxuD5qQkB8iU5gk6QASlx2ZRMw==", + "dependencies": { + "@emotion/is-prop-valid": "1.2.2", + "@emotion/unitless": "0.8.1", + "@types/stylis": "4.2.5", + "css-to-react-native": "3.2.0", + "csstype": "3.1.3", + "postcss": "8.4.38", + "shallowequal": "1.1.0", + "stylis": "4.3.2", + "tslib": "2.6.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, + "node_modules/styled-components/node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==" + }, "node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", @@ -16224,6 +16320,11 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-key-enum": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/ts-key-enum/-/ts-key-enum-2.0.13.tgz", + "integrity": "sha512-zixs6j8+NhzazLUQ1SiFrlo1EFWG/DbqLuUGcWWZ5zhwjRT7kbi1hBlofxdqel+h28zrby2It5TrOyKp04kvqw==" + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", diff --git a/package.json b/package.json index b42878b..dcf359a 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "react-infinite-scroller": "^1.2.6", "react-intersection-observer": "^9.13.0", "react-json-view-lite": "^2.0.1", + "react-loader-spinner": "^6.1.6", "react-qr-code": "^2.0.15", "react-quill": "^2.0.0", "react-redux": "^9.1.2", @@ -87,6 +88,7 @@ "slate-react": "^0.109.0", "tippy.js": "^6.3.7", "tiptap-extension-resize-image": "^1.1.8", + "ts-key-enum": "^2.0.12", "vite-plugin-top-level-await": "^1.4.4", "vite-plugin-wasm": "^3.3.0" }, diff --git a/src/App-styles.ts b/src/App-styles.ts index bf3a146..08773ea 100644 --- a/src/App-styles.ts +++ b/src/App-styles.ts @@ -136,6 +136,47 @@ border-radius: 5px; } } `; +interface CustomButtonProps { + bgColor?: string; + color?: string; +} +export const CustomButtonAccept = styled(Box)( + ({ bgColor, color }) => ({ + boxSizing: "border-box", + padding: "15px 20px", + gap: "10px", + border: "0.5px solid rgba(255, 255, 255, 0.5)", + filter: "drop-shadow(1px 4px 10.5px rgba(0,0,0,0.3))", + borderRadius: 5, + display: "inline-flex", + justifyContent: "center", + alignItems: "center", + width: "fit-content", + transition: "all 0.2s", + minWidth: 160, + cursor: "pointer", + fontWeight: 600, + fontFamily: "Inter", + textAlign: "center", + opacity: 0.7, + // Use the passed-in props or fallback defaults + backgroundColor: bgColor || "transparent", + color: color || "white", + + "&:hover": { + opacity: 1, + backgroundColor: bgColor + ? bgColor + : "rgba(41, 41, 43, 1)", // fallback hover bg + color: color || "white", + svg: { + path: { + fill: color || "white", + }, + }, + }, + }) +); export const CustomInput = styled(TextField)({ diff --git a/src/App.tsx b/src/App.tsx index 77d2585..9e14601 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -40,11 +40,12 @@ import { CopyToClipboard } from "react-copy-to-clipboard"; import Download from "./assets/svgs/Download.svg"; import Logout from "./assets/svgs/Logout.svg"; import Return from "./assets/svgs/Return.svg"; +import WarningIcon from '@mui/icons-material/Warning'; import Success from "./assets/svgs/Success.svg"; import Info from "./assets/svgs/Info.svg"; import CloseIcon from "@mui/icons-material/Close"; import './utils/seedPhrase/RandomSentenceGenerator'; - +import EngineeringIcon from '@mui/icons-material/Engineering'; import { createAccount, generateRandomSentence, @@ -62,6 +63,7 @@ import { AuthenticatedContainerInnerLeft, AuthenticatedContainerInnerRight, CustomButton, + CustomButtonAccept, CustomInput, CustomLabel, TextItalic, @@ -79,6 +81,8 @@ import { LoadingButton } from "@mui/lab"; import { Label } from "./components/Group/AddGroup"; import { CustomizedSnackbars } from "./components/Snackbar/Snackbar"; import SettingsIcon from "@mui/icons-material/Settings"; +import HelpIcon from '@mui/icons-material/Help'; + import { cleanUrl, getFee, @@ -110,7 +114,11 @@ import { enabledDevModeAtom, fullScreenAtom, hasSettingsChangedAtom, + isDisabledEditorEnterAtom, + isUsingImportExportSettingsAtom, + mailsAtom, oldPinnedAppsAtom, + qMailLastEnteredTimestampAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom, @@ -127,6 +135,16 @@ import { CoreSyncStatus } from "./components/CoreSyncStatus"; import { Wallets } from "./Wallets"; import { RandomSentenceGenerator } from "./utils/seedPhrase/RandomSentenceGenerator"; import { useFetchResources } from "./common/useFetchResources"; +import { Tutorials } from "./components/Tutorials/Tutorials"; +import { useHandleTutorials } from "./components/Tutorials/useHandleTutorials"; +import BoundedNumericTextField from "./common/BoundedNumericTextField"; +import { useHandleUserInfo } from "./components/Group/useHandleUserInfo"; +import { Minting } from "./components/Minting/Minting"; +import { isRunningGateway } from "./qortalRequests"; +import { QMailStatus } from "./components/QMailStatus"; +import { GlobalActions } from "./components/GlobalActions/GlobalActions"; +import { useBlockedAddresses } from "./components/Group/useBlockUsers"; +import { WalletIcon } from "./assets/Icons/WalletIcon"; type extStates = | "not-authenticated" @@ -246,8 +264,12 @@ export const resumeAllQueues = () => { }; - +const defaultValuesGlobal = { + openTutorialModal: null, + setOpenTutorialModal: ()=> {} +} export const MyContext = createContext(defaultValues); +export const GlobalContext = createContext(defaultValuesGlobal); export let globalApiKey: string | null = null; @@ -338,6 +360,7 @@ function App() { const {downloadResource} = useFetchResources() const holdRefExtState = useRef("not-authenticated"); const isFocusedRef = useRef(true); + const {showTutorial, openTutorialModal, shownTutorialsInitiated, setOpenTutorialModal, hasSeenGettingStarted} = useHandleTutorials() const { isShow, onCancel, onOk, show, message } = useModal(); const { isShow: isShowUnsavedChanges, @@ -353,7 +376,7 @@ function App() { show: showInfo, message: messageInfo, } = useModal(); - + const { onCancel: onCancelQortalRequest, onOk: onOkQortalRequest, @@ -381,14 +404,26 @@ function App() { const [isOpenSendQort, setIsOpenSendQort] = useState(false); const [isOpenSendQortSuccess, setIsOpenSendQortSuccess] = useState(false); const [rootHeight, setRootHeight] = useState("100%"); + const {isUserBlocked, + addToBlockList, + removeBlockFromList, getAllBlockedUsers} = useBlockedAddresses() + const [currentNode, setCurrentNode] = useState({ + url: "http://127.0.0.1:12391", + }); + const [useLocalNode, setUseLocalNode] = useState(false); + const [isSettingsOpen, setIsSettingsOpen] = useState(false); + const [showSeed, setShowSeed] = useState(false) + const [creationStep, setCreationStep] = useState(1) + const {getIndividualUserInfo} = useHandleUserInfo() const qortalRequestCheckbox1Ref = useRef(null); useRetrieveDataLocalStorage(); - useQortalGetSaveSettings(userInfo?.name); + useQortalGetSaveSettings(userInfo?.name, extState === "authenticated"); const [fullScreen, setFullScreen] = useRecoilState(fullScreenAtom); const [isEnabledDevMode, setIsEnabledDevMode] = useRecoilState(enabledDevModeAtom); - + const setIsDisabledEditorEnter = useSetRecoilState(isDisabledEditorEnterAtom) + const [isOpenMinting, setIsOpenMinting] = useState(false) const { toggleFullScreen } = useAppFullScreen(setFullScreen); const generatorRef = useRef(null) const exportSeedphrase = ()=> { @@ -408,6 +443,17 @@ function App() { } }, []); + useEffect(()=> { + if(!shownTutorialsInitiated) return + if(extState === 'not-authenticated'){ + showTutorial('create-account') + } else if(extState === "create-wallet" && walletToBeDownloaded){ + showTutorial('important-information') + } else if(extState === "authenticated"){ + showTutorial('getting-started') + } + }, [extState, walletToBeDownloaded, shownTutorialsInitiated]) + useEffect(() => { // Attach a global event listener for double-click const handleDoubleClick = () => { @@ -429,6 +475,7 @@ function App() { const resetAtomSortablePinnedAppsAtom = useResetRecoilState( sortablePinnedAppsAtom ); + const resetAtomIsUsingImportExportSettingsAtom = useResetRecoilState(isUsingImportExportSettingsAtom) const resetAtomCanSaveSettingToQdnAtom = useResetRecoilState( canSaveSettingToQdnAtom ); @@ -439,6 +486,8 @@ function App() { settingsLocalLastUpdatedAtom ); const resetAtomOldPinnedAppsAtom = useResetRecoilState(oldPinnedAppsAtom); + const resetAtomQMailLastEnteredTimestampAtom = useResetRecoilState(qMailLastEnteredTimestampAtom) + const resetAtomMailsAtom = useResetRecoilState(mailsAtom) const resetAllRecoil = () => { resetAtomSortablePinnedAppsAtom(); @@ -446,6 +495,9 @@ function App() { resetAtomSettingsQDNLastUpdatedAtom(); resetAtomSettingsLocalLastUpdatedAtom(); resetAtomOldPinnedAppsAtom(); + resetAtomIsUsingImportExportSettingsAtom() + resetAtomQMailLastEnteredTimestampAtom() + resetAtomMailsAtom() }; useEffect(() => { if (!isMobile) return; @@ -476,7 +528,9 @@ function App() { globalApiKey = key; }; useEffect(() => { - window + try { + setIsLoading(true); + window .sendMessage("getApiKey") .then((response) => { if (response) { @@ -489,7 +543,36 @@ function App() { "Failed to get API key:", error?.message || "An error occurred" ); - }); + }).finally(()=> { + window + .sendMessage("getWalletInfo") + .then((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 (response?.hasKeyPair) { + setExtstate("authenticated"); + } else { + setExtstate("wallet-dropped"); + } + } + }) + .catch((error) => { + console.error("Failed to get wallet info:", error); + }); + }) + } catch (error) { + + } finally { + setIsLoading(false); + + } + }, []); useEffect(() => { if (extState) { @@ -497,6 +580,20 @@ function App() { } }, [extState]); + useEffect(()=> { + try { + const val = localStorage.getItem('settings-disable-editor-enter'); + if(val){ + const parsedVal = JSON.parse(val) + if(parsedVal === false || parsedVal === true){ + setIsDisabledEditorEnter(parsedVal) + } + } + } catch (error) { + + } + }, []) + useEffect(() => { isFocusedRef.current = isFocused; }, [isFocused]); @@ -573,8 +670,6 @@ function App() { setdecryptedWallet(null); } catch (e) { console.log(e); - - error = e; } }, }); @@ -583,7 +678,7 @@ function App() { let wallet = structuredClone(rawWallet); const res = await decryptStoredWallet(password, wallet); - const wallet2 = new PhraseWallet(res, walletVersion); + const wallet2 = new PhraseWallet(res, wallet?.version || walletVersion); wallet = await wallet2.generateSaveWalletData( password, crypto.kdfThreads, @@ -630,7 +725,8 @@ function App() { setLtcBalanceLoading(false); }); }; - const sendCoinFunc = () => { + const sendCoinFunc = async() => { + try { setSendPaymentError(""); setSendPaymentSuccess(""); if (!paymentTo) { @@ -645,6 +741,12 @@ function App() { setSendPaymentError("Please enter your wallet password"); return; } + const fee = await getFee('PAYMENT') + + await show({ + message: `Would you like to transfer ${Number(paymentAmount)} QORT?` , + paymentFee: fee.fee + ' QORT' + }) setIsLoading(true); window .sendMessage("sendCoin", { @@ -665,6 +767,9 @@ function App() { console.error("Failed to send coin:", error); setIsLoading(false); }); + } catch (error) { + // error + } }; const clearAllStates = () => { @@ -784,36 +889,6 @@ function App() { // REMOVED FOR MOBILE APP }; - useEffect(() => { - try { - setIsLoading(true); - - window - .sendMessage("getWalletInfo") - .then((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 (response?.hasKeyPair) { - setExtstate("authenticated"); - } else { - setExtstate("wallet-dropped"); - } - } - }) - .catch((error) => { - console.error("Failed to get wallet info:", error); - }); - } catch (error) { - } finally { - setIsLoading(false); - } - }, []); const getUserInfo = useCallback(async (useTimer?: boolean) => { try { @@ -970,6 +1045,11 @@ function App() { message: "Your settings have changed. If you logout you will lose your changes. Click on the save button in the header to keep your changed settings.", }); + } else if(extState === 'authenticated') { + await showUnsavedChanges({ + message: + "Are you sure you would like to logout?", + }); } window .sendMessage("logout", {}) @@ -997,6 +1077,8 @@ function App() { setCountdown(null); setWalletToBeDownloaded(null); setWalletToBeDownloadedPassword(""); + setShowSeed(false) + setCreationStep(1) setExtstate("authenticated"); setIsOpenSendQort(false); setIsOpenSendQortSuccess(false); @@ -1022,6 +1104,9 @@ function App() { setCountdown(null); setWalletToBeDownloaded(null); setWalletToBeDownloadedPassword(""); + setShowSeed(false) + setCreationStep(1) + setWalletToBeDownloadedPasswordConfirm(""); setWalletToBeDownloadedError(""); setSendqortState(null); @@ -1186,6 +1271,7 @@ function App() { const registerName = async () => { try { if (!userInfo?.address) throw new Error("Your address was not found"); + if(!registerNameValue) throw new Error('Enter a name') const fee = await getFee("REGISTER_NAME"); await show({ message: "Would you like to register this name?", @@ -1249,6 +1335,244 @@ function App() { } }; + const renderProfileLeft = ()=> { + + return + + + {authenticatedMode === "qort" && ( + LITECOIN WALLET} + placement="left" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > + { + + setAuthenticatedMode("ltc"); + }} + src={ltcLogo} + style={{ + cursor: "pointer", + width: "20px", + height: "auto", + }} + /> + + )} + {authenticatedMode === "ltc" && ( + QORTAL WALLET} + placement="left" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > + { + setAuthenticatedMode("qort"); + }} + src={qortLogo} + style={{ + cursor: "pointer", + width: "20px", + height: "auto", + }} + /> + + )} + + + + {authenticatedMode === "ltc" ? ( + <> + + + + + {rawWallet?.ltcAddress?.slice(0, 6)}... + {rawWallet?.ltcAddress?.slice(-4)} + + + + {ltcBalanceLoading && ( + + )} + {!isNaN(+ltcBalance) && !ltcBalanceLoading && ( + + + {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 + + )} + + { + setIsOpenSendQort(true); + // setExtstate("send-qort"); + setIsOpenDrawerProfile(false); + }} + > + Transfer QORT + + + + )} + { + executeEvent("addTab", { + data: { service: "APP", name: "q-trade" }, + }); + executeEvent("open-apps-mode", {}); + }} + > + Get QORT at Q-Trade + + + } + const renderProfile = () => { return ( - - - {authenticatedMode === "ltc" ? ( - <> - - - - - {rawWallet?.ltcAddress?.slice(0, 6)}... - {rawWallet?.ltcAddress?.slice(-4)} - - - - {ltcBalanceLoading && ( - - )} - {!isNaN(+ltcBalance) && !ltcBalanceLoading && ( - - - {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 - - )} - - { - setIsOpenSendQort(true); - // setExtstate("send-qort"); - setIsOpenDrawerProfile(false); - }} - > - Transfer QORT - - - - )} - { - executeEvent("addTab", { - data: { service: "APP", name: "q-trade" }, - }); - executeEvent("open-apps-mode", {}); - }} - > - Get QORT at Q-Trade - - + <> + {renderProfileLeft()} + )} - { - logoutFunc(); - setIsOpenDrawerProfile(false); + LOG OUT} + placement="left" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, }} - style={{ - cursor: "pointer", - width: '20px', - height: 'auto' - }} - /> + > + { + logoutFunc(); + setIsOpenDrawerProfile(false); + }} + style={{ + cursor: "pointer", + width: '20px', + height: 'auto' + }} + /> + )} @@ -1483,41 +1670,140 @@ function App() { setIsSettingsOpen(true); }} > - SETTINGS} + placement="left" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, }} - /> + > + + - - {authenticatedMode === "qort" && ( - { - setAuthenticatedMode("ltc"); + + {desktopViewMode !== 'home' && ( + <> + + + WALLET} + placement="left" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > + { + setIsOpenDrawerProfile(true); + }}> + + + + + + + )} + + {/* {authenticatedMode === "qort" && ( + LITECOIN WALLET} + placement="left" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, }} - src={ltcLogo} - style={{ - cursor: "pointer", - width: "20px", - height: "auto", - }} - /> + > + { + if(desktopViewMode !== 'home'){ + setIsOpenDrawerProfile((prev)=> !prev) + } + setAuthenticatedMode("ltc"); + }} + src={ltcLogo} + style={{ + cursor: "pointer", + width: "20px", + height: "auto", + }} + /> + )} {authenticatedMode === "ltc" && ( - { - setAuthenticatedMode("qort"); + QORTAL WALLET} + placement="left" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, }} - src={qortLogo} - style={{ - cursor: "pointer", - width: "20px", - height: "auto", - }} - /> - )} + > + { + setAuthenticatedMode("qort"); + }} + src={qortLogo} + style={{ + cursor: "pointer", + width: "20px", + height: "auto", + }} + /> + + )} */} + + + {extState === "authenticated" && isMainWindow && ( + + + + + + )} + + { + try { + const res = await isRunningGateway() + if(res) throw new Error('Cannot view minting details on the gateway') + setIsOpenMinting(true) + + } catch (error) { + setOpenSnack(true) + setInfoSnack({ + type: 'error', + message: error?.message + }) + } + }}> + MINTING STATUS} + placement="left" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > + + + + + + {(desktopViewMode === "apps" || desktopViewMode === "home") && ( + { + if(desktopViewMode === "apps"){ + showTutorial('qapps', true) + } else { + showTutorial('getting-started', true) + } + }} > + TUTORIAL} + placement="left" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > + + + + )} + + + BACKUP WALLET} + placement="left" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > { setExtstate("download-wallet"); @@ -1538,6 +1947,7 @@ function App() { width: '20px' }} /> + @@ -1555,6 +1965,14 @@ function App() { // backgroundRepeat: desktopViewMode === "apps" && "no-repeat", }} > + + {extState === "not-authenticated" && ( )} {/* {extState !== "not-authenticated" && ( @@ -1588,7 +2010,12 @@ function App() { setOpenSnackGlobal: setOpenSnack, infoSnackCustom: infoSnack, setInfoSnackCustom: setInfoSnack, - downloadResource + downloadResource, + getIndividualUserInfo, + isUserBlocked, + addToBlockList, + removeBlockFromList, + getAllBlockedUsers }} > - - - + )} {isOpenSendQort && isMainWindow && ( @@ -1716,12 +2133,14 @@ function App() { Amount - setPaymentAmount(+e.target.value)} - autoComplete="off" + minValue={0} + maxValue={+balance} + allowDecimals={true} + initialValue={'0'} + allowNegatives={false} + afterChange={(e: string) => setPaymentAmount(+e)} /> @@ -2124,8 +2543,7 @@ function App() { height: "154px", }} > - - + - - + - - + - + {useLocalNode ? ( + <> + + + {"Using node: "} {currentNode?.url} + + + ) : ( + <> + + + {"Using gateway"} + + + )} + + Authenticate @@ -2362,8 +2802,7 @@ function App() { height: "154px", }} > - - + setWalletToBeDownloadedPassword(e.target.value) + } /> @@ -2441,7 +2881,15 @@ function App() { cursor: "pointer", }} onClick={() => { + if(creationStep === 2){ + setCreationStep(1) + return + } setExtstate("not-authenticated"); + setShowSeed(false) + setCreationStep(1) + setWalletToBeDownloadedPasswordConfirm('') + setWalletToBeDownloadedPassword('') }} src={Return} /> @@ -2454,14 +2902,14 @@ function App() { height: "154px", }} > - - + Set up your Qortal account @@ -2473,33 +2921,110 @@ function App() { justifyContent: 'center', padding: '10px' }}> - Your seedphrase - Only shown once! Please copy and keep safe! + }}> + A ‘ { + setShowSeed(true) + }} style={{ + fontSize: '14px', + color: 'steelblue', + cursor: 'pointer' + }}>SEEDPHRASE ’ has been randomly generated in the background. - + + If you wish to VIEW THE SEEDPHRASE, click the word 'SEEDPHRASE' in this text. Seedphrases are used to generate the private key for your Qortal account. For security by default, seedphrases are NOT displayed unless specifically chosen. + + + Create your Qortal account by clicking NEXT below. + + + + { + setCreationStep(2) + }}> + Next + + +
+ +
+ + + + Your seedphrase + + + {generatorRef.current?.parsedString} - - Export Seedphrase +
+ + + + + + + + +
+ Wallet Password @@ -2524,11 +3049,14 @@ function App() { setWalletToBeDownloadedPasswordConfirm(e.target.value) } /> + + There is no minimum length requirement Create Account + {walletToBeDownloadedError} )} @@ -2546,7 +3074,17 @@ function App() { > Congrats, you’re all set up!
- + + + + Save your account in a place where you will remember it! + + { await saveFileToDiskFunc(); @@ -2673,21 +3211,49 @@ function App() { aria-labelledby="alert-dialog-title" aria-describedby="alert-dialog-description" > - {"Publish"} + {message.paymentFee ? "Payment" : "Publish"} {message.message} - - publish fee: {message.publishFee} - + {message?.paymentFee && ( + + payment fee: {message.paymentFee} + + )} + {message?.publishFee && ( + + publish fee: {message.publishFee} + + )} - - @@ -2898,6 +3464,24 @@ function App() { )} + {messageQortalRequestExtension?.appFee && ( + <> + + {"App Fee: "} + {messageQortalRequestExtension?.appFee} + {" QORT"} + + + + )} {messageQortalRequestExtension?.foreignFee && ( <> @@ -2921,7 +3505,6 @@ function App() { - onOkQortalRequestExtension("accepted")} > accept - - + onCancelQortalRequestExtension()} > decline - + {sendPaymentError}
@@ -3046,8 +3633,26 @@ function App() { open={isOpenDrawerProfile} setOpen={setIsOpenDrawerProfile} > - {renderProfile()} + {renderProfileLeft()} + +
+ {extState === "create-wallet" && walletToBeDownloaded && ( + { + showTutorial('important-information', true) + }} sx={{ + position: 'fixed', + bottom: '25px', + right: '25px' + }}> + + + )} + {isOpenMinting && ( + + )} ); } diff --git a/src/ExtStates/NotAuthenticated.tsx b/src/ExtStates/NotAuthenticated.tsx index d560e7f..f693a38 100644 --- a/src/ExtStates/NotAuthenticated.tsx +++ b/src/ExtStates/NotAuthenticated.tsx @@ -1,9 +1,10 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; +import React, { useCallback, useContext, useEffect, useRef, useState } from "react"; import { Spacer } from "../common/Spacer"; import { CustomButton, TextItalic, TextP, TextSpan } from "../App-styles"; import { Box, Button, + ButtonBase, Checkbox, Dialog, DialogActions, @@ -11,21 +12,41 @@ import { DialogTitle, FormControlLabel, Input, + styled, Switch, - Tooltip, Typography, } from "@mui/material"; import Logo1 from "../assets/svgs/Logo1.svg"; import Logo1Dark from "../assets/svgs/Logo1Dark.svg"; import Info from "../assets/svgs/Info.svg"; +import HelpIcon from '@mui/icons-material/Help'; import { CustomizedSnackbars } from "../components/Snackbar/Snackbar"; import { set } from "lodash"; -import { cleanUrl, isUsingLocal } from "../background"; +import { cleanUrl, gateways, isUsingLocal } from "../background"; +import { GlobalContext } from "../App"; +import Tooltip, { TooltipProps, tooltipClasses } from '@mui/material/Tooltip'; const manifestData = { - version: "0.3.7", + version: "0.5.2", }; + +export const HtmlTooltip = styled(({ className, ...props }: TooltipProps) => ( + +))(({ theme }) => ({ + [`& .${tooltipClasses.tooltip}`]: { + backgroundColor: '#232428', + color: 'white', + maxWidth: 320, + padding: '20px', + fontSize: theme.typography.pxToRem(12), + }, +})); +function removeTrailingSlash(url) { + return url.replace(/\/+$/, ''); +} + + export const NotAuthenticated = ({ getRootProps, getInputProps, @@ -35,24 +56,30 @@ export const NotAuthenticated = ({ setApiKey, globalApiKey, handleSetGlobalApikey, + currentNode, + setCurrentNode, + useLocalNode, + setUseLocalNode }) => { const [isValidApiKey, setIsValidApiKey] = useState(null); const [hasLocalNode, setHasLocalNode] = useState(null); - const [useLocalNode, setUseLocalNode] = useState(false); + // const [useLocalNode, setUseLocalNode] = useState(false); const [openSnack, setOpenSnack] = React.useState(false); const [infoSnack, setInfoSnack] = React.useState(null); const [show, setShow] = React.useState(false); const [mode, setMode] = React.useState("list"); const [customNodes, setCustomNodes] = React.useState(null); - const [currentNode, setCurrentNode] = React.useState({ - url: "http://127.0.0.1:12391", - }); + // const [currentNode, setCurrentNode] = React.useState({ + // url: "http://127.0.0.1:12391", + // }); const [importedApiKey, setImportedApiKey] = React.useState(null); //add and edit states - const [url, setUrl] = React.useState("http://"); + const [url, setUrl] = React.useState("https://"); const [customApikey, setCustomApiKey] = React.useState(""); const [customNodeToSaveIndex, setCustomNodeToSaveIndex] = React.useState(null); + const { showTutorial, hasSeenGettingStarted } = useContext(GlobalContext); + const importedApiKeyRef = useRef(null); const currentNodeRef = useRef(null); const hasLocalNodeRef = useRef(null); @@ -65,6 +92,34 @@ export const NotAuthenticated = ({ const text = e.target.result; // Get the file content setImportedApiKey(text); // Store the file content in the state + if(customNodes){ + setCustomNodes((prev)=> { + const copyPrev = [...prev] + const findLocalIndex = copyPrev?.findIndex((item)=> item?.url === 'http://127.0.0.1:12391') + if(findLocalIndex === -1){ + copyPrev.unshift({ + url: "http://127.0.0.1:12391", + apikey: text + }) + } else { + copyPrev[findLocalIndex] = { + url: "http://127.0.0.1:12391", + apikey: text + } + } + window + .sendMessage("setCustomNodes", copyPrev) + .catch((error) => { + console.error( + "Failed to set custom nodes:", + error.message || "An error occurred" + ); + }); + return copyPrev + }) + + } + }; reader.readAsText(file); // Read the file as text } @@ -82,8 +137,14 @@ export const NotAuthenticated = ({ const data = await response.json(); if (data?.height) { setHasLocalNode(true); + return true } - } catch (error) {} + return false + + } catch (error) { + return false + + } }, []); useEffect(() => { @@ -94,11 +155,18 @@ export const NotAuthenticated = ({ window .sendMessage("getCustomNodesFromStorage") .then((response) => { - if (response) { + setCustomNodes(response || []); - window.electronAPI.setAllowedDomains(response?.map((node)=> node.url)) - - } + if(window?.electronAPI?.setAllowedDomains){ + window.electronAPI.setAllowedDomains(response?.map((node)=> node.url)) + } + if(Array.isArray(response)){ + const findLocal = response?.find((item)=> item?.url === 'http://127.0.0.1:12391') + if(findLocal && findLocal?.apikey){ + setImportedApiKey(findLocal?.apikey) + } + } + }) .catch((error) => { console.error( @@ -119,13 +187,54 @@ export const NotAuthenticated = ({ hasLocalNodeRef.current = hasLocalNode; }, [hasLocalNode]); + + const validateApiKey = useCallback(async (key, fromStartUp) => { try { - if (!currentNodeRef.current) return; + if(key === "isGateway") return const isLocalKey = cleanUrl(key?.url) === "127.0.0.1:12391"; - if (isLocalKey && !hasLocalNodeRef.current && !fromStartUp) { + if (fromStartUp && key?.url && key?.apikey && !isLocalKey && !gateways.some(gateway => key?.url?.includes(gateway))) { + setCurrentNode({ + url: key?.url, + apikey: key?.apikey, + }); + + let isValid = false + + + const url = `${key?.url}/admin/settings/localAuthBypassEnabled`; + const response = await fetch(url); + + // Assuming the response is in plain text and will be 'true' or 'false' + const data = await response.text(); + if(data && data === 'true'){ + isValid = true + } else { + const url2 = `${key?.url}/admin/apikey/test?apiKey=${key?.apikey}`; + const response2 = await fetch(url2); + + // Assuming the response is in plain text and will be 'true' or 'false' + const data2 = await response2.text(); + if (data2 === "true") { + isValid = true + } + } + + if (isValid) { + setIsValidApiKey(true); + setUseLocalNode(true); + return + } + + } + if (!currentNodeRef.current) return; + const stillHasLocal = await checkIfUserHasLocalNode() + + if (isLocalKey && !stillHasLocal && !fromStartUp) { throw new Error("Please turn on your local node"); } + //check custom nodes + // !gateways.some(gateway => apiKey?.url?.includes(gateway)) const isCurrentNodeLocal = cleanUrl(currentNodeRef.current?.url) === "127.0.0.1:12391"; if (isLocalKey && !isCurrentNodeLocal) { @@ -143,18 +252,29 @@ export const NotAuthenticated = ({ } else if (currentNodeRef.current) { payload = currentNodeRef.current; } - const url = `${payload?.url}/admin/apikey/test`; - const response = await fetch(url, { - method: "GET", - headers: { - accept: "text/plain", - "X-API-KEY": payload?.apikey, // Include the API key here - }, - }); + let isValid = false + + + const url = `${payload?.url}/admin/settings/localAuthBypassEnabled`; + const response = await fetch(url); // Assuming the response is in plain text and will be 'true' or 'false' const data = await response.text(); - if (data === "true") { + if(data && data === 'true'){ + isValid = true + } else { + const url2 = `${payload?.url}/admin/apikey/test?apiKey=${payload?.apikey}`; + const response2 = await fetch(url2); + + // Assuming the response is in plain text and will be 'true' or 'false' + const data2 = await response2.text(); + if (data2 === "true") { + isValid = true + } + } + + + if (isValid) { window .sendMessage("setApiKey", payload) .then((response) => { @@ -176,20 +296,45 @@ export const NotAuthenticated = ({ } else { setIsValidApiKey(false); setUseLocalNode(false); - setInfoSnack({ - type: "error", - message: "Select a valid apikey", - }); - setOpenSnack(true); + if(!fromStartUp){ + setInfoSnack({ + type: "error", + message: "Select a valid apikey", + }); + setOpenSnack(true); + } + } } catch (error) { setIsValidApiKey(false); setUseLocalNode(false); + if (fromStartUp) { + setCurrentNode({ + url: "http://127.0.0.1:12391", + }); + window + .sendMessage("setApiKey", "isGateway") + .then((response) => { + if (response) { + setApiKey(null); + handleSetGlobalApikey(null); + } + }) + .catch((error) => { + console.error( + "Failed to set API key:", + error.message || "An error occurred" + ); + }); + return + } + if(!fromStartUp){ setInfoSnack({ type: "error", message: error?.message || "Select a valid apikey", }); setOpenSnack(true); + } console.error("Error validating API key:", error); } }, []); @@ -203,24 +348,22 @@ export const NotAuthenticated = ({ const addCustomNode = () => { setMode("add-node"); }; - - const saveCustomNodes = (myNodes) => { + const saveCustomNodes = (myNodes, isFullListOfNodes) => { let nodes = [...(myNodes || [])]; - if (customNodeToSaveIndex !== null) { + if (!isFullListOfNodes && customNodeToSaveIndex !== null) { nodes.splice(customNodeToSaveIndex, 1, { - url, + url: removeTrailingSlash(url), apikey: customApikey, }); - } else if (url && customApikey) { + } else if (!isFullListOfNodes && url) { nodes.push({ - url, + url: removeTrailingSlash(url), apikey: customApikey, }); } setCustomNodes(nodes); - window.electronAPI.setAllowedDomains(nodes?.map((node)=> node.url)) - + setCustomNodeToSaveIndex(null); if (!nodes) return; window @@ -228,8 +371,11 @@ export const NotAuthenticated = ({ .then((response) => { if (response) { setMode("list"); - setUrl("http://"); + setUrl("https://"); setCustomApiKey(""); + if(window?.electronAPI?.setAllowedDomains){ + window.electronAPI.setAllowedDomains(nodes?.map((node) => node.url)) + } // add alert if needed } }) @@ -251,19 +397,24 @@ export const NotAuthenticated = ({ height: "154px", }} > - - + - WELCOME TO YOUR

- QORTAL WALLET + WELCOME TO YOUR

+ QORTAL WALLET
+ + + Your wallet is like your digital ID on Qortal, and is how you will login to the Qortal User Interface. It holds your public address and the Qortal name you will eventually choose. Every transaction you make is linked to your ID, and this is where you manage all your QORT and other tradeable cryptocurrencies on Qortal. + + } > setExtstate('wallets')}> {/* */} Wallets + {/* */} @@ -287,16 +450,41 @@ export const NotAuthenticated = ({ display: "flex", gap: "10px", alignItems: "center", + }} + > + + New users start here! + + Creating an account means creating a new wallet and digital ID to start using Qortal. Once you have made your account, you can start doing things like obtaining some QORT, buying a name and avatar, publishing videos and blogs, and much more. + + } > { setExtstate("create-wallet"); }} + sx={{ + backgroundColor: hasSeenGettingStarted === false && 'var(--green)', + color: hasSeenGettingStarted === false && 'black', + "&:hover": { + backgroundColor: hasSeenGettingStarted === false && 'var(--green)', + color: hasSeenGettingStarted === false && 'black' + } + }} > - Create account + Create wallet - + @@ -317,9 +505,15 @@ export const NotAuthenticated = ({ gap: "10px", alignItems: "center", flexDirection: "column", + outline: '0.5px solid rgba(255, 255, 255, 0.5)', + padding: '20px 30px', + borderRadius: '5px', }} > <> + For advanced users item?.url !== node?.url); - saveCustomNodes(nodesToSave); + saveCustomNodes(nodesToSave, true); }} variant="contained" > @@ -648,7 +848,7 @@ export const NotAuthenticated = ({ - - - - + { + if (e.key === "Enter" && domain && port) { + onOk({ portVal: port, domainVal: domain }); + } + }} + > + + {"Add custom framework"} + + + + + setDomain(e.target.value)} + /> + + + + setPort(e.target.value)} + /> + + + + + + + )} ); diff --git a/src/components/Apps/AppsDevModeTabComponent.tsx b/src/components/Apps/AppsDevModeTabComponent.tsx index eee87e2..46372a5 100644 --- a/src/components/Apps/AppsDevModeTabComponent.tsx +++ b/src/components/Apps/AppsDevModeTabComponent.tsx @@ -1,59 +1,67 @@ -import React from 'react' -import { TabParent } from './Apps-styles' +import React from "react"; +import { TabParent } from "./Apps-styles"; import NavCloseTab from "../../assets/svgs/NavCloseTab.svg"; -import { getBaseApiReact } from '../../App'; -import { Avatar, ButtonBase } from '@mui/material'; +import { getBaseApiReact } from "../../App"; +import { Avatar, ButtonBase } from "@mui/material"; import LogoSelected from "../../assets/svgs/LogoSelected.svg"; -import { executeEvent } from '../../utils/events'; +import { executeEvent } from "../../utils/events"; -export const AppsDevModeTabComponent = ({isSelected, app}) => { +export const AppsDevModeTabComponent = ({ isSelected, app }) => { return ( - { - if(isSelected){ - executeEvent('removeTabDevMode', { - data: app - }) - return - } - executeEvent('setSelectedTabDevMode', { + { + if (isSelected) { + executeEvent("removeTabDevMode", { data: app, - isDevMode: true - }) - }}> - + }); + return; + } + executeEvent("setSelectedTabDevMode", { + data: app, + isDevMode: true, + }); + }} + > + {isSelected && ( - - - - ) } - - center-icon - - + + )} + + center-icon + + - ) -} - + ); +}; diff --git a/src/components/Apps/AppsHomeDesktop.tsx b/src/components/Apps/AppsHomeDesktop.tsx index 8856a34..32f5abe 100644 --- a/src/components/Apps/AppsHomeDesktop.tsx +++ b/src/components/Apps/AppsHomeDesktop.tsx @@ -16,11 +16,13 @@ import { Spacer } from "../../common/Spacer"; import { SortablePinnedApps } from "./SortablePinnedApps"; import { extractComponents } from "../Chat/MessageDisplay"; import ArrowOutwardIcon from '@mui/icons-material/ArrowOutward'; +import { AppsPrivate } from "./AppsPrivate"; export const AppsHomeDesktop = ({ setMode, myApp, myWebsite, availableQapps, + myName }) => { const [qortalUrl, setQortalUrl] = useState('') @@ -115,7 +117,7 @@ export const AppsHomeDesktop = ({ @@ -135,7 +137,7 @@ export const AppsHomeDesktop = ({ Library - + { setDebouncedValue(searchValue); }, 350); - + setTimeout(() => { + virtuosoRef.current.scrollToIndex({ + index: 0 + }); + }, 500); // Cleanup timeout if searchValue changes before the timeout completes return () => { clearTimeout(handler); @@ -134,7 +139,7 @@ export const AppsLibraryDesktop = ({ const searchedList = useMemo(() => { if (!debouncedValue) return []; return availableQapps.filter((app) => - app.name.toLowerCase().includes(debouncedValue.toLowerCase()) + app.name.toLowerCase().includes(debouncedValue.toLowerCase()) || (app?.metadata?.title && app?.metadata?.title?.toLowerCase().includes(debouncedValue.toLowerCase())) ); }, [debouncedValue]); @@ -145,6 +150,9 @@ export const AppsLibraryDesktop = ({ key={`${app?.service}-${app?.name}`} app={app} myName={myName} + parentStyles={{ + padding: '0px 10px' + }} /> ); }; @@ -261,7 +269,7 @@ export const AppsLibraryDesktop = ({ + ) : searchedList?.length === 0 && debouncedValue ? ( + + No results + ) : ( <> - + {officialApps?.map((qapp) => { return ( { // executeEvent("addTab", { @@ -315,8 +330,8 @@ export const AppsLibraryDesktop = ({ > + { + executeEvent("selectedCategory", { + data: { + id: 'all', + name: 'All' + }, + }); + }} + > + + All + + {categories?.map((category) => { return ( { - const isSelectedAppPinned = !!sortablePinnedApps?.find( - (item) => - item?.name === selectedTab?.name && item?.service === selectedTab?.service - ); + const isSelectedAppPinned = useMemo(()=> { + if(selectedTab?.isPrivate){ + return !!sortablePinnedApps?.find( + (item) => + item?.privateAppProperties?.name === selectedTab?.privateAppProperties?.name && item?.privateAppProperties?.service === selectedTab?.privateAppProperties?.service && item?.privateAppProperties?.identifier === selectedTab?.privateAppProperties?.identifier + ); + } else { + return !!sortablePinnedApps?.find( + (item) => + item?.name === selectedTab?.name && item?.service === selectedTab?.service + ); + } + }, [selectedTab,sortablePinnedApps]) return ( { if (isSelectedAppPinned) { // Remove the selected app if it is pinned - updatedApps = prev.filter( - (item) => - !( - item?.name === selectedTab?.name && - item?.service === selectedTab?.service - ) - ); + if(selectedTab?.isPrivate){ + updatedApps = prev.filter( + (item) => + !( + item?.privateAppProperties?.name === selectedTab?.privateAppProperties?.name && + item?.privateAppProperties?.service === selectedTab?.privateAppProperties?.service && + item?.privateAppProperties?.identifier === selectedTab?.privateAppProperties?.identifier + ) + ); + } else { + updatedApps = prev.filter( + (item) => + !( + item?.name === selectedTab?.name && + item?.service === selectedTab?.service + ) + ); + } + } else { // Add the selected app if it is not pinned - updatedApps = [ + if(selectedTab?.isPrivate){ + updatedApps = [ ...prev, { - name: selectedTab?.name, - service: selectedTab?.service, + isPreview: true, + isPrivate: true, + privateAppProperties: { + ...(selectedTab?.privateAppProperties || {}) + } + }, ]; + } else { + updatedApps = [ + ...prev, + { + name: selectedTab?.name, + service: selectedTab?.service, + }, + ]; + } + } saveToLocalStorage( @@ -321,7 +357,7 @@ export const AppsNavBarDesktop = ({disableBack}) => { @@ -330,7 +366,7 @@ export const AppsNavBarDesktop = ({disableBack}) => { "& .MuiTypography-root": { fontSize: "12px", fontWeight: 600, - color: isSelectedAppPinned ? "red" : "rgba(250, 250, 250, 0.5)", + color: isSelectedAppPinned ? "var(--danger)" : "rgba(250, 250, 250, 0.5)", }, }} primary={`${isSelectedAppPinned ? "Unpin app" : "Pin app"}`} @@ -338,9 +374,15 @@ export const AppsNavBarDesktop = ({disableBack}) => { { - executeEvent("refreshApp", { - tabId: selectedTab?.tabId, - }); + if (selectedTab?.refreshFunc) { + selectedTab.refreshFunc(selectedTab?.tabId); + + } else { + executeEvent("refreshApp", { + tabId: selectedTab?.tabId, + }); + } + handleClose(); }} > @@ -368,38 +410,40 @@ export const AppsNavBarDesktop = ({disableBack}) => { primary="Refresh" /> - { - executeEvent("copyLink", { - tabId: selectedTab?.tabId, - }); - handleClose(); - }} - > - { + executeEvent("copyLink", { + tabId: selectedTab?.tabId, + }); + handleClose(); }} > - + + + - - - + + )} ); diff --git a/src/components/Apps/AppsPrivate.tsx b/src/components/Apps/AppsPrivate.tsx new file mode 100644 index 0000000..b014cd2 --- /dev/null +++ b/src/components/Apps/AppsPrivate.tsx @@ -0,0 +1,542 @@ +import React, { useContext, useState } from "react"; +import { + Avatar, + Box, + Button, + ButtonBase, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Input, + MenuItem, + Select, + Tab, + Tabs, + Typography, +} from "@mui/material"; +import { useDropzone } from "react-dropzone"; +import { useHandlePrivateApps } from "./useHandlePrivateApps"; +import { useRecoilState, useSetRecoilState } from "recoil"; +import { myGroupsWhereIAmAdminAtom } from "../../atoms/global"; +import { Label } from "../Group/AddGroup"; +import { Spacer } from "../../common/Spacer"; +import { + Add, + AppCircle, + AppCircleContainer, + AppCircleLabel, + PublishQAppChoseFile, + PublishQAppInfo, +} from "./Apps-styles"; +import ImageUploader from "../../common/ImageUploader"; +import { isMobile, MyContext } from "../../App"; +import { fileToBase64 } from "../../utils/fileReading"; +import { objectToBase64 } from "../../qdn/encryption/group-encryption"; +import { getFee } from "../../background"; + +const maxFileSize = 50 * 1024 * 1024; // 50MB + +export const AppsPrivate = ({myName}) => { + const { openApp } = useHandlePrivateApps(); + const [file, setFile] = useState(null); + const [logo, setLogo] = useState(null); + const [qortalUrl, setQortalUrl] = useState(""); + const [selectedGroup, setSelectedGroup] = useState(0); + + const [valueTabPrivateApp, setValueTabPrivateApp] = useState(0); + const [myGroupsWhereIAmAdmin, setMyGroupsWhereIAmAdmin] = useRecoilState( + myGroupsWhereIAmAdminAtom + ); + const [isOpenPrivateModal, setIsOpenPrivateModal] = useState(false); + const { show, setInfoSnackCustom, setOpenSnackGlobal, memberGroups } = useContext(MyContext); + + const [privateAppValues, setPrivateAppValues] = useState({ + name: "", + service: "DOCUMENT", + identifier: "", + groupId: 0, + }); + + const [newPrivateAppValues, setNewPrivateAppValues] = useState({ + service: "DOCUMENT", + identifier: "", + name: "", + }); + const { getRootProps, getInputProps } = useDropzone({ + accept: { + "application/zip": [".zip"], // Only accept zip files + }, + maxSize: maxFileSize, + multiple: false, // Disable multiple file uploads + onDrop: (acceptedFiles) => { + if (acceptedFiles.length > 0) { + setFile(acceptedFiles[0]); // Set the file name + } + }, + onDropRejected: (fileRejections) => { + fileRejections.forEach(({ file, errors }) => { + errors.forEach((error) => { + if (error.code === "file-too-large") { + console.error( + `File ${file.name} is too large. Max size allowed is ${ + maxFileSize / (1024 * 1024) + } MB.` + ); + } + }); + }); + }, + }); + + const addPrivateApp = async () => { + try { + if (privateAppValues?.groupId === 0) return; + + await openApp(privateAppValues, true); + } catch (error) { + console.log('error', error?.message) + + } + }; + + const clearFields = () => { + setPrivateAppValues({ + name: "", + service: "DOCUMENT", + identifier: "", + groupId: 0, + }); + setNewPrivateAppValues({ + service: "DOCUMENT", + identifier: "", + name: "", + }); + setFile(null); + setValueTabPrivateApp(0); + setSelectedGroup(0); + setLogo(null); + }; + + const publishPrivateApp = async () => { + try { + if (selectedGroup === 0) return; + if (!logo) throw new Error("Please select an image for a logo"); + if (!myName) throw new Error("You need a Qortal name to publish"); + if (!newPrivateAppValues?.name) throw new Error("Your app needs a name"); + const base64Logo = await fileToBase64(logo); + const base64App = await fileToBase64(file); + const objectToSave = { + app: base64App, + logo: base64Logo, + name: newPrivateAppValues.name, + }; + const object64 = await objectToBase64(objectToSave); + const decryptedData = await window.sendMessage( + "ENCRYPT_QORTAL_GROUP_DATA", + + { + base64: object64, + groupId: selectedGroup, + } + ); + if (decryptedData?.error) { + throw new Error( + decryptedData?.error || "Unable to encrypt app. App not published" + ); + } + const fee = await getFee("ARBITRARY"); + + await show({ + message: "Would you like to publish this app?", + publishFee: fee.fee + " QORT", + }); + await new Promise((res, rej) => { + window + .sendMessage("publishOnQDN", { + data: decryptedData, + identifier: newPrivateAppValues?.identifier, + service: newPrivateAppValues?.service, + }) + .then((response) => { + if (!response?.error) { + res(response); + return; + } + rej(response.error); + }) + .catch((error) => { + rej(error.message || "An error occurred"); + }); + }); + openApp( + { + identifier: newPrivateAppValues?.identifier, + service: newPrivateAppValues?.service, + name: myName, + groupId: selectedGroup, + }, + true + ); + clearFields(); + } catch (error) { + setOpenSnackGlobal(true) + setInfoSnackCustom({ + type: "error", + message: error?.message || "Unable to publish app", + }); + } + }; + + const handleChange = (event: React.SyntheticEvent, newValue: number) => { + setValueTabPrivateApp(newValue); + }; + + function a11yProps(index: number) { + return { + id: `simple-tab-${index}`, + "aria-controls": `simple-tabpanel-${index}`, + }; + } + return ( + <> + { + setIsOpenPrivateModal(true); + }} + sx={{ + width: "80px", + }} + > + + + + + + Private + + + {isOpenPrivateModal && ( + { + if (e.key === "Enter") { + if (valueTabPrivateApp === 0) { + if ( + !privateAppValues.name || + !privateAppValues.service || + !privateAppValues.identifier || + !privateAppValues?.groupId + ) + return; + addPrivateApp(); + } + } + }} + maxWidth="md" + fullWidth={true} + > + + {valueTabPrivateApp === 0 + ? "Access private app" + : "Publish private app"} + + + + + + + + + {valueTabPrivateApp === 0 && ( + <> + + + + + + + + + + + setPrivateAppValues((prev) => { + return { + ...prev, + name: e.target.value, + }; + }) + } + /> + + + + + setPrivateAppValues((prev) => { + return { + ...prev, + identifier: e.target.value, + }; + }) + } + /> + + + + + + + + )} + {valueTabPrivateApp === 1 && ( + <> + + + Select .zip file containing static content:{" "} + + + {` + 50mb MB maximum`} + {file && ( + <> + + {`Selected: (${file?.name})`} + + )} + + + + {" "} + + {file ? "Change" : "Choose"} File + + + + + + + + + + + + + setNewPrivateAppValues((prev) => { + return { + ...prev, + identifier: e.target.value, + }; + }) + } + /> + + + + + + setNewPrivateAppValues((prev) => { + return { + ...prev, + name: e.target.value, + }; + }) + } + /> + + + + setLogo(file)}> + + + {logo?.name} + + + + + + + + )} + + )} + + ); +}; diff --git a/src/components/Apps/SortablePinnedApps.tsx b/src/components/Apps/SortablePinnedApps.tsx index 1a44a2f..98c2287 100644 --- a/src/components/Apps/SortablePinnedApps.tsx +++ b/src/components/Apps/SortablePinnedApps.tsx @@ -1,18 +1,21 @@ -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'; import { DndContext, closestCenter } from '@dnd-kit/core'; import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable } from '@dnd-kit/sortable'; import { KeyboardSensor, PointerSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core'; import { CSS } from '@dnd-kit/utilities'; import { Avatar, ButtonBase } from '@mui/material'; import { AppCircle, AppCircleContainer, AppCircleLabel } from './Apps-styles'; -import { getBaseApiReact } from '../../App'; +import { getBaseApiReact, MyContext } from '../../App'; import { executeEvent } from '../../utils/events'; import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from '../../atoms/global'; import { useRecoilState, useSetRecoilState } from 'recoil'; import { saveToLocalStorage } from './AppsNavBar'; import { ContextMenuPinnedApps } from '../ContextMenuPinnedApps'; - +import LockIcon from "@mui/icons-material/Lock"; +import { useHandlePrivateApps } from './useHandlePrivateApps'; const SortableItem = ({ id, name, app, isDesktop }) => { + const {openApp} = useHandlePrivateApps() + const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id }); const style = { transform: CSS.Transform.toString(transform), @@ -28,18 +31,27 @@ const SortableItem = ({ id, name, app, isDesktop }) => { return ( - { - executeEvent("addTab", { - data: app - }) + onClick={async ()=> { + if(app?.isPrivate){ + try { + await openApp(app?.privateAppProperties) + } catch (error) { + console.error(error) + } + + } else { + executeEvent("addTab", { + data: app + }) + } + }} > { border: "none", }} > - + ) : ( + @@ -73,10 +93,19 @@ const SortableItem = ({ id, name, app, isDesktop }) => { alt="center-icon" /> + )} + - + {app?.isPrivate ? ( + + {`${app?.privateAppProperties?.appName || "Private"}`} + + ) : ( + {app?.metadata?.title || app?.name} + )} + diff --git a/src/components/Apps/TabComponent.tsx b/src/components/Apps/TabComponent.tsx index aca6b55..ecf17a7 100644 --- a/src/components/Apps/TabComponent.tsx +++ b/src/components/Apps/TabComponent.tsx @@ -5,6 +5,7 @@ import { getBaseApiReact } from '../../App'; import { Avatar, ButtonBase } from '@mui/material'; import LogoSelected from "../../assets/svgs/LogoSelected.svg"; import { executeEvent } from '../../utils/events'; +import LockIcon from "@mui/icons-material/Lock"; const TabComponent = ({isSelected, app}) => { return ( @@ -34,25 +35,34 @@ const TabComponent = ({isSelected, app}) => { } src={NavCloseTab}/> ) } - - center-icon - + {app?.isPrivate && !app?.privateAppProperties?.logo ? ( + + ) : ( + + center-icon + + )} ) diff --git a/src/components/Apps/useHandlePrivateApps.tsx b/src/components/Apps/useHandlePrivateApps.tsx new file mode 100644 index 0000000..2eaa5f9 --- /dev/null +++ b/src/components/Apps/useHandlePrivateApps.tsx @@ -0,0 +1,237 @@ +import React, { useContext, useState } from "react"; +import { executeEvent } from "../../utils/events"; +import { getBaseApiReact, MyContext } from "../../App"; +import { createEndpoint } from "../../background"; +import { useRecoilState, useSetRecoilState } from "recoil"; +import { + settingsLocalLastUpdatedAtom, + sortablePinnedAppsAtom, +} from "../../atoms/global"; +import { saveToLocalStorage } from "./AppsNavBarDesktop"; +import { base64ToBlobUrl } from "../../utils/fileReading"; +import { base64ToUint8Array } from "../../qdn/encryption/group-encryption"; +import { uint8ArrayToObject } from "../../backgroundFunctions/encryption"; + +export const useHandlePrivateApps = () => { + const [status, setStatus] = useState(""); + const { + openSnackGlobal, + setOpenSnackGlobal, + infoSnackCustom, + setInfoSnackCustom, + } = useContext(MyContext); + const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState( + sortablePinnedAppsAtom + ); + const setSettingsLocalLastUpdated = useSetRecoilState( + settingsLocalLastUpdatedAtom + ); + const openApp = async ( + privateAppProperties, + addToPinnedApps, + setLoadingStatePrivateApp + ) => { + try { + + + if(setLoadingStatePrivateApp){ + setLoadingStatePrivateApp(`Downloading and decrypting private app.`); + + } + setOpenSnackGlobal(true); + + setInfoSnackCustom({ + type: "info", + message: "Fetching app data", + duration: null + }); + const urlData = `${getBaseApiReact()}/arbitrary/${ + privateAppProperties?.service + }/${privateAppProperties?.name}/${ + privateAppProperties?.identifier + }?encoding=base64`; + let data; + try { + const responseData = await fetch(urlData, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if(!responseData?.ok){ + if(setLoadingStatePrivateApp){ + setLoadingStatePrivateApp("Error! Unable to download private app."); + } + + throw new Error("Unable to fetch app"); + } + + data = await responseData.text(); + if (data?.error) { + if(setLoadingStatePrivateApp){ + + setLoadingStatePrivateApp("Error! Unable to download private app."); + } + throw new Error("Unable to fetch app"); + } + } catch (error) { + if(setLoadingStatePrivateApp){ + + setLoadingStatePrivateApp("Error! Unable to download private app."); + } + throw error; + } + + let decryptedData; + // eslint-disable-next-line no-useless-catch + try { + decryptedData = await window.sendMessage( + "DECRYPT_QORTAL_GROUP_DATA", + + { + base64: data, + groupId: privateAppProperties?.groupId, + } + ); + if (decryptedData?.error) { + if(setLoadingStatePrivateApp){ + + setLoadingStatePrivateApp("Error! Unable to decrypt private app."); + } + throw new Error(decryptedData?.error); + } + } catch (error) { + if(setLoadingStatePrivateApp){ + + setLoadingStatePrivateApp("Error! Unable to decrypt private app."); + } + throw error; + } + + try { + const convertToUint = base64ToUint8Array(decryptedData); + const UintToObject = uint8ArrayToObject(convertToUint); + + if (decryptedData) { + setInfoSnackCustom({ + type: "info", + message: "Building app", + }); + const endpoint = await createEndpoint( + `/arbitrary/APP/${privateAppProperties?.name}/zip?preview=true` + ); + const response = await fetch(endpoint, { + method: "POST", + headers: { + "Content-Type": "text/plain", + }, + body: UintToObject?.app, + }); + const previewPath = await response.text(); + const refreshfunc = async (tabId, privateAppProperties) => { + const checkIfPreviewLinkStillWorksUrl = await createEndpoint( + `/render/hash/HmtnZpcRPwisMfprUXuBp27N2xtv5cDiQjqGZo8tbZS?secret=E39WTiG4qBq3MFcMPeRZabtQuzyfHg9ZuR5SgY7nW1YH` + ); + const res = await fetch(checkIfPreviewLinkStillWorksUrl); + if (res.ok) { + executeEvent("refreshApp", { + tabId: tabId, + }); + } else { + const endpoint = await createEndpoint( + `/arbitrary/APP/${privateAppProperties?.name}/zip?preview=true` + ); + const response = await fetch(endpoint, { + method: "POST", + headers: { + "Content-Type": "text/plain", + }, + body: UintToObject?.app, + }); + const previewPath = await response.text(); + executeEvent("updateAppUrl", { + tabId: tabId, + url: await createEndpoint(previewPath), + }); + + setTimeout(() => { + executeEvent("refreshApp", { + tabId: tabId, + }); + }, 300); + } + }; + + const appName = UintToObject?.name; + const logo = UintToObject?.logo + ? `data:image/png;base64,${UintToObject?.logo}` + : null; + + const dataBody = { + url: await createEndpoint(previewPath), + isPreview: true, + isPrivate: true, + privateAppProperties: { ...privateAppProperties, logo, appName }, + filePath: "", + refreshFunc: (tabId) => { + refreshfunc(tabId, privateAppProperties); + }, + }; + executeEvent("addTab", { + data: dataBody, + }); + setInfoSnackCustom({ + type: "success", + message: "Opened", + }); + if(setLoadingStatePrivateApp){ + + setLoadingStatePrivateApp(``); + } + if (addToPinnedApps) { + setSortablePinnedApps((prev) => { + const updatedApps = [ + ...prev, + { + isPrivate: true, + isPreview: true, + privateAppProperties: { + ...privateAppProperties, + logo, + appName, + }, + }, + ]; + + saveToLocalStorage( + "ext_saved_settings", + "sortablePinnedApps", + updatedApps + ); + return updatedApps; + }); + setSettingsLocalLastUpdated(Date.now()); + } + } + } catch (error) { + if(setLoadingStatePrivateApp){ + + setLoadingStatePrivateApp(`Error! ${error?.message || 'Unable to build private app.'}`); + } + throw error + } + } + catch (error) { + setInfoSnackCustom({ + type: "error", + message: error?.message || "Unable to fetch app", + }); + } + + }; + return { + openApp, + status, + }; +}; diff --git a/src/components/Apps/useQortalMessageListener.tsx b/src/components/Apps/useQortalMessageListener.tsx index 00bff36..635aba6 100644 --- a/src/components/Apps/useQortalMessageListener.tsx +++ b/src/components/Apps/useQortalMessageListener.tsx @@ -174,16 +174,55 @@ export function openIndexedDB() { } - -const UIQortalRequests = [ +export const listOfAllQortalRequests = [ 'GET_USER_ACCOUNT', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS', 'ADD_LIST_ITEMS', 'DELETE_LIST_ITEM', 'VOTE_ON_POLL', 'CREATE_POLL', 'SEND_CHAT_MESSAGE', 'JOIN_GROUP', 'DEPLOY_AT', 'GET_USER_WALLET', 'GET_WALLET_BALANCE', 'GET_USER_WALLET_INFO', 'GET_CROSSCHAIN_SERVER_INFO', 'GET_TX_ACTIVITY_SUMMARY', 'GET_FOREIGN_FEE', 'UPDATE_FOREIGN_FEE', 'GET_SERVER_CONNECTION_HISTORY', 'SET_CURRENT_FOREIGN_SERVER', - 'ADD_FOREIGN_SERVER', 'REMOVE_FOREIGN_SERVER', 'GET_DAY_SUMMARY', 'CREATE_TRADE_BUY_ORDER', 'CREATE_TRADE_SELL_ORDER', 'CANCEL_TRADE_SELL_ORDER', 'IS_USING_GATEWAY', 'ADMIN_ACTION', 'SIGN_TRANSACTION', 'OPEN_NEW_TAB', 'CREATE_AND_COPY_EMBED_LINK', 'DECRYPT_QORTAL_GROUP_DATA' + 'ADD_FOREIGN_SERVER', 'REMOVE_FOREIGN_SERVER', 'GET_DAY_SUMMARY', 'CREATE_TRADE_BUY_ORDER', 'CREATE_TRADE_SELL_ORDER', 'CANCEL_TRADE_SELL_ORDER', 'IS_USING_PUBLIC_NODE', 'ADMIN_ACTION', 'SIGN_TRANSACTION', 'OPEN_NEW_TAB', 'CREATE_AND_COPY_EMBED_LINK', 'DECRYPT_QORTAL_GROUP_DATA', 'DECRYPT_DATA_WITH_SHARING_KEY', 'DELETE_HOSTED_DATA', 'GET_HOSTED_DATA', 'PUBLISH_MULTIPLE_QDN_RESOURCES', + 'PUBLISH_QDN_RESOURCE', + 'ENCRYPT_DATA', + 'ENCRYPT_DATA_WITH_SHARING_KEY', + 'ENCRYPT_QORTAL_GROUP_DATA', + 'SAVE_FILE', + 'GET_ACCOUNT_DATA', + 'GET_ACCOUNT_NAMES', + 'SEARCH_NAMES', + 'GET_NAME_DATA', + 'GET_QDN_RESOURCE_URL', + 'LINK_TO_QDN_RESOURCE', + 'LIST_QDN_RESOURCES', + 'SEARCH_QDN_RESOURCES', + 'FETCH_QDN_RESOURCE', + 'GET_QDN_RESOURCE_STATUS', + 'GET_QDN_RESOURCE_PROPERTIES', + 'GET_QDN_RESOURCE_METADATA', + 'SEARCH_CHAT_MESSAGES', + 'LIST_GROUPS', + 'GET_BALANCE', + 'GET_AT', + 'GET_AT_DATA', + 'LIST_ATS', + 'FETCH_BLOCK', + 'FETCH_BLOCK_RANGE', + 'SEARCH_TRANSACTIONS', + 'GET_PRICE', + 'SHOW_ACTIONS', + 'GET_USER_WALLET_TRANSACTIONS' +] + +export const UIQortalRequests = [ + 'GET_USER_ACCOUNT', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS', + 'ADD_LIST_ITEMS', 'DELETE_LIST_ITEM', 'VOTE_ON_POLL', 'CREATE_POLL', + 'SEND_CHAT_MESSAGE', 'JOIN_GROUP', 'DEPLOY_AT', 'GET_USER_WALLET', + 'GET_WALLET_BALANCE', 'GET_USER_WALLET_INFO', 'GET_CROSSCHAIN_SERVER_INFO', + 'GET_TX_ACTIVITY_SUMMARY', 'GET_FOREIGN_FEE', 'UPDATE_FOREIGN_FEE', + 'GET_SERVER_CONNECTION_HISTORY', 'SET_CURRENT_FOREIGN_SERVER', + 'ADD_FOREIGN_SERVER', 'REMOVE_FOREIGN_SERVER', 'GET_DAY_SUMMARY', 'CREATE_TRADE_BUY_ORDER', 'CREATE_TRADE_SELL_ORDER', 'CANCEL_TRADE_SELL_ORDER', 'IS_USING_PUBLIC_NODE', 'ADMIN_ACTION', 'SIGN_TRANSACTION', 'OPEN_NEW_TAB', 'CREATE_AND_COPY_EMBED_LINK', 'DECRYPT_QORTAL_GROUP_DATA', 'DECRYPT_DATA_WITH_SHARING_KEY', 'DELETE_HOSTED_DATA', 'GET_HOSTED_DATA', 'SHOW_ACTIONS', 'REGISTER_NAME', 'UPDATE_NAME', 'LEAVE_GROUP', 'INVITE_TO_GROUP', 'KICK_FROM_GROUP', 'BAN_FROM_GROUP', 'CANCEL_GROUP_BAN', 'ADD_GROUP_ADMIN', 'REMOVE_GROUP_ADMIN', 'DECRYPT_AESGCM', 'CANCEL_GROUP_INVITE', 'CREATE_GROUP', 'GET_USER_WALLET_TRANSACTIONS' ]; +// TODO listOfAllQortalRequests @@ -320,7 +359,7 @@ const UIQortalRequests = [ // Handle the obj.file if it exists and is a File instance if (obj.file) { - const fileId = "objFile_qortalfile"; + const fileId = Date.now() + "objFile_qortalfile"; // Store the file in IndexedDB const fileData = { @@ -334,7 +373,7 @@ const UIQortalRequests = [ delete obj.file; } if (obj.blob) { - const fileId = "objFile_qortalfile"; + const fileId = Date.now() + "objFile_qortalfile"; // Store the file in IndexedDB const fileData = { @@ -355,8 +394,8 @@ const UIQortalRequests = [ // Iterate through resources to find files and save them to IndexedDB for (let resource of (obj?.resources || [])) { if (resource.file) { - const fileId = resource.identifier + "_qortalfile"; - + const fileId = resource.identifier + Date.now() + "_qortalfile"; + // Store the file in IndexedDB const fileData = { id: fileId, @@ -444,7 +483,10 @@ isDOMContentLoaded: false if (response.error) { eventPort.postMessage({ result: null, - error: response, + error: { + error: response?.error, + message: typeof response?.error === 'string' ? response?.error : 'An error has occurred' + }, }); } else { eventPort.postMessage({ @@ -484,17 +526,8 @@ isDOMContentLoaded: false event?.data?.action === 'ENCRYPT_DATA' || event?.data?.action === 'ENCRYPT_DATA_WITH_SHARING_KEY' || event?.data?.action === 'ENCRYPT_QORTAL_GROUP_DATA' ) { - let data; - try { - data = await storeFilesInIndexedDB(event.data); - } catch (error) { - console.error('Error storing files in IndexedDB:', error); - event.ports[0].postMessage({ - result: null, - error: 'Failed to store files in IndexedDB', - }); - return; - } + const data = event.data; + if (data) { sendMessageToRuntime( { action: event.data.action, type: 'qortalRequest', payload: data, isExtension: true }, diff --git a/src/components/Chat/AdminSpace.tsx b/src/components/Chat/AdminSpace.tsx index f340e45..6eaf541 100644 --- a/src/components/Chat/AdminSpace.tsx +++ b/src/components/Chat/AdminSpace.tsx @@ -27,7 +27,8 @@ export const AdminSpace = ({ myAddress, hide, defaultThread, - setDefaultThread + setDefaultThread, + setIsForceShowCreationKeyPopup }) => { const { rootHeight } = useContext(MyContext); const [isMoved, setIsMoved] = useState(false); @@ -59,7 +60,7 @@ export const AdminSpace = ({ justifyContent: 'center', paddingTop: '25px' }}>Sorry, this space is only for Admins.} - {isAdmin && } + {isAdmin && } ); diff --git a/src/components/Chat/AdminSpaceInner.tsx b/src/components/Chat/AdminSpaceInner.tsx index 1307ad1..fa70120 100644 --- a/src/components/Chat/AdminSpaceInner.tsx +++ b/src/components/Chat/AdminSpaceInner.tsx @@ -1,150 +1,248 @@ -import React, { useCallback, useContext, useEffect, useState } from 'react' -import { MyContext, getArbitraryEndpointReact, getBaseApiReact } from '../../App'; -import { Box, Button, Typography } from '@mui/material'; -import { decryptResource, validateSecretKey } from '../Group/Group'; -import { getFee } from '../../background'; -import { base64ToUint8Array } from '../../qdn/encryption/group-encryption'; -import { uint8ArrayToObject } from '../../backgroundFunctions/encryption'; -import { formatTimestampForum } from '../../utils/time'; -import { Spacer } from '../../common/Spacer'; +import React, { useCallback, useContext, useEffect, useState } from "react"; +import { + MyContext, + getArbitraryEndpointReact, + getBaseApiReact, +} from "../../App"; +import { Box, Button, Typography } from "@mui/material"; +import { + decryptResource, + getPublishesFromAdmins, + validateSecretKey, +} from "../Group/Group"; +import { getFee } from "../../background"; +import { base64ToUint8Array } from "../../qdn/encryption/group-encryption"; +import { uint8ArrayToObject } from "../../backgroundFunctions/encryption"; +import { formatTimestampForum } from "../../utils/time"; +import { Spacer } from "../../common/Spacer"; +export const getPublishesFromAdminsAdminSpace = async ( + admins: string[], + groupId +) => { + const queryString = admins.map((name) => `name=${name}`).join("&"); + const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT_PRIVATE&identifier=admins-symmetric-qchat-group-${groupId}&exactmatchnames=true&limit=0&reverse=true&${queryString}&prefix=true`; + const response = await fetch(url); + if (!response.ok) { + throw new Error("network error"); + } + const adminData = await response.json(); -export const getPublishesFromAdminsAdminSpace = async (admins: string[], groupId) => { - const queryString = admins.map((name) => `name=${name}`).join("&"); - const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT_PRIVATE&identifier=admins-symmetric-qchat-group-${ - groupId - }&exactmatchnames=true&limit=0&reverse=true&${queryString}&prefix=true`; - const response = await fetch(url); - if (!response.ok) { - throw new Error("network error"); + const filterId = adminData.filter( + (data: any) => data.identifier === `admins-symmetric-qchat-group-${groupId}` + ); + if (filterId?.length === 0) { + return false; + } + const sortedData = filterId.sort((a: any, b: any) => { + // Get the most recent date for both a and b + const dateA = a.updated ? new Date(a.updated) : new Date(a.created); + const dateB = b.updated ? new Date(b.updated) : new Date(b.created); + + // Sort by most recent + return dateB.getTime() - dateA.getTime(); + }); + + return sortedData[0]; +}; + +export const AdminSpaceInner = ({ + selectedGroup, + adminsWithNames, + setIsForceShowCreationKeyPopup, +}) => { + const [adminGroupSecretKey, setAdminGroupSecretKey] = useState(null); + const [isFetchingAdminGroupSecretKey, setIsFetchingAdminGroupSecretKey] = + useState(true); + const [isFetchingGroupSecretKey, setIsFetchingGroupSecretKey] = + useState(true); + const [ + adminGroupSecretKeyPublishDetails, + setAdminGroupSecretKeyPublishDetails, + ] = useState(null); + const [groupSecretKeyPublishDetails, setGroupSecretKeyPublishDetails] = + useState(null); + const [isLoadingPublishKey, setIsLoadingPublishKey] = useState(false); + const { show, setTxList, setInfoSnackCustom, setOpenSnackGlobal } = + useContext(MyContext); + + const getAdminGroupSecretKey = useCallback(async () => { + try { + if (!selectedGroup) return; + const getLatestPublish = await getPublishesFromAdminsAdminSpace( + adminsWithNames.map((admin) => admin?.name), + selectedGroup + ); + if (getLatestPublish === false) return; + let data; + + const res = await fetch( + `${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${ + getLatestPublish.name + }/${getLatestPublish.identifier}?encoding=base64` + ); + 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"); + setAdminGroupSecretKey(decryptedKeyToObject); + setAdminGroupSecretKeyPublishDetails(getLatestPublish); + } catch (error) { + } finally { + setIsFetchingAdminGroupSecretKey(false); } - const adminData = await response.json(); - - const filterId = adminData.filter( - (data: any) => - data.identifier === `admins-symmetric-qchat-group-${groupId}` - ); - if (filterId?.length === 0) { - return false; + }, [adminsWithNames, selectedGroup]); + + const getGroupSecretKey = useCallback(async () => { + try { + if (!selectedGroup) return; + const getLatestPublish = await getPublishesFromAdmins( + adminsWithNames.map((admin) => admin?.name), + selectedGroup + ); + if (getLatestPublish === false) setGroupSecretKeyPublishDetails(false); + setGroupSecretKeyPublishDetails(getLatestPublish); + } catch (error) { + } finally { + setIsFetchingGroupSecretKey(false); } - const sortedData = filterId.sort((a: any, b: any) => { - // Get the most recent date for both a and b - const dateA = a.updated ? new Date(a.updated) : new Date(a.created); - const dateB = b.updated ? new Date(b.updated) : new Date(b.created); - - // Sort by most recent - return dateB.getTime() - dateA.getTime(); - }); - - return sortedData[0]; + }, [adminsWithNames, selectedGroup]); + + const createCommonSecretForAdmins = async () => { + try { + const fee = await getFee("ARBITRARY"); + await show({ + message: "Would you like to perform an ARBITRARY transaction?", + publishFee: fee.fee + " QORT", + }); + setIsLoadingPublishKey(true); + + window + .sendMessage("encryptAndPublishSymmetricKeyGroupChatForAdmins", { + groupId: selectedGroup, + previousData: adminGroupSecretKey, + admins: adminsWithNames, + }) + .then((response) => { + if (!response?.error) { + setInfoSnackCustom({ + type: "success", + message: + "Successfully re-encrypted secret key. It may take a couple of minutes for the changes to propagate. Refresh the group in 5 mins.", + }); + setOpenSnackGlobal(true); + return; + } + setInfoSnackCustom({ + type: "error", + message: response?.error || "unable to re-encrypt secret key", + }); + setOpenSnackGlobal(true); + }) + .catch((error) => { + setInfoSnackCustom({ + type: "error", + message: error?.message || "unable to re-encrypt secret key", + }); + setOpenSnackGlobal(true); + }); + } catch (error) {} }; -export const AdminSpaceInner = ({selectedGroup, adminsWithNames}) => { - const [adminGroupSecretKey, setAdminGroupSecretKey] = useState(null) - const [isFetchingAdminGroupSecretKey, setIsFetchingAdminGroupSecretKey] = useState(true) - const [adminGroupSecretKeyPublishDetails, setAdminGroupSecretKeyPublishDetails] = useState(null) - - const [isLoadingPublishKey, setIsLoadingPublishKey] = useState(false) - const { show, setTxList, setInfoSnackCustom, - setOpenSnackGlobal } = useContext(MyContext); - - - const getAdminGroupSecretKey = useCallback(async ()=> { - try { - if(!selectedGroup) return - const getLatestPublish = await getPublishesFromAdminsAdminSpace(adminsWithNames.map((admin)=> admin?.name), selectedGroup) - if(getLatestPublish === false) return - let data; - - const res = await fetch( - `${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${getLatestPublish.name}/${ - getLatestPublish.identifier - }?encoding=base64` - ); - 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"); - setAdminGroupSecretKey(decryptedKeyToObject) - setAdminGroupSecretKeyPublishDetails(getLatestPublish) - } catch (error) { - - } finally { - setIsFetchingAdminGroupSecretKey(false) - } - }, [adminsWithNames, selectedGroup]) - - const createCommonSecretForAdmins = async ()=> { - try { - const fee = await getFee('ARBITRARY') - await show({ - message: "Would you like to perform an ARBITRARY transaction?" , - publishFee: fee.fee + ' QORT' - }) - setIsLoadingPublishKey(true) - - - window.sendMessage("encryptAndPublishSymmetricKeyGroupChatForAdmins", { - groupId: selectedGroup, - previousData: null, - admins: adminsWithNames - }) - .then((response) => { - - if (!response?.error) { - setInfoSnackCustom({ - type: "success", - message: "Successfully re-encrypted secret key. It may take a couple of minutes for the changes to propagate. Refresh the group in 5 mins.", - }); - setOpenSnackGlobal(true); - return - } - setInfoSnackCustom({ - type: "error", - message: response?.error || "unable to re-encrypt secret key", - }); - setOpenSnackGlobal(true); - }) - .catch((error) => { - setInfoSnackCustom({ - type: "error", - message: error?.message || "unable to re-encrypt secret key", - }); - setOpenSnackGlobal(true); - }); - - } catch (error) { - - } - } - useEffect(() => { - getAdminGroupSecretKey() - }, [getAdminGroupSecretKey]); + useEffect(() => { + getAdminGroupSecretKey(); + getGroupSecretKey(); + }, [getAdminGroupSecretKey, getGroupSecretKey]); return ( - + + Reminder: After publishing the key, it will take a couple of minutes for it to appear. Please just wait. - - {isFetchingAdminGroupSecretKey && Fetching Admins secret keys} - {!isFetchingAdminGroupSecretKey && !adminGroupSecretKey && No secret key published yet} - {adminGroupSecretKeyPublishDetails && ( - Last encryption date: {formatTimestampForum(adminGroupSecretKeyPublishDetails?.updated || adminGroupSecretKeyPublishDetails?.created)} + + {isFetchingGroupSecretKey && ( + Fetching Group secret key publishes )} - + {!isFetchingGroupSecretKey && + groupSecretKeyPublishDetails === false && ( + No secret key published yet + )} + {groupSecretKeyPublishDetails && ( + + Last encryption date:{" "} + {formatTimestampForum( + groupSecretKeyPublishDetails?.updated || + groupSecretKeyPublishDetails?.created + )}{" "} + {` by ${groupSecretKeyPublishDetails?.name}`} + + )} + + + This key is to encrypt GROUP related content. This is the only one used in this UI as of now. All group members will be able to see content encrypted with this key. + + + + {isFetchingAdminGroupSecretKey && ( + Fetching Admins secret key + )} + {!isFetchingAdminGroupSecretKey && !adminGroupSecretKey && ( + No secret key published yet + )} + {adminGroupSecretKeyPublishDetails && ( + + Last encryption date:{" "} + {formatTimestampForum( + adminGroupSecretKeyPublishDetails?.updated || + adminGroupSecretKeyPublishDetails?.created + )} + + )} + + + This key is to encrypt ADMIN related content. Only admins would see content encrypted with it. - ) -} + ); +}; diff --git a/src/components/Chat/AnnouncementDiscussion.tsx b/src/components/Chat/AnnouncementDiscussion.tsx index cc6af24..5603de0 100644 --- a/src/components/Chat/AnnouncementDiscussion.tsx +++ b/src/components/Chat/AnnouncementDiscussion.tsx @@ -6,7 +6,7 @@ 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 { decryptPublishes, getTempPublish, handleUnencryptedPublishes, saveTempPublish } from "./GroupAnnouncements"; import { AnnouncementList } from "./AnnouncementList"; import { Spacer } from "../../common/Spacer"; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; @@ -22,7 +22,8 @@ export const AnnouncementDiscussion = ({ secretKey, setSelectedAnnouncement, show, - myName + myName, + isPrivate }) => { const [isSending, setIsSending] = useState(false); const [isLoading, setIsLoading] = useState(false); @@ -49,7 +50,7 @@ export const AnnouncementDiscussion = ({ } }; - const getData = async ({ identifier, name }) => { + const getData = async ({ identifier, name }, isPrivate) => { try { const res = await fetch( @@ -57,7 +58,7 @@ export const AnnouncementDiscussion = ({ ); if(!res?.ok) return const data = await res.text(); - const response = await decryptPublishes([{ data }], secretKey); + const response = isPrivate === false ? handleUnencryptedPublishes([data]) : await decryptPublishes([{ data }], secretKey); const messageData = response[0]; setData((prev) => { @@ -132,10 +133,10 @@ export const AnnouncementDiscussion = ({ extra: {}, message: htmlContent, }; - const secretKeyObject = await getSecretKey(false, true); - const message64: any = await objectToBase64(message); + const secretKeyObject = isPrivate === false ? null : await getSecretKey(false, true); + const message64: any = await objectToBase64(message); - const encryptSingle = await encryptChatMessage( + const encryptSingle = isPrivate === false ? message64 : await encryptChatMessage( message64, secretKeyObject ); @@ -169,7 +170,7 @@ export const AnnouncementDiscussion = ({ }; const getComments = React.useCallback( - async (selectedAnnouncement) => { + async (selectedAnnouncement, isPrivate) => { try { setIsLoading(true); @@ -190,7 +191,7 @@ export const AnnouncementDiscussion = ({ setComments(responseData); setIsLoading(false); for (const data of responseData) { - getData({ name: data.name, identifier: data.identifier }); + getData({ name: data.name, identifier: data.identifier }, isPrivate); } } catch (error) { } finally { @@ -220,7 +221,7 @@ export const AnnouncementDiscussion = ({ setComments((prev)=> [...prev, ...responseData]); setIsLoading(false); for (const data of responseData) { - getData({ name: data.name, identifier: data.identifier }); + getData({ name: data.name, identifier: data.identifier }, isPrivate); } } catch (error) { @@ -245,11 +246,12 @@ export const AnnouncementDiscussion = ({ }, [tempPublishedList, comments]); React.useEffect(() => { - if (selectedAnnouncement && secretKey && !firstMountRef.current) { - getComments(selectedAnnouncement); + if(!secretKey && isPrivate) return + if (selectedAnnouncement && !firstMountRef.current && isPrivate !== null) { + getComments(selectedAnnouncement, isPrivate); firstMountRef.current = true } - }, [selectedAnnouncement, secretKey]); + }, [selectedAnnouncement, secretKey, isPrivate]); return (
{ - if (!response?.error) { - processWithNewMessages(response, selectedDirect?.address); + .then((decryptResponse) => { + if (!decryptResponse?.error) { + const response = processWithNewMessages(decryptResponse, selectedDirect?.address); res(response); if (isInitiated) { @@ -363,7 +363,7 @@ useEffect(() => { const htmlContent = editorRef?.current.getHTML(); const stringified = JSON.stringify(htmlContent); const size = new Blob([stringified]).size; - setMessageSize(size + 100); + setMessageSize(size + 200); }; // Add a listener for the editorRef?.current's content updates @@ -377,7 +377,8 @@ useEffect(() => { const sendMessage = async ()=> { try { - + if(messageSize > 4000) return + if(+balance < 4) throw new Error('You need at least 4 QORT to send a message') if(isSending) return @@ -646,21 +647,22 @@ useEffect(() => { )} -
{messageSize > 750 && ( 4000 ? 'var(--unread)' : 'unset' + color: messageSize > 4000 ? 'var(--danger)' : 'unset' }}>{`Your message size is of ${messageSize} bytes out of a maximum of 4000`} )} + + { { - if(messageSize > 4000) return if(isSending) return sendMessage() diff --git a/src/components/Chat/ChatGroup.tsx b/src/components/Chat/ChatGroup.tsx index b7e15d8..e054bd9 100644 --- a/src/components/Chat/ChatGroup.tsx +++ b/src/components/Chat/ChatGroup.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react' +import React, { useCallback, useContext, useEffect, useMemo, useReducer, useRef, useState } from 'react' import { CreateCommonSecret } from './CreateCommonSecret' import { reusableGet } from '../../qdn/publish/pubish' import { uint8ArrayToObject } from '../../backgroundFunctions/encryption' @@ -10,11 +10,11 @@ import Tiptap from './TipTap' import { CustomButton } from '../../App-styles' import CircularProgress from '@mui/material/CircularProgress'; import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar' -import { getBaseApiReact, getBaseApiReactSocket, isMobile, pauseAllQueues, resumeAllQueues } from '../../App' +import { getBaseApiReact, getBaseApiReactSocket, isMobile, MyContext, pauseAllQueues, resumeAllQueues } from '../../App' import { CustomizedSnackbars } from '../Snackbar/Snackbar' import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from '../../constants/codes' import { useMessageQueue } from '../../MessageQueueContext' -import { executeEvent } from '../../utils/events' +import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from '../../utils/events' import { Box, ButtonBase, Divider, Typography } from '@mui/material' import ShortUniqueId from "short-unique-id"; import { ReplyPreview } from './MessageItem' @@ -27,7 +27,8 @@ import { throttle } from 'lodash' const uid = new ShortUniqueId({ length: 5 }); -export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, myAddress, handleNewEncryptionNotification, hide, handleSecretKeyCreationInProgress, triedToFetchSecretKey, myName, balance, getTimestampEnterChatParent, hideView}) => { +export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, myAddress, handleNewEncryptionNotification, hide, handleSecretKeyCreationInProgress, triedToFetchSecretKey, myName, balance, getTimestampEnterChatParent, hideView, isPrivate}) => { + const {isUserBlocked} = useContext(MyContext) const [messages, setMessages] = useState([]) const [chatReferences, setChatReferences] = useState({}) const [isSending, setIsSending] = useState(false) @@ -54,14 +55,14 @@ const [messageSize, setMessageSize] = useState(0) const handleUpdateRef = useRef(null); - const getTimestampEnterChat = async () => { + const getTimestampEnterChat = async (selectedGroup) => { try { return new Promise((res, rej) => { window.sendMessage("getTimestampEnterChat") .then((response) => { if (!response?.error) { - if(response && selectedGroup && response[selectedGroup]){ - lastReadTimestamp.current = response[selectedGroup] + if(response && selectedGroup){ + lastReadTimestamp.current = response[selectedGroup] || undefined window.sendMessage("addTimestampEnterChat", { timestamp: Date.now(), groupId: selectedGroup @@ -89,8 +90,9 @@ const [messageSize, setMessageSize] = useState(0) }; useEffect(()=> { - getTimestampEnterChat() - }, []) + if(!selectedGroup) return + getTimestampEnterChat(selectedGroup) + }, [selectedGroup]) @@ -157,10 +159,28 @@ const [messageSize, setMessageSize] = useState(0) }) } + const updateChatMessagesWithBlocksFunc = (e) => { + if(e.detail){ + setMessages((prev)=> prev?.filter((item)=> { + return !isUserBlocked(item?.sender, item?.senderName) + })) + } + }; + + useEffect(() => { + subscribeToEvent("updateChatMessagesWithBlocks", updateChatMessagesWithBlocksFunc); + + return () => { + unsubscribeFromEvent("updateChatMessagesWithBlocks", updateChatMessagesWithBlocksFunc); + }; + }, []); + const middletierFunc = async (data: any, groupId: string) => { try { if (hasInitialized.current) { - decryptMessages(data, true); + const dataRemovedBlock = data?.filter((item)=> !isUserBlocked(item?.sender, item?.senderName)) + + decryptMessages(dataRemovedBlock, true); return; } hasInitialized.current = true; @@ -172,7 +192,11 @@ const [messageSize, setMessageSize] = useState(0) }, }); const responseData = await response.json(); - decryptMessages(responseData, false); + const dataRemovedBlock = responseData?.filter((item)=> { + return !isUserBlocked(item?.sender, item?.senderName) + }) + + decryptMessages(dataRemovedBlock, false); } catch (error) { console.error(error); } @@ -193,9 +217,9 @@ const [messageSize, setMessageSize] = useState(0) const filterUIMessages = encryptedMessages.filter((item) => !isExtMsg(item.data)); const decodedUIMessages = decodeBase64ForUIChatMessages(filterUIMessages); - const combineUIAndExtensionMsgs = [...decodedUIMessages, ...response]; - processWithNewMessages( - combineUIAndExtensionMsgs.map((item) => ({ + const combineUIAndExtensionMsgsBefore = [...decodedUIMessages, ...response]; + const combineUIAndExtensionMsgs = processWithNewMessages( + combineUIAndExtensionMsgsBefore.map((item) => ({ ...item, ...(item?.decryptedData || {}), })), @@ -208,7 +232,9 @@ const [messageSize, setMessageSize] = useState(0) const formatted = combineUIAndExtensionMsgs .filter((rawItem) => !rawItem?.chatReference) .map((item) => { - + const additionalFields = item?.data === 'NDAwMQ==' ? { + text: "

First group key created.

" + } : {} return { ...item, id: item.signature, @@ -216,6 +242,7 @@ const [messageSize, setMessageSize] = useState(0) repliedTo: item?.repliedTo || item?.decryptedData?.repliedTo, unread: item?.sender === myAddress ? false : !!item?.chatReference ? false : true, isNotEncrypted: !!item?.messageText, + ...additionalFields } }); setMessages((prev) => [...prev, ...formatted]); @@ -223,19 +250,24 @@ const [messageSize, setMessageSize] = useState(0) setChatReferences((prev) => { const organizedChatReferences = { ...prev }; combineUIAndExtensionMsgs - .filter((rawItem) => rawItem && rawItem.chatReference && (rawItem.decryptedData?.type === "reaction" || rawItem.decryptedData?.type === "edit")) + .filter((rawItem) => rawItem && rawItem.chatReference && (rawItem?.decryptedData?.type === "reaction" || rawItem?.decryptedData?.type === "edit" || rawItem?.type === "edit" || rawItem?.isEdited || rawItem?.type === "reaction")) .forEach((item) => { try { - if(item.decryptedData?.type === "edit"){ + if(item?.decryptedData?.type === "edit"){ organizedChatReferences[item.chatReference] = { ...(organizedChatReferences[item.chatReference] || {}), edit: item.decryptedData, }; + } else if(item?.type === "edit" || item?.isEdited){ + organizedChatReferences[item.chatReference] = { + ...(organizedChatReferences[item.chatReference] || {}), + edit: item, + }; } else { - const content = item.decryptedData?.content; + const content = item?.content || item.decryptedData?.content; const sender = item.sender; const newTimestamp = item.timestamp; - const contentState = item.decryptedData?.contentState; + const contentState = item?.contentState !== undefined ? item?.contentState : item.decryptedData?.contentState; if (!content || typeof content !== "string" || !sender || typeof sender !== "string" || !newTimestamp) { console.warn("Invalid content, sender, or timestamp in reaction data", item); @@ -285,6 +317,9 @@ const [messageSize, setMessageSize] = useState(0) const formatted = combineUIAndExtensionMsgs .filter((rawItem) => !rawItem?.chatReference) .map((item) => { + const additionalFields = item?.data === 'NDAwMQ==' ? { + text: "

First group key created.

" + } : {} const divide = lastReadTimestamp.current && !firstUnreadFound && item.timestamp > lastReadTimestamp.current && myAddress !== item?.sender; if(divide){ @@ -297,7 +332,8 @@ const [messageSize, setMessageSize] = useState(0) repliedTo: item?.repliedTo || item?.decryptedData?.repliedTo, isNotEncrypted: !!item?.messageText, unread: false, - divide + divide, + ...additionalFields } }); setMessages(formatted); @@ -306,19 +342,24 @@ const [messageSize, setMessageSize] = useState(0) const organizedChatReferences = { ...prev }; combineUIAndExtensionMsgs - .filter((rawItem) => rawItem && rawItem.chatReference && (rawItem.decryptedData?.type === "reaction" || rawItem.decryptedData?.type === "edit")) + .filter((rawItem) => rawItem && rawItem.chatReference && (rawItem?.decryptedData?.type === "reaction" || rawItem?.decryptedData?.type === "edit" || rawItem?.type === "edit" || rawItem?.isEdited || rawItem?.type === "reaction")) .forEach((item) => { try { - if(item.decryptedData?.type === "edit"){ + if(item?.decryptedData?.type === "edit"){ organizedChatReferences[item.chatReference] = { ...(organizedChatReferences[item.chatReference] || {}), edit: item.decryptedData, }; + } else if(item?.type === "edit" || item?.isEdited){ + organizedChatReferences[item.chatReference] = { + ...(organizedChatReferences[item.chatReference] || {}), + edit: item, + }; } else { - const content = item.decryptedData?.content; + const content = item?.content || item.decryptedData?.content; const sender = item.sender; const newTimestamp = item.timestamp; - const contentState = item.decryptedData?.contentState; + const contentState = item?.contentState !== undefined ? item?.contentState : item.decryptedData?.contentState; if (!content || typeof content !== "string" || !sender || typeof sender !== "string" || !newTimestamp) { console.warn("Invalid content, sender, or timestamp in reaction data", item); @@ -453,10 +494,11 @@ const [messageSize, setMessageSize] = useState(0) setIsLoading(true) initWebsocketMessageGroup() } - }, [triedToFetchSecretKey, secretKey]) + }, [triedToFetchSecretKey, secretKey, isPrivate]) useEffect(()=> { - if(!secretKey || hasInitializedWebsocket.current) return + if(isPrivate === null) return + if(isPrivate === false || !secretKey || hasInitializedWebsocket.current) return forceCloseWebSocket() setMessages([]) setIsLoading(true) @@ -466,7 +508,7 @@ const [messageSize, setMessageSize] = useState(0) }, 6000); initWebsocketMessageGroup() hasInitializedWebsocket.current = true - }, [secretKey]) + }, [secretKey, isPrivate]) useEffect(()=> { @@ -551,6 +593,8 @@ const clearEditorContent = () => { const sendMessage = async ()=> { try { + if(messageSize > 4000) return + if(isPrivate === null) throw new Error('Unable to determine if group is private') if(isSending) return if(+balance < 4) throw new Error('You need at least 4 QORT to send a message') pauseAllQueues() @@ -558,8 +602,10 @@ const clearEditorContent = () => { const htmlContent = editorRef.current.getHTML(); if(!htmlContent?.trim() || htmlContent?.trim() === '

') return + + setIsSending(true) - const message = htmlContent + const message = isPrivate === false ? editorRef.current.getJSON() : htmlContent const secretKeyObject = await getSecretKey(false, true) let repliedTo = replyMessage?.signature @@ -569,19 +615,24 @@ const clearEditorContent = () => { } let chatReference = onEditMessage?.signature + const publicData = isPrivate ? {} : { + isEdited : chatReference ? true : false, + } const otherData = { repliedTo, ...(onEditMessage?.decryptedData || {}), type: chatReference ? 'edit' : '', specialId: uid.rnd(), + ...publicData } const objectMessage = { ...(otherData || {}), - message + [isPrivate ? 'message' : 'messageText']: message, + version: 3 } const message64: any = await objectToBase64(objectMessage) - const encryptSingle = await encryptChatMessage(message64, secretKeyObject) + const encryptSingle = isPrivate === false ? JSON.stringify(objectMessage) : await encryptChatMessage(message64, secretKeyObject) // const res = await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle}) const sendMessageFunc = async () => { @@ -591,7 +642,7 @@ const clearEditorContent = () => { // Add the function to the queue const messageObj = { message: { - text: message, + text: htmlContent, timestamp: Date.now(), senderName: myName, sender: myAddress, @@ -626,10 +677,24 @@ const clearEditorContent = () => { useEffect(() => { if (!editorRef?.current) return; - handleUpdateRef.current = throttle(() => { - const htmlContent = editorRef.current.getHTML(); - const size = new TextEncoder().encode(htmlContent).length; - setMessageSize(size + 100); + handleUpdateRef.current = throttle(async () => { + try { + if(isPrivate){ + const htmlContent = editorRef.current.getHTML(); + const message64 = await objectToBase64(JSON.stringify(htmlContent)) + const secretKeyObject = await getSecretKey(false, true) + const encryptSingle = await encryptChatMessage(message64, secretKeyObject) + setMessageSize((encryptSingle?.length || 0) + 200); + } else { + const htmlContent = editorRef.current.getJSON(); + const message = JSON.stringify(htmlContent) + const size = new Blob([message]).size + setMessageSize(size + 300); + } + + } catch (error) { + // calc size error + } }, 1200); const currentEditor = editorRef.current; @@ -639,7 +704,7 @@ const clearEditorContent = () => { return () => { currentEditor.off("update", handleUpdateRef.current); }; - }, [editorRef, setMessageSize]); + }, [editorRef, setMessageSize, isPrivate]); useEffect(() => { if (hide) { @@ -662,7 +727,7 @@ const clearEditorContent = () => { const onEdit = useCallback((message)=> { setOnEditMessage(message) setReplyMessage(null) - editorRef.current.chain().focus().setContent(message?.text).run(); + editorRef.current.chain().focus().setContent(message?.messageText || message?.text).run(); }, []) const handleReaction = useCallback(async (reaction, chatMessage, reactionState = true)=> { @@ -690,7 +755,7 @@ const clearEditorContent = () => { } const message64: any = await objectToBase64(objectMessage) const reactiontypeNumber = RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS - const encryptSingle = await encryptChatMessage(message64, secretKeyObject, reactiontypeNumber) + const encryptSingle = isPrivate === false ? JSON.stringify(objectMessage) : await encryptChatMessage(message64, secretKeyObject, reactiontypeNumber) // const res = await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle}) const sendMessageFunc = async () => { @@ -729,7 +794,7 @@ const clearEditorContent = () => { setIsSending(false) resumeAllQueues() } - }, []) + }, [isPrivate]) const openQManager = useCallback(()=> { setIsOpenQManager(true) @@ -746,9 +811,9 @@ const clearEditorContent = () => { left: hide && '-100000px', }}> - + - {!!secretKey && ( + {(!!secretKey || isPrivate === false) && (
{ - -
- {messageSize > 750 && ( + {messageSize > 750 && ( 4000 ? 'var(--unread)' : 'unset' + color: messageSize > 4000 ? 'var(--danger)' : 'unset' }}>{`Your message size is of ${messageSize} bytes out of a maximum of 4000`} )} + + { { - if(messageSize > 4000) return + if(isSending) return sendMessage() }} @@ -927,7 +992,8 @@ const clearEditorContent = () => {
diff --git a/src/components/Chat/ChatList.tsx b/src/components/Chat/ChatList.tsx index 3cae591..9d5e42b 100644 --- a/src/components/Chat/ChatList.tsx +++ b/src/components/Chat/ChatList.tsx @@ -28,22 +28,21 @@ export const ChatList = ({ selectedGroup, enableMentions, openQManager, - hasSecretKey + hasSecretKey, + isPrivate }) => { const parentRef = useRef(); const [messages, setMessages] = useState(initialMessages); const [showScrollButton, setShowScrollButton] = useState(false); const [showScrollDownButton, setShowScrollDownButton] = useState(false); - const hasLoadedInitialRef = useRef(false); const scrollingIntervalRef = useRef(null); const lastSeenUnreadMessageTimestamp = useRef(null); - // Initialize the virtualizer const rowVirtualizer = useVirtualizer({ count: messages.length, - getItemKey: (index) => messages[index].signature, + getItemKey: (index) => messages[index]?.tempSignature || messages[index].signature, getScrollElement: () => parentRef?.current, estimateSize: useCallback(() => 80, []), // Provide an estimated height of items, adjust this as needed overscan: 10, // Number of items to render outside the visible area to improve smoothness @@ -273,7 +272,10 @@ export const ChatList = ({ message.text = chatReferences[message.signature]?.edit?.message; message.isEdit = true } - + if (chatReferences[message.signature]?.edit?.messageText && message?.messageText) { + message.messageText = chatReferences[message.signature]?.edit?.messageText; + message.isEdit = true + } } @@ -316,7 +318,6 @@ export const ChatList = ({ ); } - return (
@@ -375,7 +377,7 @@ export const ChatList = ({ position: "absolute", right: 20, backgroundColor: "var(--unread)", - color: "white", + color: "black", padding: "10px 20px", borderRadius: "20px", cursor: "pointer", @@ -409,7 +411,7 @@ export const ChatList = ({ )} - {enableMentions && hasSecretKey && ( + {enableMentions && (hasSecretKey || isPrivate === false) && ( )}
diff --git a/src/components/Chat/ChatOptions.tsx b/src/components/Chat/ChatOptions.tsx index 6791d68..4e7a7d3 100644 --- a/src/components/Chat/ChatOptions.tsx +++ b/src/components/Chat/ChatOptions.tsx @@ -6,6 +6,7 @@ import { MenuItem, Select, Typography, + Tooltip } from "@mui/material"; import React, { useEffect, useMemo, useRef, useState } from "react"; import SearchIcon from "@mui/icons-material/Search"; @@ -13,6 +14,10 @@ import { Spacer } from "../../common/Spacer"; import AlternateEmailIcon from "@mui/icons-material/AlternateEmail"; import CloseIcon from "@mui/icons-material/Close"; import InsertLinkIcon from '@mui/icons-material/InsertLink'; +import Highlight from "@tiptap/extension-highlight"; +import Mention from "@tiptap/extension-mention"; +import StarterKit from "@tiptap/starter-kit"; +import Underline from "@tiptap/extension-underline"; import { AppsSearchContainer, AppsSearchLeft, @@ -32,6 +37,8 @@ import { useVirtualizer } from "@tanstack/react-virtual"; import { formatTimestamp } from "../../utils/time"; import { ContextMenuMentions } from "../ContextMenuMentions"; import { convert } from 'html-to-text'; +import { generateHTML } from "@tiptap/react"; +import ErrorBoundary from "../../common/ErrorBoundary"; const extractTextFromHTML = (htmlString = '') => { return convert(htmlString, { @@ -43,7 +50,7 @@ const cache = new CellMeasurerCache({ defaultHeight: 50, }); -export const ChatOptions = ({ messages, goToMessage, members, myName, selectedGroup, openQManager }) => { +export const ChatOptions = ({ messages : untransformedMessages, goToMessage, members, myName, selectedGroup, openQManager, isPrivate }) => { const [mode, setMode] = useState("default"); const [searchValue, setSearchValue] = useState(""); const [selectedMember, setSelectedMember] = useState(0); @@ -52,7 +59,27 @@ export const ChatOptions = ({ messages, goToMessage, members, myName, selectedGr const parentRefMentions = useRef(); const [lastMentionTimestamp, setLastMentionTimestamp] = useState(null) const [debouncedValue, setDebouncedValue] = useState(""); // Debounced value - + const messages = useMemo(()=> { + return untransformedMessages?.map((item)=> { + if(item?.messageText){ + let transformedMessage = item?.messageText + try { + transformedMessage = generateHTML(item?.messageText, [ + StarterKit, + Underline, + Highlight, + Mention + ]) + return { + ...item, + messageText: transformedMessage + } + } catch (error) { + // error + } + } else return item + }) + }, [untransformedMessages]) const getTimestampMention = async () => { try { return new Promise((res, rej) => { @@ -124,7 +151,7 @@ export const ChatOptions = ({ messages, goToMessage, members, myName, selectedGr .filter( (message) => message?.senderName === selectedMember && - extractTextFromHTML(message?.decryptedData?.message)?.includes( + extractTextFromHTML(isPrivate ? message?.messageText : message?.decryptedData?.message)?.includes( debouncedValue.toLowerCase() ) ) @@ -132,20 +159,27 @@ export const ChatOptions = ({ messages, goToMessage, members, myName, selectedGr } return messages .filter((message) => - extractTextFromHTML(message?.decryptedData?.message)?.includes(debouncedValue.toLowerCase()) + extractTextFromHTML(isPrivate === false ? message?.messageText : message?.decryptedData?.message)?.includes(debouncedValue.toLowerCase()) ) ?.sort((a, b) => b?.timestamp - a?.timestamp); - }, [debouncedValue, messages, selectedMember]); + }, [debouncedValue, messages, selectedMember, isPrivate]); const mentionList = useMemo(() => { if(!messages || messages.length === 0 || !myName) return [] - - return messages + if(isPrivate === false){ + return messages .filter((message) => - extractTextFromHTML(message?.decryptedData?.message)?.includes(`@${myName}`) + extractTextFromHTML(message?.messageText)?.includes(`@${myName?.toLowerCase()}`) ) ?.sort((a, b) => b?.timestamp - a?.timestamp); - }, [messages, myName]); + + } + return messages + .filter((message) => + extractTextFromHTML(message?.decryptedData?.message)?.includes(`@${myName?.toLowerCase()}`) + ) + ?.sort((a, b) => b?.timestamp - a?.timestamp); + }, [messages, myName, isPrivate]); const rowVirtualizer = useVirtualizer({ count: searchedList.length, @@ -291,79 +325,8 @@ export const ChatOptions = ({ messages, goToMessage, members, myName, selectedGr gap: "5px", }} > - - - - - {message?.senderName?.charAt(0)} - - - {message?.senderName} - - - - - {formatTimestamp(message.timestamp)} - { - const findMsgIndex = messages.findIndex( - (item) => - item?.signature === message?.signature - ); - if (findMsgIndex !== -1) { - goToMessage(findMsgIndex); - } - }} - > -

" - } - /> -
-
+ + ); })} @@ -544,6 +507,7 @@ export const ChatOptions = ({ messages, goToMessage, members, myName, selectedGr const index = virtualRow.index; let message = searchedList[index]; return ( +
- + Error loading content: Invalid Data + + } + > + + +
+ + ); + })} + + + + +
+ + + ); + } + return ( + + + { + setMode("search") + }}> + SEARCH} + placement="left" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > + + + + { + setMode("default") + setSearchValue('') + setSelectedMember(0) + openQManager() + }}> + Q-MANAGER} + placement="left" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > + + + + + { + setMode("mentions") + setSearchValue('') + setSelectedMember(0) + }}> + MENTIONED} + placement="left" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > + 0 && (!lastMentionTimestamp || lastMentionTimestamp < mentionList[0]?.timestamp) ? 'var(--unread)' : 'white' + }} /> + + + + + + + + + ); +}; + + +const ShowMessage = ({message, goToMessage, messages})=> { + + return ( + -

" - } - /> + {message?.messageText && ( + + )} + {message?.decryptedData?.message && ( +

" + } + /> + )} +
- - ); - })} - - - - - - - - ); - } - return ( - - - { - setMode("search") - }}> - - - { - setMode("default") - setSearchValue('') - setSelectedMember(0) - openQManager() - }}> - - - - { - setMode("mentions") - setSearchValue('') - setSelectedMember(0) - }}> - 0 && (!lastMentionTimestamp || lastMentionTimestamp < mentionList[0]?.timestamp) ? 'var(--unread)' : 'white' - }} /> - - - - - - - - ); -}; + ) +} \ No newline at end of file diff --git a/src/components/Chat/CreateCommonSecret.tsx b/src/components/Chat/CreateCommonSecret.tsx index c371c55..e729386 100644 --- a/src/components/Chat/CreateCommonSecret.tsx +++ b/src/components/Chat/CreateCommonSecret.tsx @@ -8,7 +8,7 @@ import { decryptResource, getGroupAdmins, validateSecretKey } from '../Group/Gro import { base64ToUint8Array } from '../../qdn/encryption/group-encryption'; import { uint8ArrayToObject } from '../../backgroundFunctions/encryption'; -export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, secretKeyDetails, userInfo, noSecretKey, setHideCommonKeyPopup}) => { +export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, secretKeyDetails, userInfo, noSecretKey, setHideCommonKeyPopup, setIsForceShowCreationKeyPopup, isForceShowCreationKeyPopup}) => { const { show, setTxList } = useContext(MyContext); const [openSnack, setOpenSnack] = React.useState(false); @@ -131,6 +131,9 @@ export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, sec ]); } setIsLoading(false); + setTimeout(() => { + setIsForceShowCreationKeyPopup(false) + }, 1000); }) .catch((error) => { console.error("Failed to encrypt and publish symmetric key for group chat:", error.message || "An error occurred"); @@ -161,7 +164,7 @@ export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, sec 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 - ): ( + ): isForceShowCreationKeyPopup ? null : ( The group member list has changed. Please re-encrypt the secret key. @@ -173,6 +176,7 @@ export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, sec }}> diff --git a/src/components/Chat/GroupAnnouncements.tsx b/src/components/Chat/GroupAnnouncements.tsx index a28f189..cdcd5db 100644 --- a/src/components/Chat/GroupAnnouncements.tsx +++ b/src/components/Chat/GroupAnnouncements.tsx @@ -94,18 +94,6 @@ export const decryptPublishes = async (encryptedMessages: any[], secretKey) => { .then((response) => { if (!response?.error) { res(response); - // if(hasInitialized.current){ - // setMessages((prev) => [...prev, ...formatted]); - // } else { - // const formatted = response.map((item) => ({ - // ...item, - // id: item.signature, - // text: item.text, - // unread: false - // })); - // setMessages(formatted); - // hasInitialized.current = true; - // } return; } rej(response.error); @@ -117,6 +105,21 @@ export const decryptPublishes = async (encryptedMessages: any[], secretKey) => { }); } catch (error) {} }; +export const handleUnencryptedPublishes = (publishes) => { + let publishesData = [] + publishes.forEach((pub)=> { + try { + const decryptToUnit8Array = base64ToUint8Array(pub); + const decodedData = uint8ArrayToObject(decryptToUnit8Array); + if(decodedData){ + publishesData.push({decryptedData: decodedData}) + } + } catch (error) { + + } + }) + return publishesData +}; export const GroupAnnouncements = ({ selectedGroup, secretKey, @@ -127,6 +130,7 @@ export const GroupAnnouncements = ({ isAdmin, hide, myName, + isPrivate }) => { const [messages, setMessages] = useState([]); const [isSending, setIsSending] = useState(false); @@ -160,7 +164,7 @@ export const GroupAnnouncements = ({ })(); }, [selectedGroup]); - const getAnnouncementData = async ({ identifier, name, resource }) => { + const getAnnouncementData = async ({ identifier, name, resource }, isPrivate) => { try { let data = dataPublishes.current[`${name}-${identifier}`]; if ( @@ -180,9 +184,9 @@ export const GroupAnnouncements = ({ data = data.data; } - const response = await decryptPublishes([{ data }], secretKey); - + const response = isPrivate === false ? handleUnencryptedPublishes([data]) : await decryptPublishes([{ data }], secretKey); const messageData = response[0]; + if(!messageData) return setAnnouncementData((prev) => { return { ...prev, @@ -195,11 +199,11 @@ export const GroupAnnouncements = ({ }; useEffect(() => { - if (!secretKey || hasInitializedWebsocket.current) return; + if ((!secretKey && isPrivate) || hasInitializedWebsocket.current || isPrivate === null) return; setIsLoading(true); // initWebsocketMessageGroup() hasInitializedWebsocket.current = true; - }, [secretKey]); + }, [secretKey, isPrivate]); const encryptChatMessage = async (data: string, secretKeyObject: any) => { try { @@ -257,12 +261,12 @@ export const GroupAnnouncements = ({ } }; - const setTempData = async () => { + const setTempData = async (selectedGroup) => { try { const getTempAnnouncements = await getTempPublish(); if (getTempAnnouncements?.announcement) { let tempData = []; - Object.keys(getTempAnnouncements?.announcement || {}).map((key) => { + Object.keys(getTempAnnouncements?.announcement || {}).filter((annKey)=> annKey?.startsWith(`grp-${selectedGroup}-anc`)).map((key) => { const value = getTempAnnouncements?.announcement[key]; tempData.push(value.data); }); @@ -289,9 +293,9 @@ export const GroupAnnouncements = ({ extra: {}, message: htmlContent, }; - const secretKeyObject = await getSecretKey(false, true); - const message64: any = await objectToBase64(message); - const encryptSingle = await encryptChatMessage( + const secretKeyObject = isPrivate === false ? null : await getSecretKey(false, true); + const message64: any = await objectToBase64(message); + const encryptSingle = isPrivate === false ? message64 : await encryptChatMessage( message64, secretKeyObject ); @@ -313,7 +317,7 @@ export const GroupAnnouncements = ({ data: dataToSaveToStorage, key: "announcement", }); - setTempData(); + setTempData(selectedGroup); clearEditorContent(); } // send chat message @@ -331,7 +335,7 @@ export const GroupAnnouncements = ({ }; const getAnnouncements = React.useCallback( - async (selectedGroup) => { + async (selectedGroup, isPrivate) => { try { const offset = 0; @@ -346,7 +350,7 @@ export const GroupAnnouncements = ({ }); const responseData = await response.json(); - setTempData(); + setTempData(selectedGroup); setAnnouncements(responseData); setIsLoading(false); for (const data of responseData) { @@ -354,7 +358,7 @@ export const GroupAnnouncements = ({ name: data.name, identifier: data.identifier, resource: data, - }); + }, isPrivate); } } catch (error) { } finally { @@ -365,11 +369,12 @@ export const GroupAnnouncements = ({ ); React.useEffect(() => { - if (selectedGroup && secretKey && !hasInitialized.current && !hide) { - getAnnouncements(selectedGroup); + if(!secretKey && isPrivate) return + if (selectedGroup && !hasInitialized.current && !hide && isPrivate !== null) { + getAnnouncements(selectedGroup, isPrivate); hasInitialized.current = true; } - }, [selectedGroup, secretKey, hide]); + }, [selectedGroup, secretKey, hide, isPrivate]); const loadMore = async () => { try { @@ -389,7 +394,7 @@ export const GroupAnnouncements = ({ setAnnouncements((prev) => [...prev, ...responseData]); setIsLoading(false); for (const data of responseData) { - getAnnouncementData({ name: data.name, identifier: data.identifier }); + getAnnouncementData({ name: data.name, identifier: data.identifier }, isPrivate); } } catch (error) {} }; @@ -414,7 +419,7 @@ export const GroupAnnouncements = ({ getAnnouncementData({ name: data.name, identifier: data.identifier, - }); + }, isPrivate); } catch (error) {} } setAnnouncements(responseData); @@ -429,7 +434,7 @@ export const GroupAnnouncements = ({ for (const data of newArray) { try { - getAnnouncementData({ name: data.name, identifier: data.identifier }); + getAnnouncementData({ name: data.name, identifier: data.identifier }, isPrivate); } catch (error) {} } setAnnouncements((prev) => [...newArray, ...prev]); @@ -449,14 +454,14 @@ export const GroupAnnouncements = ({ }, [checkNewMessages]); useEffect(() => { - if (!secretKey || hide) return; + if ((!secretKey && isPrivate) || hide || isPrivate === null) return; checkNewMessagesFunc(); return () => { if (interval?.current) { clearInterval(interval.current); } }; - }, [checkNewMessagesFunc, hide]); + }, [checkNewMessagesFunc, hide, isPrivate]); const combinedListTempAndReal = useMemo(() => { // Combine the two lists @@ -498,11 +503,13 @@ export const GroupAnnouncements = ({ setSelectedAnnouncement={setSelectedAnnouncement} encryptChatMessage={encryptChatMessage} getSecretKey={getSecretKey} + isPrivate={isPrivate} /> ); } + return (
{ const { rootHeight } = useContext(MyContext); const [isMoved, setIsMoved] = useState(false); @@ -50,7 +51,7 @@ export const GroupForum = ({ left: hide && '-1000px' }} > - +
); diff --git a/src/components/Chat/MessageDisplay.tsx b/src/components/Chat/MessageDisplay.tsx index edda7d8..a686a00 100644 --- a/src/components/Chat/MessageDisplay.tsx +++ b/src/components/Chat/MessageDisplay.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; import DOMPurify from 'dompurify'; import './styles.css'; import { executeEvent } from '../../utils/events'; @@ -63,30 +63,34 @@ function processText(input) { return wrapper.innerHTML; } -export const MessageDisplay = ({ htmlContent, isReply }) => { - const linkify = (text) => { - if (!text) return ""; // Return an empty string if text is null or undefined - - let textFormatted = text; - const urlPattern = /(\bhttps?:\/\/[^\s<]+|\bwww\.[^\s<]+)/g; - textFormatted = text.replace(urlPattern, (url) => { - const href = url.startsWith('http') ? url : `https://${url}`; - return `${DOMPurify.sanitize(url)}`; - }); - return processText(textFormatted); - }; - +const linkify = (text) => { + if (!text) return ""; // Return an empty string if text is null or undefined - 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', 'data-url' - ], - }).replace(/]*data-url="qortal:\/\/use-embed\/[^"]*"[^>]*>.*?<\/span>/g, '');; + let textFormatted = text; + const urlPattern = /(\bhttps?:\/\/[^\s<]+|\bwww\.[^\s<]+)/g; + textFormatted = text.replace(urlPattern, (url) => { + const href = url.startsWith('http') ? url : `https://${url}`; + return `${DOMPurify.sanitize(url)}`; + }); + return processText(textFormatted); +}; + + +export const MessageDisplay = ({ htmlContent, isReply }) => { + + + const sanitizedContent = useMemo(()=> { + return 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', 's', 'hr' + ], + ALLOWED_ATTR: [ + 'href', 'target', 'rel', 'class', 'src', 'alt', 'title', + 'width', 'height', 'style', 'align', 'valign', 'colspan', 'rowspan', 'border', 'cellpadding', 'cellspacing', 'data-url' + ], + }).replace(/]*data-url="qortal:\/\/use-embed\/[^"]*"[^>]*>.*?<\/span>/g, ''); + }, [htmlContent]) const handleClick = async (e) => { e.preventDefault(); @@ -97,6 +101,28 @@ export const MessageDisplay = ({ htmlContent, isReply }) => { window.electronAPI.openExternal(href); } else if (target.getAttribute('data-url')) { const url = target.getAttribute('data-url'); + + let copyUrl = url + + try { + copyUrl = copyUrl.replace(/^(qortal:\/\/)/, '') + if (copyUrl.startsWith('use-')) { + // Handle the new 'use' format + const parts = copyUrl.split('/') + const type = parts[0].split('-')[1] // e.g., 'group' from 'use-group' + parts.shift() + const action = parts.length > 0 ? parts[0].split('-')[1] : null // e.g., 'invite' from 'action-invite' + parts.shift() + const idPrefix = parts.length > 0 ? parts[0].split('-')[0] : null // e.g., 'groupid' from 'groupid-321' + const id = parts.length > 0 ? parts[0].split('-')[1] : null // e.g., '321' from 'groupid-321' + if(action === 'join'){ + executeEvent("globalActionJoinGroup", { groupId: id}); + return + } + } + } catch (error) { + //error + } const res = extractComponents(url); if (res) { const { service, name, identifier, path } = res; @@ -106,7 +132,7 @@ export const MessageDisplay = ({ htmlContent, isReply }) => { } }; - const embedLink = htmlContent.match(/qortal:\/\/use-embed\/[^\s<>]+/); + const embedLink = htmlContent?.match(/qortal:\/\/use-embed\/[^\s<>]+/); let embedData = null; diff --git a/src/components/Chat/MessageItem.tsx b/src/components/Chat/MessageItem.tsx index bd2d86c..5123ee2 100644 --- a/src/components/Chat/MessageItem.tsx +++ b/src/components/Chat/MessageItem.tsx @@ -1,13 +1,14 @@ import { Message } from "@chatscope/chat-ui-kit-react"; -import React, { useEffect, useState } from "react"; +import React, { useCallback, useContext, useEffect, useMemo, useState } from "react"; import { useInView } from "react-intersection-observer"; import { MessageDisplay } from "./MessageDisplay"; -import { Avatar, Box, Button, ButtonBase, List, ListItem, ListItemText, Popover, Typography } from "@mui/material"; +import { Avatar, Box, Button, ButtonBase, List, ListItem, ListItemText, Popover, Tooltip, Typography } from "@mui/material"; import { formatTimestamp } from "../../utils/time"; import { getBaseApi } from "../../background"; -import { getBaseApiReact } from "../../App"; +import { MyContext, getBaseApiReact } from "../../App"; import { generateHTML } from "@tiptap/react"; import Highlight from "@tiptap/extension-highlight"; +import Mention from "@tiptap/extension-mention"; import StarterKit from "@tiptap/starter-kit"; import Underline from "@tiptap/extension-underline"; import { executeEvent } from "../../utils/events"; @@ -17,8 +18,39 @@ import { Spacer } from "../../common/Spacer"; import { ReactionPicker } from "../ReactionPicker"; import KeyOffIcon from '@mui/icons-material/KeyOff'; import EditIcon from '@mui/icons-material/Edit'; +import TextStyle from '@tiptap/extension-text-style'; +import { addressInfoKeySelector } from "../../atoms/global"; +import { useRecoilValue } from "recoil"; +import level0Img from "../../assets/badges/level-0.png" +import level1Img from "../../assets/badges/level-1.png" +import level2Img from "../../assets/badges/level-2.png" +import level3Img from "../../assets/badges/level-3.png" +import level4Img from "../../assets/badges/level-4.png" +import level5Img from "../../assets/badges/level-5.png" +import level6Img from "../../assets/badges/level-6.png" +import level7Img from "../../assets/badges/level-7.png" +import level8Img from "../../assets/badges/level-8.png" +import level9Img from "../../assets/badges/level-9.png" +import level10Img from "../../assets/badges/level-10.png" -export const MessageItem = ({ +const getBadgeImg = (level)=> { + switch(level?.toString()){ + + case '0': return level0Img + case '1': return level1Img + case '2': return level2Img + case '3': return level3Img + case '4': return level4Img + case '5': return level5Img + case '6': return level6Img + case '7': return level7Img + case '8': return level8Img + case '9': return level9Img + case '10': return level10Img + default: return level0Img + } +} +export const MessageItem = React.memo(({ message, onSeen, isLast, @@ -33,34 +65,84 @@ export const MessageItem = ({ reactions, isUpdating, lastSignature, - onEdit + onEdit, + isPrivate }) => { - const { ref, inView } = useInView({ - threshold: 0.7, // Fully visible - triggerOnce: false, // Only trigger once when it becomes visible - }); +const {getIndividualUserInfo} = useContext(MyContext) const [anchorEl, setAnchorEl] = useState(null); const [selectedReaction, setSelectedReaction] = useState(null); + const [userInfo, setUserInfo] = useState(null) - useEffect(() => { - if (inView && isLast && onSeen) { - onSeen(message.id); +useEffect(()=> { + const getInfo = async ()=> { + if(!message?.sender) return + try { + const res = await getIndividualUserInfo(message?.sender) + if(!res) return null + setUserInfo(res) + } catch (error) { + // } - }, [inView, message.id, isLast]); + } + getInfo() +}, [message?.sender, getIndividualUserInfo]) + +const htmlText = useMemo(()=> { + + if(message?.messageText){ + return generateHTML(message?.messageText, [ + StarterKit, + Underline, + Highlight, + Mention, + TextStyle + ]) + } + +}, []) + + + +const htmlReply = useMemo(()=> { + + if(reply?.messageText){ + return generateHTML(reply?.messageText, [ + StarterKit, + Underline, + Highlight, + Mention, + TextStyle + ]) + } + +}, []) + +const userAvatarUrl = useMemo(()=> { + return message?.senderName ? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${ + message?.senderName + }/qortal_avatar?async=true` : '' +}, []) + +const onSeenFunc = useCallback(()=> { + onSeen(message.id); +}, [message?.id]) return ( <> - {message?.divide && ( + {message?.divide && (
Unread messages below
)} + + + +
) : ( + + {message?.senderName?.charAt(0)} + + + + + + + + )} - {message?.sender === myAddress && !message?.isNotEncrypted && ( + {message?.sender === myAddress && (!message?.isNotEncrypted || isPrivate === false) && ( { onEdit(message); @@ -201,11 +302,7 @@ export const MessageItem = ({ }}>Replied to {reply?.senderName || reply?.senderAddress} {reply?.messageText && ( )} {reply?.decryptedData?.type === "notification" ? ( @@ -217,15 +314,13 @@ export const MessageItem = ({ )} - {message?.messageText && ( + {htmlText && ( + htmlContent={htmlText} + /> )} + + {message?.decryptedData?.type === "notification" ? ( ) : ( @@ -341,7 +436,7 @@ export const MessageItem = ({ alignItems: 'center', gap: '15px' }}> - {message?.isNotEncrypted && ( + {message?.isNotEncrypted && isPrivate && ( - {/* */} - {/* {!message.unread && Seen} */} +
+
); -}; +}); export const ReplyPreview = ({message, isEdit})=> { @@ -456,6 +542,8 @@ export const ReplyPreview = ({message, isEdit})=> { StarterKit, Underline, Highlight, + Mention, + TextStyle ])} /> )} @@ -468,4 +556,36 @@ export const ReplyPreview = ({message, isEdit})=> { ) +} + +const MessageWragger = ({lastMessage, onSeen, isLast, children})=> { + + if(lastMessage){ + return ( + {children} + ) + } + return children +} + +const WatchComponent = ({onSeen, isLast, children})=> { + const { ref, inView } = useInView({ + threshold: 0.7, // Fully visible + triggerOnce: true, // Only trigger once when it becomes visible + }); + + useEffect(() => { + if (inView && isLast && onSeen) { + onSeen(); + } + }, [inView, isLast, onSeen]); + + return
+ {children} +
+ } \ No newline at end of file diff --git a/src/components/Chat/TipTap.tsx b/src/components/Chat/TipTap.tsx index 234d66e..929ab41 100644 --- a/src/components/Chat/TipTap.tsx +++ b/src/components/Chat/TipTap.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useRef, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { EditorProvider, useCurrentEditor, useEditor } from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; import { Color } from "@tiptap/extension-color"; @@ -34,6 +34,9 @@ import ListItemButton from '@mui/material/ListItemButton'; import ListItemText from '@mui/material/ListItemText'; import { ReactRenderer } from '@tiptap/react' import MentionList from './MentionList.jsx' +import { useRecoilState } from "recoil"; +import { isDisabledEditorEnterAtom } from "../../atoms/global.js"; +import { Box, Checkbox, Typography } from "@mui/material"; function textMatcher(doc, from) { const textBeforeCursor = doc.textBetween(0, from, ' ', ' '); @@ -44,7 +47,7 @@ function textMatcher(doc, from) { const query = match[0]; return { start, query }; } -const MenuBar = ({ setEditorRef, isChat }) => { +const MenuBar = ({ setEditorRef, isChat, isDisabledEditorEnter, setIsDisabledEditorEnter }) => { const { editor } = useCurrentEditor(); const fileInputRef = useRef(null); @@ -120,7 +123,9 @@ const MenuBar = ({ setEditorRef, isChat }) => { return (
-
+
editor.chain().focus().toggleBold().run()} disabled={!editor.can().chain().focus().toggleBold().run()} @@ -244,6 +249,43 @@ const MenuBar = ({ setEditorRef, isChat }) => { > + {isChat && ( + { + setIsDisabledEditorEnter(!isDisabledEditorEnter) + }} + > + + + disable enter + + + )} {!isChat && ( <> { + const [isDisabledEditorEnter, setIsDisabledEditorEnter] = useRecoilState(isDisabledEditorEnterAtom) const extensionsFiltered = isChat ? extensions.filter((item) => item?.name !== "image") @@ -421,12 +464,24 @@ export default ({ ] }, [enableMentions]) + const handleSetIsDisabledEditorEnter = useCallback((val)=> { + setIsDisabledEditorEnter(val) + localStorage.setItem('settings-disable-editor-enter', JSON.stringify(val)); + + }, []) + + return ( -
+
+ ) } extensions={[...extensionsFiltered, ...additionalExtensions @@ -450,7 +505,7 @@ export default ({ }; max-height:calc(100svh - ${customEditorHeight || "140px"})`: `overflow: auto; max-height: 250px`, }, handleKeyDown(view, event) { - if (!disableEnter && event.key === "Enter") { + if (!disableEnter && !isDisabledEditorEnter && event.key === "Enter") { if (event.shiftKey) { view.dispatch( view.state.tr.replaceSelectionWith( diff --git a/src/components/Chat/styles.css b/src/components/Chat/styles.css index 7f3f66e..3c7c570 100644 --- a/src/components/Chat/styles.css +++ b/src/components/Chat/styles.css @@ -93,7 +93,7 @@ .tiptap hr { border: none; border-top: 1px solid var(--gray-2); - margin: 2rem 0; + margin: 1rem 0; } .ProseMirror:focus-visible { @@ -103,6 +103,7 @@ .tiptap p { font-size: 16px; color: white; /* Ensure paragraph text color is white */ + margin: 0px; } .tiptap p.is-editor-empty:first-child::before { color: #adb5bd; @@ -112,6 +113,11 @@ pointer-events: none; } + .tiptap p:empty::before { + content: ''; + display: inline-block; + } + .tiptap a { color: cadetblue } @@ -125,7 +131,7 @@ font-size: 12px !important; } -.tiptap .mention { +.tiptap [data-type="mention"] { box-decoration-break: clone; color: lightblue; padding: 0.1rem 0.3rem; diff --git a/src/components/ContextMenuPinnedApps.tsx b/src/components/ContextMenuPinnedApps.tsx index be0ae46..bb64a4c 100644 --- a/src/components/ContextMenuPinnedApps.tsx +++ b/src/components/ContextMenuPinnedApps.tsx @@ -124,11 +124,19 @@ export const ContextMenuPinnedApps = ({ children, app, isMine }) => { { handleClose(e); setSortablePinnedApps((prev) => { - const updatedApps = prev.filter( - (item) => !(item?.name === app?.name && item?.service === app?.service) - ); - saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps); - return updatedApps; + if(app?.isPrivate){ + const updatedApps = prev.filter( + (item) => !(item?.privateAppProperties?.name === app?.privateAppProperties?.name && item?.privateAppProperties?.service === app?.privateAppProperties?.service && item?.privateAppProperties?.identifier === app?.privateAppProperties?.identifier) + ); + saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps); + return updatedApps; + } else { + const updatedApps = prev.filter( + (item) => !(item?.name === app?.name && item?.service === app?.service) + ); + saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps); + return updatedApps; + } }); }}> diff --git a/src/components/Desktop/DesktopFooter.tsx b/src/components/Desktop/DesktopFooter.tsx index faac91b..6da8f4e 100644 --- a/src/components/Desktop/DesktopFooter.tsx +++ b/src/components/Desktop/DesktopFooter.tsx @@ -150,7 +150,7 @@ export const DesktopFooter = ({ height={30} color={ hasUnreadGroups - ? "var(--unread)" + ? "var(--danger)" : isGroups ? "white" : "rgba(250, 250, 250, 0.5)" @@ -172,7 +172,7 @@ export const DesktopFooter = ({ height={30} color={ hasUnreadDirects - ? "var(--unread)" + ? "var(--danger)" : isDirects ? "white" : "rgba(250, 250, 250, 0.5)" diff --git a/src/components/Desktop/DesktopHeader.tsx b/src/components/Desktop/DesktopHeader.tsx index 7067153..4820054 100644 --- a/src/components/Desktop/DesktopHeader.tsx +++ b/src/components/Desktop/DesktopHeader.tsx @@ -19,6 +19,8 @@ import { ChatIcon } from "../../assets/Icons/ChatIcon"; import { ThreadsIcon } from "../../assets/Icons/ThreadsIcon"; import { MembersIcon } from "../../assets/Icons/MembersIcon"; import { AdminsIcon } from "../../assets/Icons/AdminsIcon"; +import LockIcon from '@mui/icons-material/Lock'; +import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred'; const IconWrapper = ({ children, label, color, selected, selectColor, customHeight }) => { return ( @@ -80,7 +82,8 @@ export const DesktopHeader = ({ hasUnreadChat, isChat, isForum, - setGroupSection + setGroupSection, + isPrivate }) => { const [value, setValue] = React.useState(0); return ( @@ -95,14 +98,27 @@ export const DesktopHeader = ({ padding: "10px", }} > - + + {isPrivate && ( + + )} + {isPrivate === false && ( + + )} - {selectedGroup?.groupName} + {selectedGroup?.groupId === '0' ? 'General' :selectedGroup?.groupName} - {/* { - goToHome(); - }} - > - - - - - { - setDesktopSideView("groups"); - }} - > - - - - - { - setDesktopSideView("directs"); - }} - > - - - - */} - {/* */} + { goToAnnouncements() diff --git a/src/components/DesktopSideBar.tsx b/src/components/DesktopSideBar.tsx index ae55a50..76f2f2e 100644 --- a/src/components/DesktopSideBar.tsx +++ b/src/components/DesktopSideBar.tsx @@ -11,7 +11,7 @@ import { useRecoilState } from 'recoil'; import { enabledDevModeAtom } from '../atoms/global'; import { AppsIcon } from '../assets/Icons/AppsIcon'; -export const DesktopSideBar = ({goToHome, setDesktopSideView, toggleSideViewDirects, hasUnreadDirects, isDirects, toggleSideViewGroups,hasUnreadGroups, isGroups, isApps, setDesktopViewMode, desktopViewMode }) => { +export const DesktopSideBar = ({goToHome, setDesktopSideView, toggleSideViewDirects, hasUnreadDirects, isDirects, toggleSideViewGroups,hasUnreadGroups, isGroups, isApps, setDesktopViewMode, desktopViewMode, myName }) => { const [isEnabledDevMode, setIsEnabledDevMode] = useRecoilState(enabledDevModeAtom) return ( @@ -90,7 +90,7 @@ export const DesktopSideBar = ({goToHome, setDesktopSideView, toggleSideViewDire height={30} color={ hasUnreadGroups - ? "var(--unread)" + ? "var(--danger)" : isGroups ? "white" : "rgba(250, 250, 250, 0.5)" @@ -98,7 +98,7 @@ export const DesktopSideBar = ({goToHome, setDesktopSideView, toggleSideViewDire /> */} - + {/* */} {isEnabledDevMode && ( {errorMsg} @@ -263,7 +263,7 @@ export const AttachmentCard = ({ }}> - {resourceDetails?.status?.status} + {resourceDetails?.status?.status === 'DOWNLOADED' ? 'BUILDING' : resourceDetails?.status?.status} {!resourceDetails && ( <> @@ -271,7 +271,7 @@ export const AttachmentCard = ({ )} - {resourceDetails && resourceDetails?.status?.status !== 'READY' && ( + {resourceDetails && resourceDetails?.status?.status !== 'READY' && resourceDetails?.status?.status !== 'FAILED_TO_DOWNLOAD' && ( <> {errorMsg} diff --git a/src/components/Embeds/PollEmbed.tsx b/src/components/Embeds/PollEmbed.tsx index 69d1369..3da02c3 100644 --- a/src/components/Embeds/PollEmbed.tsx +++ b/src/components/Embeds/PollEmbed.tsx @@ -221,7 +221,7 @@ export const PollCard = ({ {errorMsg} diff --git a/src/components/Embeds/VideoPlayer.tsx b/src/components/Embeds/VideoPlayer.tsx new file mode 100644 index 0000000..c28bc99 --- /dev/null +++ b/src/components/Embeds/VideoPlayer.tsx @@ -0,0 +1,723 @@ +import React, { useContext, useEffect, useMemo, useRef, useState } from 'react' +import ReactDOM from 'react-dom' +import { Box, IconButton, Slider } from '@mui/material' +import { CircularProgress, Typography } from '@mui/material' +import { Key } from 'ts-key-enum' +import { + PlayArrow, + Pause, + VolumeUp, + Fullscreen, + PictureInPicture, VolumeOff, Calculate +} from '@mui/icons-material' +import { styled } from '@mui/system' +import { Refresh } from '@mui/icons-material' + +import { Menu, MenuItem } from '@mui/material' +import { MoreVert as MoreIcon } from '@mui/icons-material' +import { GlobalContext, getBaseApiReact } from '../../App' +import { resourceKeySelector } from '../../atoms/global' +import { useRecoilValue } from 'recoil' +const VideoContainer = styled(Box)` + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + margin: 0px; + padding: 0px; +` + +const VideoElement = styled('video')` + width: 100%; + height: auto; + max-height: calc(100vh - 150px); + background: rgb(33, 33, 33); +` + +const ControlsContainer = styled(Box)` + position: absolute; + display: flex; + align-items: center; + justify-content: space-between; + bottom: 0; + left: 0; + right: 0; + padding: 8px; + background-color: rgba(0, 0, 0, 0.6); +` + +interface VideoPlayerProps { + src?: string + poster?: string + name?: string + identifier?: string + service?: string + autoplay?: boolean + from?: string | null + customStyle?: any + user?: string +} + +export const VideoPlayer: React.FC = ({ + poster, + name, + identifier, + service, + autoplay = true, + from = null, + customStyle = {}, + node +}) => { + + const keyIdentifier = useMemo(()=> { + + if(name && identifier && service){ + return `${service}-${name}-${identifier}` + } else { + return undefined + } + }, [service, name, identifier]) + const download = useRecoilValue(resourceKeySelector(keyIdentifier)); + const { downloadResource } = useContext(GlobalContext); + + const videoRef = useRef(null) + const [playing, setPlaying] = useState(false) + const [volume, setVolume] = useState(1) + const [mutedVolume, setMutedVolume] = useState(1) + const [isMuted, setIsMuted] = useState(false) + const [progress, setProgress] = useState(0) + const [isLoading, setIsLoading] = useState(false) + const [canPlay, setCanPlay] = useState(false) + const [startPlay, setStartPlay] = useState(false) + const [isMobileView, setIsMobileView] = useState(false) + const [playbackRate, setPlaybackRate] = useState(1) + const [anchorEl, setAnchorEl] = useState(null) + const reDownload = useRef(false) + + const resetVideoState = () => { + // Reset all states to their initial values + setPlaying(false); + setVolume(1); + setMutedVolume(1); + setIsMuted(false); + setProgress(0); + setIsLoading(false); + setCanPlay(false); + setStartPlay(false); + setIsMobileView(false); + setPlaybackRate(1); + setAnchorEl(null); + + // Reset refs to their initial values + if (videoRef.current) { + videoRef.current.pause(); // Ensure the video is paused + videoRef.current.currentTime = 0; // Reset video progress + } + reDownload.current = false; + }; + + const src = useMemo(() => { + if(name && identifier && service){ + return `${node || getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}` + } + return '' + }, [service, name, identifier]) + + useEffect(()=> { + resetVideoState() + }, [keyIdentifier]) + const resourceStatus = useMemo(() => { + return download?.status || {} + }, [download]) + + const minSpeed = 0.25; + const maxSpeed = 4.0; + const speedChange = 0.25; + + const updatePlaybackRate = (newSpeed: number) => { + if (videoRef.current) { + if (newSpeed > maxSpeed || newSpeed < minSpeed) + newSpeed = minSpeed + videoRef.current.playbackRate = newSpeed + setPlaybackRate(newSpeed) + } + } + + const increaseSpeed = (wrapOverflow = true) => { + const changedSpeed = playbackRate + speedChange + let newSpeed = wrapOverflow ? changedSpeed : Math.min(changedSpeed, maxSpeed) + + + if (videoRef.current) { + updatePlaybackRate(newSpeed); + } + } + + const decreaseSpeed = () => { + if (videoRef.current) { + updatePlaybackRate(playbackRate - speedChange); + } + } + + + const togglePlay = async () => { + if (!videoRef.current) return + setStartPlay(true) + if (!src || resourceStatus?.status !== 'READY') { + ReactDOM.flushSync(() => { + setIsLoading(true) + }) + getSrc() + } + if (playing) { + videoRef.current.pause() + } else { + videoRef.current.play() + } + setPlaying(!playing) + } + + + const onVolumeChange = (_: any, value: number | number[]) => { + if (!videoRef.current) return + videoRef.current.volume = value as number + setVolume(value as number) + setIsMuted(false) + } + + const onProgressChange = (_: any, value: number | number[]) => { + if (!videoRef.current) return + videoRef.current.currentTime = value as number + setProgress(value as number) + if (!playing) { + videoRef.current.play() + setPlaying(true) + } + } + + const handleEnded = () => { + setPlaying(false) + } + + const updateProgress = () => { + if (!videoRef.current) return + setProgress(videoRef.current.currentTime) + } + + const [isFullscreen, setIsFullscreen] = useState(false) + + const enterFullscreen = () => { + if (!videoRef.current) return + if (videoRef.current.requestFullscreen) { + videoRef.current.requestFullscreen() + } + } + + const exitFullscreen = () => { + if (document.exitFullscreen) { + document.exitFullscreen() + } + } + + const toggleFullscreen = () => { + isFullscreen ? exitFullscreen() : enterFullscreen() + } + + + useEffect(() => { + const handleFullscreenChange = () => { + setIsFullscreen(!!document.fullscreenElement) + } + + document.addEventListener('fullscreenchange', handleFullscreenChange) + return () => { + document.removeEventListener('fullscreenchange', handleFullscreenChange) + } + }, []) + + + + const handleCanPlay = () => { + setIsLoading(false) + setCanPlay(true) + } + + const getSrc = React.useCallback(async () => { + if (!name || !identifier || !service) return + try { + downloadResource({ + name, + service, + identifier + }) + } catch (error) { + console.error(error) + } + }, [identifier, name, service]) + + + + + function formatTime(seconds: number): string { + seconds = Math.floor(seconds) + let minutes: number | string = Math.floor(seconds / 60) + let hours: number | string = Math.floor(minutes / 60) + + let remainingSeconds: number | string = seconds % 60 + let remainingMinutes: number | string = minutes % 60 + + if (remainingSeconds < 10) { + remainingSeconds = '0' + remainingSeconds + } + + if (remainingMinutes < 10) { + remainingMinutes = '0' + remainingMinutes + } + + if (hours === 0) { + hours = '' + } + else { + hours = hours + ':' + } + + return hours + remainingMinutes + ':' + remainingSeconds + } + + const reloadVideo = () => { + if (!videoRef.current) return + const currentTime = videoRef.current.currentTime + videoRef.current.src = src + videoRef.current.load() + videoRef.current.currentTime = currentTime + if (playing) { + videoRef.current.play() + } + } + + useEffect(() => { + if ( + resourceStatus?.status === 'DOWNLOADED' && + reDownload?.current === false + ) { + getSrc() + reDownload.current = true + } + }, [getSrc, resourceStatus]) + + const handleMenuOpen = (event: any) => { + setAnchorEl(event.currentTarget) + } + + const handleMenuClose = () => { + setAnchorEl(null) + } + + useEffect(() => { + const videoWidth = videoRef?.current?.offsetWidth + if (videoWidth && videoWidth <= 600) { + setIsMobileView(true) + } + }, [canPlay]) + + const getDownloadProgress = (current: number, total: number) => { + const progress = current / total * 100; + return Number.isNaN(progress) ? '' : progress.toFixed(0) + '%' + } + const mute = () => { + setIsMuted(true) + setMutedVolume(volume) + setVolume(0) + if (videoRef.current) videoRef.current.volume = 0 + } + const unMute = () => { + setIsMuted(false) + setVolume(mutedVolume) + if (videoRef.current) videoRef.current.volume = mutedVolume + } + + const toggleMute = () => { + isMuted ? unMute() : mute(); + } + + const changeVolume = (volumeChange: number) => { + if (videoRef.current) { + const minVolume = 0; + const maxVolume = 1; + + + let newVolume = volumeChange + volume + + newVolume = Math.max(newVolume, minVolume) + newVolume = Math.min(newVolume, maxVolume) + + setIsMuted(false) + setMutedVolume(newVolume) + videoRef.current.volume = newVolume + setVolume(newVolume); + } + + } + const setProgressRelative = (secondsChange: number) => { + if (videoRef.current) { + const currentTime = videoRef.current?.currentTime + const minTime = 0 + const maxTime = videoRef.current?.duration || 100 + + let newTime = currentTime + secondsChange; + newTime = Math.max(newTime, minTime) + newTime = Math.min(newTime, maxTime) + videoRef.current.currentTime = newTime; + setProgress(newTime); + } + } + + const setProgressAbsolute = (videoPercent: number) => { + if (videoRef.current) { + videoPercent = Math.min(videoPercent, 100) + videoPercent = Math.max(videoPercent, 0) + const finalTime = videoRef.current?.duration * videoPercent / 100 + videoRef.current.currentTime = finalTime + setProgress(finalTime); + } + } + + + const keyboardShortcutsDown = (e: React.KeyboardEvent) => { + e.preventDefault() + + switch (e.key) { + case Key.Add: increaseSpeed(false); break; + case '+': increaseSpeed(false); break; + case '>': increaseSpeed(false); break; + + case Key.Subtract: decreaseSpeed(); break; + case '-': decreaseSpeed(); break; + case '<': decreaseSpeed(); break; + + case Key.ArrowLeft: { + if (e.shiftKey) setProgressRelative(-300); + else if (e.ctrlKey) setProgressRelative(-60); + else if (e.altKey) setProgressRelative(-10); + else setProgressRelative(-5); + } break; + + case Key.ArrowRight: { + if (e.shiftKey) setProgressRelative(300); + else if (e.ctrlKey) setProgressRelative(60); + else if (e.altKey) setProgressRelative(10); + else setProgressRelative(5); + } break; + + case Key.ArrowDown: changeVolume(-0.05); break; + case Key.ArrowUp: changeVolume(0.05); break; + } + } + + const keyboardShortcutsUp = (e: React.KeyboardEvent) => { + e.preventDefault() + + switch (e.key) { + case ' ': togglePlay(); break; + case 'm': toggleMute(); break; + + case 'f': enterFullscreen(); break; + case Key.Escape: exitFullscreen(); break; + + case '0': setProgressAbsolute(0); break; + case '1': setProgressAbsolute(10); break; + case '2': setProgressAbsolute(20); break; + case '3': setProgressAbsolute(30); break; + case '4': setProgressAbsolute(40); break; + case '5': setProgressAbsolute(50); break; + case '6': setProgressAbsolute(60); break; + case '7': setProgressAbsolute(70); break; + case '8': setProgressAbsolute(80); break; + case '9': setProgressAbsolute(90); break; + } + } + + return ( + + + {isLoading && ( + + + + + {resourceStatus?.status === 'REFETCHING' ? ( + <> + <> + {getDownloadProgress(resourceStatus?.localChunkCount, resourceStatus?.totalChunkCount)} + + + <> Refetching data in 25 seconds + + ) : resourceStatus?.status === 'DOWNLOADED' ? ( + <>Download Completed: building tutorial video... + ) : resourceStatus?.status !== 'READY' ? ( + <> + {getDownloadProgress(resourceStatus?.localChunkCount || 0, resourceStatus?.totalChunkCount || 100)} + + + ) : ( + <>Fetching tutorial from the Qortal Network... + )} + + + + )} + {((!src && !isLoading) || !startPlay) && ( + { + togglePlay() + }} + sx={{ + cursor: 'pointer' + }} + > + + + )} + + + + + {isMobileView && canPlay ? ( + <> + + {playing ? : } + + + + + + + + + + + + + + increaseSpeed()}> + + Speed: {playbackRate}x + + + + + + + + ) : canPlay ? ( + <> + + {playing ? : } + + + + + + + {progress && videoRef.current?.duration && formatTime(progress)}/ + {progress && + videoRef.current?.duration && + formatTime(videoRef.current?.duration)} + + + {isMuted ? : } + + + increaseSpeed()} + > + Speed: {playbackRate}x + + + + + + ) : null} + + + ) +} diff --git a/src/components/Explore/Explore.tsx b/src/components/Explore/Explore.tsx new file mode 100644 index 0000000..7cc96a0 --- /dev/null +++ b/src/components/Explore/Explore.tsx @@ -0,0 +1,101 @@ +import { Box, ButtonBase, Typography } from "@mui/material"; +import React from "react"; +import ChatIcon from "@mui/icons-material/Chat"; +import qTradeLogo from "../../assets/Icons/q-trade-logo.webp"; +import AppsIcon from "@mui/icons-material/Apps"; +import { executeEvent } from "../../utils/events"; +export const Explore = ({setDesktopViewMode}) => { + return ( + + { + executeEvent("addTab", { + data: { service: "APP", name: "q-trade" }, + }); + executeEvent("open-apps-mode", {}); + }} + > + + + Trade QORT + + + { + setDesktopViewMode('apps') + + }} + > + + + See Apps + + + { + executeEvent("openGroupMessage", { + from: "0" , + }); + }} + > + + + General Chat + + + + ); +}; diff --git a/src/components/GlobalActions/GlobalActions.tsx b/src/components/GlobalActions/GlobalActions.tsx new file mode 100644 index 0000000..6dd845c --- /dev/null +++ b/src/components/GlobalActions/GlobalActions.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import { JoinGroup } from './JoinGroup' + +export const GlobalActions = ({memberGroups}) => { + return ( + <> + + + ) +} diff --git a/src/components/GlobalActions/JoinGroup.tsx b/src/components/GlobalActions/JoinGroup.tsx new file mode 100644 index 0000000..f03430b --- /dev/null +++ b/src/components/GlobalActions/JoinGroup.tsx @@ -0,0 +1,284 @@ +import React, { useContext, useEffect, useMemo, useState } from "react"; +import { subscribeToEvent, unsubscribeFromEvent } from "../../utils/events"; +import { + Box, + Button, + ButtonBase, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + Typography, +} from "@mui/material"; +import { CustomButton, CustomButtonAccept } from "../../App-styles"; +import { getBaseApiReact, MyContext } from "../../App"; +import { getFee } from "../../background"; +import { CustomizedSnackbars } from "../Snackbar/Snackbar"; +import { FidgetSpinner } from "react-loader-spinner"; + +export const JoinGroup = ({ memberGroups }) => { + const { show, setTxList } = useContext(MyContext); + const [openSnack, setOpenSnack] = useState(false); + const [infoSnack, setInfoSnack] = useState(null); + const [groupInfo, setGroupInfo] = useState(null); + const [isLoadingInfo, setIsLoadingInfo] = useState(false); + const [isOpen, setIsOpen] = useState(false); + const [isLoadingJoinGroup, setIsLoadingJoinGroup] = useState(false); + const handleJoinGroup = async (e) => { + setGroupInfo(null); + const groupId = e?.detail?.groupId; + if (groupId) { + try { + setIsOpen(true); + setIsLoadingInfo(true); + const response = await fetch(`${getBaseApiReact()}/groups/${groupId}`); + const groupData = await response.json(); + setGroupInfo(groupData); + } catch (error) { + } finally { + setIsLoadingInfo(false); + } + } + }; + + useEffect(() => { + subscribeToEvent("globalActionJoinGroup", handleJoinGroup); + + return () => { + unsubscribeFromEvent("globalActionJoinGroup", handleJoinGroup); + }; + }, []); + + const isInGroup = useMemo(()=> { + return !!memberGroups.find((item)=> +item?.groupId === +groupInfo?.groupId) + }, [memberGroups, groupInfo]) + const joinGroup = 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", + }); + setIsLoadingJoinGroup(true); + await new Promise((res, rej) => { + window + .sendMessage("joinGroup", { + groupId, + }) + .then((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); + res(response); + return; + } else { + setInfoSnack({ + type: "error", + message: response?.error, + }); + setOpenSnack(true); + rej(response.error); + } + }) + .catch((error) => { + setInfoSnack({ + type: "error", + message: error.message || "An error occurred", + }); + setOpenSnack(true); + rej(error); + }); + }); + setIsLoadingJoinGroup(false); + } catch (error) { + } finally { + setIsLoadingJoinGroup(false); + } + }; + return ( + <> + + + {!groupInfo && ( + + {" "} + {" "} + + )} + + + Group name: {` ${groupInfo?.groupName}`} + + + Number of members: {` ${groupInfo?.memberCount}`} + + {groupInfo?.description && ( + + {groupInfo?.description} + + )} + {isInGroup && ( + + *You are already in this group! + + )} + {!isInGroup && groupInfo?.isOpen === false && ( + + *This is a closed/private group, so you will need to wait until + an admin accepts your request + + )} + + + + { + joinGroup(groupInfo, groupInfo?.isOpen); + + setIsOpen(false); + }} disabled={isInGroup}> + + Join + + + + setIsOpen(false)} + > + Close + + + + + + {isLoadingJoinGroup && ( + + + + )} + + ); +}; diff --git a/src/components/Group/AddGroup.tsx b/src/components/Group/AddGroup.tsx index ed90c8c..1cf86fe 100644 --- a/src/components/Group/AddGroup.tsx +++ b/src/components/Group/AddGroup.tsx @@ -217,6 +217,8 @@ export const AddGroup = ({ address, open, setOpen }) => { flexGrow: 1, overflowY: "auto", color: "white", + flexDirection: 'column', + display: 'flex' }} > @@ -454,7 +456,10 @@ export const AddGroup = ({ address, open, setOpen }) => { {value === 1 && ( @@ -465,7 +470,10 @@ export const AddGroup = ({ address, open, setOpen }) => { {value === 2 && ( diff --git a/src/components/Group/AddGroupList.tsx b/src/components/Group/AddGroupList.tsx index 17eb3a8..ccd4850 100644 --- a/src/components/Group/AddGroupList.tsx +++ b/src/components/Group/AddGroupList.tsx @@ -26,7 +26,9 @@ import _ from "lodash"; import { MyContext, getBaseApiReact } from "../../App"; import { LoadingButton } from "@mui/lab"; import { getBaseApi, getFee } from "../../background"; - +import LockIcon from '@mui/icons-material/Lock'; +import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred'; +import { Spacer } from "../../common/Spacer"; const cache = new CellMeasurerCache({ fixedWidth: true, defaultHeight: 50, @@ -231,7 +233,17 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => { handlePopoverOpen(event, index)} > - + {group?.isOpen === false && ( + + )} + {group?.isOpen === true && ( + + )} + { }; return ( -
+

Groups list

{
@@ -278,6 +293,6 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => { )}
-
+
); }; diff --git a/src/components/Group/BlockedUsersModal.tsx b/src/components/Group/BlockedUsersModal.tsx new file mode 100644 index 0000000..84fa3fa --- /dev/null +++ b/src/components/Group/BlockedUsersModal.tsx @@ -0,0 +1,190 @@ +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + TextField, + Typography, +} from "@mui/material"; +import React, { useContext, useEffect, useState } from "react"; +import { MyContext } from "../../App"; +import { Spacer } from "../../common/Spacer"; +import { executeEvent } from "../../utils/events"; + +export const BlockedUsersModal = ({ close }) => { + const [hasChanged, setHasChanged] = useState(false); + const [value, setValue] = useState(""); + + const { getAllBlockedUsers, removeBlockFromList, addToBlockList } = useContext(MyContext); + const [blockedUsers, setBlockedUsers] = useState({ + addresses: {}, + names: {}, + }); + const fetchBlockedUsers = () => { + setBlockedUsers(getAllBlockedUsers()); + }; + + useEffect(() => { + fetchBlockedUsers(); + }, []); + return ( + + Blocked Users + + + { + setValue(e.target.value); + }} + /> + + + + {Object.entries(blockedUsers?.addresses).length > 0 && ( + <> + + + Blocked Users for Chat ( addresses ) + + + + )} + + + {Object.entries(blockedUsers?.addresses || {})?.map( + ([key, value]) => { + return ( + + {key} + + + ); + } + )} + + {Object.entries(blockedUsers?.names).length > 0 && ( + <> + + + Blocked Users for QDN and Chat (names) + + + + )} + + + {Object.entries(blockedUsers?.names || {})?.map(([key, value]) => { + return ( + + {key} + + + ); + })} + + + + + + + ); +}; diff --git a/src/components/Group/Forum/GroupMail.tsx b/src/components/Group/Forum/GroupMail.tsx index d569b27..38d93e9 100644 --- a/src/components/Group/Forum/GroupMail.tsx +++ b/src/components/Group/Forum/GroupMail.tsx @@ -46,7 +46,7 @@ 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 { decryptPublishes, getTempPublish, handleUnencryptedPublishes } from "../../Chat/GroupAnnouncements"; import CheckSVG from "../../../assets/svgs/Check.svg"; import SortSVG from "../../../assets/svgs/Sort.svg"; import ArrowDownSVG from "../../../assets/svgs/ArrowDown.svg"; @@ -66,7 +66,8 @@ export const GroupMail = ({ secretKey, defaultThread, setDefaultThread, - hide + hide, + isPrivate }) => { const [viewedThreads, setViewedThreads] = React.useState({}); const [filterMode, setFilterMode] = useState("Recently active"); @@ -123,7 +124,7 @@ export const GroupMail = ({ } - const getEncryptedResource = async ({ name, identifier, resource }) => { + const getEncryptedResource = async ({ name, identifier, resource }, isPrivate) => { let data = dataPublishes.current[`${name}-${identifier}`] if(!data || (data?.update || data?.created !== (resource?.updated || resource?.created))){ const res = await fetch( @@ -136,7 +137,7 @@ export const GroupMail = ({ } else { data = data.data } - const response = await decryptPublishes([{ data }], secretKey); + const response = isPrivate === false ? handleUnencryptedPublishes([data]) : await decryptPublishes([{ data }], secretKey); const messageData = response[0]; return messageData.decryptedData; @@ -212,7 +213,7 @@ export const GroupMail = ({ name: message.name, identifier: message.identifier, resource: message - }), + }, isPrivate), delay(5000), ]); } catch (error) {} @@ -255,7 +256,7 @@ export const GroupMail = ({ } } }, - [allThreads] + [allThreads, isPrivate] ); const getMailMessages = React.useCallback( async (groupId: string, members: any) => { @@ -327,7 +328,7 @@ export const GroupMail = ({ name: thread.name, identifier: message.threadId, resource: thread - }), + }, isPrivate), delay(10000), ]); if (threadRes?.title) { @@ -356,16 +357,16 @@ export const GroupMail = ({ // dispatch(setIsLoadingCustom(null)); } }, - [secretKey] + [secretKey, isPrivate] ); const getMessages = React.useCallback(async () => { // if ( !groupId || members?.length === 0) return; - if (!groupId) return; + if (!groupId || isPrivate === null) return; await getMailMessages(groupId, members); - }, [getMailMessages, groupId, members, secretKey]); + }, [getMailMessages, groupId, members, secretKey, isPrivate]); const interval = useRef(null); @@ -378,7 +379,7 @@ export const GroupMail = ({ firstMount.current = false; } // if (groupId && !firstMount.current && members.length > 0) { - if (groupId && !firstMount.current) { + if (groupId && !firstMount.current && isPrivate !== null) { if (filterMode === "Recently active") { getMessages(); } else if (filterMode === "Newest") { @@ -389,7 +390,7 @@ export const GroupMail = ({ setTempData() firstMount.current = true; } - }, [groupId, members, filterMode, hide]); + }, [groupId, members, filterMode, hide, isPrivate]); const closeThread = useCallback(() => { setCurrentThread(null); @@ -468,7 +469,7 @@ export const GroupMail = ({ } else if (filterMode === "Oldest") { getAllThreads(groupId, "Oldest", true); } - }, [filterMode]) + }, [filterMode, isPrivate]) const updateThreadActivityCurrentThread = ()=> { if(!currentThread) return @@ -540,6 +541,7 @@ export const GroupMail = ({ secretKey={secretKey} getSecretKey={getSecretKey} updateThreadActivityCurrentThread={updateThreadActivityCurrentThread} + isPrivate={isPrivate} /> ); @@ -620,6 +622,7 @@ export const GroupMail = ({ userInfo={userInfo} getSecretKey={getSecretKey} myName={userInfo?.name} + isPrivate={isPrivate} /> { const { show } = React.useContext(MyContext); @@ -245,8 +246,8 @@ export const NewThread = ({ reply, }; - const secretKey = await getSecretKey(false, true); - if (!secretKey) { + const secretKey = isPrivate === false ? null : await getSecretKey(false, true); + if (!secretKey && isPrivate) { throw new Error("Cannot get group secret key"); } @@ -254,7 +255,7 @@ export const NewThread = ({ const idThread = uid.rnd(); const idMsg = uid.rnd(); const messageToBase64 = await objectToBase64(mailObject); - const encryptSingleFirstPost = await encryptSingleFunc( + const encryptSingleFirstPost = isPrivate === false ? messageToBase64 : await encryptSingleFunc( messageToBase64, secretKey ); @@ -266,7 +267,7 @@ export const NewThread = ({ }; const threadToBase64 = await objectToBase64(threadObject); - const encryptSingleThread = await encryptSingleFunc( + const encryptSingleThread = isPrivate === false ? threadToBase64 : await encryptSingleFunc( threadToBase64, secretKey ); @@ -321,7 +322,7 @@ export const NewThread = ({ if (!currentThread) throw new Error("unable to locate thread Id"); const idThread = currentThread.threadId; const messageToBase64 = await objectToBase64(mailObject); - const encryptSinglePost = await encryptSingleFunc( + const encryptSinglePost = isPrivate === false ? messageToBase64 : await encryptSingleFunc( messageToBase64, secretKey ); @@ -533,14 +534,12 @@ export const NewThread = ({ {isMessage ? ( ) : ( { +}, isPrivate) => { let data = dataPublishes[`${name}-${identifier}`]; if ( !data || @@ -99,7 +100,7 @@ const getEncryptedResource = async ({ } else { data = data.data; } - const response = await decryptPublishes([{ data }], secretKey); + const response = isPrivate === false ? handleUnencryptedPublishes([data]) : await decryptPublishes([{ data }], secretKey); const messageData = response[0]; return messageData.decryptedData; @@ -114,6 +115,7 @@ export const Thread = ({ secretKey, getSecretKey, updateThreadActivityCurrentThread, + isPrivate }: ThreadProps) => { const [tempPublishedList, setTempPublishedList] = useState([]); const [messages, setMessages] = useState([]); @@ -164,7 +166,7 @@ export const Thread = ({ resource: message, groupId: groupInfo?.groupId, dataPublishes: dataPublishes.current, - }); + }, isPrivate); if (responseDataMessage?.error) { const fullObject = { @@ -323,9 +325,9 @@ export const Thread = ({ [messages, secretKey] ); const getMessages = React.useCallback(async () => { - if (!currentThread || !secretKey || !groupInfo?.groupId) return; + if (!currentThread || (!secretKey && isPrivate) || !groupInfo?.groupId || isPrivate === null) return; await getMailMessages(currentThread, null, null, false, groupInfo?.groupId); - }, [getMailMessages, currentThread, secretKey, groupInfo?.groupId]); + }, [getMailMessages, currentThread, secretKey, groupInfo?.groupId, isPrivate]); const firstMount = useRef(false); const saveTimestamp = useCallback((currentThread: any, username?: string) => { @@ -380,10 +382,11 @@ export const Thread = ({ if (currentThreadRef.current?.threadId !== currentThread?.threadId) { firstMount.current = false; } - if (currentThread && secretKey && !firstMount.current) { + if(!secretKey && isPrivate) return + if (currentThread && !firstMount.current && isPrivate !== null) { getMessagesMiddleware(); } - }, [currentThread, secretKey]); + }, [currentThread, secretKey, isPrivate]); const messageCallback = useCallback((msg: any) => { // dispatch(addToHashMapMail(msg)) // setMessages((prev) => [msg, ...prev]) @@ -576,6 +579,7 @@ export const Thread = ({ myName={userInfo?.name} publishCallback={setTempData} setPostReply={setPostReply} + isPrivate={isPrivate} /> touchStartY) { -// event.preventDefault(); -// } -// }); - -const isWithinLast15Minutes = (timestamp) => { - const now = Date.now(); // Current timestamp in milliseconds - const fifteenMinutes = 15 * 60 * 1000; // 15 minutes in milliseconds - - return now - timestamp < fifteenMinutes; -}; export const getPublishesFromAdmins = async (admins: string[], groupId) => { - // const validApi = await findUsableApi(); const queryString = admins.map((name) => `name=${name}`).join("&"); const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT_PRIVATE&identifier=symmetric-qchat-group-${ groupId @@ -157,12 +110,12 @@ export const getPublishesFromAdmins = async (admins: string[], groupId) => { return dateB.getTime() - dateA.getTime(); }); + return sortedData[0]; }; interface GroupProps { myAddress: string; isFocused: boolean; - isMain: boolean; userInfo: any; balance: number; } @@ -242,7 +195,8 @@ export const getGroupMembers = async (groupNumber: number) => { return groupData; }; -export const decryptResource = async (data: string) => { + +export const decryptResource = async (data: string, fromQortalRequest) => { try { return new Promise((res, rej) => { window.sendMessage("decryptGroupEncryption", { @@ -253,10 +207,19 @@ export const decryptResource = async (data: string) => { res(response); return; } - rej(response.error); + if(fromQortalRequest){ + rej({error: response.error, message: response?.error}); + } else { + rej(response.error); + + } }) .catch((error) => { - rej(error.message || "An error occurred"); + if(fromQortalRequest){ + rej({message: error.message || "An error occurred", error: error.message || "An error occurred"}); + } else { + rej(error.message || "An error occurred",); + } }); }); @@ -329,16 +292,7 @@ export const getGroupAdmins = async (groupNumber: number) => { let members: any = []; let membersAddresses = []; let both = []; - // 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) { @@ -384,17 +338,9 @@ export const getNames = async (listOfMembers) => { 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(() => { @@ -412,15 +358,25 @@ export const getNamesForAdmins = async (admins) => { return members; }; +function areKeysEqual(array1, array2) { + // If lengths differ, the arrays cannot be equal + if (array1?.length !== array2?.length) { + return false; + } + + // Sort both arrays and compare their elements + const sortedArray1 = [...array1].sort(); + const sortedArray2 = [...array2].sort(); + + return sortedArray1.every((key, index) => key === sortedArray2[index]); +} + export const Group = ({ myAddress, isFocused, - isMain, userInfo, balance, - isOpenDrawerProfile, setIsOpenDrawerProfile, - logoutFunc, setDesktopViewMode, desktopViewMode }: GroupProps) => { @@ -448,7 +404,7 @@ export const Group = ({ const [openAddGroup, setOpenAddGroup] = useState(false); const [isInitialGroups, setIsInitialGroups] = useState(false); const [openManageMembers, setOpenManageMembers] = useState(false); - const { setMemberGroups, memberGroups, rootHeight } = useContext(MyContext); + const { setMemberGroups, rootHeight } = useContext(MyContext); const lastGroupNotification = useRef(null); const [timestampEnterData, setTimestampEnterData] = useState({}); const [chatMode, setChatMode] = useState("groups"); @@ -464,6 +420,8 @@ export const Group = ({ const [groupAnnouncements, setGroupAnnouncements] = React.useState({}); const [defaultThread, setDefaultThread] = React.useState(null); const [isOpenDrawer, setIsOpenDrawer] = React.useState(false); + const [isOpenBlockedUserModal, setIsOpenBlockedUserModal] = React.useState(false); + const [hideCommonKeyPopup, setHideCommonKeyPopup] = React.useState(false); const [isLoadingGroupMessage, setIsLoadingGroupMessage] = React.useState(""); const [drawerMode, setDrawerMode] = React.useState("groups"); @@ -486,6 +444,20 @@ export const Group = ({ const [appsModeDev, setAppsModeDev] = useState('home') const [isOpenSideViewDirects, setIsOpenSideViewDirects] = useState(false) const [isOpenSideViewGroups, setIsOpenSideViewGroups] = useState(false) + const [isForceShowCreationKeyPopup, setIsForceShowCreationKeyPopup] = useState(false) + + const [groupsProperties, setGroupsProperties] = useState({}) + const setUserInfoForLevels = useSetRecoilState(addressInfoControllerAtom); + + const isPrivate = useMemo(()=> { + if(selectedGroup?.groupId === '0') return false + if(!selectedGroup?.groupId || !groupsProperties[selectedGroup?.groupId]) return null + if(groupsProperties[selectedGroup?.groupId]?.isOpen === true) return false + if(groupsProperties[selectedGroup?.groupId]?.isOpen === false) return true + return null + }, [selectedGroup]) + + const setSelectedGroupId = useSetRecoilState(selectedGroupIdAtom) const toggleSideViewDirects = ()=> { if(isOpenSideViewGroups){ @@ -519,6 +491,8 @@ export const Group = ({ selectedDirectRef.current = selectedDirect; }, [selectedDirect]); + + const getUserSettings = async () => { try { return new Promise((res, rej) => { @@ -566,28 +540,7 @@ export const Group = ({ }); } catch (error) {} }; - const getGroupDataSingle = async (groupId) => { - try { - return new Promise((res, rej) => { - window.sendMessage("getGroupDataSingle", { - groupId, - }) - .then((response) => { - if (!response?.error) { - res(response); - return; - } - rej(response.error); - }) - .catch((error) => { - rej(error.message || "An error occurred"); - }); - - }); - } catch (error) { - return {}; - } - }; + const refreshHomeDataFunc = () => { setGroupSection("default"); setTimeout(() => { @@ -615,6 +568,13 @@ export const Group = ({ } catch (error) {} }; + useEffect(()=> { + if(myAddress){ + getGroupAnnouncements() + getTimestampEnterChat() + } + }, [myAddress]) + const getGroupOwner = async (groupId) => { try { const url = `${getBaseApiReact()}/groups/${groupId}`; @@ -629,43 +589,8 @@ export const Group = ({ } 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; @@ -691,9 +616,8 @@ export const Group = ({ if ( group?.data && - isExtMsg(group?.data) && group?.sender !== myAddress && - group?.timestamp && (!isUpdateMsg(group?.data) || groupChatTimestamps[group?.groupId]) && + group?.timestamp && groupChatTimestamps[group?.groupId] && ((!timestampEnterData[group?.groupId] && Date.now() - group?.timestamp < timeDifferenceForNotificationChats) || timestampEnterData[group?.groupId] < group?.timestamp) @@ -702,7 +626,7 @@ export const Group = ({ } }); return hasUnread; - }, [timestampEnterData, groups, myAddress]); + }, [timestampEnterData, groups, myAddress, groupChatTimestamps]); const groupsAnnHasUnread = useMemo(() => { let hasUnread = false; @@ -717,15 +641,7 @@ export const Group = ({ return hasUnread; }, [groupAnnouncements, groups]); - // useEffect(() => { - // if (!myAddress) return; - // checkGroupListFunc(myAddress); - // return () => { - // if (checkGroupInterval?.current) { - // clearInterval(checkGroupInterval.current); - // } - // }; - // }, [checkGroupListFunc, myAddress]); + const getSecretKey = async ( @@ -734,27 +650,16 @@ export const Group = ({ ) => { try { setIsLoadingGroupMessage("Locating encryption keys"); - // setGroupDataLastSet(null) pauseAllQueues(); let dataFromStorage; let publishFromStorage; let adminsFromStorage; - // const groupData = await getGroupDataSingle(selectedGroup?.groupId); - // if ( - // groupData?.secretKeyData && - // Date.now() - groupData?.timestampLastSet < 3600000 - // ) { - // dataFromStorage = groupData.secretKeyData; - // publishFromStorage = groupData.secretKeyResource; - // adminsFromStorage = groupData.admins; - // // setGroupDataLastSet(groupData.timestampLastSet) - // } - if ( secretKeyToPublish && secretKey && - lastFetchedSecretKey.current && - Date.now() - lastFetchedSecretKey.current < 1800000 + lastFetchedSecretKey.current + && + Date.now() - lastFetchedSecretKey.current < 600000 ) return secretKey; if (loadingGroupParam) { @@ -839,34 +744,44 @@ export const Group = ({ } finally { setIsLoadingGroup(false); setIsLoadingGroupMessage(""); - if (!secretKeyToPublish) { - // await getAdmins(selectedGroup?.groupId); - } resumeAllQueues(); } }; - + const getAdminsForPublic = async(selectedGroup)=> { + try { + const { names, addresses, both } = + await getGroupAdmins(selectedGroup?.groupId) + setAdmins(addresses); + setAdminsWithNames(both); + } catch (error) { + //error + } + } useEffect(() => { - if (selectedGroup) { - setTriedToFetchSecretKey(false); - getSecretKey(true); + if (selectedGroup && isPrivate !== null) { + if(isPrivate){ + setTriedToFetchSecretKey(false); + getSecretKey(true); + } + getGroupOwner(selectedGroup?.groupId); } - }, [selectedGroup]); + if(isPrivate === false){ + setTriedToFetchSecretKey(true); + if(selectedGroup?.groupId !== '0'){ + getAdminsForPublic(selectedGroup) + } + + + } + }, [selectedGroup, isPrivate]); - const getAdmins = async (groupId) => { - try { - const res = await getGroupAdminsAddress(groupId); - setAdmins(res); - const adminsWithNames = await getNamesForAdmins(res); - setAdminsWithNames(adminsWithNames); - } catch (error) {} - }; + const getCountNewMesg = async (groupId, after)=> { try { @@ -886,9 +801,8 @@ export const Group = ({ const groupData = {} const getGroupData = groups.map(async(group)=> { - const isUpdate = isUpdateMsg(group?.data) if(!group.groupId || !group?.timestamp) return null - if(isUpdate && (!groupData[group.groupId] || groupData[group.groupId] < group.timestamp)){ + if((!groupData[group.groupId] || groupData[group.groupId] < group.timestamp)){ const hasMoreRecentMsg = await getCountNewMesg(group.groupId, timestampEnterDataRef.current[group?.groupId] || Date.now() - 24 * 60 * 60 * 1000) if(hasMoreRecentMsg){ groupData[group.groupId] = hasMoreRecentMsg @@ -905,6 +819,32 @@ export const Group = ({ } } + const getGroupsProperties = useCallback(async(address)=> { + try { + const url = `${getBaseApiReact()}/groups/member/${address}`; + const response = await fetch(url); + if(!response.ok) throw new Error('Cannot get group properties') + let data = await response.json(); + const transformToObject = data.reduce((result, item) => { + + result[item.groupId] = item + return result; + }, {}); + setGroupsProperties(transformToObject) + } catch (error) { + // error + } + }, []) + + + useEffect(()=> { + if(!myAddress) return + if(areKeysEqual(groups?.map((grp)=> grp?.groupId), Object.keys(groupsProperties))){ + } else { + getGroupsProperties(myAddress) + } + }, [groups, myAddress]) + useEffect(() => { @@ -919,7 +859,7 @@ export const Group = ({ // Update the component state with the received 'sendqort' state setGroups(sortArrayByTimestampAndGroupName(message.payload)); getLatestRegularChat(message.payload); - setMemberGroups(message.payload); + setMemberGroups(message.payload?.filter((item)=> item?.groupId !== '0')); if (selectedGroupRef.current && groupSectionRef.current === "chat") { window.sendMessage("addTimestampEnterChat", { @@ -1010,7 +950,7 @@ export const Group = ({ !initiatedGetMembers.current && selectedGroup?.groupId && secretKey && - admins.includes(myAddress) + admins.includes(myAddress) && selectedGroup?.groupId !== '0' ) { // getAdmins(selectedGroup?.groupId); getMembers(selectedGroup?.groupId); @@ -1092,9 +1032,9 @@ export const Group = ({ .filter((group) => group?.sender !== myAddress) .find((gr) => gr?.groupId === selectedGroup?.groupId); if (!findGroup) return false; - if (!findGroup?.data || !isExtMsg(findGroup?.data)) return false; + if (!findGroup?.data) return false; return ( - findGroup?.timestamp && (!isUpdateMsg(findGroup?.data) || groupChatTimestamps[findGroup?.groupId]) && + findGroup?.timestamp && groupChatTimestamps[findGroup?.groupId] && ((!timestampEnterData[selectedGroup?.groupId] && Date.now() - findGroup?.timestamp < timeDifferenceForNotificationChats) || @@ -1128,9 +1068,7 @@ export const Group = ({ } else { setMobileViewModeKeepOpen("messaging"); } - // setChatMode("directs"); setSelectedDirect(null); - // setSelectedGroup(null); setNewChat(false); @@ -1166,7 +1104,6 @@ export const Group = ({ setMobileViewModeKeepOpen("messaging"); } setSelectedDirect(null); - // setSelectedGroup(null); setNewChat(false); @@ -1255,6 +1192,7 @@ export const Group = ({ setSecretKeyDetails(null); setNewEncryptionNotification(null); setMemberCountFromSecretKeyData(null); + setIsForceShowCreationKeyPopup(false) setSelectedGroup(null); setSelectedDirect(null); setGroups([]); @@ -1361,11 +1299,11 @@ export const Group = ({ 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; - + setChatMode("groups"); + setDesktopViewMode('chat') return; } if (findGroup) { @@ -1384,6 +1322,7 @@ export const Group = ({ setAdminsWithNames([]); setMembers([]); setMemberCountFromSecretKeyData(null); + setIsForceShowCreationKeyPopup(false) setTriedToFetchSecretKey(false); setFirstSecretKeyInCreation(false); setGroupSection("chat"); @@ -1437,6 +1376,7 @@ export const Group = ({ setAdminsWithNames([]); setMembers([]); setMemberCountFromSecretKeyData(null); + setIsForceShowCreationKeyPopup(false) setTriedToFetchSecretKey(false); setFirstSecretKeyInCreation(false); setGroupSection("announcement"); @@ -1480,11 +1420,7 @@ export const Group = ({ if (findGroup?.groupId === selectedGroup?.groupId) { setGroupSection("forum"); setDefaultThread(data); - // setTimeout(() => { - // executeEvent("setThreadByEvent", { - // data: data - // }); - // }, 400); + return; } if (findGroup) { @@ -1500,6 +1436,7 @@ export const Group = ({ setAdminsWithNames([]); setMembers([]); setMemberCountFromSecretKeyData(null); + setIsForceShowCreationKeyPopup(false) setTriedToFetchSecretKey(false); setFirstSecretKeyInCreation(false); setGroupSection("forum"); @@ -1536,31 +1473,13 @@ export const Group = ({ } setDesktopViewMode('home') - // setGroupSection("default"); - // clearAllQueues(); + await new Promise((res) => { setTimeout(() => { res(null); }, 200); }); - // setGroupSection("home"); - // setSelectedGroup(null); - // setNewChat(false); - // setSelectedDirect(null); - // setSecretKey(null); - // setGroupOwner(null) - // lastFetchedSecretKey.current = null; - // initiatedGetMembers.current = false; - // setSecretKeyPublishDate(null); - // setAdmins([]); - // setSecretKeyDetails(null); - // setAdminsWithNames([]); - // setMembers([]); - // setMemberCountFromSecretKeyData(null); - // setTriedToFetchSecretKey(false); - // setFirstSecretKeyInCreation(false); - // setIsOpenSideViewDirects(false) - // setIsOpenSideViewGroups(false) + }; const goToAnnouncements = async () => { @@ -1622,6 +1541,7 @@ export const Group = ({ }; + const renderDirects = () => { return (
)} - {isMobile && ( - - - - - - - - { - setMobileViewModeKeepOpen('') - }} - > - - - - - - )}
{directs.map((direct: any) => ( @@ -1766,15 +1635,9 @@ export const Group = ({ dense={true} > - // - // - // } onClick={() => { setSelectedDirect(null); setNewChat(false); - // setSelectedGroup(null); setIsOpenDrawer(false); window.sendMessage("addTimestampEnterChat", { timestamp: Date.now(), @@ -1815,7 +1678,6 @@ export const Group = ({ color: "white", }} alt={direct?.name || direct?.address} - // src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${groupOwner?.name}/qortal_group_avatar_${group.groupId}?async=true`} > {(direct?.name || direct?.address)?.charAt(0)} @@ -1855,7 +1717,7 @@ export const Group = ({ direct?.timestamp) && ( )} @@ -1876,7 +1738,6 @@ export const Group = ({ onClick={() => { setNewChat(true); setSelectedDirect(null); - // setSelectedGroup(null); setIsOpenDrawer(false); }} > @@ -1892,6 +1753,7 @@ export const Group = ({ ); }; + const renderGroups = () => { return (
- // - // - // } onClick={() => { setMobileViewMode("group"); setDesktopSideView('groups') @@ -2002,6 +1859,7 @@ export const Group = ({ setTriedToFetchSecretKey(false); setNewChat(false); setSelectedGroup(null); + setUserInfoForLevels({}) setSecretKey(null); lastFetchedSecretKey.current = null; setSecretKeyPublishDate(null); @@ -2013,28 +1871,13 @@ export const Group = ({ setMemberCountFromSecretKeyData(null); setHideCommonKeyPopup(false); setFirstSecretKeyInCreation(false); - // setGroupSection("announcement"); setGroupSection("chat"); setIsOpenDrawer(false); + setIsForceShowCreationKeyPopup(false) setTimeout(() => { setSelectedGroup(group); - // getTimestampEnterChat(); }, 200); - - // window.sendMessage("addTimestampEnterChat", { - // timestamp: Date.now(), - // groupId: group.groupId, - // }).catch((error) => { - // console.error("Failed to add timestamp:", error.message || "An error occurred"); - // }); - - - // setTimeout(() => { - // getTimestampEnterChat(); - // }, 200); - - }} sx={{ display: "flex", @@ -2061,19 +1904,48 @@ export const Group = ({ }} > - - {group.groupName?.charAt(0)} - + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + }}> + + + ): ( + + + + // + // {group.groupName?.charAt(0)} + // + )} + )} {group?.data && - isExtMsg(group?.data) && (!isUpdateMsg(group?.data) || groupChatTimestamps[group?.groupId]) && + groupChatTimestamps[group?.groupId] && group?.sender !== myAddress && group?.timestamp && ((!timestampEnterData[group?.groupId] && @@ -2116,7 +1988,7 @@ export const Group = ({ group?.timestamp) && ( )} @@ -2132,9 +2004,11 @@ export const Group = ({ width: "100%", justifyContent: "center", padding: "10px", + gap: '10px' }} > {chatMode === "groups" && ( + <> { setOpenAddGroup(true); @@ -2147,13 +2021,28 @@ export const Group = ({ /> Group Mgmt + { + setIsOpenBlockedUserModal(true); + }} + sx={{ + minWidth: 'unset', + padding: '10px' + }} + > + + + )} {chatMode === "directs" && ( { setNewChat(true); setSelectedDirect(null); - // setSelectedGroup(null); setIsOpenDrawer(false); }} > @@ -2183,26 +2072,7 @@ export const Group = ({ setInfo={setInfoSnack} /> - {isMobile && ( -
- )} +
- {mobileViewMode === "groups" && !mobileViewModeKeepOpen && renderGroups()} - {mobileViewModeKeepOpen === "messaging" && renderDirects()} {newChat && ( <> {isMobile && ( @@ -2353,7 +2221,7 @@ export const Group = ({ )} - {mobileViewMode !== 'groups' && ( +
)} - {isMobile && ( - - - - { - setMobileViewMode("groups"); - }} - > - - - - - {selectedGroup?.groupName} - - - {/* */} - - - - )} + - {isMobile && mobileViewMode === "group" && ( - <> - - - )} + )} - {firstSecretKeyInCreation && + {isPrivate && firstSecretKeyInCreation && triedToFetchSecretKey && !secretKeyPublishDate && (
)} - {!admins.includes(myAddress) && + {isPrivate && !admins.includes(myAddress) && !secretKey && triedToFetchSecretKey ? ( <> @@ -2576,7 +2380,7 @@ export const Group = ({ ) : null} ) : admins.includes(myAddress) && - !secretKey && + (!secretKey && isPrivate) && triedToFetchSecretKey ? null : !triedToFetchSecretKey ? null : ( <> - + {groupSection === "adminSpace" && ( + + )} + )} @@ -2618,12 +2427,13 @@ export const Group = ({ zIndex: 100, }} > - {admins.includes(myAddress) && + {((isPrivate && admins.includes(myAddress) && shouldReEncrypt && triedToFetchSecretKey && !firstSecretKeyInCreation && - !hideCommonKeyPopup && ( + !hideCommonKeyPopup) || isForceShowCreationKeyPopup) && ( )}
- )} + {isOpenBlockedUserModal && ( + { + setIsOpenBlockedUserModal(false) + }} /> + )} {selectedDirect && !newChat && ( <> @@ -2696,62 +2511,7 @@ export const Group = ({ )} - {/* {!isMobile && groupSection === "home" && ( - - )} */} - {isMobile && mobileViewMode === "home" && ( - - )} - {isMobile && ( - - )} + {!isMobile && ( + )} @@ -2789,7 +2553,6 @@ export const Group = ({ sx={{ marginLeft: "auto", width: "31px", - // minWidth: "135px", padding: "5px", display: (isMobile || desktopViewMode === 'apps' || desktopViewMode === 'dev' || desktopViewMode === 'chat') ? "none" : "flex", }} @@ -2811,54 +2574,6 @@ export const Group = ({ }} />
- - {(isMobile && mobileViewMode === "home" || (isMobile && mobileViewMode === "apps" && appsMode === 'home')) && !mobileViewModeKeepOpen && ( - <> -
- {/* - {renderGroups()} - */} - {isMobile && ( - - )} - - )} - {(isMobile && mobileViewMode === "apps" && appsMode !== 'home') && !mobileViewModeKeepOpen && ( - <> - - - )} ); }; diff --git a/src/components/Group/GroupInvites.tsx b/src/components/Group/GroupInvites.tsx index 0eda0f6..13e5850 100644 --- a/src/components/Group/GroupInvites.tsx +++ b/src/components/Group/GroupInvites.tsx @@ -10,16 +10,20 @@ 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 { Box, ButtonBase, Collapse, Typography } from "@mui/material"; import { Spacer } from "../../common/Spacer"; import { getGroupNames } from "./UserListOfInvites"; import { CustomLoader } from "../../common/CustomLoader"; import { getBaseApiReact, isMobile } from "../../App"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import ExpandLessIcon from "@mui/icons-material/ExpandLess"; export const GroupInvites = ({ myAddress, setOpenAddGroup }) => { const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState( [] ); + const [isExpanded, setIsExpanded] = React.useState(false); + const [loading, setLoading] = React.useState(true); const getJoinRequests = async () => { @@ -53,121 +57,129 @@ export const GroupInvites = ({ myAddress, setOpenAddGroup }) => { alignItems: "center", }} > - setIsExpanded((prev)=> !prev)} > - Group Invites: + Group Invites {groupsWithJoinRequests?.length > 0 && ` (${groupsWithJoinRequests?.length})`} - - + {isExpanded ? : ( + + )} + + + - {loading && groupsWithJoinRequests.length === 0 && ( - + {loading && groupsWithJoinRequests.length === 0 && ( + + + + )} + {!loading && groupsWithJoinRequests.length === 0 && ( + + + Nothing to display + + + )} + - - - )} - {!loading && groupsWithJoinRequests.length === 0 && ( - - - Nothing to display - - - )} - - {groupsWithJoinRequests?.map((group) => { - return ( - { - setOpenAddGroup(true); - setTimeout(() => { - executeEvent("openGroupInvitesRequest", {}); - }, 300); - }} - disablePadding - secondaryAction={ - - { + return ( + { + setOpenAddGroup(true); + setTimeout(() => { + executeEvent("openGroupInvitesRequest", {}); + }, 300); + }} + disablePadding + secondaryAction={ + + + + } + > + + - - } - > - - - - - ); - })} - - + + + ); + })} + + + ); }; diff --git a/src/components/Group/GroupJoinRequests.tsx b/src/components/Group/GroupJoinRequests.tsx index 4c484aa..76f958a 100644 --- a/src/components/Group/GroupJoinRequests.tsx +++ b/src/components/Group/GroupJoinRequests.tsx @@ -11,16 +11,20 @@ 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 { Box, ButtonBase, Collapse, Typography } from "@mui/material"; import { Spacer } from "../../common/Spacer"; import { CustomLoader } from "../../common/CustomLoader"; import { getBaseApi } from "../../background"; import { MyContext, getBaseApiReact, isMobile } from "../../App"; import { myGroupsWhereIAmAdminAtom } from "../../atoms/global"; import { useSetRecoilState } from "recoil"; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ExpandLessIcon from '@mui/icons-material/ExpandLess'; export const requestQueueGroupJoinRequests = new RequestQueueWithPromise(2) export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, getTimestampEnterChat, setSelectedGroup, setGroupSection, setMobileViewMode, setDesktopViewMode }) => { + const [isExpanded, setIsExpanded] = React.useState(false) + const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState([]) const [loading, setLoading] = React.useState(true) const {txList, setTxList} = React.useContext(MyContext) @@ -34,7 +38,7 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get setLoading(true) let groupsAsAdmin = [] - const getAllGroupsAsAdmin = groups.map(async (group)=> { + const getAllGroupsAsAdmin = groups.filter((item)=> item.groupId !== '0').map(async (group)=> { const isAdminResponse = await requestQueueGroupJoinRequests.enqueue(()=> { return fetch( @@ -109,26 +113,33 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get flexDirection: "column", alignItems: 'center' }}> - setIsExpanded((prev)=> !prev)} > - Join Requests: + Join Requests {filteredJoinRequests?.filter((group)=> group?.data?.length > 0)?.length > 0 && ` (${filteredJoinRequests?.filter((group)=> group?.data?.length > 0)?.length})`} - - - + {isExpanded ? : ( + + )} + + + ); }; diff --git a/src/components/Group/GroupMenu.tsx b/src/components/Group/GroupMenu.tsx index a02dbff..a44c480 100644 --- a/src/components/Group/GroupMenu.tsx +++ b/src/components/Group/GroupMenu.tsx @@ -73,13 +73,13 @@ export const GroupMenu = ({ setGroupSection, groupSection, setOpenManageMembers, }} > {groupSection === "announcement" &&( - <> {" Announcements"} + <> {" Announcements"} )} {groupSection === "chat" &&( - <> {" Group Chats"} + <> {" Group Chats"} )} {groupSection === "forum" &&( - <> {" Threads"} + <> {" Threads"} )} @@ -128,13 +128,13 @@ export const GroupMenu = ({ setGroupSection, groupSection, setOpenManageMembers, minWidth: '24px !important' }}> - + @@ -148,13 +148,13 @@ export const GroupMenu = ({ setGroupSection, groupSection, setOpenManageMembers, minWidth: '24px !important' }}> - + diff --git a/src/components/Group/HomeDesktop.tsx b/src/components/Group/HomeDesktop.tsx index 67bf811..26cf612 100644 --- a/src/components/Group/HomeDesktop.tsx +++ b/src/components/Group/HomeDesktop.tsx @@ -1,4 +1,4 @@ -import { Box, Button, Typography } from "@mui/material"; +import { Box, Button, Divider, Typography } from "@mui/material"; import React from "react"; import { Spacer } from "../../common/Spacer"; import { ListOfThreadPostsWatched } from "./ListOfThreadPostsWatched"; @@ -7,7 +7,10 @@ import { GroupJoinRequests } from "./GroupJoinRequests"; import { GroupInvites } from "./GroupInvites"; import RefreshIcon from "@mui/icons-material/Refresh"; import { ListOfGroupPromotions } from "./ListOfGroupPromotions"; - +import { QortPrice } from "../Home/QortPrice"; +import ExploreIcon from "@mui/icons-material/Explore"; +import { Explore } from "../Explore/Explore"; +import { NewUsersCTA } from "../Home/NewUsersCTA"; export const HomeDesktop = ({ refreshHomeDataFunc, myAddress, @@ -21,115 +24,184 @@ export const HomeDesktop = ({ setOpenManageMembers, setOpenAddGroup, setMobileViewMode, - setDesktopViewMode + setDesktopViewMode, + desktopViewMode, }) => { return ( - - 15 ? "16px" : "20px", - padding: '10px' + display: "flex", + width: "100%", + flexDirection: "column", + height: "100%", + alignItems: "flex-start", + maxWidth: "1036px", }} > - Welcome - {userInfo?.name ? ( - {`, ${userInfo?.name}`} - ) : null} - - - {!isLoadingGroups && ( - 15 ? "16px" : "20px", + padding: "10px", }} > - - - - {`, ${userInfo?.name}`} + ) : null} + + + {!isLoadingGroups && ( + + + + item?.groupId !== "0").length !== 0 + } + /> + + + {desktopViewMode === "home" && ( + <> + {/* + */} + + + + + + + + )} + + - - + )} + + {!isLoadingGroups && ( + <> + + + + {" "} + + Explore + {" "} - - + + + + - - )} - {!isLoadingGroups && ( - - )} + + + + )} - {/* */} - + ); diff --git a/src/components/Group/ListOfGroupPromotions.tsx b/src/components/Group/ListOfGroupPromotions.tsx index fdc13f2..a983f56 100644 --- a/src/components/Group/ListOfGroupPromotions.tsx +++ b/src/components/Group/ListOfGroupPromotions.tsx @@ -9,6 +9,8 @@ import { Avatar, Box, Button, + ButtonBase, + Collapse, Dialog, DialogActions, DialogContent, @@ -24,15 +26,12 @@ import { TextField, Typography, } 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 LockIcon from "@mui/icons-material/Lock"; +import NoEncryptionGmailerrorredIcon from "@mui/icons-material/NoEncryptionGmailerrorred"; import { MyContext, getArbitraryEndpointReact, @@ -43,13 +42,20 @@ import { Spacer } from "../../common/Spacer"; import { CustomLoader } from "../../common/CustomLoader"; import { RequestQueueWithPromise } from "../../utils/queue/queue"; import { useRecoilState } from "recoil"; -import { myGroupsWhereIAmAdminAtom, promotionTimeIntervalAtom, promotionsAtom } from "../../atoms/global"; +import { + myGroupsWhereIAmAdminAtom, + promotionTimeIntervalAtom, + promotionsAtom, +} from "../../atoms/global"; import { Label } from "./AddGroup"; import ShortUniqueId from "short-unique-id"; import { CustomizedSnackbars } from "../Snackbar/Snackbar"; import { getGroupNames } from "./UserListOfInvites"; import { WrapperUserAction } from "../WrapperUserAction"; - +import { useVirtualizer } from "@tanstack/react-virtual"; +import ErrorBoundary from "../../common/ErrorBoundary"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import ExpandLessIcon from "@mui/icons-material/ExpandLess"; export const requestQueuePromos = new RequestQueueWithPromise(20); export function utf8ToBase64(inputString: string): string { @@ -66,11 +72,6 @@ export function utf8ToBase64(inputString: string): string { const uid = new ShortUniqueId({ length: 8 }); -const cache = new CellMeasurerCache({ - fixedWidth: true, - defaultHeight: 50, -}); - export function getGroupId(str) { const match = str.match(/group-(\d+)-/); return match ? match[1] : null; @@ -86,12 +87,12 @@ export const ListOfGroupPromotions = () => { const [myGroupsWhereIAmAdmin, setMyGroupsWhereIAmAdmin] = useRecoilState( myGroupsWhereIAmAdminAtom ); - const [promotions, setPromotions] = useRecoilState( - promotionsAtom - ); + const [promotions, setPromotions] = useRecoilState(promotionsAtom); const [promotionTimeInterval, setPromotionTimeInterval] = useRecoilState( promotionTimeIntervalAtom ); + const [isExpanded, setIsExpanded] = React.useState(false); + const [openSnack, setOpenSnack] = useState(false); const [infoSnack, setInfoSnack] = useState(null); const [fee, setFee] = useState(null); @@ -100,6 +101,16 @@ export const ListOfGroupPromotions = () => { const { show, setTxList } = useContext(MyContext); const listRef = useRef(); + const rowVirtualizer = useVirtualizer({ + count: promotions.length, + getItemKey: React.useCallback( + (index) => promotions[index]?.identifier, + [promotions] + ), + getScrollElement: () => listRef.current, + estimateSize: () => 80, // Provide an estimated height of items, adjust this as needed + overscan: 10, // Number of items to render outside the visible area to improve smoothness + }); useEffect(() => { try { @@ -111,7 +122,7 @@ export const ListOfGroupPromotions = () => { }, []); const getPromotions = useCallback(async () => { try { - setPromotionTimeInterval(Date.now()) + setPromotionTimeInterval(Date.now()); const identifier = `group-promotions-ui24-`; const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=100&includemetadata=false&reverse=true&prefix=true`; const response = await fetch(url, { @@ -162,7 +173,9 @@ export const ListOfGroupPromotions = () => { }); await Promise.all(getPromos); - const groupWithInfo = await getGroupNames(data.sort((a, b) => b.created - a.created)); + const groupWithInfo = await getGroupNames( + data.sort((a, b) => b.created - a.created) + ); setPromotions(groupWithInfo); } catch (error) { console.error(error); @@ -171,24 +184,25 @@ export const ListOfGroupPromotions = () => { useEffect(() => { const now = Date.now(); - + const timeSinceLastFetch = now - promotionTimeInterval; - const initialDelay = timeSinceLastFetch >= THIRTY_MINUTES - ? 0 - : THIRTY_MINUTES - timeSinceLastFetch; + const initialDelay = + timeSinceLastFetch >= THIRTY_MINUTES + ? 0 + : THIRTY_MINUTES - timeSinceLastFetch; const initialTimeout = setTimeout(() => { getPromotions(); - + // Start a 30-minute interval const interval = setInterval(() => { getPromotions(); }, THIRTY_MINUTES); - + return () => clearInterval(interval); }, initialDelay); - + return () => clearTimeout(initialTimeout); - }, [getPromotions]); + }, [getPromotions, promotionTimeInterval]); const handlePopoverOpen = (event, index) => { setPopoverAnchor(event.currentTarget); @@ -322,375 +336,444 @@ export const ListOfGroupPromotions = () => { } }; - // const handleCancelInvitation = async (address)=> { - // try { - // const fee = await getFee('CANCEL_GROUP_INVITE') - // await show({ - // message: "Would you like to perform a CANCEL_GROUP_INVITE transaction?" , - // publishFee: fee.fee + ' QORT' - // }) - // setIsLoadingCancelInvite(true) - // await new Promise((res, rej)=> { - // window.sendMessage("cancelInvitationToGroup", { - // groupId, - // qortalAddress: address, - // }) - // .then((response) => { - // if (!response?.error) { - // setInfoSnack({ - // type: "success", - // message: "Successfully canceled invitation. It may take a couple of minutes for the changes to propagate", - // }); - // setOpenSnack(true); - // handlePopoverClose(); - // setIsLoadingCancelInvite(true); - // res(response); - // return; - // } - // setInfoSnack({ - // type: "error", - // message: response?.error, - // }); - // setOpenSnack(true); - // rej(response.error); - // }) - // .catch((error) => { - // setInfoSnack({ - // type: "error", - // message: error.message || "An error occurred", - // }); - // setOpenSnack(true); - // rej(error); - // }); - - // }) - // } catch (error) { - - // } finally { - // setIsLoadingCancelInvite(false) - // } - // } - - const rowRenderer = ({ index, key, parent, style }) => { - const promotion = promotions[index]; - - return ( - - {({ measure }) => ( -
- - { - if (reason === "backdropClick") { - // Prevent closing on backdrop click - return; - } - handlePopoverClose(); // Close only on other events like Esc key press - }} - anchorOrigin={{ - vertical: "top", - horizontal: "center", - }} - transformOrigin={{ - vertical: "bottom", - horizontal: "center", - }} - style={{ marginTop: "8px" }} - > - - - Group name: {` ${promotion?.groupName}`} - - - Number of members: {` ${promotion?.memberCount}`} - - {promotion?.description && ( - - {promotion?.description} - - )} - {promotion?.isOpen === false && ( - - *This is a closed/private group, so you will need to wait - until an admin accepts your request - - )} - - - - Close - - - handleJoinGroup(promotion, promotion?.isOpen) - } - > - Join - - - - - - - - - {promotion?.name?.charAt(0)} - - - {promotion?.name} - - - - {promotion?.groupName} - - - - - {promotion?.data} - - - - - - - -
- )} -
- ); - }; - - return ( - - + setIsExpanded((prev) => !prev)} > - Group Promotions + Group promotions {promotions.length > 0 && ` (${promotions.length})`} - - - + {isExpanded ? ( + + ) : ( + + )} + + - - {loading && promotions.length === 0 && ( + + <> - - - )} - {!loading && promotions.length === 0 && ( - - - Nothing to display - + + + + - )} -
- - {({ height, width }) => ( - + + {loading && promotions.length === 0 && ( + + + )} - -
-
+ {!loading && promotions.length === 0 && ( + + + Nothing to display + + + )} +
+
+
+
+ {rowVirtualizer.getVirtualItems().map((virtualRow) => { + const index = virtualRow.index; + const promotion = promotions[index]; + return ( +
+ + Error loading content: Invalid Data + + } + > + + { + if (reason === "backdropClick") { + // Prevent closing on backdrop click + return; + } + handlePopoverClose(); // Close only on other events like Esc key press + }} + anchorOrigin={{ + vertical: "top", + horizontal: "center", + }} + transformOrigin={{ + vertical: "bottom", + horizontal: "center", + }} + style={{ marginTop: "8px" }} + > + + + Group name: {` ${promotion?.groupName}`} + + + Number of members:{" "} + {` ${promotion?.memberCount}`} + + {promotion?.description && ( + + {promotion?.description} + + )} + {promotion?.isOpen === false && ( + + *This is a closed/private group, so you + will need to wait until an admin accepts + your request + + )} + + + + Close + + + handleJoinGroup( + promotion, + promotion?.isOpen + ) + } + > + Join + + + + + + + + + {promotion?.name?.charAt(0)} + + + {promotion?.name} + + + + {promotion?.groupName} + + + + + {promotion?.isOpen === false && ( + + )} + {promotion?.isOpen === true && ( + + )} + + {promotion?.isOpen + ? "Public group" + : "Private group"} + + + + + {promotion?.data} + + + + + + + + +
+ ); + })} +
+
+
+
+
+ + {isShowModal && ( diff --git a/src/components/Group/ManageMembers.tsx b/src/components/Group/ManageMembers.tsx index dedc5be..8dab53f 100644 --- a/src/components/Group/ManageMembers.tsx +++ b/src/components/Group/ManageMembers.tsx @@ -17,9 +17,9 @@ import { InviteMember } from "./InviteMember"; import { ListOfInvites } from "./ListOfInvites"; import { ListOfBans } from "./ListOfBans"; import { ListOfJoinRequests } from "./ListOfJoinRequests"; -import { Box, Tab, Tabs } from "@mui/material"; +import { Box, Card, Tab, Tabs } from "@mui/material"; import { CustomizedSnackbars } from "../Snackbar/Snackbar"; -import { MyContext, isMobile } from "../../App"; +import { MyContext, getBaseApiReact, isMobile } from "../../App"; import { getGroupMembers, getNames } from "./Group"; import { LoadingSnackbar } from "../Snackbar/LoadingSnackbar"; import { getFee } from "../../background"; @@ -59,6 +59,7 @@ export const ManageMembers = ({ const [infoSnack, setInfoSnack] = React.useState(null); const [isLoadingMembers, setIsLoadingMembers] = React.useState(false) const [isLoadingLeave, setIsLoadingLeave] = React.useState(false) + const [groupInfo, setGroupInfo] = React.useState(null) const handleChange = (event: React.SyntheticEvent, newValue: number) => { setValue(newValue); }; @@ -68,6 +69,7 @@ export const ManageMembers = ({ setOpen(false); }; + const handleLeaveGroup = async () => { try { setIsLoadingLeave(true) @@ -130,10 +132,20 @@ export const ManageMembers = ({ setMembersWithNames(res?.members || []); } catch (error) {} }; + const getGroupInfo = async (groupId) => { + try { + const response = await fetch( + `${getBaseApiReact()}/groups/${groupId}` + ); + const groupData = await response.json(); + setGroupInfo(groupData) + } catch (error) {} + }; React.useEffect(()=> { if(selectedGroup?.groupId){ getMembers(selectedGroup?.groupId) + getGroupInfo(selectedGroup?.groupId) } }, [selectedGroup?.groupId]) @@ -248,14 +260,23 @@ export const ManageMembers = ({ />
- + + + GroupId: {groupInfo?.groupId} + GroupName: {groupInfo?.groupName} + Number of members: {groupInfo?.memberCount} + + {selectedGroup?.groupId && !isOwner && ( - Leave Group )} - + {value === 0 && ( { +import { useRecoilState } from 'recoil'; +import { mailsAtom, qMailLastEnteredTimestampAtom } from '../../atoms/global'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ExpandLessIcon from '@mui/icons-material/ExpandLess'; +import MarkEmailUnreadIcon from '@mui/icons-material/MarkEmailUnread'; +import { last } from 'slate'; +export const isLessThanOneWeekOld = (timestamp) => { // Current time in milliseconds const now = Date.now(); @@ -39,8 +45,9 @@ export function formatEmailDate(timestamp: number) { } } export const QMailMessages = ({userName, userAddress}) => { - const [mails, setMails] = useState([]) - const [lastEnteredTimestamp, setLastEnteredTimestamp] = useState(null) + const [isExpanded, setIsExpanded] = useState(false) + const [mails, setMails] = useRecoilState(mailsAtom) + const [lastEnteredTimestamp, setLastEnteredTimestamp] = useRecoilState(qMailLastEnteredTimestampAtom) const [loading, setLoading] = useState(true) const getMails = useCallback(async () => { @@ -97,7 +104,16 @@ export const QMailMessages = ({userName, userAddress}) => { }, [getMails, userName, userAddress]); - + const anyUnread = useMemo(()=> { + let unread = false + + mails.forEach((mail)=> { + if(lastEnteredTimestamp && isLessThanOneWeekOld(mail?.created)){ + unread = true + } + }) + return unread + }, [mails, lastEnteredTimestamp]) return ( { }} > - setIsExpanded((prev)=> !prev)} > Latest Q-Mails - - - + + {isExpanded ? : ( + + )} + + { onClick={()=> { executeEvent("addTab", { data: { service: 'APP', name: 'q-mail' } }); executeEvent("open-apps-mode", { }); + setLastEnteredTimestamp(Date.now()) + }} > { - ): lastEnteredTimestamp < mail?.created ? ( + ): (lastEnteredTimestamp < mail?.created) && isLessThanOneWeekOld(mail?.created) ? ( @@ -244,6 +274,7 @@ export const QMailMessages = ({userName, userAddress}) => { + ) } diff --git a/src/components/Group/ThingsToDoInitial.tsx b/src/components/Group/ThingsToDoInitial.tsx index 6825947..0a02f36 100644 --- a/src/components/Group/ThingsToDoInitial.tsx +++ b/src/components/Group/ThingsToDoInitial.tsx @@ -64,6 +64,7 @@ if(hasDoneNameAndBalanceAndIsLoaded){ ); } +if(!isLoaded) return null return ( @@ -96,7 +97,6 @@ if(hasDoneNameAndBalanceAndIsLoaded){ { handlePopoverOpen(event, index)}> - + {invite?.isOpen === false && ( + + )} + {invite?.isOpen === true && ( + + )} + @@ -194,9 +206,21 @@ export const UserListOfInvites = ({myAddress, setInfoSnack, setOpenSnack}) => { }; return ( -
+

Invite list

-
+
{({ height, width }) => ( { )}
-
+
); } diff --git a/src/components/Group/WebsocketActive.tsx b/src/components/Group/WebsocketActive.tsx index 294e196..b9da4d2 100644 --- a/src/components/Group/WebsocketActive.tsx +++ b/src/components/Group/WebsocketActive.tsx @@ -78,12 +78,20 @@ export const WebSocketActive = ({ myAddress, setIsLoadingGroups }) => { } const data = JSON.parse(e.data); - const filteredGroups = data.groups?.filter(item => item?.groupId !== 0) || []; + const copyGroups = [...(data?.groups || [])] + const findIndex = copyGroups?.findIndex(item => item?.groupId === 0) + if(findIndex !== -1){ + copyGroups[findIndex] = { + ...(copyGroups[findIndex] || {}), + groupId: "0" + } + } + const filteredGroups = copyGroups 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)); - + window.sendMessage("handleActiveGroupDataFromSocket", { groups: sortedGroups, directs: sortedDirects, diff --git a/src/components/Group/useBlockUsers.tsx b/src/components/Group/useBlockUsers.tsx new file mode 100644 index 0000000..05cbe90 --- /dev/null +++ b/src/components/Group/useBlockUsers.tsx @@ -0,0 +1,192 @@ +import React, { useCallback, useEffect, useRef } from "react"; +import { getBaseApiReact } from "../../App"; +import { truncate } from "lodash"; + + + +export const useBlockedAddresses = () => { + const userBlockedRef = useRef({}) + const userNamesBlockedRef = useRef({}) + + const getAllBlockedUsers = useCallback(()=> { + + return { + names: userNamesBlockedRef.current, + addresses: userBlockedRef.current + } + }, []) + + const isUserBlocked = useCallback((address, name)=> { + try { + if(!address) return false + if(userBlockedRef.current[address] || userNamesBlockedRef.current[name]) return true + return false + + + } catch (error) { + //error + } + }, []) + + useEffect(()=> { + const fetchBlockedList = async ()=> { + try { + const response = await new Promise((res, rej) => { + window.sendMessage("listActions", { + + type: 'get', + listName: `blockedAddresses`, + + }) + .then((response) => { + if (response.error) { + rej(response?.message); + return; + } else { + res(response); + } + }) + .catch((error) => { + console.error("Failed qortalRequest", error); + }); + }) + const blockedUsers = {} + response?.forEach((item)=> { + blockedUsers[item] = true + }) + userBlockedRef.current = blockedUsers + + const response2 = await new Promise((res, rej) => { + window.sendMessage("listActions", { + + type: 'get', + listName: `blockedNames`, + + }) + .then((response) => { + if (response.error) { + rej(response?.message); + return; + } else { + res(response); + } + }) + .catch((error) => { + console.error("Failed qortalRequest", error); + }); + }) + const blockedUsers2 = {} + response2?.forEach((item)=> { + blockedUsers2[item] = true + }) + userNamesBlockedRef.current = blockedUsers2 + + + } catch (error) { + console.error(error) + } + } + fetchBlockedList() + }, []) + + const removeBlockFromList = useCallback(async (address, name)=> { + await new Promise((res, rej) => { + window.sendMessage("listActions", { + + type: 'remove', + items: name ? [name] : [address], + listName: name ? 'blockedNames' : 'blockedAddresses' + + }) + .then((response) => { + if (response.error) { + rej(response?.message); + return; + } else { + if(!name){ + const copyObject = {...userBlockedRef.current} + delete copyObject[address] + userBlockedRef.current = copyObject + } else { + const copyObject = {...userNamesBlockedRef.current} + delete copyObject[name] + userNamesBlockedRef.current = copyObject + } + + res(response); + } + }) + .catch((error) => { + console.error("Failed qortalRequest", error); + }); + }) + if(name && userBlockedRef.current[address]){ + await new Promise((res, rej) => { + window.sendMessage("listActions", { + + type: 'remove', + items: !name ? [name] : [address], + listName: !name ? 'blockedNames' : 'blockedAddresses' + + }) + .then((response) => { + if (response.error) { + rej(response?.message); + return; + } else { + const copyObject = {...userBlockedRef.current} + delete copyObject[address] + userBlockedRef.current = copyObject + res(response); + } + }) + .catch((error) => { + console.error("Failed qortalRequest", error); + }); + }) + } + + }, []) + + const addToBlockList = useCallback(async (address, name)=> { + await new Promise((res, rej) => { + window.sendMessage("listActions", { + + type: 'add', + items: name ? [name] : [address], + listName: name ? 'blockedNames' : 'blockedAddresses' + + }) + .then((response) => { + if (response.error) { + rej(response?.message); + return; + } else { + if(name){ + + const copyObject = {...userNamesBlockedRef.current} + copyObject[name] = true + userNamesBlockedRef.current = copyObject + }else { + const copyObject = {...userBlockedRef.current} + copyObject[address] = true + userBlockedRef.current = copyObject + + } + + res(response); + } + }) + .catch((error) => { + console.error("Failed qortalRequest", error); + }); + }) + }, []) + + return { + isUserBlocked, + addToBlockList, + removeBlockFromList, + getAllBlockedUsers + }; +}; diff --git a/src/components/Group/useHandleUserInfo.tsx b/src/components/Group/useHandleUserInfo.tsx new file mode 100644 index 0000000..a497259 --- /dev/null +++ b/src/components/Group/useHandleUserInfo.tsx @@ -0,0 +1,34 @@ +import React, { useCallback, useRef } from "react"; +import { getBaseApiReact } from "../../App"; + + + +export const useHandleUserInfo = () => { + const userInfoRef = useRef({}) + + + const getIndividualUserInfo = useCallback(async (address)=> { + try { + if(!address) return null + if(userInfoRef.current[address] !== undefined) return userInfoRef.current[address] + + const url = `${getBaseApiReact()}/addresses/${address}`; + const response = await fetch(url); + if (!response.ok) { + throw new Error("network error"); + } + const data = await response.json(); + userInfoRef.current = { + ...userInfoRef.current, + [address]: data?.level + } + return data?.level + } catch (error) { + //error + } + }, []) + + return { + getIndividualUserInfo, + }; +}; diff --git a/src/components/Home/NewUsersCTA.tsx b/src/components/Home/NewUsersCTA.tsx new file mode 100644 index 0000000..4ad5c79 --- /dev/null +++ b/src/components/Home/NewUsersCTA.tsx @@ -0,0 +1,91 @@ +import { Box, ButtonBase, Typography } from "@mui/material"; +import React from "react"; +import { Spacer } from "../../common/Spacer"; + +export const NewUsersCTA = ({ balance }) => { + if (balance === undefined || +balance > 0) return null; + return ( + + + + + + Are you a new user? + + + + Please message us on Telegram or Discord if you need 4 QORT to start + chatting without any limitations + + + + { + if (window?.electronAPI?.openExternal) { + window.electronAPI.openExternal( + "https://link.qortal.dev/telegram-invite" + ); + } else { + window.open( + "https://link.qortal.dev/telegram-invite", + "_blank" + ); + } + }} + > + Telegram + + { + if (window?.electronAPI?.openExternal) { + window.electronAPI.openExternal( + "https://link.qortal.dev/discord-invite" + ); + } else { + window.open("https://link.qortal.dev/discord-invite", "_blank"); + } + }} + > + Discord + + + + + ); +}; diff --git a/src/components/Home/QortPrice.tsx b/src/components/Home/QortPrice.tsx new file mode 100644 index 0000000..3d54a04 --- /dev/null +++ b/src/components/Home/QortPrice.tsx @@ -0,0 +1,209 @@ +import React, { useCallback, useEffect, useState } from 'react' +import { getBaseApiReact } from '../../App'; +import { Box, Tooltip, Typography } from '@mui/material'; +import { BarSpinner } from '../../common/Spinners/BarSpinner/BarSpinner'; + +function getAverageLtcPerQort(trades) { + let totalQort = 0; + let totalLtc = 0; + + trades.forEach((trade) => { + const qort = parseFloat(trade.qortAmount); + const ltc = parseFloat(trade.foreignAmount); + + totalQort += qort; + totalLtc += ltc; + }); + + // Avoid division by zero + if (totalQort === 0) return 0; + + // Weighted average price + return parseFloat((totalLtc / totalQort).toFixed(8)); + } + + function getTwoWeeksAgoTimestamp() { + const now = new Date(); + now.setDate(now.getDate() - 14); // Subtract 14 days + return now.getTime(); // Get timestamp in milliseconds + } + + function formatWithCommasAndDecimals(number) { + + return Number(number).toLocaleString(); + } + + +export const QortPrice = () => { + const [ltcPerQort, setLtcPerQort] = useState(null) + const [supply, setSupply] = useState(null) + const [lastBlock, setLastBlock] = useState(null) + const [loading, setLoading] = useState(true) + + const getPrice = useCallback(async () => { + try { + setLoading(true) + + const response = await fetch(`${getBaseApiReact()}/crosschain/trades?foreignBlockchain=LITECOIN&minimumTimestamp=${getTwoWeeksAgoTimestamp()}&limit=20&reverse=true`); + const data = await response.json(); + + + setLtcPerQort(getAverageLtcPerQort(data)); + } catch (error) { + console.error(error); + } finally { + setLoading(false) + + } + }, []) + + const getLastBlock = useCallback(async () => { + try { + setLoading(true) + + const response = await fetch(`${getBaseApiReact()}/blocks/last`); + const data = await response.json(); + + setLastBlock(data); + } catch (error) { + console.error(error); + } finally { + setLoading(false) + + } + }, []) + + const getSupplyInCirculation = useCallback(async () => { + try { + setLoading(true) + + const response = await fetch(`${getBaseApiReact()}/stats/supply/circulating`); + const data = await response.text(); + formatWithCommasAndDecimals(data) + setSupply(formatWithCommasAndDecimals(data)); + } catch (error) { + console.error(error); + } finally { + setLoading(false) + + } + }, []) + + + + + useEffect(() => { + + getPrice(); + getSupplyInCirculation() + getLastBlock() + const interval = setInterval(() => { + getPrice(); + getSupplyInCirculation() + getLastBlock() + }, 900000); + + return () => clearInterval(interval); + + }, [getPrice]); + + console.log('supply', supply) + + return ( + + Based on the latest 20 trades} + placement="bottom" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > + + Price + {!ltcPerQort ? ( + + ): ( + {ltcPerQort} LTC/QORT + )} + + + + + + Supply + {!supply ? ( + + ): ( + {supply} QORT + )} + + + + Last height + {!lastBlock?.height ? ( + + ): ( + {lastBlock?.height} + + )} + + + + ) +} diff --git a/src/components/Minting/Minting.tsx b/src/components/Minting/Minting.tsx new file mode 100644 index 0000000..af63ece --- /dev/null +++ b/src/components/Minting/Minting.tsx @@ -0,0 +1,856 @@ +import { + Alert, + Box, + Button, + Card, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Divider, + IconButton, + InputBase, + InputLabel, + Snackbar, + Typography, +} from "@mui/material"; +import React, { + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from "react"; +import CloseIcon from "@mui/icons-material/Close"; +import { MyContext, getBaseApiReact } from "../../App"; +import { + executeEvent, + subscribeToEvent, + unsubscribeFromEvent, +} from "../../utils/events"; +import { getFee, getNameOrAddress } from "../../background"; +import CopyToClipboard from "react-copy-to-clipboard"; +import { AddressBox } from "../../App-styles"; +import { Spacer } from "../../common/Spacer"; +import Copy from "../../assets/svgs/Copy.svg"; +import { Loader } from "../Loader"; +import { FidgetSpinner } from "react-loader-spinner"; +import { useModal } from "../../common/useModal"; + +export const Minting = ({ + setIsOpenMinting, + myAddress, + groups, + show, + setTxList, + txList, +}) => { + const [mintingAccounts, setMintingAccounts] = useState([]); + const [accountInfo, setAccountInfo] = useState(null); + const [rewardSharePublicKey, setRewardSharePublicKey] = useState(""); + const [mintingKey, setMintingKey] = useState(""); + const [rewardsharekey, setRewardsharekey] = useState(""); + const [rewardShares, setRewardShares] = useState([]); + const [nodeInfos, setNodeInfos] = useState({}); + const [openSnack, setOpenSnack] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const { show: showKey, message } = useModal(); + const { isShow: isShowNext, onOk, show: showNext } = useModal(); + + const [info, setInfo] = useState(null); + const [names, setNames] = useState({}); + const [accountInfos, setAccountInfos] = useState({}); + const [showWaitDialog, setShowWaitDialog] = useState(false) + const isPartOfMintingGroup = useMemo(() => { + if (groups?.length === 0) return false; + return !!groups?.find((item) => item?.groupId?.toString() === "694"); + }, [groups]); + const getMintingAccounts = useCallback(async () => { + try { + const url = `${getBaseApiReact()}/admin/mintingaccounts`; + const response = await fetch(url); + if (!response.ok) { + throw new Error("network error"); + } + const data = await response.json(); + setMintingAccounts(data); + } catch (error) {} + }, []); + + const accountIsMinting = useMemo(() => { + return !!mintingAccounts?.find( + (item) => item?.recipientAccount === myAddress + ); + }, [mintingAccounts, myAddress]); + + const getName = async (address) => { + try { + const response = await fetch( + `${getBaseApiReact()}/names/address/${address}` + ); + const nameData = await response.json(); + if (nameData?.length > 0) { + setNames((prev) => { + return { + ...prev, + [address]: nameData[0].name, + }; + }); + } else { + setNames((prev) => { + return { + ...prev, + [address]: null, + }; + }); + } + } catch (error) { + // error + } + }; + + const getAccountInfo = async (address: string, others?: boolean) => { + try { + if (!others) { + setIsLoading(true); + } + const url = `${getBaseApiReact()}/addresses/${address}`; + const response = await fetch(url); + if (!response.ok) { + throw new Error("network error"); + } + const data = await response.json(); + if (others) { + setAccountInfos((prev) => { + return { + ...prev, + [address]: data, + }; + }); + } else { + setAccountInfo(data); + } + } catch (error) { + } finally { + if (!others) { + setIsLoading(false); + } + } + }; + + const refreshRewardShare = () => { + if (!myAddress) return; + getRewardShares(myAddress); + }; + + useEffect(() => { + subscribeToEvent("refresh-rewardshare-list", refreshRewardShare); + + return () => { + unsubscribeFromEvent("refresh-rewardshare-list", refreshRewardShare); + }; + }, [myAddress]); + + const handleNames = (address) => { + if (!address) return undefined; + if (names[address]) return names[address]; + if (names[address] === null) return address; + getName(address); + return address; + }; + + const handleAccountInfos = (address, field) => { + if (!address) return undefined; + if (accountInfos[address]) return accountInfos[address]?.[field]; + if (accountInfos[address] === null) return undefined; + getAccountInfo(address, true); + return undefined; + }; + + const calculateBlocksRemainingToLevel1 = (address) => { + if (!address) return undefined; + if (!accountInfos[address]) return undefined; + return 7200 - accountInfos[address]?.blocksMinted || 0; + }; + + const getNodeInfos = async () => { + try { + const url = `${getBaseApiReact()}/admin/status`; + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + const data = await response.json(); + setNodeInfos(data); + } catch (error) { + console.error("Request failed", error); + } + }; + + const getRewardShares = useCallback(async (address) => { + try { + const url = `${getBaseApiReact()}/addresses/rewardshares?involving=${address}`; + const response = await fetch(url); + if (!response.ok) { + throw new Error("network error"); + } + const data = await response.json(); + setRewardShares(data); + return data + } catch (error) {} + }, []); + + const addMintingAccount = useCallback(async (val) => { + try { + setIsLoading(true); + return await new Promise((res, rej) => { + window + .sendMessage( + "ADMIN_ACTION", + + { + type: "addmintingaccount", + value: val, + }, + 180000, + true + ) + .then((response) => { + if (!response?.error) { + res(response); + setMintingKey(""); + setTimeout(() => { + getMintingAccounts(); + }, 300); + return; + } + rej({ message: response.error }); + }) + .catch((error) => { + rej({ message: error.message || "An error occurred" }); + }); + }); + } catch (error) { + setInfo({ + type: "error", + message: error?.message || "Unable to add minting account", + }); + setOpenSnack(true); + } finally { + setIsLoading(false); + } + }, []); + + const removeMintingAccount = useCallback(async (val, acct) => { + try { + setIsLoading(true); + return await new Promise((res, rej) => { + window + .sendMessage( + "ADMIN_ACTION", + + { + type: "removemintingaccount", + value: val, + }, + 180000, + true + ) + .then((response) => { + if (!response?.error) { + res(response); + + setTimeout(() => { + getMintingAccounts(); + }, 300); + return; + } + rej({ message: response.error }); + }) + .catch((error) => { + rej({ message: error.message || "An error occurred" }); + }); + }); + } catch (error) { + setInfo({ + type: "error", + message: error?.message || "Unable to remove minting account", + }); + setOpenSnack(true); + } finally { + setIsLoading(false); + } + }, []); + + const createRewardShare = useCallback(async (publicKey, recipient) => { + const fee = await getFee("REWARD_SHARE"); + await show({ + message: "Would you like to perform an REWARD_SHARE transaction?", + publishFee: fee.fee + " QORT", + }); + return await new Promise((res, rej) => { + window + .sendMessage("createRewardShare", { + recipientPublicKey: publicKey, + }) + .then((response) => { + if (!response?.error) { + setTxList((prev) => [ + { + recipient, + ...response, + type: "add-rewardShare", + label: `Add rewardshare: awaiting confirmation`, + labelDone: `Add rewardshare: success!`, + done: false, + }, + ...prev, + ]); + res(response); + return; + } + rej({ message: response.error }); + }) + .catch((error) => { + rej({ message: error.message || "An error occurred" }); + }); + }); + }, []); + + const getRewardSharePrivateKey = useCallback(async (publicKey) => { + return await new Promise((res, rej) => { + window + .sendMessage("getRewardSharePrivateKey", { + recipientPublicKey: publicKey, + }) + .then((response) => { + if (!response?.error) { + res(response); + return; + } + rej({ message: response.error }); + }) + .catch((error) => { + rej({ message: error.message || "An error occurred" }); + }); + }); + }, []); + + const waitUntilRewardShareIsConfirmed = async (timeoutMs = 600000) => { + const pollingInterval = 30000; + const startTime = Date.now(); + + const sleep = (ms) => new Promise((res) => setTimeout(res, ms)); + + while (Date.now() - startTime < timeoutMs) { + + const rewardShares = await getRewardShares(myAddress); + const findRewardShare = rewardShares?.find( + (item) => + item?.recipient === myAddress && item?.mintingAccount === myAddress + ); + + if (findRewardShare) { + return true; // Exit early if found + } + + + await sleep(pollingInterval); // Wait before the next poll + } + + throw new Error("Timeout waiting for reward share confirmation"); + }; + + const startMinting = async () => { + try { + setIsLoading(true); + const findRewardShare = rewardShares?.find( + (item) => + item?.recipient === myAddress && item?.mintingAccount === myAddress + ); + if (findRewardShare) { + const privateRewardShare = await getRewardSharePrivateKey( + accountInfo?.publicKey + ); + addMintingAccount(privateRewardShare); + } else { + await createRewardShare(accountInfo?.publicKey, myAddress); + setShowWaitDialog(true) + await waitUntilRewardShareIsConfirmed() + await showNext({ + message: '' + }) + const privateRewardShare = await getRewardSharePrivateKey( + accountInfo?.publicKey + ); + setShowWaitDialog(false) + addMintingAccount(privateRewardShare); + + } + } catch (error) { + setShowWaitDialog(false) + setInfo({ + type: "error", + message: error?.message || "Unable to start minting", + }); + setOpenSnack(true); + } finally { + setIsLoading(false); + } + }; + + const getPublicKeyFromAddress = async (address) => { + const url = `${getBaseApiReact()}/addresses/publickey/${address}`; + const response = await fetch(url); + const data = await response.text(); + return data; + }; + + const checkIfMinterGroup = async (address) => { + const url = `${getBaseApiReact()}/groups/member/${address}`; + const response = await fetch(url); + const data = await response.json(); + return !!data?.find((grp) => grp?.groupId?.toString() === "694"); + }; + + const removeRewardShare = useCallback(async (rewardShare) => { + return await new Promise((res, rej) => { + window + .sendMessage("removeRewardShare", { + rewardShareKeyPairPublicKey: rewardShare.rewardSharePublicKey, + recipient: rewardShare.recipient, + percentageShare: -1, + }) + .then((response) => { + if (!response?.error) { + res(response); + setTxList((prev) => [ + { + ...rewardShare, + ...response, + type: "remove-rewardShare", + label: `Remove rewardshare: awaiting confirmation`, + labelDone: `Remove rewardshare: success!`, + done: false, + }, + ...prev, + ]); + return; + } + rej({ message: response.error }); + }) + .catch((error) => { + rej({ message: error.message || "An error occurred" }); + }); + }); + }, []); + + const handleRemoveRewardShare = async (rewardShare) => { + try { + setIsLoading(true); + + const privateRewardShare = await removeRewardShare(rewardShare); + } catch (error) { + setInfo({ + type: "error", + message: error?.message || "Unable to remove reward share", + }); + setOpenSnack(true); + } finally { + setIsLoading(false); + } + }; + + const createRewardShareForPotentialMinter = async (receiver) => { + try { + setIsLoading(true); + const confirmReceiver = await getNameOrAddress(receiver); + if (confirmReceiver.error) + throw new Error("Invalid receiver address or name"); + const isInMinterGroup = await checkIfMinterGroup(confirmReceiver); + if (!isInMinterGroup) throw new Error("Account not in Minter Group"); + const publicKey = await getPublicKeyFromAddress(confirmReceiver); + const findRewardShare = rewardShares?.find( + (item) => + item?.recipient === confirmReceiver && + item?.mintingAccount === myAddress + ); + if (findRewardShare) { + const privateRewardShare = await getRewardSharePrivateKey(publicKey); + setRewardsharekey(privateRewardShare); + } else { + await createRewardShare(publicKey, confirmReceiver); + const privateRewardShare = await getRewardSharePrivateKey(publicKey); + setRewardsharekey(privateRewardShare); + } + } catch (error) { + setInfo({ + type: "error", + message: error?.message || "Unable to create reward share", + }); + setOpenSnack(true); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + getNodeInfos(); + getMintingAccounts(); + }, []); + + useEffect(() => { + if (!myAddress) return; + getRewardShares(myAddress); + + getAccountInfo(myAddress); + }, [myAddress]); + + const _blocksNeed = () => { + if (accountInfo?.level === 0) { + return 7200; + } else if (accountInfo?.level === 1) { + return 72000; + } else if (accountInfo?.level === 2) { + return 201600; + } else if (accountInfo?.level === 3) { + return 374400; + } else if (accountInfo?.level === 4) { + return 618400; + } else if (accountInfo?.level === 5) { + return 964000; + } else if (accountInfo?.level === 6) { + return 1482400; + } else if (accountInfo?.level === 7) { + return 2173600; + } else if (accountInfo?.level === 8) { + return 3037600; + } else if (accountInfo?.level === 9) { + return 4074400; + } + }; + + const handleClose = () => { + setOpenSnack(false); + setTimeout(() => { + setInfo(null); + }, 250); + }; + + const _levelUpBlocks = () => { + if ( + accountInfo?.blocksMinted === undefined || + nodeInfos?.height === undefined + ) + return null; + let countBlocks = + _blocksNeed() - + (accountInfo?.blocksMinted + accountInfo?.blocksMintedAdjustment); + + let countBlocksString = countBlocks.toString(); + return "" + countBlocksString; + }; + + + + return ( + + {"Manage your minting"} + setIsOpenMinting(false)} + aria-label="close" + > + + + + {isLoading && ( + + + + )} + + Account: {handleNames(accountInfo?.address)} + Level: {accountInfo?.level} + + blocks remaining until next level: {_levelUpBlocks()} + + + This node is minting: {nodeInfos?.isMintingPossible?.toString()} + + + + {isPartOfMintingGroup && !accountIsMinting && ( + + + {mintingAccounts?.length > 1 && ( + + Only 2 minting keys are allowed per node. Please remove one if + you would like to mint with this account. + + )} + + )} + + {mintingAccounts?.length > 0 && ( + Node's minting accounts + )} + + {accountIsMinting && ( + + + You currently have a minting key for this account attached to + this node + + + )} + + {mintingAccounts?.map((acct) => ( + + + Minting account: {handleNames(acct?.mintingAccount)} + + + + + + ))} + + {mintingAccounts?.length > 1 && ( + + Only 2 minting keys are allowed per node. Please remove one if you + would like to add a different account. + + )} + + + + {!isPartOfMintingGroup && ( + + + + You are currently not part of the MINTER group + + + Visit the Q-Mintership app to apply to be a minter + + + + + + )} + + {showWaitDialog && ( + + + {isShowNext ? "Confirmed" : "Please Wait"} + + + {!isShowNext && ( + + Confirming creation of rewardshare on chain. Please be patient, this could take up to 90 seconds. + + )} + {isShowNext && ( + + Rewardshare confirmed. Please click Next. + + )} + + + + + + + + + + )} + + + + + + + {info?.message} + + + + ); +}; diff --git a/src/components/Mobile/MobileFooter.tsx b/src/components/Mobile/MobileFooter.tsx index 7179f6e..52e5c7c 100644 --- a/src/components/Mobile/MobileFooter.tsx +++ b/src/components/Mobile/MobileFooter.tsx @@ -108,7 +108,7 @@ export const MobileFooter = ({ }} icon={ - + } sx={{ @@ -175,7 +175,7 @@ export const MobileFooter = ({ }} icon={ - + } sx={{ diff --git a/src/components/Mobile/MobileHeader.tsx b/src/components/Mobile/MobileHeader.tsx index 9902a28..cd98ba9 100644 --- a/src/components/Mobile/MobileHeader.tsx +++ b/src/components/Mobile/MobileHeader.tsx @@ -92,7 +92,7 @@ const Header = ({ onClick={handleClick} > - + {fullScreen && ( { @@ -135,7 +135,7 @@ const Header = ({ setMobileViewModeKeepOpen("messaging"); }} > - @@ -197,13 +197,13 @@ const Header = ({ minWidth: '24px !important' }}> - + @@ -218,13 +218,13 @@ const Header = ({ minWidth: '24px !important' }}> - + @@ -358,7 +358,7 @@ const Header = ({ }} > - + @@ -433,13 +433,13 @@ const Header = ({ minWidth: '24px !important' }}> - + @@ -454,13 +454,13 @@ const Header = ({ minWidth: '24px !important' }}> - + diff --git a/src/components/QMailStatus.tsx b/src/components/QMailStatus.tsx new file mode 100644 index 0000000..cb1642a --- /dev/null +++ b/src/components/QMailStatus.tsx @@ -0,0 +1,63 @@ +import React, { useMemo } from 'react' +import QMailLogo from '../assets/QMailLogo.png' +import { useRecoilState } from 'recoil' +import { mailsAtom, qMailLastEnteredTimestampAtom } from '../atoms/global' +import { isLessThanOneWeekOld } from './Group/QMailMessages' +import { ButtonBase, Tooltip } from '@mui/material' +import { executeEvent } from '../utils/events' +export const QMailStatus = () => { + const [lastEnteredTimestamp, setLastEnteredTimestamp] = useRecoilState(qMailLastEnteredTimestampAtom) + const [mails, setMails] = useRecoilState(mailsAtom) + + const hasNewMail = useMemo(()=> { + if(mails?.length === 0) return false + const latestMail = mails[0] + if(!lastEnteredTimestamp && isLessThanOneWeekOld(latestMail?.created)) return true + if((lastEnteredTimestamp < latestMail?.created) && isLessThanOneWeekOld(latestMail?.created)) return true + return false + }, [lastEnteredTimestamp, mails]) + return ( + { + executeEvent("addTab", { data: { service: 'APP', name: 'q-mail' } }); + executeEvent("open-apps-mode", { }); + setLastEnteredTimestamp(Date.now()) + }} style={{ + position: 'relative' + }}> + {hasNewMail && ( +
+ )} + Q-MAIL} + placement="left" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > + + + + ) +} diff --git a/src/components/Save/Save.tsx b/src/components/Save/Save.tsx index 1129cee..0c01315 100644 --- a/src/components/Save/Save.tsx +++ b/src/components/Save/Save.tsx @@ -4,19 +4,59 @@ import isEqual from "lodash/isEqual"; // Import deep comparison utility import { canSaveSettingToQdnAtom, hasSettingsChangedAtom, + isUsingImportExportSettingsAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom, } from "../../atoms/global"; -import { ButtonBase } from "@mui/material"; +import { Box, Button, ButtonBase, Popover, Typography } from "@mui/material"; import { objectToBase64 } from "../../qdn/encryption/group-encryption"; import { MyContext } from "../../App"; import { getFee } from "../../background"; import { CustomizedSnackbars } from "../Snackbar/Snackbar"; import { SaveIcon } from "../../assets/svgs/SaveIcon"; import { IconWrapper } from "../Desktop/DesktopFooter"; -export const Save = ({ isDesktop, disableWidth }) => { +import { Spacer } from "../../common/Spacer"; +import { LoadingButton } from "@mui/lab"; +import { saveToLocalStorage } from "../Apps/AppsNavBar"; +import { decryptData, encryptData } from "../../qortalRequests/get"; +import { saveFileToDiskGeneric } from "../../utils/generateWallet/generateWallet"; +import { base64ToUint8Array, uint8ArrayToObject } from "../../backgroundFunctions/encryption"; + + +export const handleImportClick = async () => { + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.accept = '.base64,.txt'; + + // Create a promise to handle file selection and reading synchronously + return await new Promise((resolve, reject) => { + fileInput.onchange = () => { + const file = fileInput.files[0]; + if (!file) { + reject(new Error('No file selected')); + return; + } + + const reader = new FileReader(); + reader.onload = (e) => { + resolve(e.target.result); // Resolve with the file content + }; + reader.onerror = () => { + reject(new Error('Error reading file')); + }; + + reader.readAsText(file); // Read the file as text (Base64 string) + }; + + // Trigger the file input dialog + fileInput.click(); + }); + +} + +export const Save = ({ isDesktop, disableWidth, myName }) => { const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom); const [settingsQdnLastUpdated, setSettingsQdnLastUpdated] = useRecoilState( settingsQDNLastUpdatedAtom @@ -25,13 +65,14 @@ export const Save = ({ isDesktop, disableWidth }) => { settingsLocalLastUpdatedAtom ); const setHasSettingsChangedAtom = useSetRecoilState(hasSettingsChangedAtom); + const [isUsingImportExportSettings, setIsUsingImportExportSettings] = useRecoilState(isUsingImportExportSettingsAtom); const [canSave] = useRecoilState(canSaveSettingToQdnAtom); const [openSnack, setOpenSnack] = useState(false); const [isLoading, setIsLoading] = useState(false); const [infoSnack, setInfoSnack] = useState(null); const [oldPinnedApps, setOldPinnedApps] = useRecoilState(oldPinnedAppsAtom); - + const [anchorEl, setAnchorEl] = useState(null); const { show } = useContext(MyContext); const hasChanged = useMemo(() => { @@ -63,6 +104,8 @@ export const Save = ({ isDesktop, disableWidth }) => { settingsLocalLastUpdated, ]); + + useEffect(() => { setHasSettingsChangedAtom(hasChanged); }, [hasChanged]); @@ -83,9 +126,7 @@ export const Save = ({ isDesktop, disableWidth }) => { .sendMessage( "ENCRYPT_DATA", { - - data64, - + data64, }, 60000 ) @@ -135,6 +176,7 @@ export const Save = ({ isDesktop, disableWidth }) => { message: "Sucessfully published to QDN", }); setOpenSnack(true); + setAnchorEl(null) } } } catch (error) { @@ -147,20 +189,31 @@ export const Save = ({ isDesktop, disableWidth }) => { setIsLoading(false); } }; + const handlePopupClick = (event) => { + event.stopPropagation(); // Prevent parent onClick from firing + setAnchorEl(event.currentTarget); + }; + + const revertChanges = () => { + setPinnedApps(oldPinnedApps); + saveToLocalStorage("ext_saved_settings", "sortablePinnedApps", null); + setAnchorEl(null) + }; + return ( <> {isDesktop ? ( { /> )} + setAnchorEl(null)} // Close popover on click outside + anchorOrigin={{ + vertical: "bottom", + horizontal: "center", + }} + transformOrigin={{ + vertical: "top", + horizontal: "center", + }} + sx={{ + width: "300px", + maxWidth: "90%", + maxHeight: "80%", + overflow: "auto", + }} + > + {isUsingImportExportSettings && ( + + + + You are using the export/import way of saving settings. + + + + + + )} + {!isUsingImportExportSettings && ( + + {!myName ? ( + + + You need a registered Qortal name to save your pinned apps to QDN. + + + ) : ( + <> + {hasChanged && ( + + + You have unsaved changes to your pinned apps. Save them to QDN. + + + + Save to QDN + + + {!isNaN(settingsQdnLastUpdated) && settingsQdnLastUpdated > 0 && ( + <> + + Don't like your current local changes? Would you like to + reset to your saved QDN pinned apps? + + + + Revert to QDN + + + )} + {!isNaN(settingsQdnLastUpdated) && settingsQdnLastUpdated === 0 && ( + <> + + Don't like your current local changes? Would you like to + reset to the default pinned apps? + + + + Revert to default + + + )} + + )} + {!isNaN(settingsQdnLastUpdated) && settingsQdnLastUpdated === -100 && isUsingImportExportSettings !== true && ( + + + The app was unable to download your existing QDN-saved pinned + apps. Would you like to overwrite those changes? + + + + Overwrite to QDN + + + )} + {!hasChanged && ( + + + You currently do not have any changes to your pinned apps + + + + )} + + )} + + + )} + + + { + try { + const fileContent = await handleImportClick(); + const decryptedData = await decryptData({ + encryptedData: fileContent, + }); + const decryptToUnit8ArraySubject = + base64ToUint8Array(decryptedData); + const responseData = uint8ArrayToObject( + decryptToUnit8ArraySubject + ); + if(Array.isArray(responseData)){ + saveToLocalStorage("ext_saved_settings_import_export", "sortablePinnedApps", responseData, { + isUsingImportExport: true + }); + setPinnedApps(responseData) + setOldPinnedApps(responseData) + setIsUsingImportExportSettings(true) + } + + } catch (error) { + console.log("error", error); + } + }}> + + Import + + { + try { + const data64 = await objectToBase64(pinnedApps); + + const encryptedData = await encryptData({ + data64, + }); + const blob = new Blob([encryptedData], { + type: "text/plain", + }); + + const timestamp = new Date() + .toISOString() + .replace(/:/g, "-"); // Safe timestamp for filenames + const filename = `qortal-new-ui-backup-settings-${timestamp}.txt`; + await saveFileToDiskGeneric(blob, filename) + + } catch (error) { + console.log('error', error) + } + }}> + Export + + + + - + { const { txList, setTxList, memberGroups } = useContext(MyContext); @@ -39,7 +40,7 @@ export const TaskManager = ({ getUserInfo }) => { await new Promise((res) => setTimeout(() => { res(null); - }, 300000) + }, 60000) ); setTxList((prev) => { let previousData = [...prev]; @@ -62,7 +63,7 @@ export const TaskManager = ({ getUserInfo }) => { } }; - intervals.current[signature] = setInterval(getAnswer, 120000); + intervals.current[signature] = setInterval(getAnswer, 60000); }; useEffect(() => { @@ -96,7 +97,15 @@ export const TaskManager = ({ getUserInfo }) => { } }); - prev.forEach((tx) => { + + + return previousData; + }); + }, [memberGroups, getUserInfo]); + + useEffect(()=> { + + txList.forEach((tx) => { if ( ["created-common-secret", "joined-group-request", "join-request-accept"].includes( tx?.type @@ -113,11 +122,17 @@ export const TaskManager = ({ getUserInfo }) => { getStatus({ signature: tx.signature }, getUserInfo); } } + if((tx?.type === "remove-rewardShare" || tx?.type === "add-rewardShare") && tx?.signature && !tx.done){ + if (!intervals.current[tx.signature]) { + const sendEventForRewardShare = ()=> { + executeEvent('refresh-rewardshare-list', {}) + } + getStatus({ signature: tx.signature }, sendEventForRewardShare); + } + } }); - return previousData; - }); - }, [memberGroups, getUserInfo]); + }, [txList]) if (isMobile || txList?.length === 0 || txList.every((item) => item?.done)) return null; @@ -128,9 +143,9 @@ export const TaskManager = ({ getUserInfo }) => { { + const { openTutorialModal, setOpenTutorialModal } = useContext(GlobalContext); + const [multiNumber, setMultiNumber] = useState(0) + const handleClose = ()=> { + setOpenTutorialModal(null) + setMultiNumber(0) + } + if(!openTutorialModal) return null + if(openTutorialModal?.multi){ + const selectedTutorial = openTutorialModal?.multi[multiNumber] + return ( + + setMultiNumber(value)} aria-label="basic tabs example"> + {openTutorialModal?.multi?.map((item, index)=> { + return ( + + + ) + })} + + + {selectedTutorial?.title} {` Tutorial`} + + ({ + position: 'absolute', + right: 8, + top: 8, + color: theme.palette.grey[500], + })} + > + + + + + + + + + + + ) + } + return ( + <> + + + {openTutorialModal?.title} {` Tutorial`} + + ({ + position: 'absolute', + right: 8, + top: 8, + color: theme.palette.grey[500], + })} + > + + + + + + + + + + + + ) +} diff --git a/src/components/Tutorials/img/creation.webp b/src/components/Tutorials/img/creation.webp new file mode 100644 index 0000000..46310e0 Binary files /dev/null and b/src/components/Tutorials/img/creation.webp differ diff --git a/src/components/Tutorials/img/dashboard.webp b/src/components/Tutorials/img/dashboard.webp new file mode 100644 index 0000000..6e33ffd Binary files /dev/null and b/src/components/Tutorials/img/dashboard.webp differ diff --git a/src/components/Tutorials/img/groups.webp b/src/components/Tutorials/img/groups.webp new file mode 100644 index 0000000..8a0ad22 Binary files /dev/null and b/src/components/Tutorials/img/groups.webp differ diff --git a/src/components/Tutorials/img/important.webp b/src/components/Tutorials/img/important.webp new file mode 100644 index 0000000..880a84e Binary files /dev/null and b/src/components/Tutorials/img/important.webp differ diff --git a/src/components/Tutorials/img/navigation.webp b/src/components/Tutorials/img/navigation.webp new file mode 100644 index 0000000..a568960 Binary files /dev/null and b/src/components/Tutorials/img/navigation.webp differ diff --git a/src/components/Tutorials/img/obtaining-qort.jpg b/src/components/Tutorials/img/obtaining-qort.jpg new file mode 100644 index 0000000..1d213dd Binary files /dev/null and b/src/components/Tutorials/img/obtaining-qort.jpg differ diff --git a/src/components/Tutorials/img/overview.webp b/src/components/Tutorials/img/overview.webp new file mode 100644 index 0000000..bc413b4 Binary files /dev/null and b/src/components/Tutorials/img/overview.webp differ diff --git a/src/components/Tutorials/img/started.webp b/src/components/Tutorials/img/started.webp new file mode 100644 index 0000000..76099a5 Binary files /dev/null and b/src/components/Tutorials/img/started.webp differ diff --git a/src/components/Tutorials/useHandleTutorials.tsx b/src/components/Tutorials/useHandleTutorials.tsx new file mode 100644 index 0000000..2fabfa0 --- /dev/null +++ b/src/components/Tutorials/useHandleTutorials.tsx @@ -0,0 +1,192 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { saveToLocalStorage } from "../Apps/AppsNavBar"; +import creationImg from './img/creation.webp' +import dashboardImg from './img/dashboard.webp' +import groupsImg from './img/groups.webp' +import importantImg from './img/important.webp' +import navigationImg from './img/navigation.webp' +import overviewImg from './img/overview.webp' +import startedImg from './img/started.webp' +import obtainingImg from './img/obtaining-qort.jpg' + +const checkIfGatewayIsOnline = async () => { + try { + const url = `https://ext-node.qortal.link/admin/status`; + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + const data = await response.json(); + if (data?.height) { + return true + } + return false + + } catch (error) { + return false + + } + } +export const useHandleTutorials = () => { + const [openTutorialModal, setOpenTutorialModal] = useState(null); +const [shownTutorials, setShowTutorials] = useState(null) + +useEffect(()=> { + try { + const storedData = localStorage.getItem('shown-tutorials'); + + + if (storedData) { + setShowTutorials(JSON.parse(storedData)); + } else { + setShowTutorials({}) + } + } catch (error) { + //error + } +}, []) + + const saveShowTutorial = useCallback((type)=> { + try { + + setShowTutorials((prev)=> { + return { + ...(prev || {}), + [type]: true + } + }) + saveToLocalStorage('shown-tutorials', type, true) + } catch (error) { + //error + } + }, []) + const showTutorial = useCallback(async (type, isForce) => { + try { + const isOnline = await checkIfGatewayIsOnline() + if(!isOnline) return + switch (type) { + case "create-account": + { + if((shownTutorials || {})['create-account'] && !isForce) return + saveShowTutorial('create-account') + setOpenTutorialModal({ + title: "Account Creation", + resource: { + name: "a-test", + service: "VIDEO", + identifier: "account-creation-hub", + poster: creationImg + }, + }); + } + break; + case "important-information": + { + if((shownTutorials || {})['important-information'] && !isForce) return + saveShowTutorial('important-information') + + setOpenTutorialModal({ + title: "Important Information!", + resource: { + name: "a-test", + service: "VIDEO", + identifier: "important-information-hub", + poster: importantImg + }, + }); + } + break; + case "getting-started": + { + if((shownTutorials || {})['getting-started'] && !isForce) return + saveShowTutorial('getting-started') + + setOpenTutorialModal({ + multi: [ + + { + title: "1. Getting Started", + resource: { + name: "a-test", + service: "VIDEO", + identifier: "getting-started-hub", + poster: startedImg + }, + }, + { + title: "2. Overview", + resource: { + name: "a-test", + service: "VIDEO", + identifier: "overview-hub", + poster: overviewImg + }, + }, + { + title: "3. Qortal Groups", + resource: { + name: "a-test", + service: "VIDEO", + identifier: "groups-hub", + poster: groupsImg + }, + }, + { + title: "4. Obtaining Qort", + resource: { + name: "a-test", + service: "VIDEO", + identifier: "obtaining-qort", + poster: obtainingImg + }, + }, + ], + }); + } + break; + case "qapps": + { + if((shownTutorials || {})['qapps'] && !isForce) return + saveShowTutorial('qapps') + + setOpenTutorialModal({ + multi: [ + { + title: "1. Apps Dashboard", + resource: { + name: "a-test", + service: "VIDEO", + identifier: "apps-dashboard-hub", + poster: dashboardImg + }, + }, + { + title: "2. Apps Navigation", + resource: { + name: "a-test", + service: "VIDEO", + identifier: "apps-navigation-hub", + poster: navigationImg + }, + } + ], + }); + } + break; + default: + break; + } + } catch (error) { + //error + } + }, [shownTutorials]); + return { + showTutorial, + hasSeenGettingStarted: shownTutorials === null ? null : !!(shownTutorials || {})['getting-started'], + openTutorialModal, + setOpenTutorialModal, + shownTutorialsInitiated: !!shownTutorials + }; +}; diff --git a/src/components/WrapperUserAction.tsx b/src/components/WrapperUserAction.tsx index aa0d19a..8bcc03a 100644 --- a/src/components/WrapperUserAction.tsx +++ b/src/components/WrapperUserAction.tsx @@ -1,6 +1,7 @@ -import React, { useState } from 'react'; -import { Popover, Button, Box } from '@mui/material'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; +import { Popover, Button, Box, CircularProgress } from '@mui/material'; import { executeEvent } from '../utils/events'; +import { MyContext } from '../App'; export const WrapperUserAction = ({ children, address, name, disabled }) => { const [anchorEl, setAnchorEl] = useState(null); @@ -12,9 +13,9 @@ export const WrapperUserAction = ({ children, address, name, disabled }) => { }; // Handle closing the Popover - const handleClose = () => { + const handleClose = useCallback(() => { setAnchorEl(null); - }; + }, []); // Determine if the popover is open const open = Boolean(anchorEl); @@ -46,65 +47,137 @@ export const WrapperUserAction = ({ children, address, name, disabled }) => { {/* Popover */} - event.stopPropagation(), // Stop propagation inside popover - }, - }} - > - - {/* Option 1: Message */} - - - {/* Option 2: Send QORT */} - - - + {open && ( + event.stopPropagation(), // Stop propagation inside popover + }, + }} + > + + {/* Option 1: Message */} + + + {/* Option 2: Send QORT */} + + + + + + )} ); }; + + +const BlockUser = ({address, name, handleClose})=> { + const [isAlreadyBlocked, setIsAlreadyBlocked] = useState(null) + const [isLoading, setIsLoading] = useState(false) + const {isUserBlocked, + addToBlockList, + removeBlockFromList} = useContext(MyContext) + +useEffect(()=> { + if(!address) return + setIsAlreadyBlocked(isUserBlocked(address, name)) +}, [address, setIsAlreadyBlocked, isUserBlocked, name]) + + return ( + + ) +} \ No newline at end of file diff --git a/src/index.css b/src/index.css index 7db06e7..867382c 100644 --- a/src/index.css +++ b/src/index.css @@ -34,8 +34,10 @@ --bg-primary : rgba(31, 32, 35, 1); --bg-2: #27282c; --bg-3: rgba(0, 0, 0, 0.1); - --unread: #B14646; - --apps-circle: #1F2023 + --unread: #4297e2; + --danger: #B14646; + --apps-circle: #1F2023; + --green: #5EB049; } body { @@ -63,7 +65,7 @@ body { } .image-container:hover .base-image { - opacity: 0; + opacity: 0.6; } ::-webkit-scrollbar-track { @@ -74,7 +76,7 @@ body { } ::-webkit-scrollbar { - width: 14px; + width: 16px; height: 10px; } @@ -83,6 +85,10 @@ body { border-radius: 8px; background-clip: content-box; border: 4px solid transparent; + transition: 0.3s background-color; +} +::-webkit-scrollbar-thumb:hover { + background-color: #363636; } @property --var1 { diff --git a/src/messaging/messagesToBackground.tsx b/src/messaging/messagesToBackground.tsx index 1d5c724..7da16d4 100644 --- a/src/messaging/messagesToBackground.tsx +++ b/src/messaging/messagesToBackground.tsx @@ -52,7 +52,7 @@ export const sendMessageBackground = (action, data = {}, timeout = 180000, isExt }); }).then((response) => { // Return payload or error based on response content - if (response?.payload) { + if (response?.payload !== null && response?.payload !== undefined) { return response.payload; } else if (response?.error) { return { error: response.error, message: response?.message || "An error occurred" }; diff --git a/src/qdn/encryption/group-encryption.ts b/src/qdn/encryption/group-encryption.ts index 3a3066b..82734ee 100644 --- a/src/qdn/encryption/group-encryption.ts +++ b/src/qdn/encryption/group-encryption.ts @@ -145,7 +145,7 @@ export const encryptDataGroup = ({ data64, publicKeys, privateKey, userPublicKey } } -export const encryptSingle = async ({ data64, secretKeyObject, typeNumber = 1 }: any) => { +export const encryptSingle = async ({ data64, secretKeyObject, typeNumber = 2 }: any) => { // Find the highest key in the secretKeyObject const highestKey = Math.max(...Object.keys(secretKeyObject).filter(item => !isNaN(+item)).map(Number)); const highestKeyObject = secretKeyObject[highestKey]; @@ -188,7 +188,22 @@ export const encryptSingle = async ({ data64, secretKeyObject, typeNumber = 1 }: // Concatenate the highest key, type number, nonce, and encrypted data (new format) const highestKeyStr = highestKey.toString().padStart(10, '0'); // Fixed length of 10 digits - finalEncryptedData = btoa(highestKeyStr + typeNumberStr + nonceBase64 + encryptedDataBase64); + + const highestKeyBytes = new TextEncoder().encode(highestKeyStr.padStart(10, '0')); +const typeNumberBytes = new TextEncoder().encode(typeNumberStr.padStart(3, '0')); + +// Step 3: Concatenate all binary +const combinedBinary = new Uint8Array( + highestKeyBytes.length + typeNumberBytes.length + nonce.length + encryptedData.length +); + // finalEncryptedData = btoa(highestKeyStr) + btoa(typeNumberStr) + nonceBase64 + encryptedDataBase64; + combinedBinary.set(highestKeyBytes, 0); +combinedBinary.set(typeNumberBytes, highestKeyBytes.length); +combinedBinary.set(nonce, highestKeyBytes.length + typeNumberBytes.length); +combinedBinary.set(encryptedData, highestKeyBytes.length + typeNumberBytes.length + nonce.length); + +// Step 4: Base64 encode once + finalEncryptedData = uint8ArrayToBase64(combinedBinary); } return finalEncryptedData; @@ -202,22 +217,20 @@ export const decodeBase64ForUIChatMessages = (messages)=> { try { const decoded = atob(msg?.data); const parseDecoded =JSON.parse(decodeURIComponent(escape(decoded))) - if(parseDecoded?.messageText){ + msgs.push({ ...msg, ...parseDecoded }) - } + } catch (error) { } } return msgs } - - - export const decryptSingle = async ({ data64, secretKeyObject, skipDecodeBase64 }: any) => { +export const decryptSingle = async ({ data64, secretKeyObject, skipDecodeBase64 }: any) => { // First, decode the base64-encoded input (if skipDecodeBase64 is not set) const decodedData = skipDecodeBase64 ? data64 : atob(data64); @@ -249,6 +262,28 @@ export const decodeBase64ForUIChatMessages = (messages)=> { encryptedDataBase64 = decodeForNumber.slice(10); // The remaining part is the encrypted data } else { if (hasTypeNumber) { + // const typeNumberStr = new TextDecoder().decode(typeNumberBytes); + if(decodeForNumber.slice(10, 13) !== '001'){ + const decodedBinary = base64ToUint8Array(decodedData); + const highestKeyBytes = decodedBinary.slice(0, 10); // if ASCII digits only + const highestKeyStr = new TextDecoder().decode(highestKeyBytes); + +const nonce = decodedBinary.slice(13, 13 + 24); +const encryptedData = decodedBinary.slice(13 + 24); +const highestKey = parseInt(highestKeyStr, 10); + +const messageKey = base64ToUint8Array(secretKeyObject[+highestKey].messageKey); +const decryptedBytes = nacl.secretbox.open(encryptedData, nonce, messageKey); + + // Check if decryption was successful + if (!decryptedBytes) { + throw new Error("Decryption failed"); + } + + // Convert the decrypted Uint8Array back to a Base64 string + return uint8ArrayToBase64(decryptedBytes); + + } // New format: Extract type number and nonce typeNumberStr = possibleTypeNumberStr; // Extract type number nonceBase64 = decodeForNumber.slice(13, 45); // Extract nonce (next 32 characters after type number) @@ -280,6 +315,9 @@ export const decodeBase64ForUIChatMessages = (messages)=> { // Convert the decrypted Uint8Array back to a Base64 string return uint8ArrayToBase64(decryptedData); }; + + + export const decryptGroupEncryptionWithSharingKey = async ({ data64EncryptedData, key }: any) => { diff --git a/src/qortalRequests.ts b/src/qortalRequests.ts index 658badf..0397538 100644 --- a/src/qortalRequests.ts +++ b/src/qortalRequests.ts @@ -1,5 +1,6 @@ import { gateways, getApiKeyFromStorage } from "./background"; -import { addForeignServer, addListItems, adminAction, cancelSellOrder, createAndCopyEmbedLink, createBuyOrder, createPoll, createSellOrder, decryptData, decryptDataWithSharingKey, decryptQortalGroupData, deleteListItems, deployAt, encryptData, encryptDataWithSharingKey, encryptQortalGroupData, getCrossChainServerInfo, getDaySummary, getForeignFee, getListItems, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getWalletBalance, joinGroup, openNewTab, publishMultipleQDNResources, publishQDNResource, removeForeignServer, saveFile, sendChatMessage, sendCoin, setCurrentForeignServer, signTransaction, updateForeignFee, voteOnPoll } from "./qortalRequests/get"; +import { listOfAllQortalRequests } from "./components/Apps/useQortalMessageListener"; +import { addForeignServer, addGroupAdminRequest, addListItems, adminAction, banFromGroupRequest, cancelGroupBanRequest, cancelGroupInviteRequest, cancelSellOrder, createAndCopyEmbedLink, createBuyOrder, createGroupRequest, createPoll, createSellOrder, decryptAESGCMRequest, decryptData, decryptDataWithSharingKey, decryptQortalGroupData, deleteHostedData, deleteListItems, deployAt, encryptData, encryptDataWithSharingKey, encryptQortalGroupData, getCrossChainServerInfo, getDaySummary, getForeignFee, getHostedData, getListItems, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getUserWalletTransactions, getWalletBalance, inviteToGroupRequest, joinGroup, kickFromGroupRequest, leaveGroupRequest, openNewTab, publishMultipleQDNResources, publishQDNResource, registerNameRequest, removeForeignServer, removeGroupAdminRequest, saveFile, sendChatMessage, sendCoin, setCurrentForeignServer, signTransaction, updateForeignFee, updateNameRequest, voteOnPoll } from "./qortalRequests/get"; import { getData, storeData } from "./utils/chromeStorage"; @@ -376,7 +377,7 @@ export const isRunningGateway = async ()=> { case "GET_USER_WALLET": { try { - const res = await getUserWallet(request.payload, isFromExtension); + const res = await getUserWallet(request.payload, isFromExtension, appInfo); event.source.postMessage({ requestId: request.requestId, action: request.action, @@ -413,10 +414,30 @@ export const isRunningGateway = async ()=> { } break; } + + case "GET_USER_WALLET_TRANSACTIONS": { + try { + const res = await getUserWalletTransactions(request.payload, isFromExtension, appInfo); + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + payload: res, + type: "backgroundMessageResponse", + }, event.origin); + } catch (error) { + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + error: error.message, + type: "backgroundMessageResponse", + }, event.origin); + } + break; + } case "GET_USER_WALLET_INFO": { try { - const res = await getUserWalletInfo(request.payload, isFromExtension); + const res = await getUserWalletInfo(request.payload, isFromExtension, appInfo); event.source.postMessage({ requestId: request.requestId, action: request.action, @@ -691,13 +712,13 @@ export const isRunningGateway = async ()=> { } break; } - case "IS_USING_GATEWAY": { + case "IS_USING_PUBLIC_NODE": { try { let isGateway = await isRunningGateway() event.source.postMessage({ requestId: request.requestId, action: request.action, - payload: {isGateway}, + payload: isGateway, type: "backgroundMessageResponse", }, event.origin); } catch (error) { @@ -827,7 +848,297 @@ export const isRunningGateway = async ()=> { } break; } - + + case "DELETE_HOSTED_DATA" : { + try { + const res = await deleteHostedData(request.payload, isFromExtension) + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + payload: res, + type: "backgroundMessageResponse", + }, event.origin); + } catch (error) { + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + error: error?.message, + type: "backgroundMessageResponse", + }, event.origin); + } + break; + } + case "GET_HOSTED_DATA" : { + try { + const res = await getHostedData(request.payload, isFromExtension) + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + payload: res, + type: "backgroundMessageResponse", + }, event.origin); + } catch (error) { + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + error: error?.message, + type: "backgroundMessageResponse", + }, event.origin); + } + break; + } + case "SHOW_ACTIONS" : { + try { + + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + payload: listOfAllQortalRequests, + type: "backgroundMessageResponse", + }, event.origin); + } catch (error) { + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + error: error?.message, + type: "backgroundMessageResponse", + }, event.origin); + } + break; + } + case "REGISTER_NAME" : { + try { + const res = await registerNameRequest(request.payload, isFromExtension) + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + payload: res, + type: "backgroundMessageResponse", + }, event.origin); + } catch (error) { + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + error: error?.message, + type: "backgroundMessageResponse", + }, event.origin); + } + break; + } + case "UPDATE_NAME" : { + try { + const res = await updateNameRequest(request.payload, isFromExtension) + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + payload: res, + type: "backgroundMessageResponse", + }, event.origin); + } catch (error) { + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + error: error?.message, + type: "backgroundMessageResponse", + }, event.origin); + } + break; + } + + case "LEAVE_GROUP" : { + try { + const res = await leaveGroupRequest(request.payload, isFromExtension) + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + payload: res, + type: "backgroundMessageResponse", + }, event.origin); + } catch (error) { + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + error: error?.message, + type: "backgroundMessageResponse", + }, event.origin); + } + break; + } + + case "INVITE_TO_GROUP" : { + try { + const res = await inviteToGroupRequest(request.payload, isFromExtension) + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + payload: res, + type: "backgroundMessageResponse", + }, event.origin); + } catch (error) { + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + error: error?.message, + type: "backgroundMessageResponse", + }, event.origin); + } + break; + } + case "KICK_FROM_GROUP" : { + try { + const res = await kickFromGroupRequest(request.payload, isFromExtension) + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + payload: res, + type: "backgroundMessageResponse", + }, event.origin); + } catch (error) { + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + error: error?.message, + type: "backgroundMessageResponse", + }, event.origin); + } + break; + } + case "BAN_FROM_GROUP" : { + try { + const res = await banFromGroupRequest(request.payload, isFromExtension) + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + payload: res, + type: "backgroundMessageResponse", + }, event.origin); + } catch (error) { + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + error: error?.message, + type: "backgroundMessageResponse", + }, event.origin); + } + break; + } + + case "CANCEL_GROUP_BAN" : { + try { + const res = await cancelGroupBanRequest(request.payload, isFromExtension) + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + payload: res, + type: "backgroundMessageResponse", + }, event.origin); + } catch (error) { + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + error: error?.message, + type: "backgroundMessageResponse", + }, event.origin); + } + break; + } + case "ADD_GROUP_ADMIN" : { + try { + const res = await addGroupAdminRequest(request.payload, isFromExtension) + + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + payload: res, + type: "backgroundMessageResponse", + }, event.origin); + } catch (error) { + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + error: error?.message, + type: "backgroundMessageResponse", + }, event.origin); + } + break; + } + case "DECRYPT_AESGCM" : { + try { + const res = await decryptAESGCMRequest(request.payload, isFromExtension) + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + payload: res, + type: "backgroundMessageResponse", + }, event.origin); + } catch (error) { + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + error: error?.message, + type: "backgroundMessageResponse", + }, event.origin); + } + break; + } + case "REMOVE_GROUP_ADMIN" : { + try { + const res = await removeGroupAdminRequest(request.payload, isFromExtension) + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + payload: res, + type: "backgroundMessageResponse", + }, event.origin); + } catch (error) { + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + error: error?.message, + type: "backgroundMessageResponse", + }, event.origin); + } + break; + } + + case "CANCEL_GROUP_INVITE" : { + try { + const res = await cancelGroupInviteRequest(request.payload, isFromExtension) + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + payload: res, + type: "backgroundMessageResponse", + }, event.origin); + } catch (error) { + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + error: error?.message, + type: "backgroundMessageResponse", + }, event.origin); + } + break; + } + case "CREATE_GROUP" : { + try { + const res = await createGroupRequest(request.payload, isFromExtension) + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + payload: res, + type: "backgroundMessageResponse", + }, event.origin); + } catch (error) { + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + error: error?.message, + type: "backgroundMessageResponse", + }, event.origin); + } + break; + } default: break; } diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index c7f7046..956fa07 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -1,3 +1,4 @@ +import { Sha256 } from "asmcrypto.js"; import { computePow, createEndpoint, @@ -17,6 +18,18 @@ import { performPowTask, parseErrorResponse, groupSecretkeys, + registerName, + updateName, + leaveGroup, + inviteToGroup, + getNameInfoForOthers, + kickFromGroup, + banFromGroup, + cancelBan, + makeAdmin, + removeAdmin, + cancelInvitationToGroup, + createGroup, } from "../background"; import { getNameInfo, uint8ArrayToObject } from "../backgroundFunctions/encryption"; import { showSaveFilePicker } from "../components/Apps/useQortalMessageListener"; @@ -25,7 +38,10 @@ import { extractComponents } from "../components/Chat/MessageDisplay"; import { decryptResource, getGroupAdmins, getPublishesFromAdmins, validateSecretKey } from "../components/Group/Group"; import { QORT_DECIMALS } from "../constants/constants"; import Base58 from "../deps/Base58"; +import ed2curve from "../deps/ed2curve"; import nacl from "../deps/nacl-fast"; + + import { base64ToUint8Array, createSymmetricKeyAndNonce, @@ -50,14 +66,38 @@ import DeleteTradeOffer from "../transactions/TradeBotDeleteRequest"; import signTradeBotTransaction from "../transactions/signTradeBotTransaction"; import { createTransaction } from "../transactions/transactions"; import { executeEvent } from "../utils/events"; +import { fileToBase64 } from "../utils/fileReading"; import { mimeToExtensionMap } from "../utils/memeTypes"; +import { RequestQueueWithPromise } from "../utils/queue/queue"; import utils from "../utils/utils"; +export const requestQueueGetAtAddresses = new RequestQueueWithPromise(10); + const sellerForeignFee = { LITECOIN: { value: "~0.00005", ticker: "LTC", }, + DOGECOIN: { + value: "~0.005", + ticker: "DOGE", + }, + BITCOIN: { + value: "~0.0001", + ticker: "BTC", + }, + DIGIBYTE: { + value: "~0.0005", + ticker: "DGB", + }, + RAVENCOIN: { + value: "~0.006", + ticker: "RVN", + }, + PIRATECHAIN: { + value: "~0.0002", + ticker: "ARRR", + }, }; const btcFeePerByte = 0.000001; @@ -374,10 +414,10 @@ export const getUserAccount = async ({ isFromExtension, appInfo }) => { }; export const encryptData = async (data, sender) => { - let data64 = data.data64; + let data64 = data.data64 || data.base64; let publicKeys = data.publicKeys || []; - if (data.fileId) { - data64 = await getFileFromContentScript(data.fileId); + if (data?.file || data?.blob) { + data64 = await fileToBase64(data?.file || data?.blob); } if (!data64) { throw new Error("Please include data to encrypt"); @@ -401,14 +441,14 @@ export const encryptData = async (data, sender) => { }; export const encryptQortalGroupData = async (data, sender) => { - let data64 = data.data64; + let data64 = data?.data64 || data?.base64; let groupId = data?.groupId let isAdmins = data?.isAdmins if(!groupId){ throw new Error('Please provide a groupId') } - if (data.fileId) { - data64 = await getFileFromContentScript(data.fileId); + if (data?.file || data?.blob) { + data64 = await fileToBase64(data?.file || data?.blob); } if (!data64) { throw new Error("Please include data to encrypt"); @@ -436,7 +476,8 @@ export const encryptQortalGroupData = async (data, sender) => { url ); const resData = await res.text(); - const decryptedKey: any = await decryptResource(resData); + + const decryptedKey: any = await decryptResource(resData, true); const dataint8Array = base64ToUint8Array(decryptedKey.data); const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); @@ -470,8 +511,7 @@ url url ); const resData = await res.text(); - const decryptedKey: any = await decryptResource(resData); - + const decryptedKey: any = await decryptResource(resData, true); const dataint8Array = base64ToUint8Array(decryptedKey.data); const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); @@ -500,7 +540,7 @@ url }; export const decryptQortalGroupData = async (data, sender) => { - let data64 = data.data64; + let data64 = data?.data64 || data?.base64; let groupId = data?.groupId let isAdmins = data?.isAdmins if(!groupId){ @@ -531,7 +571,7 @@ export const decryptQortalGroupData = async (data, sender) => { url ); const resData = await res.text(); - const decryptedKey: any = await decryptResource(resData); + const decryptedKey: any = await decryptResource(resData, true); const dataint8Array = base64ToUint8Array(decryptedKey.data); const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); @@ -562,7 +602,7 @@ url url ); const resData = await res.text(); - const decryptedKey: any = await decryptResource(resData); + const decryptedKey: any = await decryptResource(resData, true); const dataint8Array = base64ToUint8Array(decryptedKey.data); const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); @@ -589,10 +629,10 @@ url }; export const encryptDataWithSharingKey = async (data, sender) => { - let data64 = data.data64; + let data64 = data?.data64 || data?.base64; let publicKeys = data.publicKeys || []; - if (data.fileId) { - data64 = await getFileFromContentScript(data.fileId); + if (data?.file || data?.blob) { + data64 = await fileToBase64(data?.file || data?.blob); } if (!data64) { throw new Error("Please include data to encrypt"); @@ -636,6 +676,86 @@ export const decryptDataWithSharingKey = async (data, sender) => { return base64ToObject.data }; +export const getHostedData = async (data, isFromExtension) => { + const isGateway = await isRunningGateway(); + if (isGateway) { + throw new Error("This action cannot be done through a gateway"); + } + const resPermission = await getUserPermission( + { + text1: "Do you give this application permission to", + text2: `Get a list of your hosted data?`, + }, + isFromExtension + ); + const { accepted } = resPermission; + + if(accepted){ + const limit = data?.limit ? data?.limit : 20; + const query = data?.query ? data?.query : "" + const offset = data?.offset ? data?.offset : 0 + + let urlPath = `/arbitrary/hosted/resources/?limit=${limit}&offset=${offset}` + if(query){ + urlPath = urlPath + `&query=${query}` + } + + const url = await createEndpoint(urlPath); + const response = await fetch(url); + const dataResponse = await response.json(); + return dataResponse + + + } else { + throw new Error("User declined to get list of hosted resources"); + } + +}; + +export const deleteHostedData = async (data, isFromExtension) => { + const isGateway = await isRunningGateway(); + if (isGateway) { + throw new Error("This action cannot be done through a gateway"); + } + const requiredFields = ["hostedData"]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + const resPermission = await getUserPermission( + { + text1: "Do you give this application permission to", + text2: `Delete ${data?.hostedData?.length} hosted resources?`, + }, + isFromExtension + ); + const { accepted } = resPermission; + + if(accepted){ + const { hostedData } = data; + + for (const hostedDataItem of hostedData){ + try { + const url = await createEndpoint(`/arbitrary/resource/${hostedDataItem.service}/${hostedDataItem.name}/${hostedDataItem.identifier}`); + await fetch(url, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + } + }); + } catch (error) { + //error + } + } + + return true + } else { + throw new Error("User declined delete hosted resources"); + } + +}; export const decryptData = async (data) => { const { encryptedData, publicKey } = data; @@ -797,7 +917,7 @@ export const deleteListItems = async (data, isFromExtension) => { if (isGateway) { throw new Error("This action cannot be done through a gateway"); } - const requiredFields = ["list_name", "item"]; + const requiredFields = ["list_name"]; const missingFields: string[] = []; requiredFields.forEach((field) => { if (!data[field]) { @@ -809,15 +929,18 @@ export const deleteListItems = async (data, isFromExtension) => { const errorMsg = `Missing fields: ${missingFieldsString}`; throw new Error(errorMsg); } - - const item = data.item; + if(!data?.item && !data?.items){ + throw new Error('Missing fields: items') + } + const item = data?.item; + const items = data?.items const list_name = data.list_name; const resPermission = await getUserPermission( { text1: "Do you give this application permission to", text2: `Remove the following from the list ${list_name}:`, - highlightedText: item, + highlightedText: items ? JSON.stringify(items) : item, }, isFromExtension ); @@ -826,7 +949,7 @@ export const deleteListItems = async (data, isFromExtension) => { if (accepted) { const url = await createEndpoint(`/lists/${list_name}`); const body = { - items: [item], + items: items || [item], }; const bodyToString = JSON.stringify(body); const response = await fetch(url, { @@ -867,29 +990,45 @@ export const publishQDNResource = async ( const errorMsg = `Missing fields: ${missingFieldsString}`; throw new Error(errorMsg); } - if (!data.fileId && !data.data64) { + if (!data.file && !data.data64 && !data.base64) { throw new Error("No data or file was submitted"); } - // Use "default" if user hasn't specified an identifer + // Use "default" if user hasn't specified an identifier const service = data.service; + const appFee = data?.appFee ? +data.appFee : undefined + const appFeeRecipient = data?.appFeeRecipient + let hasAppFee = false + if(appFee && appFee > 0 && appFeeRecipient){ + hasAppFee = true + } const registeredName = await getNameInfo(); const name = registeredName; + if(!name){ + throw new Error('User has no Qortal name') + } let identifier = data.identifier; - let data64 = data.data64; + let data64 = data.data64 || data.base64; const filename = data.filename; const title = data.title; const description = data.description; const category = data.category; - const tag1 = data.tag1; - const tag2 = data.tag2; - const tag3 = data.tag3; - const tag4 = data.tag4; - const tag5 = data.tag5; + + const tags = data?.tags || []; +const result = {}; + +// Fill tags dynamically while maintaining backward compatibility +for (let i = 0; i < 5; i++) { + result[`tag${i + 1}`] = tags[i] || data[`tag${i + 1}`] || undefined; +} + +// Access tag1 to tag5 from result +const { tag1, tag2, tag3, tag4, tag5 } = result; + if (data.identifier == null) { identifier = "default"; } - if (data.fileId) { - data64 = await getFileFromContentScript(data.fileId); + if (data?.file || data?.blob) { + data64 = await fileToBase64(data?.file || data?.blob); } if ( data.encrypt && @@ -924,17 +1063,30 @@ export const publishQDNResource = async ( const fee = await getFee("ARBITRARY"); + const handleDynamicValues = {} + if(hasAppFee){ + const feePayment = await getFee("PAYMENT"); + + handleDynamicValues['appFee'] = +appFee + +feePayment.fee, + handleDynamicValues['checkbox1'] = { + value: true, + label: "accept app fee", + } + } + if(!!data?.encrypt){ + handleDynamicValues['highlightedText'] = `isEncrypted: ${!!data.encrypt}` + } const resPermission = await getUserPermission( { text1: "Do you give this application permission to publish to QDN?", text2: `service: ${service}`, text3: `identifier: ${identifier || null}`, - highlightedText: data?.externalEncrypt ? `App is externally encrypting the resource. Make sure you trust the app.` : `isEncrypted: ${!!data.encrypt}`, fee: fee.fee, + ...handleDynamicValues }, isFromExtension ); - const { accepted } = resPermission; + const { accepted, checkbox1 = false } = resPermission; if (accepted) { try { @@ -957,6 +1109,12 @@ export const publishQDNResource = async ( apiVersion: 2, withFee: true, }); + if(resPublish?.signature && hasAppFee && checkbox1){ + sendCoinFunc({ + amount: appFee, + receiver: appFeeRecipient + }, true) + } return resPublish; } catch (error) { throw new Error(error?.message || "Upload failed"); @@ -966,6 +1124,41 @@ export const publishQDNResource = async ( } }; +export const checkArrrSyncStatus = async (seed) => { + const _url = await createEndpoint(`/crosschain/arrr/syncstatus`); + let tries = 0; // Track the number of attempts + + while (tries < 36) { + const response = await fetch(_url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: seed, + }); + + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + + if (res.indexOf('<') > -1 || res !== "Synchronized") { + // Wait 2 seconds before trying again + await new Promise((resolve) => setTimeout(resolve, 2000)); + tries += 1; + } else { + // If the response doesn't meet the two conditions, exit the function + return; + } + } + + // If we exceed 6 tries, throw an error + throw new Error("Failed to synchronize after 36 attempts"); +}; + + export const publishMultipleQDNResources = async ( data: any, sender, @@ -991,16 +1184,54 @@ export const publishMultipleQDNResources = async ( if (resources.length === 0) { throw new Error("No resources to publish"); } - if ( - data.encrypt && - (!data.publicKeys || - (Array.isArray(data.publicKeys) && data.publicKeys.length === 0)) - ) { - throw new Error("Encrypting data requires public keys"); + + const encrypt = data?.encrypt + + for (const resource of resources) { + const resourceEncrypt = encrypt && resource?.disableEncrypt !== true + if (!resourceEncrypt && resource?.service.endsWith("_PRIVATE")) { + const errorMsg = "Only encrypted data can go into private services"; + throw new Error(errorMsg) + } else if(resourceEncrypt && !resource?.service.endsWith("_PRIVATE")){ + const errorMsg = "For an encrypted publish please use a service that ends with _PRIVATE"; + throw new Error(errorMsg) + } } + + + // if ( + // data.encrypt && + // (!data.publicKeys || + // (Array.isArray(data.publicKeys) && data.publicKeys.length === 0)) + // ) { + // throw new Error("Encrypting data requires public keys"); + // } const fee = await getFee("ARBITRARY"); const registeredName = await getNameInfo(); const name = registeredName; + if(!name){ + throw new Error('You need a Qortal name to publish.') + } + const appFee = data?.appFee ? +data.appFee : undefined + const appFeeRecipient = data?.appFeeRecipient + let hasAppFee = false + if(appFee && appFee > 0 && appFeeRecipient){ + hasAppFee = true + } + + const handleDynamicValues = {} + if(hasAppFee){ + const feePayment = await getFee("PAYMENT"); + + handleDynamicValues['appFee'] = +appFee + +feePayment.fee, + handleDynamicValues['checkbox1'] = { + value: true, + label: "accept app fee", + } + } + if(data?.encrypt){ + handleDynamicValues['highlightedText'] = `isEncrypted: ${!!data.encrypt}` + } const resPermission = await getUserPermission( { text1: "Do you give this application permission to publish to QDN?", @@ -1051,7 +1282,7 @@ export const publishMultipleQDNResources = async (
Service: ${ resource.service }
-
Name: ${resource.name}
+
Name: ${name}
Identifier: ${ resource.identifier }
@@ -1066,12 +1297,12 @@ export const publishMultipleQDNResources = async (
`, - highlightedText: `isEncrypted: ${!!data.encrypt}`, - fee: fee.fee * resources.length, + fee: +fee.fee * resources.length, + ...handleDynamicValues }, isFromExtension ); - const { accepted } = resPermission; + const { accepted, checkbox1 = false } = resPermission; if (!accepted) { throw new Error("User declined request"); } @@ -1094,7 +1325,7 @@ export const publishMultipleQDNResources = async ( }); continue; } - if (!resource.fileId && !resource.data64) { + if (!resource.file && !resource.data64 && !resource?.base64) { const errorMsg = "No data or file was submitted"; failedPublishesIdentifiers.push({ reason: errorMsg, @@ -1104,20 +1335,26 @@ export const publishMultipleQDNResources = async ( } const service = resource.service; let identifier = resource.identifier; - let data64 = resource.data64; + let data64 = resource?.data64 || resource?.base64; const filename = resource.filename; const title = resource.title; const description = resource.description; const category = resource.category; - const tag1 = resource.tag1; - const tag2 = resource.tag2; - const tag3 = resource.tag3; - const tag4 = resource.tag4; - const tag5 = resource.tag5; + const tags = resource?.tags || []; + const result = {}; + + // Fill tags dynamically while maintaining backward compatibility + for (let i = 0; i < 5; i++) { + result[`tag${i + 1}`] = tags[i] || resource[`tag${i + 1}`] || undefined; + } + + // Access tag1 to tag5 from result + const { tag1, tag2, tag3, tag4, tag5 } = result; + const resourceEncrypt = encrypt && resource?.disableEncrypt !== true if (resource.identifier == null) { identifier = "default"; } - if (!data.encrypt && service.endsWith("_PRIVATE")) { + if (!resourceEncrypt && service.endsWith("_PRIVATE")) { const errorMsg = "Only encrypted data can go into private services"; failedPublishesIdentifiers.push({ reason: errorMsg, @@ -1125,10 +1362,10 @@ export const publishMultipleQDNResources = async ( }); continue; } - if (resource.fileId) { - data64 = await getFileFromContentScript(resource.fileId); + if (resource.file) { + data64 = await fileToBase64(resource.file); } - if (data.encrypt) { + if (resourceEncrypt) { try { const resKeyPair = await getKeyPair(); const parsedData = resKeyPair; @@ -1188,7 +1425,7 @@ export const publishMultipleQDNResources = async ( } } catch (error) { failedPublishesIdentifiers.push({ - reason: "Unknown error", + reason: error?.message || "Unknown error", identifier: resource.identifier, }); } @@ -1200,6 +1437,12 @@ export const publishMultipleQDNResources = async ( }; return obj; } + if(hasAppFee && checkbox1){ + sendCoinFunc({ + amount: appFee, + receiver: appFeeRecipient + }, true) + } return true; }; @@ -1286,15 +1529,42 @@ export const createPoll = async (data, isFromExtension) => { } }; + +function isBase64(str) { + const base64Regex = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/; + return base64Regex.test(str) && str.length % 4 === 0; +} + +function checkValue(value) { + if (typeof value === "string") { + if (isBase64(value)) { + return 'string' + } else { + return 'string' + } + } else if (typeof value === "object" && value !== null) { + return 'object' + } else { + throw new Error('Field fullContent is in an invalid format. Either use a string, base64 or an object.') + } +} + + export const sendChatMessage = async (data, isFromExtension, appInfo) => { const message = data?.message; - const fullMessageObject = data?.fullMessageObject - const recipient = data.destinationAddress; + const fullMessageObject = data?.fullMessageObject || data?.fullContent + const recipient = data?.destinationAddress || data.recipient; const groupId = data.groupId; - const isRecipient = !groupId; - - const value = - (await getPermission(`qAPPSendChatMessage-${appInfo?.name}`)) || false; + const isRecipient = groupId === undefined; + const chatReference = data?.chatReference + if(groupId === undefined && recipient === undefined){ + throw new Error('Please provide a recipient or groupId') + } + let fullMessageObjectType + if(fullMessageObject){ + fullMessageObjectType = checkValue(fullMessageObject) + } + const value = (await getPermission(`qAPPSendChatMessage-${appInfo?.name}`)) || false; let skip = false; if (value) { skip = true; @@ -1306,7 +1576,7 @@ if (!skip) { text1: "Do you give this application permission to send this chat message?", text2: `To: ${isRecipient ? recipient : `group ${groupId}`}`, - text3: `${message?.slice(0, 25)}${message?.length > 25 ? "..." : ""}`, + text3: fullMessageObject ? fullMessageObjectType === 'string' ? `${fullMessageObject?.slice(0, 25)}${fullMessageObject?.length > 25 ? "..." : ""}` : `${JSON.stringify(fullMessageObject)?.slice(0, 25)}${JSON.stringify(fullMessageObject)?.length > 25 ? "..." : ""}` : `${message?.slice(0, 25)}${message?.length > 25 ? "..." : ""}`, checkbox1: { value: false, label: "Always allow chat messages from this app", @@ -1341,7 +1611,10 @@ if (!skip) { version: 3, }; - const stringifyMessageObject = JSON.stringify(messageObject); + let stringifyMessageObject = JSON.stringify(messageObject); + if(fullMessageObjectType === 'string'){ + stringifyMessageObject = messageObject + } const balance = await getBalanceInfo(); const hasEnoughBalance = +balance < 4 ? false : true; @@ -1397,16 +1670,22 @@ if (!skip) { publicKey: uint8PublicKey, }; + let handleDynamicValues = {} + if(chatReference){ + handleDynamicValues['chatReference'] = chatReference + } + const tx = await createTransaction(18, keyPair, { timestamp: sendTimestamp, recipient: recipient, recipientPublicKey: key, - hasChatReference: 0, + hasChatReference: chatReference ? 1 : 0, message: stringifyMessageObject, lastReference: reference, proofOfWorkNonce: 0, isEncrypted: 1, isText: 1, + ...handleDynamicValues }); const chatBytes = tx.chatBytes; @@ -1435,16 +1714,22 @@ if (!skip) { publicKey: uint8PublicKey, }; + let handleDynamicValues = {} + if(chatReference){ + handleDynamicValues['chatReference'] = chatReference + } + const txBody = { timestamp: Date.now(), groupID: Number(groupId), hasReceipient: 0, - hasChatReference: 0, + hasChatReference: chatReference ? 1 : 0, message: stringifyMessageObject, lastReference: reference, proofOfWorkNonce: 0, isEncrypted: 0, // Set default to not encrypted for groups isText: 1, + ...handleDynamicValues }; const tx = await createTransaction(181, keyPair, txBody); @@ -1543,7 +1828,6 @@ export const saveFile = async (data, sender, isFromExtension, snackMethods) => { } const filename = data.filename; const blob = data.blob; - const fileId = data.fileId; const resPermission = await getUserPermission( { text1: "Would you like to download:", @@ -1641,7 +1925,7 @@ export const deployAt = async (data, isFromExtension) => { } }; -export const getUserWallet = async (data, isFromExtension) => { +export const getUserWallet = async (data, isFromExtension, appInfo) => { const requiredFields = ["coin"]; const missingFields: string[] = []; requiredFields.forEach((field) => { @@ -1654,16 +1938,49 @@ export const getUserWallet = async (data, isFromExtension) => { const errorMsg = `Missing fields: ${missingFieldsString}`; throw new Error(errorMsg); } - const resPermission = await getUserPermission( + const isGateway = await isRunningGateway(); + + if (data?.coin === "ARRR" && isGateway) + throw new Error( + "Cannot view ARRR wallet info through the gateway. Please use your local node." + ); + + const value = + (await getPermission( + `qAPPAutoGetUserWallet-${appInfo?.name}-${data.coin}` + )) || false; + let skip = false; + if (value) { + skip = true; + } + + let resPermission; + + if (!skip) { + resPermission = await getUserPermission( { text1: "Do you give this application permission to get your wallet information?", + highlightedText: `coin: ${data.coin}`, + checkbox1: { + value: true, + label: "Always allow wallet to be retrieved automatically", + }, }, isFromExtension ); - const { accepted } = resPermission; - if (accepted) { +} +const { accepted = false, checkbox1 = false } = resPermission || {}; + +if (resPermission) { + setPermission( + `qAPPAutoGetUserWallet-${appInfo?.name}-${data.coin}`, + checkbox1 + ); +} + + if (accepted || skip) { let coin = data.coin; let userWallet = {}; let arrrAddress = ""; @@ -1700,7 +2017,7 @@ export const getUserWallet = async (data, isFromExtension) => { break; case "BTC": userWallet["address"] = parsedData.btcAddress; - userWallet["publickey"] = parsedData.derivedMasterPublicKey; + userWallet["publickey"] = parsedData.btcPublicKey; break; case "LTC": userWallet["address"] = parsedData.ltcAddress; @@ -1719,6 +2036,7 @@ export const getUserWallet = async (data, isFromExtension) => { userWallet["publickey"] = parsedData.rvnPublicKey; break; case "ARRR": + await checkArrrSyncStatus(parsedData.arrrSeed58) userWallet["address"] = arrrAddress; break; default: @@ -1836,6 +2154,7 @@ export const getWalletBalance = async ( _body = parsedData.rvnPublicKey; break; case "ARRR": + await checkArrrSyncStatus(parsedData.arrrSeed58) _url = await createEndpoint(`/crosschain/arrr/walletbalance`); _body = parsedData.arrrSeed58; break; @@ -1873,6 +2192,33 @@ export const getWalletBalance = async ( } }; +const getPirateWallet = async (arrrSeed58)=> { + const isGateway = await isRunningGateway(); + if (isGateway) { + throw new Error("Retrieving PIRATECHAIN balance is not allowed through a gateway."); + } + const bodyToString = arrrSeed58; + await checkArrrSyncStatus(bodyToString) + const url = await createEndpoint(`/crosschain/arrr/walletaddress`); + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: bodyToString, + }); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + if (res?.error && res?.message) { + throw new Error(res.message); + } + return res +} + export const getUserWalletFunc = async (coin) => { let userWallet = {}; const wallet = await getSaveWallet(); @@ -1885,26 +2231,34 @@ export const getUserWalletFunc = async (coin) => { userWallet["publickey"] = parsedData.publicKey; break; case "BTC": + case "BITCOIN": userWallet["address"] = parsedData.btcAddress; userWallet["publickey"] = parsedData.btcPublicKey; break; case "LTC": + case "LITECOIN": userWallet["address"] = parsedData.ltcAddress; userWallet["publickey"] = parsedData.ltcPublicKey; break; case "DOGE": + case "DOGECOIN": userWallet["address"] = parsedData.dogeAddress; userWallet["publickey"] = parsedData.dogePublicKey; break; case "DGB": + case "DIGIBYTE": userWallet["address"] = parsedData.dgbAddress; userWallet["publickey"] = parsedData.dgbPublicKey; break; case "RVN": + case "RAVENCOIN": userWallet["address"] = parsedData.rvnAddress; userWallet["publickey"] = parsedData.rvnPublicKey; break; case "ARRR": + case "PIRATECHAIN": + const arrrAddress = await getPirateWallet(parsedData.arrrSeed58) + userWallet["address"] = arrrAddress break; default: break; @@ -1912,7 +2266,7 @@ export const getUserWalletFunc = async (coin) => { return userWallet; }; -export const getUserWalletInfo = async (data, isFromExtension) => { +export const getUserWalletInfo = async (data, isFromExtension, appInfo) => { const requiredFields = ["coin"]; const missingFields: string[] = []; requiredFields.forEach((field) => { @@ -1925,18 +2279,51 @@ export const getUserWalletInfo = async (data, isFromExtension) => { const errorMsg = `Missing fields: ${missingFieldsString}`; throw new Error(errorMsg); } - const resPermission = await getUserPermission( + if(data?.coin === 'ARRR'){ + + throw new Error( + "ARRR is not supported for this call." + ); + } + const value = + (await getPermission( + `getUserWalletInfo-${appInfo?.name}-${data.coin}` + )) || false; +let skip = false; +if (value) { + skip = true; +} + let resPermission; + + if (!skip) { + + resPermission = await getUserPermission( { text1: "Do you give this application permission to retrieve your wallet information", + highlightedText: `coin: ${data.coin}`, + checkbox1: { + value: true, + label: "Always allow wallet info to be retrieved automatically", + }, }, isFromExtension ); - const { accepted } = resPermission; +} +const { accepted = false, checkbox1 = false } = resPermission || {}; - if (accepted) { +if (resPermission) { + setPermission( + `getUserWalletInfo-${appInfo?.name}-${data.coin}`, + checkbox1 + ); +} + + if (accepted || skip) { let coin = data.coin; let walletKeys = await getUserWalletFunc(coin); + + const _url = await createEndpoint( `/crosschain/` + data.coin.toLowerCase() + `/addressinfos` ); @@ -1970,6 +2357,99 @@ export const getUserWalletInfo = async (data, isFromExtension) => { } }; +export const getUserWalletTransactions = async (data, isFromExtension, appInfo) => { + const requiredFields = ["coin"]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(", "); + const errorMsg = `Missing fields: ${missingFieldsString}`; + throw new Error(errorMsg); + } + + const value = + (await getPermission( + `getUserWalletTransactions-${appInfo?.name}-${data.coin}` + )) || false; +let skip = false; +if (value) { + skip = true; +} + let resPermission; + + if (!skip) { + + resPermission = await getUserPermission( + { + text1: + "Do you give this application permission to retrieve your wallet transactions", + highlightedText: `coin: ${data.coin}`, + checkbox1: { + value: true, + label: "Always allow wallet txs to be retrieved automatically", + }, + }, + isFromExtension + ); +} +const { accepted = false, checkbox1 = false } = resPermission || {}; + +if (resPermission) { + setPermission( + `getUserWalletTransactions-${appInfo?.name}-${data.coin}`, + checkbox1 + ); +} + + if (accepted || skip) { + const coin = data.coin; + const walletKeys = await getUserWalletFunc(coin); + let publicKey + if(data?.coin === 'ARRR'){ + const resKeyPair = await getKeyPair(); + const parsedData = resKeyPair; + publicKey = parsedData.arrrSeed58; + } else { + publicKey = walletKeys["publickey"] + } + + const _url = await createEndpoint( + `/crosschain/` + data.coin.toLowerCase() + `/wallettransactions` + ); + const _body = publicKey; + try { + const response = await fetch(_url, { + method: "POST", + headers: { + Accept: "*/*", + "Content-Type": "application/json", + }, + body: _body, + }); + if (!response?.ok) throw new Error("Unable to fetch wallet transactions"); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + if (res?.error && res?.message) { + throw new Error(res.message); + } + + return res; + } catch (error) { + throw new Error(error?.message || "Fetch Wallet Transactions Failed"); + } + } else { + throw new Error("User declined request"); + } +}; + export const getCrossChainServerInfo = async (data) => { const requiredFields = ["coin"]; const missingFields: string[] = []; @@ -2064,7 +2544,7 @@ export const getForeignFee = async (data) => { } const { coin, type } = data; - const url = `/crosschain/${coin}/${type}`; + const url = `/crosschain/${coin.toLowerCase()}/${type}`; try { const endpoint = await createEndpoint(url); @@ -2093,8 +2573,10 @@ export const getForeignFee = async (data) => { }; export const updateForeignFee = async (data) => { - const localNodeAvailable = await isUsingLocal(); - if (!localNodeAvailable) throw new Error("Please use your local node."); + const isGateway = await isRunningGateway(); + if (isGateway) { + throw new Error("This action cannot be done through a gateway"); + } const requiredFields = ["coin", "type", "value"]; const missingFields: string[] = []; @@ -2111,7 +2593,7 @@ export const updateForeignFee = async (data) => { } const { coin, type, value } = data; - const url = `/crosschain/${coin}/update${type}`; + const url = `/crosschain/${coin.toLowerCase()}/update${type}`; try { const endpoint = await createEndpoint(url); @@ -2158,7 +2640,7 @@ export const getServerConnectionHistory = async (data) => { } const coin = data.coin.toLowerCase(); - const url = `/crosschain/${coin}/serverconnectionhistory`; + const url = `/crosschain/${coin.toLowerCase()}/serverconnectionhistory`; try { const endpoint = await createEndpoint(url); // Assuming createEndpoint is available @@ -2191,8 +2673,10 @@ export const getServerConnectionHistory = async (data) => { }; export const setCurrentForeignServer = async (data) => { - const localNodeAvailable = await isUsingLocal(); - if (!localNodeAvailable) throw new Error("Please use your local node."); + const isGateway = await isRunningGateway(); + if (isGateway) { + throw new Error("This action cannot be done through a gateway"); + } const requiredFields = ["coin"]; const missingFields: string[] = []; @@ -2216,7 +2700,7 @@ export const setCurrentForeignServer = async (data) => { connectionType: type, }; - const url = `/crosschain/${coin}/setcurrentserver`; + const url = `/crosschain/${coin.toLowerCase()}/setcurrentserver`; try { const endpoint = await createEndpoint(url); // Assuming createEndpoint is available @@ -2249,8 +2733,10 @@ export const setCurrentForeignServer = async (data) => { }; export const addForeignServer = async (data) => { - const localNodeAvailable = await isUsingLocal(); - if (!localNodeAvailable) throw new Error("Please use your local node."); + const isGateway = await isRunningGateway(); + if (isGateway) { + throw new Error("This action cannot be done through a gateway"); + } const requiredFields = ["coin"]; const missingFields: string[] = []; @@ -2274,7 +2760,7 @@ export const addForeignServer = async (data) => { connectionType: type, }; - const url = `/crosschain/${coin}/addserver`; + const url = `/crosschain/${coin.toLowerCase()}/addserver`; try { const endpoint = await createEndpoint(url); // Assuming createEndpoint is available @@ -2307,8 +2793,10 @@ export const addForeignServer = async (data) => { }; export const removeForeignServer = async (data) => { - const localNodeAvailable = await isUsingLocal(); - if (!localNodeAvailable) throw new Error("Please use your local node."); + const isGateway = await isRunningGateway(); + if (isGateway) { + throw new Error("This action cannot be done through a gateway"); + } const requiredFields = ["coin"]; const missingFields: string[] = []; @@ -2332,7 +2820,7 @@ export const removeForeignServer = async (data) => { connectionType: type, }; - const url = `/crosschain/${coin}/removeserver`; + const url = `/crosschain/${coin.toLowerCase()}/removeserver`; try { const endpoint = await createEndpoint(url); // Assuming createEndpoint is available @@ -2396,7 +2884,7 @@ export const getDaySummary = async () => { }; export const sendCoin = async (data, isFromExtension) => { - const requiredFields = ["coin", "destinationAddress", "amount"]; + const requiredFields = ["coin", "amount"]; const missingFields: string[] = []; requiredFields.forEach((field) => { if (!data[field]) { @@ -2408,6 +2896,9 @@ export const sendCoin = async (data, isFromExtension) => { const errorMsg = `Missing fields: ${missingFieldsString}`; throw new Error(errorMsg); } + if(!data?.destinationAddress && !data?.recipient){ + throw new Error('Missing fields: recipient') + } let checkCoin = data.coin; const wallet = await getSaveWallet(); const address = wallet.address0; @@ -2420,12 +2911,12 @@ export const sendCoin = async (data, isFromExtension) => { "Cannot send a non-QORT coin through the gateway. Please use your local node." ); if (checkCoin === "QORT") { - // Params: data.coin, data.destinationAddress, data.amount, data.fee + // Params: data.coin, data.recipient, data.amount, data.fee // TODO: prompt user to send. If they confirm, call `POST /crosschain/:coin/send`, or for QORT, broadcast a PAYMENT transaction // then set the response string from the core to the `response` variable (defined above) // If they decline, send back JSON that includes an `error` key, such as `{"error": "User declined request"}` const amount = Number(data.amount); - const recipient = data.destinationAddress; + const recipient = data?.recipient || data.destinationAddress; const url = await createEndpoint(`/addresses/balance/${address}`); const response = await fetch(url); @@ -2465,7 +2956,7 @@ export const sendCoin = async (data, isFromExtension) => { text1: "Do you give this application permission to send coins?", text2: `To: ${recipient}`, highlightedText: `${amount} ${checkCoin}`, - fee: fee * QORT_DECIMALS, + fee: fee, }, isFromExtension ); @@ -2482,7 +2973,7 @@ export const sendCoin = async (data, isFromExtension) => { } } else if (checkCoin === "BTC") { const amount = Number(data.amount); - const recipient = data.destinationAddress; + const recipient = data?.recipient || data.destinationAddress; const xprv58 = parsedData.btcPrivateKey; const feePerByte = data.fee ? data.fee : btcFeePerByte; @@ -2492,7 +2983,7 @@ export const sendCoin = async (data, isFromExtension) => { throw new Error("Unable to fetch BTC balance"); } const btcWalletBalanceDecimals = Number(btcWalletBalance); - const btcAmountDecimals = Number(amount) * QORT_DECIMALS; + const btcAmountDecimals = Number(amount); const fee = feePerByte * 500; // default 0.00050000 if (btcAmountDecimals + fee > btcWalletBalanceDecimals) { throw new Error("INSUFFICIENT_FUNDS"); @@ -2539,7 +3030,7 @@ export const sendCoin = async (data, isFromExtension) => { } } else if (checkCoin === "LTC") { const amount = Number(data.amount); - const recipient = data.destinationAddress; + const recipient = data?.recipient || data.destinationAddress; const xprv58 = parsedData.ltcPrivateKey; const feePerByte = data.fee ? data.fee : ltcFeePerByte; const ltcWalletBalance = await getWalletBalance({ coin: checkCoin }, true); @@ -2549,8 +3040,7 @@ export const sendCoin = async (data, isFromExtension) => { throw new Error(errorMsg); } const ltcWalletBalanceDecimals = Number(ltcWalletBalance); - const ltcAmountDecimals = Number(amount) * QORT_DECIMALS; - const balance = (Number(ltcWalletBalance) / 1e8).toFixed(8); + const ltcAmountDecimals = Number(amount); const fee = feePerByte * 1000; // default 0.00030000 if (ltcAmountDecimals + fee > ltcWalletBalanceDecimals) { throw new Error("Insufficient Funds!"); @@ -2595,8 +3085,7 @@ export const sendCoin = async (data, isFromExtension) => { } } else if (checkCoin === "DOGE") { const amount = Number(data.amount); - const recipient = data.destinationAddress; - const coin = data.coin; + const recipient = data?.recipient || data.destinationAddress; const xprv58 = parsedData.dogePrivateKey; const feePerByte = data.fee ? data.fee : dogeFeePerByte; const dogeWalletBalance = await getWalletBalance({ coin: checkCoin }, true); @@ -2605,8 +3094,7 @@ export const sendCoin = async (data, isFromExtension) => { throw new Error(errorMsg); } const dogeWalletBalanceDecimals = Number(dogeWalletBalance); - const dogeAmountDecimals = Number(amount) * QORT_DECIMALS; - const balance = (Number(dogeWalletBalance) / 1e8).toFixed(8); + const dogeAmountDecimals = Number(amount); const fee = feePerByte * 5000; // default 0.05000000 if (dogeAmountDecimals + fee > dogeWalletBalanceDecimals) { let errorMsg = "Insufficient Funds!"; @@ -2654,7 +3142,7 @@ export const sendCoin = async (data, isFromExtension) => { } } else if (checkCoin === "DGB") { const amount = Number(data.amount); - const recipient = data.destinationAddress; + const recipient = data?.recipient || data.destinationAddress; const xprv58 = parsedData.dbgPrivateKey; const feePerByte = data.fee ? data.fee : dgbFeePerByte; const dgbWalletBalance = await getWalletBalance({ coin: checkCoin }, true); @@ -2663,7 +3151,7 @@ export const sendCoin = async (data, isFromExtension) => { throw new Error(errorMsg); } const dgbWalletBalanceDecimals = Number(dgbWalletBalance); - const dgbAmountDecimals = Number(amount) * QORT_DECIMALS; + const dgbAmountDecimals = Number(amount); const fee = feePerByte * 500; // default 0.00005000 if (dgbAmountDecimals + fee > dgbWalletBalanceDecimals) { let errorMsg = "Insufficient Funds!"; @@ -2711,8 +3199,7 @@ export const sendCoin = async (data, isFromExtension) => { } } else if (checkCoin === "RVN") { const amount = Number(data.amount); - const recipient = data.destinationAddress; - const coin = data.coin; + const recipient = data?.recipient || data.destinationAddress; const xprv58 = parsedData.rvnPrivateKey; const feePerByte = data.fee ? data.fee : rvnFeePerByte; const rvnWalletBalance = await getWalletBalance({ coin: checkCoin }, true); @@ -2721,8 +3208,7 @@ export const sendCoin = async (data, isFromExtension) => { throw new Error(errorMsg); } const rvnWalletBalanceDecimals = Number(rvnWalletBalance); - const rvnAmountDecimals = Number(amount) * QORT_DECIMALS; - const balance = (Number(rvnWalletBalance) / 1e8).toFixed(8); + const rvnAmountDecimals = Number(amount); const fee = feePerByte * 500; // default 0.00562500 if (rvnAmountDecimals + fee > rvnWalletBalanceDecimals) { let errorMsg = "Insufficient Funds!"; @@ -2770,8 +3256,8 @@ export const sendCoin = async (data, isFromExtension) => { } } else if (checkCoin === "ARRR") { const amount = Number(data.amount); - const recipient = data.destinationAddress; - const memo = data.memo; + const recipient = data?.recipient || data.destinationAddress; + const memo = data?.memo; const arrrWalletBalance = await getWalletBalance({ coin: checkCoin }, true); if (isNaN(Number(arrrWalletBalance))) { @@ -2779,7 +3265,7 @@ export const sendCoin = async (data, isFromExtension) => { throw new Error(errorMsg); } const arrrWalletBalanceDecimals = Number(arrrWalletBalance); - const arrrAmountDecimals = Number(amount) * QORT_DECIMALS; + const arrrAmountDecimals = Number(amount); const fee = 0.0001; if (arrrAmountDecimals + fee > arrrWalletBalanceDecimals) { let errorMsg = "Insufficient Funds!"; @@ -2804,7 +3290,7 @@ export const sendCoin = async (data, isFromExtension) => { arrrAmount: amount, memo: memo, }; - const url = await createEndpoint(`/crosschain/btc/send`); + const url = await createEndpoint(`/crosschain/arrr/send`); const response = await fetch(url, { method: "POST", @@ -2843,11 +3329,24 @@ export const createBuyOrder = async (data, isFromExtension) => { } const isGateway = await isRunningGateway(); const foreignBlockchain = data.foreignBlockchain; - const crosschainAtInfo = data.crosschainAtInfo; const atAddresses = data.crosschainAtInfo?.map( (order) => order.qortalAtAddress ); + const atPromises = atAddresses + .map((atAddress) => + requestQueueGetAtAddresses.enqueue(async () => { + const url = await createEndpoint(`/crosschain/trade/${atAddress}`) + const resAddress = await fetch(url); + const resData = await resAddress.json(); + if(foreignBlockchain !== resData?.foreignBlockchain){ + throw new Error('All requested ATs need to be of the same foreign Blockchain.') + } + return resData + }) + ); + + const crosschainAtInfo = await Promise.all(atPromises); try { const resPermission = await getUserPermission( { @@ -2859,7 +3358,7 @@ export const createBuyOrder = async (data, isFromExtension) => { return latest + +cur?.qortAmount; }, 0)} QORT FOR ${roundUpToDecimals( crosschainAtInfo?.reduce((latest, cur) => { - return latest + +cur?.foreignAmount; + return latest + +cur?.expectedForeignAmount; }, 0) )} ${` ${crosschainAtInfo?.[0]?.foreignBlockchain}`}`, @@ -3023,7 +3522,9 @@ export const createSellOrder = async (data, isFromExtension) => { throw new Error(errorMsg); } - const receivingAddress = await getUserWalletFunc("LTC"); + const parsedForeignAmount = Number(data.foreignAmount)?.toFixed(8) + + const receivingAddress = await getUserWalletFunc(data.foreignBlockchain); try { const resPermission = await getUserPermission( { @@ -3031,7 +3532,7 @@ export const createSellOrder = async (data, isFromExtension) => { "Do you give this application permission to perform a sell order?", text2: `${data.qortAmount}${" "} ${`QORT`}`, - text3: `FOR ${data.foreignAmount} ${data.foreignBlockchain}`, + text3: `FOR ${parsedForeignAmount} ${data.foreignBlockchain}`, fee: "0.02", }, isFromExtension @@ -3051,9 +3552,9 @@ export const createSellOrder = async (data, isFromExtension) => { { creatorPublicKey: userPublicKey, qortAmount: parseFloat(data.qortAmount), - fundingQortAmount: parseFloat(data.qortAmount) + 0.001, + fundingQortAmount: parseFloat(data.qortAmount) + 0.01, foreignBlockchain: data.foreignBlockchain, - foreignAmount: parseFloat(data.foreignAmount), + foreignAmount: parseFloat(parsedForeignAmount), tradeTimeout: 120, receivingAddress: receivingAddress.address, }, @@ -3071,9 +3572,6 @@ export const createSellOrder = async (data, isFromExtension) => { export const cancelSellOrder = async (data, isFromExtension) => { const requiredFields = [ - "qortAmount", - "foreignBlockchain", - "foreignAmount", "atAddress", ]; const missingFields: string[] = []; @@ -3088,16 +3586,20 @@ export const cancelSellOrder = async (data, isFromExtension) => { throw new Error(errorMsg); } + const url = await createEndpoint(`/crosschain/trade/${data.atAddress}`) + const resAddress = await fetch(url); + const resData = await resAddress.json(); + if(!resData?.qortalAtAddress) throw new Error('Cannot find AT info.') try { const fee = await getFee("MESSAGE"); const resPermission = await getUserPermission( { text1: - "Do you give this application permission to perform cancel a sell order?", - text2: `${data.qortAmount}${" "} + "Do you give this application permission to perform: cancel a sell order?", + text2: `${resData.qortAmount}${" "} ${`QORT`}`, - text3: `FOR ${data.foreignAmount} ${data.foreignBlockchain}`, + text3: `FOR ${resData.expectedForeignAmount} ${resData.foreignBlockchain}`, fee: fee.fee, }, isFromExtension @@ -3286,6 +3788,7 @@ export const signTransaction = async (data, isFromExtension) => { throw new Error(errorMsg); } + const shouldProcess = data?.process || false; let _url = await createEndpoint( "/transactions/decode?ignoreValidityChecks=false" ); @@ -3302,7 +3805,7 @@ export const signTransaction = async (data, isFromExtension) => { const decodedData = await response.json(); const resPermission = await getUserPermission( { - text1: `Do you give this application permission to sign a transaction?`, + text1: `Do you give this application permission to ${ shouldProcess ? 'SIGN and PROCESS' : 'SIGN' } a transaction?`, highlightedText: "Read the transaction carefully before accepting!", text2: `Tx type: ${decodedData.type}`, json: decodedData, @@ -3347,7 +3850,16 @@ export const signTransaction = async (data, isFromExtension) => { keyPair.privateKey ); const signedBytes = utils.appendBuffer(arbitraryBytesBuffer, signature); - return uint8ArrayToBase64(signedBytes); + const signedBytesToBase58 = Base58.encode(signedBytes); + if(!shouldProcess){ + return signedBytesToBase58 + } + const res = await processTransactionVersion2(signedBytesToBase58); + if (!res?.signature) + throw new Error( + res?.message || "Transaction was not able to be processed" + ); + return res; } else { throw new Error("User declined request"); @@ -3448,4 +3960,539 @@ export const createAndCopyEmbedLink = async (data, isFromExtension) => { throw new Error('Invalid type') } -}; \ No newline at end of file +}; + +export const registerNameRequest = async (data, isFromExtension) => { + const requiredFields = ["name"]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + const fee = await getFee("REGISTER_NAME"); + const resPermission = await getUserPermission( + { + text1: `Do you give this application permission to register this name?`, + highlightedText: data.name, + text2: data?.description, + fee: fee.fee + }, + isFromExtension + ); + const { accepted } = resPermission; + if (accepted) { + const name = data.name + const description = data?.description + const response = await registerName({ name, description }); + return response + + } else { + throw new Error("User declined request"); + } +}; + +export const updateNameRequest = async (data, isFromExtension) => { + const requiredFields = ["newName", "oldName"]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + const oldName = data.oldName + const newName = data.newName + const description = data?.description + const fee = await getFee("UPDATE_NAME"); + const resPermission = await getUserPermission( + { + text1: `Do you give this application permission to register this name?`, + highlightedText: data.newName, + text2: data?.description, + fee: fee.fee, + }, + isFromExtension + ); + const { accepted } = resPermission; + if (accepted) { + const response = await updateName({ oldName, newName, description }); + return response + + } else { + throw new Error("User declined request"); + } +}; + +export const leaveGroupRequest = async (data, isFromExtension) => { + const requiredFields = ["groupId"]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + const groupId = data.groupId + let groupInfo = null; + try { + const url = await createEndpoint(`/groups/${groupId}`); + const response = await fetch(url); + if (!response.ok) throw new Error("Failed to fetch group"); + + groupInfo = await response.json(); + } catch (error) { + const errorMsg = (error && error.message) || "Group not found"; + throw new Error(errorMsg); + } + + const fee = await getFee("LEAVE_GROUP"); + const resPermission = await getUserPermission( + { + text1: `Do you give this application permission to leave the following group?`, + highlightedText: `${groupInfo.groupName}`, + fee: fee.fee, + }, + isFromExtension + ); + const { accepted } = resPermission; + if (accepted) { + const response = await leaveGroup({ groupId }); + return response + + } else { + throw new Error("User declined request"); + } +}; + +export const inviteToGroupRequest = async (data, isFromExtension) => { + const requiredFields = ["groupId", "inviteTime", "inviteeAddress"]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + const groupId = data.groupId + const qortalAddress = data?.inviteeAddress + const inviteTime = data?.inviteTime + + let groupInfo = null; + try { + const url = await createEndpoint(`/groups/${groupId}`); + const response = await fetch(url); + if (!response.ok) throw new Error("Failed to fetch group"); + + groupInfo = await response.json(); + } catch (error) { + const errorMsg = (error && error.message) || "Group not found"; + throw new Error(errorMsg); + } + + const displayInvitee = await getNameInfoForOthers(qortalAddress) + + const fee = await getFee("GROUP_INVITE"); + const resPermission = await getUserPermission( + { + text1: `Do you give this application permission to invite ${displayInvitee || qortalAddress}?`, + highlightedText: `Group: ${groupInfo.groupName}`, + fee: fee.fee, + }, + isFromExtension + ); + const { accepted } = resPermission; + if (accepted) { + const response = await inviteToGroup({ + groupId, + qortalAddress, + inviteTime, + }) + return response + + } else { + throw new Error("User declined request"); + } +}; + +export const kickFromGroupRequest = async (data, isFromExtension) => { + const requiredFields = ["groupId", "qortalAddress"]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + const groupId = data.groupId + const qortalAddress = data?.qortalAddress + const reason = data?.reason + + let groupInfo = null; + try { + const url = await createEndpoint(`/groups/${groupId}`); + const response = await fetch(url); + if (!response.ok) throw new Error("Failed to fetch group"); + + groupInfo = await response.json(); + } catch (error) { + const errorMsg = (error && error.message) || "Group not found"; + throw new Error(errorMsg); + } + + const displayInvitee = await getNameInfoForOthers(qortalAddress) + + const fee = await getFee("GROUP_KICK"); + const resPermission = await getUserPermission( + { + text1: `Do you give this application permission to kick ${displayInvitee || qortalAddress} from the group?`, + highlightedText: `Group: ${groupInfo.groupName}`, + fee: fee.fee, + }, + isFromExtension + ); + const { accepted } = resPermission; + if (accepted) { + const response = await kickFromGroup({ + groupId, + qortalAddress, + rBanReason: reason + }) + return response + + } else { + throw new Error("User declined request"); + } +}; + +export const banFromGroupRequest = async (data, isFromExtension) => { + const requiredFields = ["groupId", "qortalAddress"]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + const groupId = data.groupId + const qortalAddress = data?.qortalAddress + const rBanTime = data?.banTime + const reason = data?.reason + let groupInfo = null; + try { + const url = await createEndpoint(`/groups/${groupId}`); + const response = await fetch(url); + if (!response.ok) throw new Error("Failed to fetch group"); + + groupInfo = await response.json(); + } catch (error) { + const errorMsg = (error && error.message) || "Group not found"; + throw new Error(errorMsg); + } + + const displayInvitee = await getNameInfoForOthers(qortalAddress) + + const fee = await getFee("GROUP_BAN"); + const resPermission = await getUserPermission( + { + text1: `Do you give this application permission to ban ${displayInvitee || qortalAddress} from the group?`, + highlightedText: `Group: ${groupInfo.groupName}`, + fee: fee.fee, + }, + isFromExtension + ); + const { accepted } = resPermission; + if (accepted) { + const response = await banFromGroup({ + groupId, + qortalAddress, + rBanTime, + rBanReason: reason + }) + return response + + } else { + throw new Error("User declined request"); + } +}; + +export const cancelGroupBanRequest = async (data, isFromExtension) => { + const requiredFields = ["groupId", "qortalAddress"]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + const groupId = data.groupId + const qortalAddress = data?.qortalAddress + + let groupInfo = null; + try { + const url = await createEndpoint(`/groups/${groupId}`); + const response = await fetch(url); + if (!response.ok) throw new Error("Failed to fetch group"); + + groupInfo = await response.json(); + } catch (error) { + const errorMsg = (error && error.message) || "Group not found"; + throw new Error(errorMsg); + } + + const displayInvitee = await getNameInfoForOthers(qortalAddress) + + const fee = await getFee("CANCEL_GROUP_BAN"); + const resPermission = await getUserPermission( + { + text1: `Do you give this application permission to cancel the group ban for user ${displayInvitee || qortalAddress}?`, + highlightedText: `Group: ${groupInfo.groupName}`, + fee: fee.fee, + }, + isFromExtension + ); + const { accepted } = resPermission; + if (accepted) { + const response = await cancelBan({ + groupId, + qortalAddress, + }) + return response + + } else { + throw new Error("User declined request"); + } +}; + +export const addGroupAdminRequest = async (data, isFromExtension) => { + const requiredFields = ["groupId", "qortalAddress"]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + const groupId = data.groupId + const qortalAddress = data?.qortalAddress + + let groupInfo = null; + try { + const url = await createEndpoint(`/groups/${groupId}`); + const response = await fetch(url); + if (!response.ok) throw new Error("Failed to fetch group"); + + groupInfo = await response.json(); + } catch (error) { + const errorMsg = (error && error.message) || "Group not found"; + throw new Error(errorMsg); + } + + const displayInvitee = await getNameInfoForOthers(qortalAddress) + + const fee = await getFee("ADD_GROUP_ADMIN"); + const resPermission = await getUserPermission( + { + text1: `Do you give this application permission to add user ${displayInvitee || qortalAddress} as an admin?`, + highlightedText: `Group: ${groupInfo.groupName}`, + fee: fee.fee, + }, + isFromExtension + ); + const { accepted } = resPermission; + if (accepted) { + const response = await makeAdmin({ + groupId, + qortalAddress, + }) + return response + + } else { + throw new Error("User declined request"); + } +}; + +export const removeGroupAdminRequest = async (data, isFromExtension) => { + const requiredFields = ["groupId", "qortalAddress"]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + const groupId = data.groupId + const qortalAddress = data?.qortalAddress + + let groupInfo = null; + try { + const url = await createEndpoint(`/groups/${groupId}`); + const response = await fetch(url); + if (!response.ok) throw new Error("Failed to fetch group"); + + groupInfo = await response.json(); + } catch (error) { + const errorMsg = (error && error.message) || "Group not found"; + throw new Error(errorMsg); + } + + const displayInvitee = await getNameInfoForOthers(qortalAddress) + + const fee = await getFee("REMOVE_GROUP_ADMIN"); + const resPermission = await getUserPermission( + { + text1: `Do you give this application permission to remove user ${displayInvitee || qortalAddress} as admin?`, + highlightedText: `Group: ${groupInfo.groupName}`, + fee: fee.fee, + }, + isFromExtension + ); + const { accepted } = resPermission; + if (accepted) { + const response = await removeAdmin({ + groupId, + qortalAddress, + }) + return response + + } else { + throw new Error("User declined request"); + } +}; + +export const cancelGroupInviteRequest = async (data, isFromExtension) => { + const requiredFields = ["groupId", "qortalAddress"]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + const groupId = data.groupId + const qortalAddress = data?.qortalAddress + + let groupInfo = null; + try { + const url = await createEndpoint(`/groups/${groupId}`); + const response = await fetch(url); + if (!response.ok) throw new Error("Failed to fetch group"); + + groupInfo = await response.json(); + } catch (error) { + const errorMsg = (error && error.message) || "Group not found"; + throw new Error(errorMsg); + } + + const displayInvitee = await getNameInfoForOthers(qortalAddress) + + const fee = await getFee("CANCEL_GROUP_INVITE"); + const resPermission = await getUserPermission( + { + text1: `Do you give this application permission to cancel the group invite for ${displayInvitee || qortalAddress}?`, + highlightedText: `Group: ${groupInfo.groupName}`, + fee: fee.fee, + }, + isFromExtension + ); + const { accepted } = resPermission; + if (accepted) { + const response = await cancelInvitationToGroup({ + groupId, + qortalAddress, + }) + return response + + } else { + throw new Error("User declined request"); + } +}; + + +export const createGroupRequest = async (data, isFromExtension) => { + const requiredFields = ["groupId", "qortalAddress"]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + const groupName = data.groupName + const description = data?.description + const type = +data.type + const approvalThreshold = +data?.approvalThreshold + const minBlock = +data?.minBlock + const maxBlock = +data.maxBlock + + + const fee = await getFee("CREATE_GROUP"); + const resPermission = await getUserPermission( + { + text1: `Do you give this application permission to create a group?`, + highlightedText: `Group name: ${groupName}`, + fee: fee.fee, + }, + isFromExtension + ); + const { accepted } = resPermission; + if (accepted) { + const response = await createGroup({ + groupName, + groupDescription: description, + groupType: type, + groupApprovalThreshold: approvalThreshold, + minBlock, + maxBlock + }) + return response + + } else { + throw new Error("User declined request"); + } +}; + +export const decryptAESGCMRequest = async (data, isFromExtension) => { + const requiredFields = ["encryptedData", "iv", "senderPublicKey"]; + requiredFields.forEach((field) => { + if (!data[field]) { + throw new Error(`Missing required field: ${field}`); + } + }); + + const encryptedData = data.encryptedData; + const iv = data.iv; + const senderPublicKeyBase58 = data.senderPublicKey; + + + // Decode keys and IV + const senderPublicKey = Base58.decode(senderPublicKeyBase58); + const resKeyPair = await getKeyPair(); // Assume this retrieves the current user's keypair + const uint8PrivateKey = Base58.decode(resKeyPair.privateKey); + + // Convert ed25519 keys to Curve25519 + const convertedPrivateKey = ed2curve.convertSecretKey(uint8PrivateKey); + const convertedPublicKey = ed2curve.convertPublicKey(senderPublicKey); + + // Generate shared secret + const sharedSecret = new Uint8Array(32); + nacl.lowlevel.crypto_scalarmult(sharedSecret, convertedPrivateKey, convertedPublicKey); + + // Derive encryption key + const encryptionKey: Uint8Array = new Sha256().process(sharedSecret).finish().result; + + // Convert IV and ciphertext from Base64 + const base64ToUint8Array = (base64) => Uint8Array.from(atob(base64), c => c.charCodeAt(0)); + const ivUint8Array = base64ToUint8Array(iv); + const ciphertext = base64ToUint8Array(encryptedData); + // Validate IV and key lengths + if (ivUint8Array.length !== 12) { + throw new Error("Invalid IV: AES-GCM requires a 12-byte IV."); + } + if (encryptionKey.length !== 32) { + throw new Error("Invalid key: AES-GCM requires a 256-bit key."); + } + + try { + // Decrypt data + const algorithm = { name: "AES-GCM", iv: ivUint8Array }; + const cryptoKey = await crypto.subtle.importKey("raw", encryptionKey, algorithm, false, ["decrypt"]); + const decryptedArrayBuffer = await crypto.subtle.decrypt(algorithm, cryptoKey, ciphertext); + + // Return decrypted data as Base64 + return uint8ArrayToBase64(new Uint8Array(decryptedArrayBuffer)); + } catch (error) { + console.error("Decryption failed:", error); + throw new Error("Failed to decrypt the message. Ensure the data and keys are correct."); + } +}; diff --git a/src/transactions/RemoveRewardShareTransaction.ts b/src/transactions/RemoveRewardShareTransaction.ts new file mode 100644 index 0000000..375711a --- /dev/null +++ b/src/transactions/RemoveRewardShareTransaction.ts @@ -0,0 +1,46 @@ +// @ts-nocheck + +import { DYNAMIC_FEE_TIMESTAMP } from "../constants/constants" +import Base58 from "../deps/Base58" +import publicKeyToAddress from "../utils/generateWallet/publicKeyToAddress" +import TransactionBase from "./TransactionBase" + + +export default class RemoveRewardShareTransaction extends TransactionBase { + constructor() { + super() + this.type = 38 + } + + + set rewardShareKeyPairPublicKey(rewardShareKeyPairPublicKey) { + this._rewardShareKeyPairPublicKey = Base58.decode(rewardShareKeyPairPublicKey) + } + + set recipient(recipient) { + const _address = publicKeyToAddress(this._keyPair.publicKey) + this._recipient = recipient instanceof Uint8Array ? recipient : this.constructor.Base58.decode(recipient) + + if (new Date(this._timestamp).getTime() >= DYNAMIC_FEE_TIMESTAMP) { + this.fee = _address === recipient ? 0 : 0.01 + } else { + this.fee = _address === recipient ? 0 : 0.001 + } + } + + set percentageShare(share) { + this._percentageShare = share * 100 + this._percentageShareBytes = this.constructor.utils.int64ToBytes(this._percentageShare) + } + + get params() { + const params = super.params + params.push( + this._recipient, + this._rewardShareKeyPairPublicKey, + this._percentageShareBytes, + this._feeBytes + ) + return params + } +} diff --git a/src/transactions/RewardShareTransaction.ts b/src/transactions/RewardShareTransaction.ts new file mode 100644 index 0000000..8419432 --- /dev/null +++ b/src/transactions/RewardShareTransaction.ts @@ -0,0 +1,60 @@ +// @ts-nocheck + +import TransactionBase from './TransactionBase' + +import { Sha256 } from 'asmcrypto.js' +import nacl from '../deps/nacl-fast' +import ed2curve from '../deps/ed2curve' +import { DYNAMIC_FEE_TIMESTAMP } from '../constants/constants' +import publicKeyToAddress from '../utils/generateWallet/publicKeyToAddress' + +export default class RewardShareTransaction extends TransactionBase { + constructor() { + super() + this.type = 38 + } + + + + set recipientPublicKey(recipientPublicKey) { + this._base58RecipientPublicKey = recipientPublicKey instanceof Uint8Array ? this.constructor.Base58.encode(recipientPublicKey) : recipientPublicKey + this._recipientPublicKey = this.constructor.Base58.decode(this._base58RecipientPublicKey) + this.recipient = publicKeyToAddress(this._recipientPublicKey) + + const convertedPrivateKey = ed2curve.convertSecretKey(this._keyPair.privateKey) + const convertedPublicKey = ed2curve.convertPublicKey(this._recipientPublicKey) + const sharedSecret = new Uint8Array(32) + + nacl.lowlevel.crypto_scalarmult(sharedSecret, convertedPrivateKey, convertedPublicKey) + + this._rewardShareSeed = new Sha256().process(sharedSecret).finish().result + this._base58RewardShareSeed = this.constructor.Base58.encode(this._rewardShareSeed) + this._rewardShareKeyPair = nacl.sign.keyPair.fromSeed(this._rewardShareSeed) + + if (new Date(this._timestamp).getTime() >= DYNAMIC_FEE_TIMESTAMP) { + this.fee = (recipientPublicKey === this.constructor.Base58.encode(this._keyPair.publicKey) ? 0 : 0.01) + } else { + this.fee = (recipientPublicKey === this.constructor.Base58.encode(this._keyPair.publicKey) ? 0 : 0.001) + } + } + + set recipient(recipient) { + this._recipient = recipient instanceof Uint8Array ? recipient : this.constructor.Base58.decode(recipient) + } + + set percentageShare(share) { + this._percentageShare = share * 100 + this._percentageShareBytes = this.constructor.utils.int64ToBytes(this._percentageShare) + } + + get params() { + const params = super.params + params.push( + this._recipient, + this._rewardShareKeyPair.publicKey, + this._percentageShareBytes, + this._feeBytes + ) + return params + } +} diff --git a/src/transactions/TradeBotRespondRequest.ts b/src/transactions/TradeBotRespondRequest.ts new file mode 100644 index 0000000..fcd1af5 --- /dev/null +++ b/src/transactions/TradeBotRespondRequest.ts @@ -0,0 +1,41 @@ +// @ts-nocheck + +/** + * CrossChain - TradeBot Respond Request (Buy Action) + * + * These are special types of transactions (JSON ENCODED) + */ + +export default class TradeBotRespondRequest { + constructor() { + // ... + } + + createTransaction(txnReq) { + this.atAddress(txnReq.atAddress) + this.foreignKey(txnReq.foreignKey) + this.receivingAddress(txnReq.receivingAddress) + + return this.txnRequest() + } + + atAddress(atAddress) { + this._atAddress = atAddress + } + + foreignKey(foreignKey) { + this._foreignKey = foreignKey + } + + receivingAddress(receivingAddress) { + this._receivingAddress = receivingAddress + } + + txnRequest() { + return { + atAddress: this._atAddress, + foreignKey: this._foreignKey, + receivingAddress: this._receivingAddress + } + } +} diff --git a/src/transactions/UpdateNameTransaction.ts b/src/transactions/UpdateNameTransaction.ts new file mode 100644 index 0000000..e90b388 --- /dev/null +++ b/src/transactions/UpdateNameTransaction.ts @@ -0,0 +1,51 @@ +// @ts-nocheck + +import { QORT_DECIMALS } from "../constants/constants" +import TransactionBase from "./TransactionBase" + + +export default class UpdateNameTransaction extends TransactionBase { + constructor() { + super() + this.type = 4 + } + + + + 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 newName(newName) { + this.newNameText = newName + this._newNameBytes = this.constructor.utils.stringtoUTF8Array(newName) + this._newNameLength = this.constructor.utils.int32ToBytes(this._newNameBytes.length) + } + + set newData(newData) { + this.newDataText = newData.length === 0 ? "Registered Name on the Qortal Chain" : newData + this._newDataBytes = this.constructor.utils.stringtoUTF8Array(this.newDataText) + this._newDataLength = this.constructor.utils.int32ToBytes(this._newDataBytes.length) + } + + get params() { + const params = super.params + params.push( + this._nameLength, + this._nameBytes, + this._newNameLength, + this._newNameBytes, + this._newDataLength, + this._newDataBytes, + this._feeBytes + ) + return params + } +} diff --git a/src/transactions/transactions.ts b/src/transactions/transactions.ts index 400418c..62c5e6a 100644 --- a/src/transactions/transactions.ts +++ b/src/transactions/transactions.ts @@ -17,10 +17,14 @@ import RegisterNameTransaction from './RegisterNameTransaction.js' import VoteOnPollTransaction from './VoteOnPollTransaction.js' import CreatePollTransaction from './CreatePollTransaction.js' import DeployAtTransaction from './DeployAtTransaction.js' +import RewardShareTransaction from './RewardShareTransaction.js' +import RemoveRewardShareTransaction from './RemoveRewardShareTransaction.js' +import UpdateNameTransaction from './UpdateNameTransaction.js' export const transactionTypes = { 3: RegisterNameTransaction, + 4: UpdateNameTransaction, 2: PaymentTransaction, 8: CreatePollTransaction, 9: VoteOnPollTransaction, @@ -36,14 +40,14 @@ export const transactionTypes = { 29: GroupInviteTransaction, 30: CancelGroupInviteTransaction, 31: JoinGroupTransaction, - 32: LeaveGroupTransaction + 32: LeaveGroupTransaction, + 38: RewardShareTransaction, + 381: RemoveRewardShareTransaction } export const createTransaction = (type, keyPair, params) => { - const tx = new transactionTypes[type]() - tx.keyPair = keyPair Object.keys(params).forEach(param => { diff --git a/src/useQortalGetSaveSettings.tsx b/src/useQortalGetSaveSettings.tsx index 0223f12..532e049 100644 --- a/src/useQortalGetSaveSettings.tsx +++ b/src/useQortalGetSaveSettings.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect } from 'react' import { useRecoilState, useSetRecoilState } from 'recoil'; -import { canSaveSettingToQdnAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from './atoms/global'; +import { canSaveSettingToQdnAtom, isUsingImportExportSettingsAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from './atoms/global'; import { getArbitraryEndpointReact, getBaseApiReact } from './App'; import { decryptResource } from './components/Group/Group'; import { base64ToUint8Array, uint8ArrayToObject } from './backgroundFunctions/encryption'; @@ -40,7 +40,6 @@ const getPublishRecord = async (myName) => { ); data = await res.text(); - if(!data) throw new Error('Unable to fetch publish') const decryptedKey: any = await decryptResource(data); @@ -53,11 +52,14 @@ const getPublishRecord = async (myName) => { } }; -export const useQortalGetSaveSettings = (myName) => { + +export const useQortalGetSaveSettings = (myName, isAuthenticated) => { const setSortablePinnedApps = useSetRecoilState(sortablePinnedAppsAtom); const setCanSave = useSetRecoilState(canSaveSettingToQdnAtom); const setSettingsQDNLastUpdated = useSetRecoilState(settingsQDNLastUpdatedAtom); const [settingsLocalLastUpdated] = useRecoilState(settingsLocalLastUpdatedAtom); + const [isUsingImportExportSettings] = useRecoilState(isUsingImportExportSettingsAtom); + const [oldPinnedApps, setOldPinnedApps] = useRecoilState(oldPinnedAppsAtom) const getSavedSettings = useCallback(async (myName, settingsLocalLastUpdated)=> { @@ -67,7 +69,7 @@ export const useQortalGetSaveSettings = (myName) => { const settings = await getPublish(myName) if(settings?.sortablePinnedApps && timestamp > settingsLocalLastUpdated){ setSortablePinnedApps(settings.sortablePinnedApps) - + setSettingsQDNLastUpdated(timestamp || 0) } else if(settings?.sortablePinnedApps){ setSettingsQDNLastUpdated(timestamp || 0) @@ -87,8 +89,9 @@ export const useQortalGetSaveSettings = (myName) => { } }, []) useEffect(()=> { - if(!myName || !settingsLocalLastUpdated) return + if(!myName || !settingsLocalLastUpdated || !isAuthenticated || isUsingImportExportSettings === null) return + if(isUsingImportExportSettings) return getSavedSettings(myName, settingsLocalLastUpdated) - }, [getSavedSettings, myName, settingsLocalLastUpdated]) + }, [getSavedSettings, myName, settingsLocalLastUpdated, isAuthenticated, isUsingImportExportSettings]) } diff --git a/src/useRetrieveDataLocalStorage.tsx b/src/useRetrieveDataLocalStorage.tsx index 88e93f4..2a248bd 100644 --- a/src/useRetrieveDataLocalStorage.tsx +++ b/src/useRetrieveDataLocalStorage.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect } from 'react' import { useSetRecoilState } from 'recoil'; -import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from './atoms/global'; +import { isUsingImportExportSettingsAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from './atoms/global'; function fetchFromLocalStorage(key) { try { @@ -18,17 +18,38 @@ function fetchFromLocalStorage(key) { export const useRetrieveDataLocalStorage = () => { const setSortablePinnedApps = useSetRecoilState(sortablePinnedAppsAtom); const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom); - + const setIsUsingImportExportSettings = useSetRecoilState(isUsingImportExportSettingsAtom) + const setSettingsQDNLastUpdated = useSetRecoilState(settingsQDNLastUpdatedAtom); + const setOldPinnedApps = useSetRecoilState(oldPinnedAppsAtom) + const getSortablePinnedApps = useCallback(()=> { const pinnedAppsLocal = fetchFromLocalStorage('ext_saved_settings') if(pinnedAppsLocal?.sortablePinnedApps){ setSortablePinnedApps(pinnedAppsLocal?.sortablePinnedApps) + setSettingsLocalLastUpdated(pinnedAppsLocal?.timestamp || -1) + } else { + setSettingsLocalLastUpdated(-1) } - setSettingsLocalLastUpdated(pinnedAppsLocal?.timestamp || -1) + + }, []) + const getSortablePinnedAppsImportExport = useCallback(()=> { + const pinnedAppsLocal = fetchFromLocalStorage('ext_saved_settings_import_export') + if(pinnedAppsLocal?.sortablePinnedApps){ + setOldPinnedApps(pinnedAppsLocal?.sortablePinnedApps) + + + setIsUsingImportExportSettings(true) + setSettingsQDNLastUpdated(pinnedAppsLocal?.timestamp || 0) + + } else { + setIsUsingImportExportSettings(false) + } + }, []) useEffect(()=> { getSortablePinnedApps() + getSortablePinnedAppsImportExport() }, [getSortablePinnedApps]) } diff --git a/src/utils/fileReading/index.ts b/src/utils/fileReading/index.ts index a72f785..a6295c8 100644 --- a/src/utils/fileReading/index.ts +++ b/src/utils/fileReading/index.ts @@ -29,32 +29,28 @@ let semaphore = new Semaphore(1) let reader = new FileReader() export const fileToBase64 = (file) => new Promise(async (resolve, reject) => { - if (!reader) { - reader = new FileReader() - } - await semaphore.acquire() - reader.readAsDataURL(file) + const reader = new FileReader(); // Create a new instance + await semaphore.acquire(); + reader.readAsDataURL(file); reader.onload = () => { - const dataUrl = reader.result - if (typeof dataUrl === "string") { - const base64String = dataUrl.split(',')[1] - reader.onload = null - reader.onerror = null - resolve(base64String) - } else { - reader.onload = null - reader.onerror = null - reject(new Error('Invalid data URL')) - } - semaphore.release() - } + const dataUrl = reader.result; + semaphore.release(); + if (typeof dataUrl === 'string') { + resolve(dataUrl.split(',')[1]); + } else { + reject(new Error('Invalid data URL')); + } + reader.onload = null; // Clear the handler + reader.onerror = null; // Clear the handle + }; reader.onerror = (error) => { - reader.onload = null - reader.onerror = null - reject(error) - semaphore.release() - } -}) + semaphore.release(); + reject(error); + reader.onload = null; // Clear the handler + reader.onerror = null; // Clear the handle + }; + }); + export const base64ToBlobUrl = (base64, mimeType = "image/png") => { const binary = atob(base64);