Merge branch 'Qortal:feature/initial-conversion' into feature/initial-conversion
@ -11,7 +11,7 @@ module.exports = {
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
'off',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
|
1
.gitignore
vendored
@ -23,3 +23,4 @@ dist-ssr
|
||||
*.sln
|
||||
*.sw?
|
||||
release-builds/
|
||||
.env
|
35
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
|
||||
|
@ -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": {
|
||||
|
BIN
electron/assets/mac/appIcon.icns
Normal file
BIN
electron/assets/png/1024x1024.png
Normal file
After Width: | Height: | Size: 455 KiB |
BIN
electron/assets/png/128x128.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
electron/assets/png/16x16.png
Normal file
After Width: | Height: | Size: 668 B |
BIN
electron/assets/png/24x24.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
electron/assets/png/256x256.png
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
electron/assets/png/32x32.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
electron/assets/png/48x48.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
electron/assets/png/512x512.png
Normal file
After Width: | Height: | Size: 158 KiB |
BIN
electron/assets/png/64x64.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
BIN
electron/assets/png/96x96.png
Normal file
After Width: | Height: | Size: 11 KiB |
8
electron/buildmac/entitlements.mac.plist
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
BIN
electron/buildmac/logo-hub.png
Normal file
After Width: | Height: | Size: 11 KiB |
@ -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": {
|
||||
|
47
electron/electron-builder.config.arm.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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"
|
||||
}
|
||||
|
58
electron/electron-builder.config.lin.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
81
electron/electron-builder.config.mac.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
44
electron/electron-builder.config.win.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
2259
electron/package-lock.json
generated
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
29
electron/scripts/add-debian-apt-repo.sh
Executable file
@ -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
|
39
electron/scripts/afterPack.js
Normal file
@ -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
|
21
electron/scripts/notarize.js
Normal file
@ -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
|
||||
})
|
||||
}
|
34
electron/scripts/uninstall-debian-conf.sh
Normal file
@ -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'
|
@ -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
|
||||
|
@ -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) => {
|
||||
|
107
package-lock.json
generated
@ -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",
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -136,6 +136,47 @@ border-radius: 5px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
interface CustomButtonProps {
|
||||
bgColor?: string;
|
||||
color?: string;
|
||||
}
|
||||
export const CustomButtonAccept = styled(Box)<CustomButtonProps>(
|
||||
({ 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)({
|
||||
|
1193
src/App.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) => (
|
||||
<Tooltip {...props} classes={{ popper: className }} />
|
||||
))(({ 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<boolean | null>(null);
|
||||
const [hasLocalNode, setHasLocalNode] = useState<boolean | null>(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",
|
||||
}}
|
||||
>
|
||||
<img src={Logo1} className="base-image" />
|
||||
<img src={Logo1Dark} className="hover-image" />
|
||||
<img src={Logo1Dark} className="base-image" />
|
||||
</div>
|
||||
<Spacer height="30px" />
|
||||
<TextP
|
||||
sx={{
|
||||
textAlign: "center",
|
||||
lineHeight: "15px",
|
||||
lineHeight: 1.2,
|
||||
fontSize: '18px'
|
||||
}}
|
||||
>
|
||||
WELCOME TO <TextItalic>YOUR</TextItalic> <br></br>
|
||||
<TextSpan> QORTAL WALLET</TextSpan>
|
||||
WELCOME TO <TextItalic sx={{
|
||||
fontSize: '18px'
|
||||
}}>YOUR</TextItalic> <br></br>
|
||||
<TextSpan sx={{
|
||||
fontSize: '18px'
|
||||
}}> QORTAL WALLET</TextSpan>
|
||||
</TextP>
|
||||
|
||||
<Spacer height="30px" />
|
||||
<Box
|
||||
sx={{
|
||||
@ -271,11 +422,23 @@ export const NotAuthenticated = ({
|
||||
gap: "10px",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<HtmlTooltip
|
||||
disableHoverListener={hasSeenGettingStarted === true}
|
||||
placement="left"
|
||||
title={
|
||||
<React.Fragment>
|
||||
<Typography color="inherit" sx={{
|
||||
fontSize: '16px'
|
||||
}}>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.</Typography>
|
||||
</React.Fragment>
|
||||
}
|
||||
>
|
||||
<CustomButton onClick={()=> setExtstate('wallets')}>
|
||||
{/* <input {...getInputProps()} /> */}
|
||||
Wallets
|
||||
</CustomButton>
|
||||
</HtmlTooltip>
|
||||
{/* <Tooltip title="Authenticate by importing your Qortal JSON file" arrow>
|
||||
<img src={Info} />
|
||||
</Tooltip> */}
|
||||
@ -287,16 +450,41 @@ export const NotAuthenticated = ({
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
alignItems: "center",
|
||||
|
||||
}}
|
||||
>
|
||||
<HtmlTooltip
|
||||
disableHoverListener={hasSeenGettingStarted === true}
|
||||
placement="right"
|
||||
title={
|
||||
<React.Fragment>
|
||||
<Typography color="inherit" sx={{
|
||||
fontWeight: 'bold',
|
||||
fontSize: '18px'
|
||||
}}>New users start here!</Typography>
|
||||
<Spacer height='10px'/>
|
||||
<Typography color="inherit" sx={{
|
||||
fontSize: '16px'
|
||||
}}>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.</Typography>
|
||||
</React.Fragment>
|
||||
}
|
||||
>
|
||||
<CustomButton
|
||||
onClick={() => {
|
||||
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
|
||||
</CustomButton>
|
||||
|
||||
</HtmlTooltip>
|
||||
|
||||
</Box>
|
||||
<Spacer height="15px" />
|
||||
@ -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',
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<Typography sx={{
|
||||
textDecoration: 'underline'
|
||||
}}>For advanced users</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
@ -330,6 +524,12 @@ export const NotAuthenticated = ({
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
sx={{
|
||||
"& .MuiFormControlLabel-label": {
|
||||
fontSize: '14px'
|
||||
}
|
||||
|
||||
}}
|
||||
control={
|
||||
<Switch
|
||||
sx={{
|
||||
@ -575,7 +775,7 @@ export const NotAuthenticated = ({
|
||||
...(customNodes || []),
|
||||
].filter((item) => item?.url !== node?.url);
|
||||
|
||||
saveCustomNodes(nodesToSave);
|
||||
saveCustomNodes(nodesToSave, true);
|
||||
}}
|
||||
variant="contained"
|
||||
>
|
||||
@ -648,7 +848,7 @@ export const NotAuthenticated = ({
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={!customApikey || !url}
|
||||
disabled={!url}
|
||||
onClick={() => saveCustomNodes(customNodes)}
|
||||
autoFocus
|
||||
>
|
||||
@ -659,6 +859,17 @@ export const NotAuthenticated = ({
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)}
|
||||
<ButtonBase onClick={()=> {
|
||||
showTutorial('create-account', true)
|
||||
}} sx={{
|
||||
position: 'fixed',
|
||||
bottom: '25px',
|
||||
right: '25px'
|
||||
}}>
|
||||
<HelpIcon sx={{
|
||||
color: 'var(--unread)'
|
||||
}} />
|
||||
</ButtonBase>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -92,21 +92,6 @@ export const MessageQueueProvider = ({ children }) => {
|
||||
|
||||
// Remove the message from the queue after successful sending
|
||||
messageQueueRef.current.shift();
|
||||
// Remove the message from queueChats
|
||||
setQueueChats((prev) => {
|
||||
const updatedChats = { ...prev };
|
||||
if (updatedChats[groupDirectId]) {
|
||||
updatedChats[groupDirectId] = updatedChats[groupDirectId].filter(
|
||||
(item) => item.identifier !== identifier
|
||||
);
|
||||
|
||||
// If no more chats for this group, delete the groupDirectId entry
|
||||
if (updatedChats[groupDirectId].length === 0) {
|
||||
delete updatedChats[groupDirectId];
|
||||
}
|
||||
}
|
||||
return updatedChats;
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Message sending failed', error);
|
||||
@ -142,15 +127,25 @@ export const MessageQueueProvider = ({ children }) => {
|
||||
|
||||
// Method to process with new messages and groupDirectId
|
||||
const processWithNewMessages = (newMessages, groupDirectId) => {
|
||||
let updatedNewMessages = newMessages
|
||||
if (newMessages.length > 0) {
|
||||
messageQueueRef.current = messageQueueRef.current.filter((msg) => {
|
||||
return !newMessages.some(newMsg => newMsg?.specialId === msg?.specialId);
|
||||
});
|
||||
|
||||
// Remove corresponding entries in queueChats for the provided groupDirectId
|
||||
setQueueChats((prev) => {
|
||||
const updatedChats = { ...prev };
|
||||
if (updatedChats[groupDirectId]) {
|
||||
|
||||
updatedNewMessages = newMessages?.map((msg)=> {
|
||||
const findTempMsg = updatedChats[groupDirectId]?.find((msg2)=> msg2?.message?.specialId === msg?.specialId)
|
||||
if(findTempMsg){
|
||||
return {
|
||||
...msg,
|
||||
tempSignature: findTempMsg?.signature
|
||||
}
|
||||
}
|
||||
return msg
|
||||
})
|
||||
|
||||
|
||||
updatedChats[groupDirectId] = updatedChats[groupDirectId].filter((chat) => {
|
||||
return !newMessages.some(newMsg => newMsg?.specialId === chat?.message?.specialId);
|
||||
});
|
||||
@ -167,8 +162,23 @@ export const MessageQueueProvider = ({ children }) => {
|
||||
}
|
||||
return updatedChats;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if(!messageQueueRef.current.find((msg) => msg?.groupDirectId === groupDirectId)){
|
||||
setQueueChats((prev) => {
|
||||
const updatedChats = { ...prev };
|
||||
if (updatedChats[groupDirectId]) {
|
||||
delete updatedChats[groupDirectId]
|
||||
}
|
||||
|
||||
return updatedChats
|
||||
}
|
||||
)
|
||||
}
|
||||
}, 300);
|
||||
|
||||
return updatedNewMessages
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { useContext, useEffect, useRef, useState } from "react";
|
||||
import List from "@mui/material/List";
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
import Divider from "@mui/material/Divider";
|
||||
@ -19,6 +19,8 @@ import { decryptStoredWalletFromSeedPhrase } from "./utils/decryptWallet";
|
||||
import { crypto } from "./constants/decryptWallet";
|
||||
import { LoadingButton } from "@mui/lab";
|
||||
import { PasswordField } from "./components";
|
||||
import { HtmlTooltip } from "./ExtStates/NotAuthenticated";
|
||||
import { GlobalContext } from "./App";
|
||||
|
||||
const parsefilenameQortal = (filename)=> {
|
||||
return filename.startsWith("qortal_backup_") ? filename.slice(14) : filename;
|
||||
@ -30,6 +32,7 @@ export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => {
|
||||
const [seedValue, setSeedValue] = useState("");
|
||||
const [seedName, setSeedName] = useState("");
|
||||
const [seedError, setSeedError] = useState("");
|
||||
const { hasSeenGettingStarted } = useContext(GlobalContext);
|
||||
|
||||
const [password, setPassword] = useState("");
|
||||
const [isOpenSeedModal, setIsOpenSeedModal] = useState(false);
|
||||
@ -197,9 +200,11 @@ export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => {
|
||||
sx={{
|
||||
width: "100%",
|
||||
maxWidth: "500px",
|
||||
bgcolor: "background.paper",
|
||||
maxHeight: "60vh",
|
||||
overflow: "auto",
|
||||
overflowY: "auto",
|
||||
overflowX: "hidden",
|
||||
backgroundColor: "rgb(30 30 32 / 70%)",
|
||||
|
||||
}}
|
||||
>
|
||||
{wallets?.map((wallet, idx) => {
|
||||
@ -228,6 +233,17 @@ export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => {
|
||||
bottom: wallets?.length === 0 ? 'unset' : '20px',
|
||||
right: wallets?.length === 0 ? 'unset' : '20px'
|
||||
}}
|
||||
>
|
||||
<HtmlTooltip
|
||||
disableHoverListener={hasSeenGettingStarted === true}
|
||||
|
||||
title={
|
||||
<React.Fragment>
|
||||
<Typography color="inherit" sx={{
|
||||
fontSize: '16px'
|
||||
}}>Already have a Qortal account? Enter your secret backup phrase here to access it. This phrase is one of the ways to recover your account.</Typography>
|
||||
</React.Fragment>
|
||||
}
|
||||
>
|
||||
<CustomButton onClick={handleSetSeedValue} sx={{
|
||||
padding: '10px'
|
||||
@ -235,12 +251,25 @@ export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => {
|
||||
|
||||
Add seed-phrase
|
||||
</CustomButton>
|
||||
</HtmlTooltip>
|
||||
<HtmlTooltip
|
||||
disableHoverListener={hasSeenGettingStarted === true}
|
||||
|
||||
title={
|
||||
<React.Fragment>
|
||||
<Typography color="inherit" sx={{
|
||||
fontSize: '16px'
|
||||
}}>Use this option to connect additional Qortal wallets you've already made, in order to login with them afterwards. You will need access to your backup JSON file in order to do so.</Typography>
|
||||
</React.Fragment>
|
||||
}
|
||||
>
|
||||
<CustomButton sx={{
|
||||
padding: '10px'
|
||||
}} {...getRootProps()}>
|
||||
<input {...getInputProps()} />
|
||||
Add wallets
|
||||
</CustomButton>
|
||||
</HtmlTooltip>
|
||||
</Box>
|
||||
|
||||
<Dialog
|
||||
@ -344,33 +373,26 @@ const WalletItem = ({ wallet, updateWalletItem, idx, setSelectedWallet }) => {
|
||||
setSelectedWallet(wallet);
|
||||
}}
|
||||
sx={{
|
||||
width: '100%'
|
||||
width: '100%',
|
||||
padding: '10px'
|
||||
}}
|
||||
|
||||
>
|
||||
<ListItem
|
||||
|
||||
secondaryAction={
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsEdit(true);
|
||||
}}
|
||||
edge="end"
|
||||
aria-label="edit"
|
||||
>
|
||||
<EditIcon
|
||||
sx={{
|
||||
color: "white",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
}
|
||||
sx={{
|
||||
bgcolor: "background.paper",
|
||||
flexGrow: 1,
|
||||
"&:hover": { backgroundColor: "secondary.main", transform: "scale(1.01)" },
|
||||
transition: "all 0.1s ease-in-out",
|
||||
}}
|
||||
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<ListItemAvatar>
|
||||
<Avatar alt="" src="/static/images/avatar/1.jpg" />
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
|
||||
primary={wallet?.name ? wallet.name : wallet?.filename ? parsefilenameQortal(wallet?.filename) : "No name"}
|
||||
secondary={
|
||||
<Box
|
||||
@ -387,10 +409,31 @@ const WalletItem = ({ wallet, updateWalletItem, idx, setSelectedWallet }) => {
|
||||
{wallet?.address0}
|
||||
</Typography>
|
||||
{wallet?.note}
|
||||
<Typography sx={{
|
||||
textAlign: 'end',
|
||||
marginTop: '5px'
|
||||
}}>Login</Typography>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<IconButton
|
||||
sx={{
|
||||
alignSelf: 'flex-start'
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsEdit(true);
|
||||
}}
|
||||
edge="end"
|
||||
aria-label="edit"
|
||||
>
|
||||
<EditIcon
|
||||
sx={{
|
||||
color: "white",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</ButtonBase>
|
||||
{isEdit && (
|
||||
<Box
|
||||
@ -434,12 +477,12 @@ const WalletItem = ({ wallet, updateWalletItem, idx, setSelectedWallet }) => {
|
||||
</Button>
|
||||
<Button
|
||||
sx={{
|
||||
backgroundColor: 'var(--unread)',
|
||||
backgroundColor: 'var(--danger)',
|
||||
"&:hover": {
|
||||
backgroundColor: "var(--unread)",
|
||||
backgroundColor: "var(--danger)",
|
||||
},
|
||||
"&:focus": {
|
||||
backgroundColor: "var(--unread)",
|
||||
backgroundColor: "var(--danger)",
|
||||
},
|
||||
}}
|
||||
size="small"
|
||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
export const WalletIcon= ({ color, height, width }) => {
|
||||
return (
|
||||
<svg width="31" height="31" viewBox="0 0 31 31" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg width={width || 30} height={width || 30} viewBox="0 0 31 31" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.0118 22.0891C18.0124 22.8671 16.6997 23.3391 15.2618 23.3391C13.8241 23.3391 12.5113 22.8671 11.5118 22.0891" stroke={color} stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M3.20108 17.356C2.7598 14.4844 2.53917 13.0486 3.08205 11.7758C3.62493 10.503 4.82938 9.63215 7.23827 7.89044L9.03808 6.58911C12.0347 4.42245 13.5331 3.33911 15.2618 3.33911C16.9907 3.33911 18.4889 4.42245 21.4856 6.58911L23.2854 7.89044C25.6943 9.63215 26.8988 10.503 27.4417 11.7758C27.9846 13.0486 27.7639 14.4844 27.3226 17.356L26.9463 19.8046C26.3208 23.8752 26.0079 25.9106 24.5481 27.1249C23.0882 28.3391 20.9539 28.3391 16.6853 28.3391H13.8383C9.56977 28.3391 7.43548 28.3391 5.97559 27.1249C4.5157 25.9106 4.20293 23.8752 3.57738 19.8046L3.20108 17.356Z" stroke={color} stroke-width="2" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
BIN
src/assets/Icons/q-trade-logo.webp
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
src/assets/QMailLogo.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
src/assets/badges/level-0.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
src/assets/badges/level-1.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
src/assets/badges/level-10.png
Normal file
After Width: | Height: | Size: 6.3 KiB |
BIN
src/assets/badges/level-2.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
src/assets/badges/level-3.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
src/assets/badges/level-4.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
src/assets/badges/level-5.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
BIN
src/assets/badges/level-6.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
BIN
src/assets/badges/level-7.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
src/assets/badges/level-8.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
BIN
src/assets/badges/level-9.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
1
src/assets/svgs/swagger.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="256" preserveAspectRatio="xMidYMid" viewBox="0 0 256 256" width="256" xmlns="http://www.w3.org/2000/svg"><path d="m127.059657 255.996921c-68.2090026-.470449-127.51673062-57.078479-127.05700194-128.998618.44199434-69.2024402 57.94900474-127.46727058 129.10736494-126.99545745 69.157108.45954053 127.503089 57.86392555 126.885116 128.19135345.572955 69.689254-58.060868 128.29499-128.935479 127.802722zm0 0c-68.2090026-.470449-127.51673062-57.078479-127.05700194-128.998618.44199434-69.2024402 57.94900474-127.46727058 129.10736494-126.99545745 69.157108.45954053 127.503089 57.86392555 126.885116 128.19135345.572955 69.689254-58.060868 128.29499-128.935479 127.802722z" fill="#fff"/><path d="m127.184644 238.997327c-59.1522675-.408056-110.5810349-49.498583-110.1823412-111.865899.3837257-60.0128327 50.2530972-110.5397174 111.9608142-110.1289408 59.971427.3985349 110.568788 50.1800369 110.032661 111.1667638.496666 60.43313-50.348348 111.255175-111.811134 110.828076zm0 0c-59.1522675-.408056-110.5810349-49.498583-110.1823412-111.865899.3837257-60.0128327 50.2530972-110.5397174 111.9608142-110.1289408 59.971427.3985349 110.568788 50.1800369 110.032661 111.1667638.496666 60.43313-50.348348 111.255175-111.811134 110.828076z" fill="#49a32b"/><path d="m169.327319 127.956161c-.284596 5.290212-4.906213 9.683063-9.461106 8.916425-.021787 0-.044936 0-.068085 0-5.045107.006809-9.139745-4.078298-9.145192-9.123404.171575-5.058724 4.366979-9.045787 9.427064-8.96 5.045106.02451 9.51966 4.288 9.247319 9.166979zm-81.1261275 51.264c1.9022979.055829 3.8059574.014978 5.9996596.014978v13.785873c-13.6347234 2.305361-24.8660426-1.565958-27.6221277-13.091405-.9436596-4.237617-1.5237447-8.548766-1.7361702-12.885787-.292766-4.591659.2137872-9.235064-.1361702-13.818553-.9695319-12.612085-2.6035745-16.917787-14.706383-17.514213v-15.69634c.8674043-.202894 1.7470638-.352681 2.6321702-.452085 6.6355745-.326809 9.4325107-2.361192 10.916766-8.897362.6754042-3.672511 1.0757447-7.389958 1.1942127-11.1223831.5256171-7.2170212.3390639-14.5511489 1.5414468-21.6510638 1.737532-10.267234 8.1116596-15.2551489 18.6403405-15.8134468 2.9957447-.1606808 6.0010212-.0245106 9.3957447-.0245106v14.0908936c-1.3971064.0994042-2.6771064.3022979-3.9489362.2641702-8.5800851-.2628085-9.024 2.6594043-9.650383 9.7620426-.3908085 4.4541276.1484255 8.9845106-.155234 13.453617-.3172766 4.4473189-.9123405 8.8714889-1.7811064 13.2452769-1.2377873 6.338723-5.1349787 11.052936-10.5354894 15.053617 10.4837447 6.822127 11.6765958 17.422978 12.3574468 28.187234.3662979 5.78451.1988085 11.609872.7857022 17.365787.4575319 4.467745 2.1950638 5.607489 6.8085106 5.74366zm8.8360851-60.430979h.1620425c5.0124259.083064 9.0103829 4.213106 8.9273189 9.226893 0 .164766-.005447.328171-.014978.491575-.281873 4.899404-4.481362 8.641362-9.3807664 8.359489-.1974468.004085-.3935319 0-.5909787-.009532-4.9892766-.247829-8.8333617-4.493617-8.5855319-9.482893.2478298-4.989277 4.493617-8.833362 9.4828936-8.585532zm31.2360854 0c5.482212-.042213 9.123404 3.510468 9.152 8.930042.029957 5.565277-3.421958 9.126128-8.868766 9.149277-5.539405.024511-9.186043-3.479149-9.216-8.866043-.016341-.275063-.020426-.550127-.012256-.825191.153873-4.786383 4.158639-8.541958 8.945022-8.388085zm65.399829-6.865702c1.458383 5.446808 4.297532 7.361361 10.03166 7.622808.939575.043575 1.875064.202894 3.163234.345873v15.692255c-.697191.228766-1.412085.40034-2.137872.512-7.684085.477957-11.186383 3.630298-11.962553 11.334808-.49566 4.918468-.454809 9.891405-.795234 14.827575-.142979 5.419574-.635915 10.82417-1.476086 16.179745-1.960851 9.703489-8.019063 14.54434-18.028936 15.135319-3.221787.190638-6.466723.029957-9.940425.029957v-14.025532c1.869617-.115744 3.52-.275064 5.174468-.314553 5.980596-.142979 8.095319-2.071149 8.388085-8.010894.324085-6.525276.465702-13.058723.757106-19.585361.42349-9.433873 3.006639-17.861447 11.795064-23.745362-5.028766-3.585362-9.066213-7.92783-10.112-13.783149-1.265021-7.097191-1.673532-14.3509787-2.354383-21.5475744-.33634-3.597617-.32-7.2265532-.671319-10.8214468-.378553-3.8808511-3.044766-5.2234894-6.577021-5.3106383-2.02349-.0490213-4.055149-.0095319-6.642383-.0095319v-13.696c16.509276-2.7411064 27.913532 2.752 28.972936 18.5477446.443915 6.6328511.378553 13.2970213.803404 19.9298728.186553 3.60851.725787 7.189787 1.612255 10.692085z" fill="#fff"/></svg>
|
After Width: | Height: | Size: 4.3 KiB |
@ -40,6 +40,14 @@ export const sortablePinnedAppsAtom = atom({
|
||||
{
|
||||
name: 'Q-Manager',
|
||||
service: 'APP'
|
||||
},
|
||||
{
|
||||
name: 'Q-Blog',
|
||||
service: 'APP'
|
||||
},
|
||||
{
|
||||
name: 'Q-Mintership',
|
||||
service: 'APP'
|
||||
}
|
||||
],
|
||||
});
|
||||
@ -63,6 +71,11 @@ export const oldPinnedAppsAtom = atom({
|
||||
key: 'oldPinnedAppsAtom',
|
||||
default: [],
|
||||
});
|
||||
export const isUsingImportExportSettingsAtom = atom({
|
||||
key: 'isUsingImportExportSettingsAtom',
|
||||
default: null,
|
||||
});
|
||||
|
||||
|
||||
export const fullScreenAtom = atom({
|
||||
key: 'fullScreenAtom',
|
||||
@ -128,4 +141,32 @@ export const blobKeySelector = selectorFamily({
|
||||
export const selectedGroupIdAtom = atom({
|
||||
key: 'selectedGroupIdAtom',
|
||||
default: null,
|
||||
});
|
||||
|
||||
export const addressInfoControllerAtom = atom({
|
||||
key: 'addressInfoControllerAtom',
|
||||
default: {},
|
||||
});
|
||||
|
||||
export const addressInfoKeySelector = selectorFamily({
|
||||
key: 'addressInfoKeySelector',
|
||||
get: (key) => ({ get }) => {
|
||||
const userInfo = get(addressInfoControllerAtom);
|
||||
return userInfo[key] || null; // Return the value for the key or null if not found
|
||||
},
|
||||
});
|
||||
|
||||
export const isDisabledEditorEnterAtom = atom({
|
||||
key: 'isDisabledEditorEnterAtom',
|
||||
default: false,
|
||||
});
|
||||
|
||||
export const qMailLastEnteredTimestampAtom = atom({
|
||||
key: 'qMailLastEnteredTimestampAtom',
|
||||
default: null,
|
||||
});
|
||||
|
||||
export const mailsAtom = atom({
|
||||
key: 'mailsAtom',
|
||||
default: [],
|
||||
});
|
@ -12,6 +12,7 @@ import {
|
||||
checkNewMessages,
|
||||
checkThreads,
|
||||
clearAllNotifications,
|
||||
createEndpoint,
|
||||
createGroup,
|
||||
decryptDirectFunc,
|
||||
decryptSingleForPublishes,
|
||||
@ -26,6 +27,7 @@ import {
|
||||
getGroupDataSingle,
|
||||
getKeyPair,
|
||||
getLTCBalance,
|
||||
getLastRef,
|
||||
getNameInfo,
|
||||
getTempPublish,
|
||||
getTimestampEnterChat,
|
||||
@ -41,6 +43,7 @@ import {
|
||||
makeAdmin,
|
||||
notifyAdminRegenerateSecretKey,
|
||||
pauseAllQueues,
|
||||
processTransactionVersion2,
|
||||
registerName,
|
||||
removeAdmin,
|
||||
resumeAllQueues,
|
||||
@ -56,8 +59,10 @@ import {
|
||||
} from "./background";
|
||||
import { decryptGroupEncryption, encryptAndPublishSymmetricKeyGroupChat, encryptAndPublishSymmetricKeyGroupChatForAdmins, publishGroupEncryptedResource, publishOnQDN } from "./backgroundFunctions/encryption";
|
||||
import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from "./constants/codes";
|
||||
import Base58 from "./deps/Base58";
|
||||
import { encryptSingle } from "./qdn/encryption/group-encryption";
|
||||
import { _createPoll, _voteOnPoll } from "./qortalRequests/get";
|
||||
import { createTransaction } from "./transactions/transactions";
|
||||
import { getData, storeData } from "./utils/chromeStorage";
|
||||
|
||||
export function versionCase(request, event) {
|
||||
@ -232,7 +237,7 @@ export async function userInfoCase(request, event) {
|
||||
export async function decryptWalletCase(request, event) {
|
||||
try {
|
||||
const { password, wallet } = request.payload;
|
||||
const response = await decryptWallet({password, wallet, walletVersion});
|
||||
const response = await decryptWallet({password, wallet, walletVersion: wallet?.version || walletVersion});
|
||||
event.source.postMessage(
|
||||
{
|
||||
requestId: request.requestId,
|
||||
@ -1280,6 +1285,85 @@ export async function getTimestampEnterChatCase(request, event) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function listActionsCase(request, event) {
|
||||
try {
|
||||
const { type, listName = '', items = [] } = request.payload;
|
||||
let responseData
|
||||
|
||||
if(type === 'get'){
|
||||
const url = await createEndpoint(`/lists/${listName}`);
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error("Failed to fetch");
|
||||
|
||||
responseData = await response.json();
|
||||
} else if(type === 'remove'){
|
||||
const url = await createEndpoint(`/lists/${listName}`);
|
||||
const body = {
|
||||
items: items ,
|
||||
};
|
||||
const bodyToString = JSON.stringify(body);
|
||||
const response = await fetch(url, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: bodyToString,
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Failed to remove from list");
|
||||
let res;
|
||||
try {
|
||||
res = await response.clone().json();
|
||||
} catch (e) {
|
||||
res = await response.text();
|
||||
}
|
||||
responseData = res;
|
||||
} else if(type === 'add'){
|
||||
const url = await createEndpoint(`/lists/${listName}`);
|
||||
const body = {
|
||||
items: items ,
|
||||
};
|
||||
const bodyToString = JSON.stringify(body);
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: bodyToString,
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Failed to add to list");
|
||||
let res;
|
||||
try {
|
||||
res = await response.clone().json();
|
||||
} catch (e) {
|
||||
res = await response.text();
|
||||
}
|
||||
responseData = res;
|
||||
}
|
||||
|
||||
event.source.postMessage(
|
||||
{
|
||||
requestId: request.requestId,
|
||||
action: "listActions",
|
||||
payload: responseData,
|
||||
type: "backgroundMessageResponse",
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
} catch (error) {
|
||||
event.source.postMessage(
|
||||
{
|
||||
requestId: request.requestId,
|
||||
action: "listActions",
|
||||
error: error?.message,
|
||||
type: "backgroundMessageResponse",
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTimestampMentionCase(request, event) {
|
||||
try {
|
||||
const response = await getTimestampMention();
|
||||
@ -1895,4 +1979,140 @@ export async function publishGroupEncryptedResourceCase(request, event) {
|
||||
event.origin
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function createRewardShareCase(request, event) {
|
||||
try {
|
||||
const {recipientPublicKey} = request.payload;
|
||||
const resKeyPair = await getKeyPair();
|
||||
const parsedData = resKeyPair;
|
||||
const uint8PrivateKey = Base58.decode(parsedData.privateKey);
|
||||
const uint8PublicKey = Base58.decode(parsedData.publicKey);
|
||||
const keyPair = {
|
||||
privateKey: uint8PrivateKey,
|
||||
publicKey: uint8PublicKey,
|
||||
};
|
||||
let lastRef = await getLastRef();
|
||||
|
||||
const tx = await createTransaction(38, keyPair, {
|
||||
recipientPublicKey,
|
||||
percentageShare: 0,
|
||||
lastReference: lastRef,
|
||||
});
|
||||
|
||||
const signedBytes = Base58.encode(tx.signedBytes);
|
||||
|
||||
const res = await processTransactionVersion2(signedBytes);
|
||||
if (!res?.signature)
|
||||
throw new Error("Transaction was not able to be processed");
|
||||
event.source.postMessage(
|
||||
{
|
||||
requestId: request.requestId,
|
||||
action: "createRewardShare",
|
||||
payload: res,
|
||||
type: "backgroundMessageResponse",
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
} catch (error) {
|
||||
event.source.postMessage(
|
||||
{
|
||||
requestId: request.requestId,
|
||||
action: "createRewardShare",
|
||||
error: error?.message,
|
||||
type: "backgroundMessageResponse",
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeRewardShareCase(request, event) {
|
||||
try {
|
||||
const {rewardShareKeyPairPublicKey, recipient, percentageShare} = request.payload;
|
||||
const resKeyPair = await getKeyPair();
|
||||
const parsedData = resKeyPair;
|
||||
const uint8PrivateKey = Base58.decode(parsedData.privateKey);
|
||||
const uint8PublicKey = Base58.decode(parsedData.publicKey);
|
||||
const keyPair = {
|
||||
privateKey: uint8PrivateKey,
|
||||
publicKey: uint8PublicKey,
|
||||
};
|
||||
let lastRef = await getLastRef();
|
||||
|
||||
const tx = await createTransaction(381, keyPair, {
|
||||
rewardShareKeyPairPublicKey,
|
||||
recipient,
|
||||
percentageShare,
|
||||
lastReference: lastRef,
|
||||
});
|
||||
|
||||
const signedBytes = Base58.encode(tx.signedBytes);
|
||||
|
||||
const res = await processTransactionVersion2(signedBytes);
|
||||
if (!res?.signature)
|
||||
throw new Error("Transaction was not able to be processed");
|
||||
event.source.postMessage(
|
||||
{
|
||||
requestId: request.requestId,
|
||||
action: "removeRewardShare",
|
||||
payload: res,
|
||||
type: "backgroundMessageResponse",
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
} catch (error) {
|
||||
event.source.postMessage(
|
||||
{
|
||||
requestId: request.requestId,
|
||||
action: "removeRewardShare",
|
||||
error: error?.message,
|
||||
type: "backgroundMessageResponse",
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRewardSharePrivateKeyCase(request, event) {
|
||||
try {
|
||||
const {recipientPublicKey} = request.payload;
|
||||
const resKeyPair = await getKeyPair();
|
||||
const parsedData = resKeyPair;
|
||||
const uint8PrivateKey = Base58.decode(parsedData.privateKey);
|
||||
const uint8PublicKey = Base58.decode(parsedData.publicKey);
|
||||
const keyPair = {
|
||||
privateKey: uint8PrivateKey,
|
||||
publicKey: uint8PublicKey,
|
||||
};
|
||||
let lastRef = await getLastRef();
|
||||
|
||||
const tx = await createTransaction(38, keyPair, {
|
||||
recipientPublicKey,
|
||||
percentageShare: 0,
|
||||
lastReference: lastRef,
|
||||
});
|
||||
|
||||
event.source.postMessage(
|
||||
{
|
||||
requestId: request.requestId,
|
||||
action: "getRewardSharePrivateKey",
|
||||
payload: tx?._base58RewardShareSeed,
|
||||
type: "backgroundMessageResponse",
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
} catch (error) {
|
||||
event.source.postMessage(
|
||||
{
|
||||
requestId: request.requestId,
|
||||
action: "getRewardSharePrivateKey",
|
||||
error: error?.message,
|
||||
type: "backgroundMessageResponse",
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,6 +30,7 @@ import { RequestQueueWithPromise } from "./utils/queue/queue";
|
||||
import { validateAddress } from "./utils/validateAddress";
|
||||
import { Sha256 } from "asmcrypto.js";
|
||||
import { TradeBotRespondMultipleRequest } from "./transactions/TradeBotRespondMultipleRequest";
|
||||
|
||||
import { RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS } from "./constants/resourceTypes";
|
||||
import {
|
||||
addDataPublishesCase,
|
||||
@ -46,6 +47,7 @@ import {
|
||||
clearAllNotificationsCase,
|
||||
createGroupCase,
|
||||
createPollCase,
|
||||
createRewardShareCase,
|
||||
decryptDirectCase,
|
||||
decryptGroupEncryptionCase,
|
||||
decryptSingleCase,
|
||||
@ -60,6 +62,7 @@ import {
|
||||
getEnteredQmailTimestampCase,
|
||||
getGroupDataSingleCase,
|
||||
getGroupNotificationTimestampCase,
|
||||
getRewardSharePrivateKeyCase,
|
||||
getTempPublishCase,
|
||||
getThreadActivityCase,
|
||||
getTimestampEnterChatCase,
|
||||
@ -71,6 +74,7 @@ import {
|
||||
joinGroupCase,
|
||||
kickFromGroupCase,
|
||||
leaveGroupCase,
|
||||
listActionsCase,
|
||||
ltcBalanceCase,
|
||||
makeAdminCase,
|
||||
nameCase,
|
||||
@ -81,6 +85,7 @@ import {
|
||||
publishOnQDNCase,
|
||||
registerNameCase,
|
||||
removeAdminCase,
|
||||
removeRewardShareCase,
|
||||
resumeAllQueuesCase,
|
||||
saveTempPublishCase,
|
||||
sendChatDirectCase,
|
||||
@ -98,6 +103,7 @@ import {
|
||||
voteOnPollCase,
|
||||
} from "./background-cases";
|
||||
import { getData, removeKeysAndLogout, storeData } from "./utils/chromeStorage";
|
||||
import TradeBotRespondRequest from "./transactions/TradeBotRespondRequest";
|
||||
// import {BackgroundFetch} from '@transistorsoft/capacitor-background-fetch';
|
||||
|
||||
export let groupSecretkeys = {}
|
||||
@ -239,7 +245,16 @@ export const getForeignKey = async (foreignBlockchain)=> {
|
||||
switch (foreignBlockchain) {
|
||||
case "LITECOIN":
|
||||
return parsedData.ltcPrivateKey
|
||||
|
||||
case "DOGECOIN":
|
||||
return parsedData.dogePrivateKey
|
||||
case "BITCOIN":
|
||||
return parsedData.btcPrivateKey
|
||||
case "DIGIBYTE":
|
||||
return parsedData.dgbPrivateKey
|
||||
case "RAVENCOIN":
|
||||
return parsedData.rvnPrivateKey
|
||||
case "PIRATECHAIN":
|
||||
return parsedData.arrrSeed58
|
||||
default:
|
||||
return null
|
||||
}
|
||||
@ -659,13 +674,12 @@ const handleNotification = async (groups) => {
|
||||
|
||||
let mutedGroups = (await getUserSettings({ key: "mutedGroups" })) || [];
|
||||
if (!isArray(mutedGroups)) mutedGroups = [];
|
||||
|
||||
mutedGroups.push('0')
|
||||
let isFocused;
|
||||
const data = groups.filter(
|
||||
(group) =>
|
||||
group?.sender !== address &&
|
||||
!mutedGroups.includes(group.groupId) &&
|
||||
!isUpdateMsg(group?.data)
|
||||
!mutedGroups.includes(group.groupId)
|
||||
);
|
||||
const dataWithUpdates = groups.filter(
|
||||
(group) => group?.sender !== address && !mutedGroups.includes(group.groupId)
|
||||
@ -716,8 +730,7 @@ const handleNotification = async (groups) => {
|
||||
Date.now() - lastGroupNotification >= 120000
|
||||
) {
|
||||
if (
|
||||
!newestLatestTimestamp?.data ||
|
||||
!isExtMsg(newestLatestTimestamp?.data)
|
||||
!newestLatestTimestamp?.data
|
||||
)
|
||||
return;
|
||||
|
||||
@ -821,6 +834,17 @@ export async function getNameInfo() {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export async function getNameInfoForOthers(address) {
|
||||
const validApi = await getBaseApi();
|
||||
const response = await fetch(validApi + "/names/address/" + address);
|
||||
const nameData = await response.json();
|
||||
if (nameData?.length > 0) {
|
||||
return nameData[0].name;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
async function getAddressInfo(address) {
|
||||
const validApi = await getBaseApi();
|
||||
const response = await fetch(validApi + "/addresses/" + address);
|
||||
@ -1096,7 +1120,7 @@ export const sendQortFee = async (): Promise<number> => {
|
||||
return qortFee;
|
||||
};
|
||||
|
||||
async function getNameOrAddress(receiver) {
|
||||
export async function getNameOrAddress(receiver) {
|
||||
try {
|
||||
const isAddress = validateAddress(receiver);
|
||||
if (isAddress) {
|
||||
@ -1453,7 +1477,7 @@ export async function handleActiveGroupDataFromSocket({ groups, directs }) {
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
async function sendChatForBuyOrder({ qortAddress, recipientPublicKey, message, atAddresses }) {
|
||||
async function sendChatForBuyOrder({ qortAddress, recipientPublicKey, message, atAddresses, isSingle }) {
|
||||
let _reference = new Uint8Array(64);
|
||||
self.crypto.getRandomValues(_reference);
|
||||
|
||||
@ -1478,7 +1502,9 @@ async function sendChatForBuyOrder({ qortAddress, recipientPublicKey, message, a
|
||||
};
|
||||
const finalJson = {
|
||||
callRequest: jsonData,
|
||||
extra: "whatever additional data goes here",
|
||||
extra: {
|
||||
type: isSingle ? "single" : "multiple"
|
||||
},
|
||||
};
|
||||
const messageStringified = JSON.stringify(finalJson);
|
||||
|
||||
@ -1529,7 +1555,6 @@ async function sendChatForBuyOrder({ qortAddress, recipientPublicKey, message, a
|
||||
signature
|
||||
}
|
||||
}
|
||||
const path = `${import.meta.env.BASE_URL}memory-pow.wasm.full`;
|
||||
|
||||
|
||||
const chatBytes = tx.chatBytes;
|
||||
@ -1764,20 +1789,40 @@ export async function createBuyOrderTx({ crosschainAtInfo, isGateway, foreignBlo
|
||||
const wallet = await getSaveWallet();
|
||||
|
||||
const address = wallet.address0;
|
||||
|
||||
const message = {
|
||||
addresses: crosschainAtInfo.map((order)=> order.qortalAtAddress),
|
||||
foreignKey: await getForeignKey(foreignBlockchain),
|
||||
receivingAddress: address,
|
||||
};
|
||||
let responseVar;
|
||||
const txn = new TradeBotRespondMultipleRequest().createTransaction(
|
||||
message
|
||||
);
|
||||
let message
|
||||
if(foreignBlockchain === 'PIRATECHAIN'){
|
||||
message = {
|
||||
atAddress: crosschainAtInfo[0].qortalAtAddress,
|
||||
foreignKey: await getForeignKey(foreignBlockchain),
|
||||
receivingAddress: address,
|
||||
};
|
||||
} else {
|
||||
message = {
|
||||
addresses: crosschainAtInfo.map((order)=> order.qortalAtAddress),
|
||||
foreignKey: await getForeignKey(foreignBlockchain),
|
||||
receivingAddress: address,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const url = await createEndpoint('/crosschain/tradebot/respondmultiple')
|
||||
|
||||
let responseVar;
|
||||
let txn
|
||||
let url
|
||||
if(foreignBlockchain === 'PIRATECHAIN'){
|
||||
txn = new TradeBotRespondRequest().createTransaction(
|
||||
message
|
||||
);
|
||||
|
||||
|
||||
url = await createEndpoint('/crosschain/tradebot/respond')
|
||||
} else {
|
||||
txn = new TradeBotRespondMultipleRequest().createTransaction(
|
||||
message
|
||||
);
|
||||
|
||||
|
||||
url = await createEndpoint('/crosschain/tradebot/respondmultiple')
|
||||
}
|
||||
|
||||
const responseFetch = await fetch(
|
||||
url,
|
||||
{
|
||||
@ -1790,6 +1835,10 @@ export async function createBuyOrderTx({ crosschainAtInfo, isGateway, foreignBlo
|
||||
);
|
||||
|
||||
const res = await responseFetch.json();
|
||||
if(res?.error && res?.message){
|
||||
throw new Error(res?.message)
|
||||
}
|
||||
if(!responseFetch?.ok) throw new Error('Failed to submit buy order')
|
||||
|
||||
if (res === false) {
|
||||
responseVar = {
|
||||
@ -1806,7 +1855,7 @@ export async function createBuyOrderTx({ crosschainAtInfo, isGateway, foreignBlo
|
||||
callResponse: response,
|
||||
extra: {
|
||||
message: "Transaction processed successfully!",
|
||||
atAddresses: crosschainAtInfo.map((order)=> order.qortalAtAddress),
|
||||
atAddresses: foreignBlockchain === 'PIRATECHAIN' ? [crosschainAtInfo[0].qortalAtAddress] : crosschainAtInfo.map((order)=> order.qortalAtAddress),
|
||||
senderAddress: address,
|
||||
node: url
|
||||
},
|
||||
@ -1816,7 +1865,7 @@ export async function createBuyOrderTx({ crosschainAtInfo, isGateway, foreignBlo
|
||||
callResponse: "ERROR",
|
||||
extra: {
|
||||
message: response,
|
||||
atAddresses: crosschainAtInfo.map((order)=> order.qortalAtAddress),
|
||||
atAddresses: foreignBlockchain === 'PIRATECHAIN' ? [crosschainAtInfo[0].qortalAtAddress] : crosschainAtInfo.map((order)=> order.qortalAtAddress),
|
||||
senderAddress: address,
|
||||
node: url
|
||||
},
|
||||
@ -1830,7 +1879,7 @@ export async function createBuyOrderTx({ crosschainAtInfo, isGateway, foreignBlo
|
||||
|
||||
|
||||
const message = {
|
||||
addresses: crosschainAtInfo.map((order)=> order.qortalAtAddress),
|
||||
addresses: foreignBlockchain === 'PIRATECHAIN' ? [crosschainAtInfo[0].qortalAtAddress] : crosschainAtInfo.map((order)=> order.qortalAtAddress),
|
||||
foreignKey: await getForeignKey(foreignBlockchain),
|
||||
receivingAddress: address,
|
||||
};
|
||||
@ -1838,7 +1887,8 @@ export async function createBuyOrderTx({ crosschainAtInfo, isGateway, foreignBlo
|
||||
qortAddress: proxyAccountAddress,
|
||||
recipientPublicKey: proxyAccountPublicKey,
|
||||
message,
|
||||
atAddresses: crosschainAtInfo.map((order)=> order.qortalAtAddress),
|
||||
atAddresses: foreignBlockchain === 'PIRATECHAIN' ? [crosschainAtInfo[0].qortalAtAddress] : crosschainAtInfo.map((order)=> order.qortalAtAddress),
|
||||
isSingle: foreignBlockchain === 'PIRATECHAIN'
|
||||
});
|
||||
|
||||
|
||||
@ -1857,7 +1907,7 @@ export async function createBuyOrderTx({ crosschainAtInfo, isGateway, foreignBlo
|
||||
message: message?.extra?.message,
|
||||
senderAddress: address,
|
||||
node: buyTradeNodeBaseUrl,
|
||||
atAddresses: crosschainAtInfo.map((order)=> order.qortalAtAddress),
|
||||
atAddresses: foreignBlockchain === 'PIRATECHAIN' ? [crosschainAtInfo[0].qortalAtAddress] : crosschainAtInfo.map((order)=> order.qortalAtAddress),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1952,7 +2002,7 @@ export async function leaveGroup({ groupId }) {
|
||||
|
||||
const res = await processTransactionVersion2(signedBytes);
|
||||
if (!res?.signature)
|
||||
throw new Error("Transaction was not able to be processed");
|
||||
throw new Error(res?.message || "Transaction was not able to be processed");
|
||||
return res;
|
||||
}
|
||||
|
||||
@ -2008,7 +2058,7 @@ export async function cancelInvitationToGroup({ groupId, qortalAddress }) {
|
||||
|
||||
const res = await processTransactionVersion2(signedBytes);
|
||||
if (!res?.signature)
|
||||
throw new Error("Transaction was not able to be processed");
|
||||
throw new Error(res?.message || "Transaction was not able to be processed");
|
||||
return res;
|
||||
}
|
||||
|
||||
@ -2035,10 +2085,10 @@ export async function cancelBan({ groupId, qortalAddress }) {
|
||||
|
||||
const res = await processTransactionVersion2(signedBytes);
|
||||
if (!res?.signature)
|
||||
throw new Error("Transaction was not able to be processed");
|
||||
throw new Error(res?.message || "Transaction was not able to be processed");
|
||||
return res;
|
||||
}
|
||||
export async function registerName({ name }) {
|
||||
export async function registerName({ name, description = "" }) {
|
||||
const lastReference = await getLastRef();
|
||||
const resKeyPair = await getKeyPair();
|
||||
const parsedData = resKeyPair;
|
||||
@ -2053,7 +2103,7 @@ export async function registerName({ name }) {
|
||||
const tx = await createTransaction(3, keyPair, {
|
||||
fee: feeres.fee,
|
||||
name,
|
||||
value: "",
|
||||
value: description || "",
|
||||
lastReference: lastReference,
|
||||
});
|
||||
|
||||
@ -2061,7 +2111,34 @@ export async function registerName({ name }) {
|
||||
|
||||
const res = await processTransactionVersion2(signedBytes);
|
||||
if (!res?.signature)
|
||||
throw new Error("Transaction was not able to be processed");
|
||||
throw new Error(res?.message || "Transaction was not able to be processed");
|
||||
return res;
|
||||
}
|
||||
export async function updateName({ newName, oldName, description }) {
|
||||
const lastReference = await getLastRef();
|
||||
const resKeyPair = await getKeyPair();
|
||||
const parsedData = resKeyPair;
|
||||
const uint8PrivateKey = Base58.decode(parsedData.privateKey);
|
||||
const uint8PublicKey = Base58.decode(parsedData.publicKey);
|
||||
const keyPair = {
|
||||
privateKey: uint8PrivateKey,
|
||||
publicKey: uint8PublicKey,
|
||||
};
|
||||
const feeres = await getFee("UPDATE_NAME");
|
||||
|
||||
const tx = await createTransaction(4, keyPair, {
|
||||
fee: feeres.fee,
|
||||
name: oldName,
|
||||
newName,
|
||||
newData: description || "",
|
||||
lastReference: lastReference,
|
||||
});
|
||||
|
||||
const signedBytes = Base58.encode(tx.signedBytes);
|
||||
|
||||
const res = await processTransactionVersion2(signedBytes);
|
||||
if (!res?.signature)
|
||||
throw new Error(res?.message || "Transaction was not able to be processed");
|
||||
return res;
|
||||
}
|
||||
export async function makeAdmin({ groupId, qortalAddress }) {
|
||||
@ -2087,7 +2164,7 @@ export async function makeAdmin({ groupId, qortalAddress }) {
|
||||
|
||||
const res = await processTransactionVersion2(signedBytes);
|
||||
if (!res?.signature)
|
||||
throw new Error("Transaction was not able to be processed");
|
||||
throw new Error(res?.message || "Transaction was not able to be processed");
|
||||
return res;
|
||||
}
|
||||
|
||||
@ -2114,7 +2191,7 @@ export async function removeAdmin({ groupId, qortalAddress }) {
|
||||
|
||||
const res = await processTransactionVersion2(signedBytes);
|
||||
if (!res?.signature)
|
||||
throw new Error("Transaction was not able to be processed");
|
||||
throw new Error(res?.message || "Transaction was not able to be processed");
|
||||
return res;
|
||||
}
|
||||
|
||||
@ -2148,7 +2225,7 @@ export async function banFromGroup({
|
||||
|
||||
const res = await processTransactionVersion2(signedBytes);
|
||||
if (!res?.signature)
|
||||
throw new Error("Transaction was not able to be processed");
|
||||
throw new Error(res?.message || "Transaction was not able to be processed");
|
||||
return res;
|
||||
}
|
||||
|
||||
@ -2180,7 +2257,7 @@ export async function kickFromGroup({
|
||||
|
||||
const res = await processTransactionVersion2(signedBytes);
|
||||
if (!res?.signature)
|
||||
throw new Error("Transaction was not able to be processed");
|
||||
throw new Error(res?.message || "Transaction was not able to be processed");
|
||||
return res;
|
||||
}
|
||||
|
||||
@ -2222,7 +2299,7 @@ export async function createGroup({
|
||||
|
||||
const res = await processTransactionVersion2(signedBytes);
|
||||
if (!res?.signature)
|
||||
throw new Error("Transaction was not able to be processed");
|
||||
throw new Error(res?.message || "Transaction was not able to be processed");
|
||||
return res;
|
||||
}
|
||||
export async function inviteToGroup({ groupId, qortalAddress, inviteTime }) {
|
||||
@ -2267,6 +2344,7 @@ export async function sendCoin(
|
||||
let keyPair = "";
|
||||
if (skipConfirmPassword) {
|
||||
const resKeyPair = await getKeyPair();
|
||||
|
||||
const parsedData = resKeyPair;
|
||||
const uint8PrivateKey = Base58.decode(parsedData.privateKey);
|
||||
const uint8PublicKey = Base58.decode(parsedData.publicKey);
|
||||
@ -2276,14 +2354,14 @@ export async function sendCoin(
|
||||
};
|
||||
} else {
|
||||
const response = await decryptStoredWallet(password, wallet);
|
||||
const wallet2 = new PhraseWallet(response, walletVersion);
|
||||
const wallet2 = new PhraseWallet(response, wallet?.version || walletVersion);
|
||||
|
||||
keyPair = wallet2._addresses[0].keyPair;
|
||||
}
|
||||
|
||||
const lastRef = await getLastRef();
|
||||
const fee = await sendQortFee();
|
||||
const validApi = await findUsableApi();
|
||||
const validApi = null;
|
||||
|
||||
const res = await makeTransactionRequest(
|
||||
confirmReceiver,
|
||||
@ -2945,6 +3023,9 @@ function setupMessageListener() {
|
||||
case "getTimestampEnterChat":
|
||||
getTimestampEnterChatCase(request, event);
|
||||
break;
|
||||
case "listActions":
|
||||
listActionsCase(request, event);
|
||||
break;
|
||||
case "addTimestampMention":
|
||||
addTimestampMentionCase(request, event);
|
||||
break;
|
||||
@ -2966,6 +3047,9 @@ function setupMessageListener() {
|
||||
case "publishOnQDN":
|
||||
publishOnQDNCase(request, event);
|
||||
break;
|
||||
case "getUserSettings":
|
||||
getUserSettingsCase(request, event);
|
||||
break;
|
||||
case "handleActiveGroupDataFromSocket":
|
||||
handleActiveGroupDataFromSocketCase(request, event);
|
||||
break;
|
||||
@ -3007,6 +3091,15 @@ function setupMessageListener() {
|
||||
case "setupGroupWebsocket":
|
||||
setupGroupWebsocketCase(request, event);
|
||||
break;
|
||||
case "createRewardShare":
|
||||
createRewardShareCase(request, event);
|
||||
break;
|
||||
case "getRewardSharePrivateKey":
|
||||
getRewardSharePrivateKeyCase(request, event);
|
||||
break;
|
||||
case "removeRewardShare" :
|
||||
removeRewardShareCase(request, event);
|
||||
break;
|
||||
case "addEnteredQmailTimestamp":
|
||||
addEnteredQmailTimestampCase(request, event);
|
||||
break;
|
||||
@ -3065,9 +3158,16 @@ const checkGroupList = async () => {
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
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 filteredGroups =
|
||||
data.groups?.filter((item) => item?.groupId !== 0) || [];
|
||||
const sortedGroups = filteredGroups.sort(
|
||||
(a, b) => (b.timestamp || 0) - (a.timestamp || 0)
|
||||
);
|
||||
@ -3093,6 +3193,7 @@ export const checkNewMessages = async () => {
|
||||
try {
|
||||
let mutedGroups = (await getUserSettings({ key: "mutedGroups" })) || [];
|
||||
if (!isArray(mutedGroups)) mutedGroups = [];
|
||||
mutedGroups.push('0')
|
||||
let myName = "";
|
||||
const userData = await getUserInfo();
|
||||
if (userData?.name) {
|
||||
|
@ -2,6 +2,10 @@ import { getBaseApi } from "../background";
|
||||
import { createSymmetricKeyAndNonce, decryptGroupData, encryptDataGroup, objectToBase64 } from "../qdn/encryption/group-encryption";
|
||||
import { publishData } from "../qdn/publish/pubish";
|
||||
import { getData } from "../utils/chromeStorage";
|
||||
import { RequestQueueWithPromise } from "../utils/queue/queue";
|
||||
|
||||
|
||||
export const requestQueueGetPublicKeys = new RequestQueueWithPromise(10);
|
||||
|
||||
const apiEndpoints = [
|
||||
"https://api.qortal.org",
|
||||
@ -65,44 +69,51 @@ async function getKeyPair() {
|
||||
throw new Error("Wallet not authenticated");
|
||||
}
|
||||
}
|
||||
const getPublicKeys = async (groupNumber: number) => {
|
||||
const validApi = await getBaseApi()
|
||||
const response = await fetch(`${validApi}/groups/members/${groupNumber}?limit=0`);
|
||||
const groupData = await response.json();
|
||||
|
||||
let members: any = [];
|
||||
if (groupData && Array.isArray(groupData?.members)) {
|
||||
for (const member of groupData.members) {
|
||||
if (member.member) {
|
||||
const getPublicKeys = async (groupNumber: number) => {
|
||||
const validApi = await getBaseApi();
|
||||
const response = await fetch(`${validApi}/groups/members/${groupNumber}?limit=0`);
|
||||
const groupData = await response.json();
|
||||
|
||||
if (groupData && Array.isArray(groupData.members)) {
|
||||
// Use the request queue for fetching public keys
|
||||
const memberPromises = groupData.members
|
||||
.filter((member) => member.member)
|
||||
.map((member) =>
|
||||
requestQueueGetPublicKeys.enqueue(async () => {
|
||||
const resAddress = await fetch(`${validApi}/addresses/${member.member}`);
|
||||
const resData = await resAddress.json();
|
||||
const publicKey = resData.publicKey;
|
||||
members.push(publicKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
const resData = await resAddress.json();
|
||||
return resData.publicKey;
|
||||
})
|
||||
);
|
||||
|
||||
const members = await Promise.all(memberPromises);
|
||||
return members;
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
return members
|
||||
}
|
||||
|
||||
export const getPublicKeysByAddress = async (admins) => {
|
||||
const validApi = await getBaseApi()
|
||||
|
||||
|
||||
let members: any = [];
|
||||
if (Array.isArray(admins)) {
|
||||
for (const address of admins) {
|
||||
if (address) {
|
||||
export const getPublicKeysByAddress = async (admins: string[]) => {
|
||||
const validApi = await getBaseApi();
|
||||
|
||||
if (Array.isArray(admins)) {
|
||||
// Use the request queue to limit concurrent fetches
|
||||
const memberPromises = admins
|
||||
.filter((address) => address) // Ensure the address is valid
|
||||
.map((address) =>
|
||||
requestQueueGetPublicKeys.enqueue(async () => {
|
||||
const resAddress = await fetch(`${validApi}/addresses/${address}`);
|
||||
const resData = await resAddress.json();
|
||||
const publicKey = resData.publicKey;
|
||||
members.push(publicKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return members
|
||||
}
|
||||
const resData = await resAddress.json();
|
||||
return resData.publicKey;
|
||||
})
|
||||
);
|
||||
|
||||
const members = await Promise.all(memberPromises);
|
||||
return members;
|
||||
}
|
||||
|
||||
return []; // Return empty array if admins is not an array
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
158
src/common/BoundedNumericTextField.tsx
Normal file
@ -0,0 +1,158 @@
|
||||
import {
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
TextField,
|
||||
TextFieldProps,
|
||||
} from "@mui/material";
|
||||
import React, { useRef, useState } from "react";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import RemoveIcon from "@mui/icons-material/Remove";
|
||||
import {
|
||||
removeTrailingZeros,
|
||||
setNumberWithinBounds,
|
||||
} from "./numberFunctions.ts";
|
||||
import { CustomInput } from "../App-styles.ts";
|
||||
|
||||
type eventType = React.ChangeEvent<HTMLInputElement>;
|
||||
type BoundedNumericTextFieldProps = {
|
||||
minValue: number;
|
||||
maxValue: number;
|
||||
addIconButtons?: boolean;
|
||||
allowDecimals?: boolean;
|
||||
allowNegatives?: boolean;
|
||||
afterChange?: (s: string) => void;
|
||||
initialValue?: string;
|
||||
maxSigDigits?: number;
|
||||
} & TextFieldProps;
|
||||
|
||||
export const BoundedNumericTextField = ({
|
||||
minValue,
|
||||
maxValue,
|
||||
addIconButtons = true,
|
||||
allowDecimals = true,
|
||||
allowNegatives = false,
|
||||
afterChange,
|
||||
initialValue,
|
||||
maxSigDigits = 6,
|
||||
...props
|
||||
}: BoundedNumericTextFieldProps) => {
|
||||
const [textFieldValue, setTextFieldValue] = useState<string>(
|
||||
initialValue || ""
|
||||
);
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const stringIsEmpty = (value: string) => {
|
||||
return value === "";
|
||||
};
|
||||
const isAllZerosNum = /^0*\.?0*$/;
|
||||
const isFloatNum = /^-?[0-9]*\.?[0-9]*$/;
|
||||
const isIntegerNum = /^-?[0-9]+$/;
|
||||
const skipMinMaxCheck = (value: string) => {
|
||||
const lastIndexIsDecimal = value.charAt(value.length - 1) === ".";
|
||||
const isEmpty = stringIsEmpty(value);
|
||||
const isAllZeros = isAllZerosNum.test(value);
|
||||
const isInteger = isIntegerNum.test(value);
|
||||
// skipping minMax on all 0s allows values less than 1 to be entered
|
||||
|
||||
return lastIndexIsDecimal || isEmpty || (isAllZeros && !isInteger);
|
||||
};
|
||||
|
||||
const setMinMaxValue = (value: string): string => {
|
||||
if (skipMinMaxCheck(value)) return value;
|
||||
const valueNum = Number(value);
|
||||
|
||||
const boundedNum = setNumberWithinBounds(valueNum, minValue, maxValue);
|
||||
|
||||
const numberInBounds = boundedNum === valueNum;
|
||||
return numberInBounds ? value : boundedNum.toString();
|
||||
};
|
||||
|
||||
const getSigDigits = (number: string) => {
|
||||
if (isIntegerNum.test(number)) return 0;
|
||||
const decimalSplit = number.split(".");
|
||||
return decimalSplit[decimalSplit.length - 1].length;
|
||||
};
|
||||
|
||||
const sigDigitsExceeded = (number: string, sigDigits: number) => {
|
||||
return getSigDigits(number) > sigDigits;
|
||||
};
|
||||
|
||||
const filterTypes = (value: string) => {
|
||||
if (allowDecimals === false) value = value.replace(".", "");
|
||||
if (allowNegatives === false) value = value.replace("-", "");
|
||||
if (sigDigitsExceeded(value, maxSigDigits)) {
|
||||
value = value.substring(0, value.length - 1);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
const filterValue = (value: string) => {
|
||||
if (stringIsEmpty(value)) return "";
|
||||
value = filterTypes(value);
|
||||
if (isFloatNum.test(value)) {
|
||||
return setMinMaxValue(value);
|
||||
}
|
||||
return textFieldValue;
|
||||
};
|
||||
|
||||
const listeners = (e: eventType) => {
|
||||
const newValue = filterValue(e.target.value);
|
||||
setTextFieldValue(newValue);
|
||||
if (afterChange) afterChange(newValue);
|
||||
};
|
||||
|
||||
const changeValueWithIncDecButton = (changeAmount: number) => {
|
||||
const changedValue = (+textFieldValue + changeAmount).toString();
|
||||
const inBoundsValue = setMinMaxValue(changedValue);
|
||||
setTextFieldValue(inBoundsValue);
|
||||
if (afterChange) afterChange(inBoundsValue);
|
||||
};
|
||||
|
||||
const formatValueOnBlur = (e: eventType) => {
|
||||
let value = e.target.value;
|
||||
if (stringIsEmpty(value) || value === ".") {
|
||||
setTextFieldValue("");
|
||||
return;
|
||||
}
|
||||
|
||||
value = setMinMaxValue(value);
|
||||
value = removeTrailingZeros(value);
|
||||
if (isAllZerosNum.test(value)) value = minValue.toString();
|
||||
|
||||
setTextFieldValue(value);
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { onChange, ...noChangeProps } = { ...props };
|
||||
return (
|
||||
<CustomInput
|
||||
{...noChangeProps}
|
||||
InputProps={{
|
||||
...props?.InputProps,
|
||||
endAdornment: addIconButtons ? (
|
||||
<InputAdornment position="end">
|
||||
<IconButton size="small" onClick={() => changeValueWithIncDecButton(1)}>
|
||||
<AddIcon sx={{
|
||||
color: 'white'
|
||||
}} />{" "}
|
||||
</IconButton>
|
||||
<IconButton size="small" onClick={() => changeValueWithIncDecButton(-1)}>
|
||||
<RemoveIcon sx={{
|
||||
color: 'white'
|
||||
}} />{" "}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
) : (
|
||||
<></>
|
||||
),
|
||||
}}
|
||||
onChange={e => listeners(e as eventType)}
|
||||
onBlur={e => {
|
||||
formatValueOnBlur(e as eventType);
|
||||
}}
|
||||
autoComplete="off"
|
||||
value={textFieldValue}
|
||||
inputRef={ref}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoundedNumericTextField;
|
10
src/common/Spinners/BarSpinner/BarSpinner.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import './barSpinner.css'
|
||||
export const BarSpinner = ({width = '20px', color}) => {
|
||||
return (
|
||||
<div style={{
|
||||
width,
|
||||
color: color || 'green'
|
||||
}} className="loader-bar"></div>
|
||||
)
|
||||
}
|
19
src/common/Spinners/BarSpinner/barSpinner.css
Normal file
@ -0,0 +1,19 @@
|
||||
/* HTML: <div class="loader"></div> */
|
||||
.loader-bar {
|
||||
width: 45px;
|
||||
aspect-ratio: .75;
|
||||
--c:no-repeat linear-gradient(currentColor 0 0);
|
||||
background:
|
||||
var(--c) 0% 100%,
|
||||
var(--c) 50% 100%,
|
||||
var(--c) 100% 100%;
|
||||
background-size: 20% 65%;
|
||||
animation: l8 1s infinite linear;
|
||||
}
|
||||
@keyframes l8 {
|
||||
16.67% {background-position: 0% 0% ,50% 100%,100% 100%}
|
||||
33.33% {background-position: 0% 0% ,50% 0% ,100% 100%}
|
||||
50% {background-position: 0% 0% ,50% 0% ,100% 0% }
|
||||
66.67% {background-position: 0% 100%,50% 0% ,100% 0% }
|
||||
83.33% {background-position: 0% 100%,50% 100%,100% 0% }
|
||||
}
|
63
src/common/numberFunctions.ts
Normal file
@ -0,0 +1,63 @@
|
||||
export const truncateNumber = (value: string | number, sigDigits: number) => {
|
||||
return Number(value).toFixed(sigDigits);
|
||||
};
|
||||
|
||||
export const removeTrailingZeros = (s: string) => {
|
||||
return Number(s).toString();
|
||||
};
|
||||
|
||||
export const setNumberWithinBounds = (
|
||||
num: number,
|
||||
minValue: number,
|
||||
maxValue: number
|
||||
) => {
|
||||
if (num > maxValue) return maxValue;
|
||||
if (num < minValue) return minValue;
|
||||
return num;
|
||||
};
|
||||
|
||||
export const numberToInt = (num: number) => {
|
||||
return Math.floor(num);
|
||||
};
|
||||
|
||||
type ByteFormat = "Decimal" | "Binary";
|
||||
export function formatBytes(
|
||||
bytes: number,
|
||||
decimals = 2,
|
||||
format: ByteFormat = "Binary"
|
||||
) {
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
|
||||
const k = format === "Binary" ? 1024 : 1000;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
|
||||
}
|
||||
|
||||
export function formatTime(seconds: number): string {
|
||||
seconds = Math.floor(seconds);
|
||||
const 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;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { resourceDownloadControllerAtom } from '../atoms/global';
|
||||
import { getBaseApiReact } from '../App';
|
||||
@ -21,9 +21,11 @@ export const useFetchResources = () => {
|
||||
let isCalling = false;
|
||||
let percentLoaded = 0;
|
||||
let timer = 24;
|
||||
let tries = 0;
|
||||
let calledFirstTime = false
|
||||
|
||||
const intervalId = setInterval(async () => {
|
||||
let intervalId
|
||||
let timeoutId
|
||||
const callFunction = async ()=> {
|
||||
if (isCalling) return;
|
||||
isCalling = true;
|
||||
|
||||
@ -40,11 +42,32 @@ export const useFetchResources = () => {
|
||||
},
|
||||
});
|
||||
res = await resCall.json()
|
||||
if(tries > 18 ){
|
||||
if(intervalId){
|
||||
clearInterval(intervalId)
|
||||
}
|
||||
if(timeoutId){
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
setResources((prev) => ({
|
||||
...prev,
|
||||
[`${service}-${name}-${identifier}`]: {
|
||||
...(prev[`${service}-${name}-${identifier}`] || {}),
|
||||
status: {
|
||||
...res,
|
||||
status: 'FAILED_TO_DOWNLOAD',
|
||||
},
|
||||
},
|
||||
}));
|
||||
return
|
||||
}
|
||||
tries = tries + 1
|
||||
|
||||
}
|
||||
|
||||
|
||||
if(build || (calledFirstTime === false && res?.status !== 'READY')){
|
||||
const url = `${getBaseApiReact()}/arbitrary/resource/status/${service}/${name}/${identifier}?build=true`;
|
||||
const url = `${getBaseApiReact()}/arbitrary/resource/properties/${service}/${name}/${identifier}?build=true`;
|
||||
const resCall = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
@ -81,10 +104,11 @@ export const useFetchResources = () => {
|
||||
},
|
||||
}));
|
||||
|
||||
setTimeout(() => {
|
||||
timeoutId = setTimeout(() => {
|
||||
isCalling = false;
|
||||
downloadResource({ name, service, identifier }, true);
|
||||
}, 25000);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@ -103,8 +127,13 @@ export const useFetchResources = () => {
|
||||
|
||||
// Check if progress is 100% and clear interval if true
|
||||
if (res?.status === 'READY') {
|
||||
clearInterval(intervalId);
|
||||
if(intervalId){
|
||||
clearInterval(intervalId);
|
||||
|
||||
}
|
||||
if(timeoutId){
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
// Update Recoil state for completion
|
||||
setResources((prev) => ({
|
||||
...prev,
|
||||
@ -114,7 +143,22 @@ export const useFetchResources = () => {
|
||||
},
|
||||
}));
|
||||
}
|
||||
}, !calledFirstTime ? 100 :5000);
|
||||
if(res?.status === 'DOWNLOADED'){
|
||||
const url = `${getBaseApiReact()}/arbitrary/resource/status/${service}/${name}/${identifier}?build=true`;
|
||||
const resCall = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
res = await resCall.json();
|
||||
}
|
||||
}
|
||||
callFunction()
|
||||
intervalId = setInterval(async () => {
|
||||
callFunction()
|
||||
}, 5000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error during resource fetch:', error);
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ import { useRecoilState, useSetRecoilState } from "recoil";
|
||||
import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from "../../atoms/global";
|
||||
import { saveToLocalStorage } from "./AppsNavBar";
|
||||
|
||||
export const AppInfoSnippet = ({ app, myName, isFromCategory }) => {
|
||||
export const AppInfoSnippet = ({ app, myName, isFromCategory, parentStyles = {} }) => {
|
||||
|
||||
const isInstalled = app?.status?.status === 'READY'
|
||||
const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(sortablePinnedAppsAtom);
|
||||
@ -30,7 +30,9 @@ export const AppInfoSnippet = ({ app, myName, isFromCategory }) => {
|
||||
const isSelectedAppPinned = !!sortablePinnedApps?.find((item)=> item?.name === app?.name && item?.service === app?.service)
|
||||
const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom);
|
||||
return (
|
||||
<AppInfoSnippetContainer>
|
||||
<AppInfoSnippetContainer sx={{
|
||||
...parentStyles
|
||||
}}>
|
||||
<AppInfoSnippetLeft>
|
||||
<ButtonBase
|
||||
sx={{
|
||||
@ -57,8 +59,8 @@ export const AppInfoSnippet = ({ app, myName, isFromCategory }) => {
|
||||
>
|
||||
<Avatar
|
||||
sx={{
|
||||
height: "31px",
|
||||
width: "31px",
|
||||
height: "42px",
|
||||
width: "42px",
|
||||
'& img': {
|
||||
objectFit: 'fill',
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ export const AppViewer = React.forwardRef(({ app , hide, isDevMode}, iframeRef)
|
||||
if(isDevMode){
|
||||
|
||||
resetHistory()
|
||||
if(!app?.isPreview){
|
||||
if(!app?.isPreview || app?.isPrivate){
|
||||
setUrl(app?.url + `?time=${Date.now()}`)
|
||||
}
|
||||
return
|
||||
@ -189,7 +189,8 @@ export const AppViewer = React.forwardRef(({ app , hide, isDevMode}, iframeRef)
|
||||
height: !isMobile ? '100vh' : `calc(${rootHeight} - 60px - 45px )`,
|
||||
border: 'none',
|
||||
width: '100%'
|
||||
}} id="browser-iframe" src={defaultUrl} sandbox="allow-scripts allow-same-origin allow-forms allow-downloads allow-modals" allow="fullscreen">
|
||||
}} id="browser-iframe" src={defaultUrl} sandbox="allow-scripts allow-same-origin allow-forms allow-downloads allow-modals"
|
||||
allow="fullscreen; clipboard-read; clipboard-write">
|
||||
|
||||
</iframe>
|
||||
</Box>
|
||||
|
@ -94,13 +94,17 @@ import {
|
||||
|
||||
}));
|
||||
export const AppCircleLabel = styled(Typography)(({ theme }) => ({
|
||||
fontSize: '12px',
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
lineHeight: 1.2,
|
||||
whiteSpace: 'nowrap',
|
||||
// whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
width: '100%'
|
||||
width: '120%',
|
||||
'-webkit-line-clamp': '2',
|
||||
'-webkit-box-orient': 'vertical',
|
||||
'display': '-webkit-box',
|
||||
|
||||
}));
|
||||
export const AppLibrarySubTitle = styled(Typography)(({ theme }) => ({
|
||||
fontSize: '16px',
|
||||
@ -109,9 +113,9 @@ import {
|
||||
}));
|
||||
export const AppCircle = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
width: "60px",
|
||||
width: "75px",
|
||||
flexDirection: "column",
|
||||
height: "60px",
|
||||
height: "75px",
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '50%',
|
||||
|
@ -97,6 +97,7 @@ export const AppsCategoryDesktop = ({
|
||||
const { rootHeight } = useContext(MyContext);
|
||||
|
||||
const categoryList = useMemo(() => {
|
||||
if(category?.id === 'all') return availableQapps
|
||||
return availableQapps.filter(
|
||||
(app) => app?.metadata?.category === category?.id
|
||||
);
|
||||
@ -108,8 +109,13 @@ export const AppsCategoryDesktop = ({
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(searchValue);
|
||||
|
||||
}, 350);
|
||||
|
||||
setTimeout(() => {
|
||||
virtuosoRef.current.scrollToIndex({
|
||||
index: 0
|
||||
});
|
||||
}, 500);
|
||||
// Cleanup timeout if searchValue changes before the timeout completes
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
@ -121,7 +127,7 @@ export const AppsCategoryDesktop = ({
|
||||
const searchedList = useMemo(() => {
|
||||
if (!debouncedValue) return categoryList;
|
||||
return categoryList.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, categoryList]);
|
||||
|
||||
@ -133,6 +139,9 @@ export const AppsCategoryDesktop = ({
|
||||
app={app}
|
||||
myName={myName}
|
||||
isFromCategory={true}
|
||||
parentStyles={{
|
||||
padding: '0px 10px'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -206,7 +215,7 @@ export const AppsCategoryDesktop = ({
|
||||
<AppsWidthLimiter>
|
||||
<StyledVirtuosoContainer
|
||||
sx={{
|
||||
height: `calc(100vh - 36px - 90px)`,
|
||||
height: `calc(100vh - 36px - 90px - 25px)`,
|
||||
}}
|
||||
>
|
||||
<Virtuoso
|
||||
@ -215,9 +224,9 @@ export const AppsCategoryDesktop = ({
|
||||
itemContent={rowRenderer}
|
||||
atBottomThreshold={50}
|
||||
followOutput="smooth"
|
||||
components={{
|
||||
Scroller: ScrollerStyled, // Use the styled scroller component
|
||||
}}
|
||||
// components={{
|
||||
// Scroller: ScrollerStyled, // Use the styled scroller component
|
||||
// }}
|
||||
/>
|
||||
</StyledVirtuosoContainer>
|
||||
</AppsWidthLimiter>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { AppsHomeDesktop } from "./AppsHomeDesktop";
|
||||
import { Spacer } from "../../common/Spacer";
|
||||
import { MyContext, getBaseApiReact } from "../../App";
|
||||
import { GlobalContext, MyContext, getBaseApiReact } from "../../App";
|
||||
import { AppInfo } from "./AppInfo";
|
||||
import {
|
||||
executeEvent,
|
||||
@ -39,6 +39,8 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
|
||||
const [categories, setCategories] = useState([])
|
||||
const iframeRefs = useRef({});
|
||||
const [isEnabledDevMode, setIsEnabledDevMode] = useRecoilState(enabledDevModeAtom)
|
||||
const { showTutorial } = useContext(GlobalContext);
|
||||
|
||||
const myApp = useMemo(()=> {
|
||||
|
||||
return availableQapps.find((app)=> app.name === myName && app.service === 'APP')
|
||||
@ -48,6 +50,13 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
|
||||
return availableQapps.find((app)=> app.name === myName && app.service === 'WEBSITE')
|
||||
}, [myName, availableQapps])
|
||||
|
||||
|
||||
useEffect(()=> {
|
||||
if(show){
|
||||
showTutorial('qapps')
|
||||
}
|
||||
}, [show])
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
executeEvent("setTabsToNav", {
|
||||
@ -381,7 +390,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
|
||||
height={30}
|
||||
color={
|
||||
hasUnreadDirects
|
||||
? "var(--unread)"
|
||||
? "var(--danger)"
|
||||
: isDirects
|
||||
? "white"
|
||||
: "rgba(250, 250, 250, 0.5)"
|
||||
@ -399,7 +408,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
|
||||
height={30}
|
||||
color={
|
||||
hasUnreadGroups
|
||||
? "var(--unread)"
|
||||
? "var(--danger)"
|
||||
: isGroups
|
||||
? "white"
|
||||
: "rgba(250, 250, 250, 0.5)"
|
||||
@ -407,7 +416,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
|
||||
/>
|
||||
|
||||
</ButtonBase> */}
|
||||
<Save isDesktop disableWidth />
|
||||
<Save isDesktop disableWidth myName={myName}/>
|
||||
{isEnabledDevMode && (
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
@ -441,7 +450,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
|
||||
}}>
|
||||
|
||||
<Spacer height="30px" />
|
||||
<AppsHomeDesktop availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
|
||||
<AppsHomeDesktop myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@ -470,6 +479,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
|
||||
isSelected={tab?.tabId === selectedTab?.tabId}
|
||||
app={tab}
|
||||
ref={iframeRefs.current[tab.tabId]}
|
||||
isDevMode={tab?.service ? false : true}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@ -485,7 +495,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
|
||||
}}>
|
||||
|
||||
<Spacer height="30px" />
|
||||
<AppsHomeDesktop availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
|
||||
<AppsHomeDesktop myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
@ -266,14 +266,14 @@ export const AppsDevMode = ({ mode, setMode, show , myName, goToHome, setDesktop
|
||||
}}
|
||||
>
|
||||
<IconWrapper
|
||||
color={desktopViewMode === 'chat' ? 'white' :"rgba(250, 250, 250, 0.5)"}
|
||||
color={(hasUnreadDirects || hasUnreadGroups) ? "var(--unread)" : desktopViewMode === 'chat' ? 'white' :"rgba(250, 250, 250, 0.5)"}
|
||||
label="Chat"
|
||||
disableWidth
|
||||
>
|
||||
<MessagingIcon
|
||||
height={30}
|
||||
color={
|
||||
hasUnreadDirects
|
||||
(hasUnreadDirects || hasUnreadGroups)
|
||||
? "var(--unread)"
|
||||
: desktopViewMode === 'chat'
|
||||
? "white"
|
||||
@ -282,7 +282,7 @@ export const AppsDevMode = ({ mode, setMode, show , myName, goToHome, setDesktop
|
||||
/>
|
||||
</IconWrapper>
|
||||
</ButtonBase>
|
||||
<Save isDesktop disableWidth />
|
||||
<Save isDesktop disableWidth myName={myName} />
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setDesktopViewMode('dev')
|
||||
@ -332,7 +332,7 @@ export const AppsDevMode = ({ mode, setMode, show , myName, goToHome, setDesktop
|
||||
isSelected={tab?.tabId === selectedTab?.tabId}
|
||||
app={tab}
|
||||
ref={iframeRefs.current[tab.tabId]}
|
||||
isDevMode={true}
|
||||
isDevMode={tab?.service ? false : true}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
AppsContainer,
|
||||
AppsParent,
|
||||
} from "./Apps-styles";
|
||||
import {Buffer} from 'buffer'
|
||||
import { Buffer } from "buffer";
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
@ -29,249 +29,239 @@ import { Spacer } from "../../common/Spacer";
|
||||
import { useModal } from "../../common/useModal";
|
||||
import { createEndpoint, isUsingLocal } from "../../background";
|
||||
import { Label } from "../Group/AddGroup";
|
||||
import ShortUniqueId from "short-unique-id";
|
||||
import swaggerSVG from '../../assets/svgs/swagger.svg'
|
||||
const uid = new ShortUniqueId({ length: 8 });
|
||||
|
||||
export const AppsDevModeHome = ({
|
||||
setMode,
|
||||
myApp,
|
||||
myWebsite,
|
||||
availableQapps,
|
||||
myName
|
||||
myName,
|
||||
}) => {
|
||||
const [domain, setDomain] = useState("127.0.0.1");
|
||||
const [port, setPort] = useState("");
|
||||
const [selectedPreviewFile, setSelectedPreviewFile] = useState(null);
|
||||
|
||||
const [domain, setDomain] = useState("127.0.0.1");
|
||||
const [port, setPort] = useState("");
|
||||
const [selectedPreviewFile, setSelectedPreviewFile] = useState(null);
|
||||
const { isShow, onCancel, onOk, show, message } = useModal();
|
||||
const {
|
||||
openSnackGlobal,
|
||||
setOpenSnackGlobal,
|
||||
infoSnackCustom,
|
||||
setInfoSnackCustom,
|
||||
} = useContext(MyContext);
|
||||
|
||||
const { isShow, onCancel, onOk, show, message } = useModal();
|
||||
const {
|
||||
openSnackGlobal,
|
||||
setOpenSnackGlobal,
|
||||
infoSnackCustom,
|
||||
setInfoSnackCustom,
|
||||
} = useContext(MyContext);
|
||||
const handleSelectFile = async (existingFilePath) => {
|
||||
const filePath = existingFilePath || (await window.electron.selectFile());
|
||||
if (filePath) {
|
||||
const content = await window.electron.readFile(filePath);
|
||||
return { buffer: content, filePath };
|
||||
} else {
|
||||
console.log("No file selected.");
|
||||
}
|
||||
};
|
||||
const handleSelectDirectry = async (existingDirectoryPath) => {
|
||||
const { buffer, directoryPath } =
|
||||
await window.electron.selectAndZipDirectory(existingDirectoryPath);
|
||||
if (buffer) {
|
||||
return { buffer, directoryPath };
|
||||
} else {
|
||||
console.log("No file selected.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectFile = async (existingFilePath) => {
|
||||
const filePath = existingFilePath || await window.electron.selectFile();
|
||||
if (filePath) {
|
||||
|
||||
const content = await window.electron.readFile(filePath);
|
||||
return {buffer: content, filePath}
|
||||
} else {
|
||||
console.log('No file selected.');
|
||||
}
|
||||
};
|
||||
const handleSelectDirectry = async (existingDirectoryPath) => {
|
||||
const {buffer, directoryPath} = await window.electron.selectAndZipDirectory(existingDirectoryPath);
|
||||
if (buffer) {
|
||||
|
||||
|
||||
return {buffer, directoryPath}
|
||||
} else {
|
||||
console.log('No file selected.');
|
||||
}
|
||||
};
|
||||
const addDevModeApp = async () => {
|
||||
try {
|
||||
const usingLocal = await isUsingLocal();
|
||||
if (!usingLocal) {
|
||||
setOpenSnackGlobal(true);
|
||||
|
||||
const addDevModeApp = async () => {
|
||||
try {
|
||||
const usingLocal = await isUsingLocal();
|
||||
if (!usingLocal) {
|
||||
setOpenSnackGlobal(true);
|
||||
|
||||
setInfoSnackCustom({
|
||||
type: "error",
|
||||
message:
|
||||
"Please use your local node for dev mode! Logout and use Local node.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const {portVal, domainVal} = await show({
|
||||
message: "",
|
||||
publishFee: "",
|
||||
setInfoSnackCustom({
|
||||
type: "error",
|
||||
message:
|
||||
"Please use your local node for dev mode! Logout and use Local node.",
|
||||
});
|
||||
const framework = domainVal + ":" + portVal;
|
||||
const response = await fetch(
|
||||
`${getBaseApiReact()}/developer/proxy/start`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
body: framework,
|
||||
}
|
||||
);
|
||||
const responseData = await response.text();
|
||||
executeEvent("appsDevModeAddTab", {
|
||||
data: {
|
||||
url: "http://127.0.0.1:" + responseData,
|
||||
return;
|
||||
}
|
||||
const { portVal, domainVal } = await show({
|
||||
message: "",
|
||||
publishFee: "",
|
||||
});
|
||||
const framework = domainVal + ":" + portVal;
|
||||
const response = await fetch(
|
||||
`${getBaseApiReact()}/developer/proxy/start`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
body: framework,
|
||||
}
|
||||
);
|
||||
const responseData = await response.text();
|
||||
executeEvent("appsDevModeAddTab", {
|
||||
data: {
|
||||
url: "http://127.0.0.1:" + responseData,
|
||||
},
|
||||
});
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
const addPreviewApp = async (isRefresh, existingFilePath, tabId) => {
|
||||
try {
|
||||
const usingLocal = await isUsingLocal();
|
||||
if (!usingLocal) {
|
||||
setOpenSnackGlobal(true);
|
||||
|
||||
setInfoSnackCustom({
|
||||
type: "error",
|
||||
message:
|
||||
"Please use your local node for dev mode! Logout and use Local node.",
|
||||
});
|
||||
} catch (error) {}
|
||||
};
|
||||
return;
|
||||
}
|
||||
if (!myName) {
|
||||
setOpenSnackGlobal(true);
|
||||
|
||||
const addPreviewApp = async (isRefresh, existingFilePath, tabId) => {
|
||||
try {
|
||||
const usingLocal = await isUsingLocal();
|
||||
if (!usingLocal) {
|
||||
setOpenSnackGlobal(true);
|
||||
setInfoSnackCustom({
|
||||
type: "error",
|
||||
message: "You need a name to use preview",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setInfoSnackCustom({
|
||||
type: "error",
|
||||
message:
|
||||
"Please use your local node for dev mode! Logout and use Local node.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!myName) {
|
||||
setOpenSnackGlobal(true);
|
||||
const { buffer, filePath } = await handleSelectFile(existingFilePath);
|
||||
|
||||
setInfoSnackCustom({
|
||||
type: "error",
|
||||
message:
|
||||
"You need a name to use preview",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!buffer) {
|
||||
setOpenSnackGlobal(true);
|
||||
|
||||
|
||||
const {buffer, filePath} = await handleSelectFile(existingFilePath)
|
||||
|
||||
if (!buffer) {
|
||||
setOpenSnackGlobal(true);
|
||||
setInfoSnackCustom({
|
||||
type: "error",
|
||||
message: "Please select a file",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const postBody = Buffer.from(buffer).toString("base64");
|
||||
|
||||
setInfoSnackCustom({
|
||||
type: "error",
|
||||
message:
|
||||
"Please select a file",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const postBody = Buffer.from(buffer).toString('base64')
|
||||
|
||||
const endpoint = await createEndpoint(`/arbitrary/APP/${myName}/zip?preview=true`)
|
||||
const response = await fetch(
|
||||
endpoint
|
||||
,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
body: postBody,
|
||||
}
|
||||
);
|
||||
if(!response?.ok) throw new Error('Invalid zip')
|
||||
const previewPath = await response.text();
|
||||
if(tabId){
|
||||
const endpoint = await createEndpoint(
|
||||
`/arbitrary/APP/${myName}/zip?preview=true`
|
||||
);
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
body: postBody,
|
||||
});
|
||||
if (!response?.ok) throw new Error("Invalid zip");
|
||||
const previewPath = await response.text();
|
||||
if (tabId) {
|
||||
executeEvent("appsDevModeUpdateTab", {
|
||||
data: {
|
||||
url: "http://127.0.0.1:12391" + previewPath,
|
||||
url: "http://127.0.0.1:12391" + previewPath,
|
||||
isPreview: true,
|
||||
filePath,
|
||||
refreshFunc: (tabId)=> {
|
||||
addPreviewApp(true, filePath, tabId)
|
||||
refreshFunc: (tabId) => {
|
||||
addPreviewApp(true, filePath, tabId);
|
||||
},
|
||||
tabId
|
||||
tabId,
|
||||
},
|
||||
});
|
||||
return
|
||||
return;
|
||||
}
|
||||
executeEvent("appsDevModeAddTab", {
|
||||
data: {
|
||||
url: "http://127.0.0.1:12391" + previewPath,
|
||||
isPreview: true,
|
||||
filePath,
|
||||
refreshFunc: (tabId)=> {
|
||||
addPreviewApp(true, filePath, tabId)
|
||||
}
|
||||
executeEvent("appsDevModeAddTab", {
|
||||
data: {
|
||||
url: "http://127.0.0.1:12391" + previewPath,
|
||||
isPreview: true,
|
||||
filePath,
|
||||
refreshFunc: (tabId) => {
|
||||
addPreviewApp(true, filePath, tabId);
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const addPreviewAppWithDirectory = async (isRefresh, existingDir, tabId) => {
|
||||
try {
|
||||
const usingLocal = await isUsingLocal();
|
||||
if (!usingLocal) {
|
||||
setOpenSnackGlobal(true);
|
||||
|
||||
setInfoSnackCustom({
|
||||
type: "error",
|
||||
message:
|
||||
"Please use your local node for dev mode! Logout and use Local node.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return;
|
||||
}
|
||||
};
|
||||
if (!myName) {
|
||||
setOpenSnackGlobal(true);
|
||||
|
||||
const addPreviewAppWithDirectory = async (isRefresh, existingDir, tabId) => {
|
||||
try {
|
||||
const usingLocal = await isUsingLocal();
|
||||
if (!usingLocal) {
|
||||
setOpenSnackGlobal(true);
|
||||
setInfoSnackCustom({
|
||||
type: "error",
|
||||
message: "You need a name to use preview",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setInfoSnackCustom({
|
||||
type: "error",
|
||||
message:
|
||||
"Please use your local node for dev mode! Logout and use Local node.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!myName) {
|
||||
setOpenSnackGlobal(true);
|
||||
const { buffer, directoryPath } = await handleSelectDirectry(existingDir);
|
||||
|
||||
setInfoSnackCustom({
|
||||
type: "error",
|
||||
message:
|
||||
"You need a name to use preview",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!buffer) {
|
||||
setOpenSnackGlobal(true);
|
||||
|
||||
|
||||
const {buffer, directoryPath} = await handleSelectDirectry(existingDir)
|
||||
|
||||
if (!buffer) {
|
||||
setOpenSnackGlobal(true);
|
||||
setInfoSnackCustom({
|
||||
type: "error",
|
||||
message: "Please select a file",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const postBody = Buffer.from(buffer).toString("base64");
|
||||
|
||||
setInfoSnackCustom({
|
||||
type: "error",
|
||||
message:
|
||||
"Please select a file",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const postBody = Buffer.from(buffer).toString('base64')
|
||||
|
||||
const endpoint = await createEndpoint(`/arbitrary/APP/${myName}/zip?preview=true`)
|
||||
const response = await fetch(
|
||||
endpoint
|
||||
,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
body: postBody,
|
||||
}
|
||||
);
|
||||
if(!response?.ok) throw new Error('Invalid zip')
|
||||
const previewPath = await response.text();
|
||||
if(tabId){
|
||||
const endpoint = await createEndpoint(
|
||||
`/arbitrary/APP/${myName}/zip?preview=true`
|
||||
);
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
body: postBody,
|
||||
});
|
||||
if (!response?.ok) throw new Error("Invalid zip");
|
||||
const previewPath = await response.text();
|
||||
if (tabId) {
|
||||
executeEvent("appsDevModeUpdateTab", {
|
||||
data: {
|
||||
url: "http://127.0.0.1:12391" + previewPath,
|
||||
url: "http://127.0.0.1:12391" + previewPath,
|
||||
isPreview: true,
|
||||
directoryPath,
|
||||
refreshFunc: (tabId)=> {
|
||||
addPreviewAppWithDirectory(true, directoryPath, tabId)
|
||||
refreshFunc: (tabId) => {
|
||||
addPreviewAppWithDirectory(true, directoryPath, tabId);
|
||||
},
|
||||
tabId
|
||||
tabId,
|
||||
},
|
||||
});
|
||||
return
|
||||
return;
|
||||
}
|
||||
executeEvent("appsDevModeAddTab", {
|
||||
data: {
|
||||
url: "http://127.0.0.1:12391" + previewPath,
|
||||
isPreview: true,
|
||||
directoryPath,
|
||||
refreshFunc: (tabId)=> {
|
||||
addPreviewAppWithDirectory(true, directoryPath, tabId)
|
||||
}
|
||||
executeEvent("appsDevModeAddTab", {
|
||||
data: {
|
||||
url: "http://127.0.0.1:12391" + previewPath,
|
||||
isPreview: true,
|
||||
directoryPath,
|
||||
refreshFunc: (tabId) => {
|
||||
addPreviewAppWithDirectory(true, directoryPath, tabId);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
};
|
||||
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppsContainer
|
||||
@ -342,67 +332,146 @@ export const AppsDevModeHome = ({
|
||||
<AppCircleLabel>Directory</AppCircleLabel>
|
||||
</AppCircleContainer>
|
||||
</ButtonBase>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
executeEvent("appsDevModeAddTab", {
|
||||
data: {
|
||||
service: "APP",
|
||||
name: "Q-Sandbox",
|
||||
tabId: uid.rnd(),
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<AppCircleContainer
|
||||
sx={{
|
||||
gap: !isMobile ? "10px" : "5px",
|
||||
}}
|
||||
>
|
||||
<AppCircle>
|
||||
<Avatar
|
||||
sx={{
|
||||
height: "42px",
|
||||
width: "42px",
|
||||
"& img": {
|
||||
objectFit: "fill",
|
||||
},
|
||||
}}
|
||||
alt="Q-Sandbox"
|
||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/Q-Sandbox/qortal_avatar?async=true`}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: "31px",
|
||||
height: "auto",
|
||||
}}
|
||||
alt="center-icon"
|
||||
/>
|
||||
</Avatar>
|
||||
</AppCircle>
|
||||
<AppCircleLabel>Q-Sandbox</AppCircleLabel>
|
||||
</AppCircleContainer>
|
||||
</ButtonBase>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
executeEvent("appsDevModeAddTab", {
|
||||
data: {
|
||||
url: "http://127.0.0.1:12391",
|
||||
isPreview: false,
|
||||
customIcon: swaggerSVG
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<AppCircleContainer
|
||||
sx={{
|
||||
gap: !isMobile ? "10px" : "5px",
|
||||
}}
|
||||
>
|
||||
<AppCircle>
|
||||
<Avatar
|
||||
sx={{
|
||||
height: "42px",
|
||||
width: "42px",
|
||||
"& img": {
|
||||
objectFit: "fill",
|
||||
},
|
||||
}}
|
||||
alt="API"
|
||||
src={swaggerSVG}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: "31px",
|
||||
height: "auto",
|
||||
}}
|
||||
alt="center-icon"
|
||||
/>
|
||||
</Avatar>
|
||||
</AppCircle>
|
||||
<AppCircleLabel>API</AppCircleLabel>
|
||||
</AppCircleContainer>
|
||||
</ButtonBase>
|
||||
</AppsContainer>
|
||||
{isShow && (
|
||||
<Dialog
|
||||
open={isShow}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && domain && port) {
|
||||
onOk({ portVal: port, domainVal: domain });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">
|
||||
{"Add custom framework"}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "5px",
|
||||
}}
|
||||
>
|
||||
<Label>Domain</Label>
|
||||
<Input
|
||||
placeholder="Domain"
|
||||
value={domain}
|
||||
onChange={(e) => setDomain(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "5px",
|
||||
marginTop: '15px'
|
||||
}}
|
||||
>
|
||||
<Label>Port</Label>
|
||||
<Input
|
||||
placeholder="Port"
|
||||
value={port}
|
||||
onChange={(e) => setPort(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="contained" onClick={onCancel}>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!domain || !port}
|
||||
variant="contained"
|
||||
onClick={() => onOk({ portVal: port, domainVal: domain })}
|
||||
autoFocus
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
open={isShow}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && domain && port) {
|
||||
onOk({ portVal: port, domainVal: domain });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">
|
||||
{"Add custom framework"}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "5px",
|
||||
}}
|
||||
>
|
||||
<Label>Domain</Label>
|
||||
<Input
|
||||
placeholder="Domain"
|
||||
value={domain}
|
||||
onChange={(e) => setDomain(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "5px",
|
||||
marginTop: "15px",
|
||||
}}
|
||||
>
|
||||
<Label>Port</Label>
|
||||
<Input
|
||||
placeholder="Port"
|
||||
value={port}
|
||||
onChange={(e) => setPort(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="contained" onClick={onCancel}>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!domain || !port}
|
||||
variant="contained"
|
||||
onClick={() => onOk({ portVal: port, domainVal: domain })}
|
||||
autoFocus
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -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 (
|
||||
<ButtonBase onClick={()=> {
|
||||
if(isSelected){
|
||||
executeEvent('removeTabDevMode', {
|
||||
data: app
|
||||
})
|
||||
return
|
||||
}
|
||||
executeEvent('setSelectedTabDevMode', {
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
if (isSelected) {
|
||||
executeEvent("removeTabDevMode", {
|
||||
data: app,
|
||||
isDevMode: true
|
||||
})
|
||||
}}>
|
||||
<TabParent sx={{
|
||||
border: isSelected && '1px solid #FFFFFF'
|
||||
}}>
|
||||
});
|
||||
return;
|
||||
}
|
||||
executeEvent("setSelectedTabDevMode", {
|
||||
data: app,
|
||||
isDevMode: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<TabParent
|
||||
sx={{
|
||||
border: isSelected && "1px solid #FFFFFF",
|
||||
}}
|
||||
>
|
||||
{isSelected && (
|
||||
|
||||
<img style={
|
||||
{
|
||||
position: 'absolute',
|
||||
top: '-5px',
|
||||
right: '-5px',
|
||||
zIndex: 1
|
||||
}
|
||||
} src={NavCloseTab}/>
|
||||
|
||||
) }
|
||||
<Avatar
|
||||
sx={{
|
||||
height: "28px",
|
||||
width: "28px",
|
||||
}}
|
||||
alt=''
|
||||
src={``}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: "28px",
|
||||
height: "auto",
|
||||
}}
|
||||
src={LogoSelected}
|
||||
alt="center-icon"
|
||||
/>
|
||||
</Avatar>
|
||||
</TabParent>
|
||||
<img
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "-5px",
|
||||
right: "-5px",
|
||||
zIndex: 1,
|
||||
}}
|
||||
src={NavCloseTab}
|
||||
/>
|
||||
)}
|
||||
<Avatar
|
||||
sx={{
|
||||
height: "28px",
|
||||
width: "28px",
|
||||
}}
|
||||
alt=""
|
||||
src={``}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: "28px",
|
||||
height: "auto",
|
||||
}}
|
||||
src={ app?.customIcon ? app?.customIcon :
|
||||
app?.service
|
||||
? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
app?.name
|
||||
}/qortal_avatar?async=true`
|
||||
: LogoSelected
|
||||
}
|
||||
alt="center-icon"
|
||||
/>
|
||||
</Avatar>
|
||||
</TabParent>
|
||||
</ButtonBase>
|
||||
)
|
||||
}
|
||||
|
||||
);
|
||||
};
|
||||
|
@ -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 = ({
|
||||
<Spacer height="45px" />
|
||||
<AppsContainer
|
||||
sx={{
|
||||
gap: "75px",
|
||||
gap: "50px",
|
||||
justifyContent: "flex-start",
|
||||
}}
|
||||
>
|
||||
@ -135,7 +137,7 @@ export const AppsHomeDesktop = ({
|
||||
<AppCircleLabel>Library</AppCircleLabel>
|
||||
</AppCircleContainer>
|
||||
</ButtonBase>
|
||||
|
||||
<AppsPrivate myName={myName} />
|
||||
<SortablePinnedApps
|
||||
isDesktop={true}
|
||||
availableQapps={availableQapps}
|
||||
|
@ -24,7 +24,7 @@ import {
|
||||
PublishQAppCTARight,
|
||||
PublishQAppDotsBG,
|
||||
} from "./Apps-styles";
|
||||
import { Avatar, Box, ButtonBase, InputBase, styled } from "@mui/material";
|
||||
import { Avatar, Box, ButtonBase, InputBase, Typography, styled } from "@mui/material";
|
||||
import { Add } from "@mui/icons-material";
|
||||
import { MyContext, getBaseApiReact } from "../../App";
|
||||
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
|
||||
@ -59,7 +59,8 @@ const officialAppList = [
|
||||
"q-trade",
|
||||
"q-support",
|
||||
"nodeinfo",
|
||||
"q-manager"
|
||||
"q-manager",
|
||||
"q-mintership"
|
||||
];
|
||||
|
||||
const ScrollerStyled = styled("div")({
|
||||
@ -122,7 +123,11 @@ export const AppsLibraryDesktop = ({
|
||||
const handler = setTimeout(() => {
|
||||
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 = ({
|
||||
<AppsWidthLimiter>
|
||||
<StyledVirtuosoContainer
|
||||
sx={{
|
||||
height: `calc(100vh - 36px - 90px)`,
|
||||
height: `calc(100vh - 36px - 90px - 90px)`,
|
||||
}}
|
||||
>
|
||||
<Virtuoso
|
||||
@ -270,12 +278,16 @@ export const AppsLibraryDesktop = ({
|
||||
itemContent={rowRenderer}
|
||||
atBottomThreshold={50}
|
||||
followOutput="smooth"
|
||||
components={{
|
||||
Scroller: ScrollerStyled, // Use the styled scroller component
|
||||
}}
|
||||
// components={{
|
||||
// Scroller: ScrollerStyled, // Use the styled scroller component
|
||||
// }}
|
||||
/>
|
||||
</StyledVirtuosoContainer>
|
||||
</AppsWidthLimiter>
|
||||
) : searchedList?.length === 0 && debouncedValue ? (
|
||||
<AppsWidthLimiter>
|
||||
<Typography>No results</Typography>
|
||||
</AppsWidthLimiter>
|
||||
) : (
|
||||
<>
|
||||
<AppLibrarySubTitle
|
||||
@ -286,13 +298,16 @@ export const AppsLibraryDesktop = ({
|
||||
Official Apps
|
||||
</AppLibrarySubTitle>
|
||||
<Spacer height="45px" />
|
||||
<AppsContainer>
|
||||
<AppsContainer sx={{
|
||||
gap: '50px',
|
||||
justifyContent: 'flex-start'
|
||||
}}>
|
||||
{officialApps?.map((qapp) => {
|
||||
return (
|
||||
<ButtonBase
|
||||
sx={{
|
||||
height: "80px",
|
||||
width: "60px",
|
||||
// height: "80px",
|
||||
width: "80px",
|
||||
}}
|
||||
onClick={() => {
|
||||
// executeEvent("addTab", {
|
||||
@ -315,8 +330,8 @@ export const AppsLibraryDesktop = ({
|
||||
>
|
||||
<Avatar
|
||||
sx={{
|
||||
height: "31px",
|
||||
width: "31px",
|
||||
height: "42px",
|
||||
width: "42px",
|
||||
}}
|
||||
alt={qapp?.name}
|
||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
@ -411,6 +426,32 @@ export const AppsLibraryDesktop = ({
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<ButtonBase
|
||||
|
||||
onClick={() => {
|
||||
executeEvent("selectedCategory", {
|
||||
data: {
|
||||
id: 'all',
|
||||
name: 'All'
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "60px",
|
||||
padding: "0px 24px",
|
||||
border: "4px solid #10242F",
|
||||
borderRadius: "6px",
|
||||
boxShadow: "2px 4px 0px 0px #000000",
|
||||
}}
|
||||
>
|
||||
All
|
||||
</Box>
|
||||
</ButtonBase>
|
||||
{categories?.map((category) => {
|
||||
return (
|
||||
<ButtonBase
|
||||
|
@ -31,8 +31,12 @@ import {
|
||||
sortablePinnedAppsAtom,
|
||||
} from "../../atoms/global";
|
||||
|
||||
export function saveToLocalStorage(key, subKey, newValue) {
|
||||
export function saveToLocalStorage(key, subKey, newValue, otherRootData = {}, deleteWholeKey) {
|
||||
try {
|
||||
if(deleteWholeKey){
|
||||
localStorage.setItem(key, null);
|
||||
return
|
||||
}
|
||||
// Fetch existing data
|
||||
const existingData = localStorage.getItem(key);
|
||||
let combinedData = {};
|
||||
@ -43,12 +47,14 @@ export function saveToLocalStorage(key, subKey, newValue) {
|
||||
// Merge with the new data under the subKey
|
||||
combinedData = {
|
||||
...parsedData,
|
||||
...otherRootData,
|
||||
timestamp: Date.now(), // Update the root timestamp
|
||||
[subKey]: newValue, // Assuming the data is an array
|
||||
};
|
||||
} else {
|
||||
// If no existing data, just use the new data under the subKey
|
||||
combinedData = {
|
||||
...otherRootData,
|
||||
timestamp: Date.now(), // Set the initial root timestamp
|
||||
[subKey]: newValue,
|
||||
};
|
||||
|
@ -133,10 +133,19 @@ export const AppsNavBarDesktop = ({disableBack}) => {
|
||||
|
||||
|
||||
|
||||
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 (
|
||||
<AppsNavBarParent
|
||||
@ -282,22 +291,49 @@ export const AppsNavBarDesktop = ({disableBack}) => {
|
||||
|
||||
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}) => {
|
||||
<PushPinIcon
|
||||
height={20}
|
||||
sx={{
|
||||
color: isSelectedAppPinned ? "red" : "rgba(250, 250, 250, 0.5)",
|
||||
color: isSelectedAppPinned ? "var(--danger)" : "rgba(250, 250, 250, 0.5)",
|
||||
}}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
@ -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}) => {
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
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"
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
executeEvent("copyLink", {
|
||||
tabId: selectedTab?.tabId,
|
||||
});
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
minWidth: "24px !important",
|
||||
marginRight: "5px",
|
||||
{!selectedTab?.isPrivate && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
executeEvent("copyLink", {
|
||||
tabId: selectedTab?.tabId,
|
||||
});
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<ContentCopyIcon
|
||||
height={20}
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
color: "rgba(250, 250, 250, 0.5)",
|
||||
minWidth: "24px !important",
|
||||
marginRight: "5px",
|
||||
}}
|
||||
>
|
||||
<ContentCopyIcon
|
||||
height={20}
|
||||
sx={{
|
||||
color: "rgba(250, 250, 250, 0.5)",
|
||||
}}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
sx={{
|
||||
"& .MuiTypography-root": {
|
||||
fontSize: "12px",
|
||||
fontWeight: 600,
|
||||
color: "rgba(250, 250, 250, 0.5)",
|
||||
},
|
||||
}}
|
||||
primary="Copy link"
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
sx={{
|
||||
"& .MuiTypography-root": {
|
||||
fontSize: "12px",
|
||||
fontWeight: 600,
|
||||
color: "rgba(250, 250, 250, 0.5)",
|
||||
},
|
||||
}}
|
||||
primary="Copy link"
|
||||
/>
|
||||
</MenuItem>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
</AppsNavBarParent>
|
||||
);
|
||||
|
542
src/components/Apps/AppsPrivate.tsx
Normal file
@ -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 (
|
||||
<>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setIsOpenPrivateModal(true);
|
||||
}}
|
||||
sx={{
|
||||
width: "80px",
|
||||
}}
|
||||
>
|
||||
<AppCircleContainer
|
||||
sx={{
|
||||
gap: !isMobile ? "10px" : "5px",
|
||||
}}
|
||||
>
|
||||
<AppCircle>
|
||||
<Add>+</Add>
|
||||
</AppCircle>
|
||||
<AppCircleLabel>Private</AppCircleLabel>
|
||||
</AppCircleContainer>
|
||||
</ButtonBase>
|
||||
{isOpenPrivateModal && (
|
||||
<Dialog
|
||||
open={isOpenPrivateModal}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
if (valueTabPrivateApp === 0) {
|
||||
if (
|
||||
!privateAppValues.name ||
|
||||
!privateAppValues.service ||
|
||||
!privateAppValues.identifier ||
|
||||
!privateAppValues?.groupId
|
||||
)
|
||||
return;
|
||||
addPrivateApp();
|
||||
}
|
||||
}
|
||||
}}
|
||||
maxWidth="md"
|
||||
fullWidth={true}
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">
|
||||
{valueTabPrivateApp === 0
|
||||
? "Access private app"
|
||||
: "Publish private app"}
|
||||
</DialogTitle>
|
||||
|
||||
<Box>
|
||||
<Tabs
|
||||
value={valueTabPrivateApp}
|
||||
onChange={handleChange}
|
||||
aria-label="basic tabs example"
|
||||
variant={isMobile ? "scrollable" : "fullWidth"} // Scrollable on mobile, full width on desktop
|
||||
scrollButtons="auto"
|
||||
allowScrollButtonsMobile
|
||||
sx={{
|
||||
"& .MuiTabs-indicator": {
|
||||
backgroundColor: "white",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tab
|
||||
label="Access app"
|
||||
{...a11yProps(0)}
|
||||
sx={{
|
||||
"&.Mui-selected": {
|
||||
color: "white",
|
||||
},
|
||||
fontSize: isMobile ? "0.75rem" : "1rem", // Adjust font size for mobile
|
||||
}}
|
||||
/>
|
||||
<Tab
|
||||
label="Publish app"
|
||||
{...a11yProps(1)}
|
||||
sx={{
|
||||
"&.Mui-selected": {
|
||||
color: "white",
|
||||
},
|
||||
fontSize: isMobile ? "0.75rem" : "1rem", // Adjust font size for mobile
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
</Box>
|
||||
{valueTabPrivateApp === 0 && (
|
||||
<>
|
||||
<DialogContent>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "5px",
|
||||
}}
|
||||
>
|
||||
<Label>Select a group</Label>
|
||||
<Label>Only private groups will be shown</Label>
|
||||
<Select
|
||||
labelId="demo-simple-select-label"
|
||||
id="demo-simple-select"
|
||||
value={privateAppValues?.groupId}
|
||||
label="Groups"
|
||||
onChange={(e) => {
|
||||
setPrivateAppValues((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
groupId: e.target.value,
|
||||
};
|
||||
});
|
||||
}}
|
||||
>
|
||||
<MenuItem value={0}>No group selected</MenuItem>
|
||||
|
||||
{memberGroups
|
||||
?.filter((item) => !item?.isOpen)
|
||||
.map((group) => {
|
||||
return (
|
||||
<MenuItem key={group?.groupId} value={group?.groupId}>
|
||||
{group?.groupName}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</Box>
|
||||
<Spacer height="10px" />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "5px",
|
||||
marginTop: "15px",
|
||||
}}
|
||||
>
|
||||
<Label>name</Label>
|
||||
<Input
|
||||
placeholder="name"
|
||||
value={privateAppValues?.name}
|
||||
onChange={(e) =>
|
||||
setPrivateAppValues((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
name: e.target.value,
|
||||
};
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "5px",
|
||||
marginTop: "15px",
|
||||
}}
|
||||
>
|
||||
<Label>identifier</Label>
|
||||
<Input
|
||||
placeholder="identifier"
|
||||
value={privateAppValues?.identifier}
|
||||
onChange={(e) =>
|
||||
setPrivateAppValues((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
identifier: e.target.value,
|
||||
};
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
setIsOpenPrivateModal(false);
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
disabled={
|
||||
!privateAppValues.name ||
|
||||
!privateAppValues.service ||
|
||||
!privateAppValues.identifier ||
|
||||
!privateAppValues?.groupId
|
||||
}
|
||||
variant="contained"
|
||||
onClick={() => addPrivateApp()}
|
||||
autoFocus
|
||||
>
|
||||
Access
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</>
|
||||
)}
|
||||
{valueTabPrivateApp === 1 && (
|
||||
<>
|
||||
<DialogContent>
|
||||
<PublishQAppInfo
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
Select .zip file containing static content:{" "}
|
||||
</PublishQAppInfo>
|
||||
<Spacer height="10px" />
|
||||
<PublishQAppInfo
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>{`
|
||||
50mb MB maximum`}</PublishQAppInfo>
|
||||
{file && (
|
||||
<>
|
||||
<Spacer height="5px" />
|
||||
<PublishQAppInfo>{`Selected: (${file?.name})`}</PublishQAppInfo>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Spacer height="18px" />
|
||||
<PublishQAppChoseFile {...getRootProps()}>
|
||||
{" "}
|
||||
<input {...getInputProps()} />
|
||||
{file ? "Change" : "Choose"} File
|
||||
</PublishQAppChoseFile>
|
||||
<Spacer height="20px" />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "5px",
|
||||
}}
|
||||
>
|
||||
<Label>Select a group</Label>
|
||||
<Label>
|
||||
Only groups where you are an admin will be shown
|
||||
</Label>
|
||||
<Select
|
||||
labelId="demo-simple-select-label"
|
||||
id="demo-simple-select"
|
||||
value={selectedGroup}
|
||||
label="Groups where you are an admin"
|
||||
onChange={(e) => setSelectedGroup(e.target.value)}
|
||||
>
|
||||
<MenuItem value={0}>No group selected</MenuItem>
|
||||
{myGroupsWhereIAmAdmin
|
||||
?.filter((item) => !item?.isOpen)
|
||||
.map((group) => {
|
||||
return (
|
||||
<MenuItem key={group?.groupId} value={group?.groupId}>
|
||||
{group?.groupName}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</Box>
|
||||
<Spacer height="20px" />
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "5px",
|
||||
marginTop: "15px",
|
||||
}}
|
||||
>
|
||||
<Label>identifier</Label>
|
||||
<Input
|
||||
placeholder="identifier"
|
||||
value={newPrivateAppValues?.identifier}
|
||||
onChange={(e) =>
|
||||
setNewPrivateAppValues((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
identifier: e.target.value,
|
||||
};
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<Spacer height="10px" />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "5px",
|
||||
marginTop: "15px",
|
||||
}}
|
||||
>
|
||||
<Label>App name</Label>
|
||||
<Input
|
||||
placeholder="App name"
|
||||
value={newPrivateAppValues?.name}
|
||||
onChange={(e) =>
|
||||
setNewPrivateAppValues((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
name: e.target.value,
|
||||
};
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Spacer height="10px" />
|
||||
<ImageUploader onPick={(file) => setLogo(file)}>
|
||||
<Button variant="contained">Choose logo</Button>
|
||||
</ImageUploader>
|
||||
{logo?.name}
|
||||
<Spacer height="25px" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
setIsOpenPrivateModal(false);
|
||||
clearFields();
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
disabled={
|
||||
!newPrivateAppValues.name ||
|
||||
!newPrivateAppValues.service ||
|
||||
!newPrivateAppValues.identifier ||
|
||||
!selectedGroup
|
||||
}
|
||||
variant="contained"
|
||||
onClick={() => publishPrivateApp()}
|
||||
autoFocus
|
||||
>
|
||||
Publish
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</>
|
||||
)}
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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 (
|
||||
<ContextMenuPinnedApps app={app} isMine={!!app?.isMine}>
|
||||
<ButtonBase
|
||||
<ButtonBase
|
||||
ref={setNodeRef} {...attributes} {...listeners}
|
||||
sx={{
|
||||
height: "80px",
|
||||
width: "60px",
|
||||
width: "80px",
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
}}
|
||||
onClick={()=> {
|
||||
executeEvent("addTab", {
|
||||
data: app
|
||||
})
|
||||
onClick={async ()=> {
|
||||
if(app?.isPrivate){
|
||||
try {
|
||||
await openApp(app?.privateAppProperties)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
} else {
|
||||
executeEvent("addTab", {
|
||||
data: app
|
||||
})
|
||||
}
|
||||
|
||||
}}
|
||||
>
|
||||
<AppCircleContainer sx={{
|
||||
@ -51,16 +63,24 @@ const SortableItem = ({ id, name, app, isDesktop }) => {
|
||||
border: "none",
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
{app?.isPrivate && !app?.privateAppProperties?.logo ? (
|
||||
<LockIcon
|
||||
sx={{
|
||||
height: "42px",
|
||||
width: "42px",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Avatar
|
||||
sx={{
|
||||
height: "31px",
|
||||
width: "31px",
|
||||
height: "42px",
|
||||
width: "42px",
|
||||
'& img': {
|
||||
objectFit: 'fill',
|
||||
}
|
||||
}}
|
||||
alt={app?.metadata?.title || app?.name}
|
||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
src={ app?.privateAppProperties?.logo ? app?.privateAppProperties?.logo :`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
app?.name
|
||||
}/qortal_avatar?async=true`}
|
||||
>
|
||||
@ -73,10 +93,19 @@ const SortableItem = ({ id, name, app, isDesktop }) => {
|
||||
alt="center-icon"
|
||||
/>
|
||||
</Avatar>
|
||||
)}
|
||||
|
||||
</AppCircle>
|
||||
<AppCircleLabel>
|
||||
{app?.isPrivate ? (
|
||||
<AppCircleLabel>
|
||||
{`${app?.privateAppProperties?.appName || "Private"}`}
|
||||
</AppCircleLabel>
|
||||
) : (
|
||||
<AppCircleLabel>
|
||||
{app?.metadata?.title || app?.name}
|
||||
</AppCircleLabel>
|
||||
)}
|
||||
|
||||
</AppCircleContainer>
|
||||
</ButtonBase>
|
||||
</ContextMenuPinnedApps>
|
||||
|
@ -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}/>
|
||||
|
||||
) }
|
||||
<Avatar
|
||||
sx={{
|
||||
height: "28px",
|
||||
width: "28px",
|
||||
}}
|
||||
alt={app?.name}
|
||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
app?.name
|
||||
}/qortal_avatar?async=true`}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: "28px",
|
||||
height: "auto",
|
||||
}}
|
||||
src={LogoSelected}
|
||||
alt="center-icon"
|
||||
/>
|
||||
</Avatar>
|
||||
{app?.isPrivate && !app?.privateAppProperties?.logo ? (
|
||||
<LockIcon
|
||||
sx={{
|
||||
height: "28px",
|
||||
width: "28px",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Avatar
|
||||
sx={{
|
||||
height: "28px",
|
||||
width: "28px",
|
||||
}}
|
||||
alt={app?.name}
|
||||
src={app?.privateAppProperties?.logo ? app?.privateAppProperties?.logo :`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
app?.name
|
||||
}/qortal_avatar?async=true`}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: "28px",
|
||||
height: "auto",
|
||||
}}
|
||||
src={LogoSelected}
|
||||
alt="center-icon"
|
||||
/>
|
||||
</Avatar>
|
||||
)}
|
||||
</TabParent>
|
||||
</ButtonBase>
|
||||
)
|
||||
|
237
src/components/Apps/useHandlePrivateApps.tsx
Normal file
@ -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,
|
||||
};
|
||||
};
|
@ -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 },
|
||||
|
@ -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'
|
||||
}}><Typography>Sorry, this space is only for Admins.</Typography></Box>}
|
||||
{isAdmin && <AdminSpaceInner adminsWithNames={adminsWithNames} selectedGroup={selectedGroup} />}
|
||||
{isAdmin && <AdminSpaceInner setIsForceShowCreationKeyPopup={setIsForceShowCreationKeyPopup} adminsWithNames={adminsWithNames} selectedGroup={selectedGroup} />}
|
||||
|
||||
</div>
|
||||
);
|
||||
|
@ -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 (
|
||||
<Box sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: '10px'
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
padding: "10px",
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Typography sx={{
|
||||
fontSize: '14px'
|
||||
}}>Reminder: After publishing the key, it will take a couple of minutes for it to appear. Please just wait.</Typography>
|
||||
<Spacer height="25px" />
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '20px',
|
||||
width: '300px',
|
||||
maxWidth: '90%'
|
||||
}}>
|
||||
{isFetchingAdminGroupSecretKey && <Typography>Fetching Admins secret keys</Typography>}
|
||||
{!isFetchingAdminGroupSecretKey && !adminGroupSecretKey && <Typography>No secret key published yet</Typography>}
|
||||
{adminGroupSecretKeyPublishDetails && (
|
||||
<Typography>Last encryption date: {formatTimestampForum(adminGroupSecretKeyPublishDetails?.updated || adminGroupSecretKeyPublishDetails?.created)}</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "20px",
|
||||
width: "300px",
|
||||
maxWidth: "90%",
|
||||
padding: '10px',
|
||||
border: '1px solid gray',
|
||||
borderRadius: '6px'
|
||||
}}
|
||||
>
|
||||
{isFetchingGroupSecretKey && (
|
||||
<Typography>Fetching Group secret key publishes</Typography>
|
||||
)}
|
||||
<Button onClick={createCommonSecretForAdmins} variant="contained">Publish admin secret key</Button>
|
||||
{!isFetchingGroupSecretKey &&
|
||||
groupSecretKeyPublishDetails === false && (
|
||||
<Typography>No secret key published yet</Typography>
|
||||
)}
|
||||
{groupSecretKeyPublishDetails && (
|
||||
<Typography>
|
||||
Last encryption date:{" "}
|
||||
{formatTimestampForum(
|
||||
groupSecretKeyPublishDetails?.updated ||
|
||||
groupSecretKeyPublishDetails?.created
|
||||
)}{" "}
|
||||
{` by ${groupSecretKeyPublishDetails?.name}`}
|
||||
</Typography>
|
||||
)}
|
||||
<Button disabled={isFetchingGroupSecretKey} onClick={()=> setIsForceShowCreationKeyPopup(true)} variant="contained">
|
||||
Publish group secret key
|
||||
</Button>
|
||||
<Spacer height="20px" />
|
||||
<Typography sx={{
|
||||
fontSize: '14px'
|
||||
}}>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.</Typography>
|
||||
</Box>
|
||||
<Spacer height="25px" />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "20px",
|
||||
width: "300px",
|
||||
maxWidth: "90%",
|
||||
padding: '10px',
|
||||
border: '1px solid gray',
|
||||
borderRadius: '6px'
|
||||
}}
|
||||
>
|
||||
{isFetchingAdminGroupSecretKey && (
|
||||
<Typography>Fetching Admins secret key</Typography>
|
||||
)}
|
||||
{!isFetchingAdminGroupSecretKey && !adminGroupSecretKey && (
|
||||
<Typography>No secret key published yet</Typography>
|
||||
)}
|
||||
{adminGroupSecretKeyPublishDetails && (
|
||||
<Typography>
|
||||
Last encryption date:{" "}
|
||||
{formatTimestampForum(
|
||||
adminGroupSecretKeyPublishDetails?.updated ||
|
||||
adminGroupSecretKeyPublishDetails?.created
|
||||
)}
|
||||
</Typography>
|
||||
)}
|
||||
<Button disabled={isFetchingAdminGroupSecretKey} onClick={createCommonSecretForAdmins} variant="contained">
|
||||
Publish admin secret key
|
||||
</Button>
|
||||
<Spacer height="20px" />
|
||||
<Typography sx={{
|
||||
fontSize: '14px'
|
||||
}}>This key is to encrypt ADMIN related content. Only admins would see content encrypted with it.</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@ -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 (
|
||||
<div
|
||||
style={{
|
||||
|
@ -118,9 +118,9 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi
|
||||
data: encryptedMessages,
|
||||
involvingAddress: selectedDirect?.address,
|
||||
})
|
||||
.then((response) => {
|
||||
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(() => {
|
||||
)}
|
||||
|
||||
<Tiptap isFocusedParent={isFocusedParent} setEditorRef={setEditorRef} onEnter={sendMessage} isChat disableEnter={isMobile ? true : false} setIsFocusedParent={setIsFocusedParent}/>
|
||||
</div>
|
||||
{messageSize > 750 && (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
justifyContent: 'flex-end',
|
||||
justifyContent: 'flex-start',
|
||||
position: 'relative',
|
||||
}}>
|
||||
<Typography sx={{
|
||||
fontSize: '12px',
|
||||
color: messageSize > 4000 ? 'var(--unread)' : 'unset'
|
||||
color: messageSize > 4000 ? 'var(--danger)' : 'unset'
|
||||
}}>{`Your message size is of ${messageSize} bytes out of a maximum of 4000`}</Typography>
|
||||
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
width: '100px',
|
||||
@ -673,7 +675,6 @@ useEffect(() => {
|
||||
|
||||
<CustomButton
|
||||
onClick={()=> {
|
||||
if(messageSize > 4000) return
|
||||
|
||||
if(isSending) return
|
||||
sendMessage()
|
||||
|
@ -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: "<p>First group key created.</p>"
|
||||
} : {}
|
||||
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: "<p>First group key created.</p>"
|
||||
} : {}
|
||||
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() === '<p></p>') 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',
|
||||
}}>
|
||||
|
||||
<ChatList hasSecretKey={!!secretKey} openQManager={openQManager} enableMentions onReply={onReply} onEdit={onEdit} chatId={selectedGroup} initialMessages={messages} myAddress={myAddress} tempMessages={tempMessages} handleReaction={handleReaction} chatReferences={chatReferences} tempChatReferences={tempChatReferences} members={members} myName={myName} selectedGroup={selectedGroup} />
|
||||
<ChatList isPrivate={isPrivate} hasSecretKey={!!secretKey} openQManager={openQManager} enableMentions onReply={onReply} onEdit={onEdit} chatId={selectedGroup} initialMessages={messages} myAddress={myAddress} tempMessages={tempMessages} handleReaction={handleReaction} chatReferences={chatReferences} tempChatReferences={tempChatReferences} members={members} myName={myName} selectedGroup={selectedGroup} />
|
||||
|
||||
{!!secretKey && (
|
||||
{(!!secretKey || isPrivate === false) && (
|
||||
<div style={{
|
||||
// position: 'fixed',
|
||||
// bottom: '0px',
|
||||
@ -822,22 +887,22 @@ const clearEditorContent = () => {
|
||||
|
||||
|
||||
<Tiptap enableMentions setEditorRef={setEditorRef} onEnter={sendMessage} isChat disableEnter={isMobile ? true : false} isFocusedParent={isFocusedParent} setIsFocusedParent={setIsFocusedParent} membersWithNames={members} />
|
||||
|
||||
</div>
|
||||
{messageSize > 750 && (
|
||||
{messageSize > 750 && (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
justifyContent: 'flex-end',
|
||||
justifyContent: 'flex-start',
|
||||
position: 'relative',
|
||||
}}>
|
||||
<Typography sx={{
|
||||
fontSize: '12px',
|
||||
color: messageSize > 4000 ? 'var(--unread)' : 'unset'
|
||||
color: messageSize > 4000 ? 'var(--danger)' : 'unset'
|
||||
}}>{`Your message size is of ${messageSize} bytes out of a maximum of 4000`}</Typography>
|
||||
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
width: '100px',
|
||||
@ -849,7 +914,7 @@ const clearEditorContent = () => {
|
||||
|
||||
<CustomButton
|
||||
onClick={()=> {
|
||||
if(messageSize > 4000) return
|
||||
|
||||
if(isSending) return
|
||||
sendMessage()
|
||||
}}
|
||||
@ -927,7 +992,8 @@ const clearEditorContent = () => {
|
||||
<AppViewerContainer customHeight="560px" app={{
|
||||
tabId: '5558588',
|
||||
name: 'Q-Manager',
|
||||
service: 'APP'
|
||||
service: 'APP',
|
||||
path: `?groupId=${selectedGroup}`
|
||||
}} isSelected />
|
||||
</Box>
|
||||
</Box>
|
||||
|
@ -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 (
|
||||
<div
|
||||
data-index={virtualRow.index} //needed for dynamic row height measurement
|
||||
@ -358,6 +359,7 @@ export const ChatList = ({
|
||||
handleReaction={handleReaction}
|
||||
reactions={reactions}
|
||||
isUpdating={isUpdating}
|
||||
isPrivate={isPrivate}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
@ -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 = ({
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{enableMentions && hasSecretKey && (
|
||||
{enableMentions && (hasSecretKey || isPrivate === false) && (
|
||||
<ChatOptions
|
||||
openQManager={openQManager}
|
||||
messages={messages}
|
||||
@ -417,6 +419,7 @@ export const ChatList = ({
|
||||
members={members}
|
||||
myName={myName}
|
||||
selectedGroup={selectedGroup}
|
||||
isPrivate={isPrivate}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
@ -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",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
padding: "0px 20px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "15px",
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
sx={{
|
||||
backgroundColor: "#27282c",
|
||||
color: "white",
|
||||
height: "25px",
|
||||
width: "25px",
|
||||
}}
|
||||
alt={message?.senderName}
|
||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
message?.senderName
|
||||
}/qortal_avatar?async=true`}
|
||||
>
|
||||
{message?.senderName?.charAt(0)}
|
||||
</Avatar>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWight: 600,
|
||||
fontFamily: "Inter",
|
||||
color: "cadetBlue",
|
||||
}}
|
||||
>
|
||||
{message?.senderName}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Spacer height="5px" />
|
||||
<Typography sx={{
|
||||
fontSize: '12px'
|
||||
}}>{formatTimestamp(message.timestamp)}</Typography>
|
||||
<Box
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
const findMsgIndex = messages.findIndex(
|
||||
(item) =>
|
||||
item?.signature === message?.signature
|
||||
);
|
||||
if (findMsgIndex !== -1) {
|
||||
goToMessage(findMsgIndex);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MessageDisplay
|
||||
htmlContent={
|
||||
message?.decryptedData?.message || "<p></p>"
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<ShowMessage messages={messages} goToMessage={goToMessage} message={message} />
|
||||
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@ -544,6 +507,7 @@ export const ChatOptions = ({ messages, goToMessage, members, myName, selectedGr
|
||||
const index = virtualRow.index;
|
||||
let message = searchedList[index];
|
||||
return (
|
||||
|
||||
<div
|
||||
data-index={virtualRow.index} //needed for dynamic row height measurement
|
||||
ref={rowVirtualizer.measureElement} //measure dynamic row height
|
||||
@ -562,7 +526,150 @@ export const ChatOptions = ({ messages, goToMessage, members, myName, selectedGr
|
||||
gap: "5px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
<ErrorBoundary
|
||||
fallback={
|
||||
<Typography>
|
||||
Error loading content: Invalid Data
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<ShowMessage message={message} goToMessage={goToMessage} messages={messages} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: "50px",
|
||||
height: "100%",
|
||||
gap: "20px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
padding: "10px",
|
||||
gap: "20px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#1F2023",
|
||||
borderBottomLeftRadius: "20px",
|
||||
borderTopLeftRadius: "20px",
|
||||
minHeight: "200px",
|
||||
}}
|
||||
>
|
||||
<ButtonBase onClick={() => {
|
||||
setMode("search")
|
||||
}}>
|
||||
<Tooltip
|
||||
title={<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>SEARCH</span>}
|
||||
placement="left"
|
||||
arrow
|
||||
sx={{ fontSize: "24" }}
|
||||
slotProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
color: "#ffffff",
|
||||
backgroundColor: "#444444",
|
||||
},
|
||||
},
|
||||
arrow: {
|
||||
sx: {
|
||||
color: "#444444",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SearchIcon />
|
||||
</Tooltip>
|
||||
</ButtonBase>
|
||||
<ButtonBase onClick={() => {
|
||||
setMode("default")
|
||||
setSearchValue('')
|
||||
setSelectedMember(0)
|
||||
openQManager()
|
||||
}}>
|
||||
<Tooltip
|
||||
title={<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>Q-MANAGER</span>}
|
||||
placement="left"
|
||||
arrow
|
||||
sx={{ fontSize: "24" }}
|
||||
slotProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
color: "#ffffff",
|
||||
backgroundColor: "#444444",
|
||||
},
|
||||
},
|
||||
arrow: {
|
||||
sx: {
|
||||
color: "#444444",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<InsertLinkIcon sx={{ color: 'white' }} />
|
||||
</Tooltip>
|
||||
</ButtonBase>
|
||||
<ContextMenuMentions getTimestampMention={getTimestampMention} groupId={selectedGroup}>
|
||||
<ButtonBase onClick={() => {
|
||||
setMode("mentions")
|
||||
setSearchValue('')
|
||||
setSelectedMember(0)
|
||||
}}>
|
||||
<Tooltip
|
||||
title={<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>MENTIONED</span>}
|
||||
placement="left"
|
||||
arrow
|
||||
sx={{ fontSize: "24" }}
|
||||
slotProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
color: "#ffffff",
|
||||
backgroundColor: "#444444",
|
||||
},
|
||||
},
|
||||
arrow: {
|
||||
sx: {
|
||||
color: "#444444",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AlternateEmailIcon sx={{
|
||||
color: mentionList?.length > 0 && (!lastMentionTimestamp || lastMentionTimestamp < mentionList[0]?.timestamp) ? 'var(--unread)' : 'white'
|
||||
}} />
|
||||
</Tooltip>
|
||||
</ButtonBase>
|
||||
|
||||
</ContextMenuMentions>
|
||||
|
||||
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const ShowMessage = ({message, goToMessage, messages})=> {
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
@ -628,80 +735,20 @@ export const ChatOptions = ({ messages, goToMessage, members, myName, selectedGr
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MessageDisplay
|
||||
htmlContent={
|
||||
message?.decryptedData?.message || "<p></p>"
|
||||
}
|
||||
/>
|
||||
{message?.messageText && (
|
||||
<MessageDisplay
|
||||
htmlContent={message?.messageText}
|
||||
/>
|
||||
)}
|
||||
{message?.decryptedData?.message && (
|
||||
<MessageDisplay
|
||||
htmlContent={
|
||||
message?.decryptedData?.message || "<p></p>"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: "50px",
|
||||
height: "100%",
|
||||
gap: "20px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
padding: "10px",
|
||||
gap: "20px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#1F2023",
|
||||
borderBottomLeftRadius: "20px",
|
||||
borderTopLeftRadius: "20px",
|
||||
minHeight: "200px",
|
||||
}}
|
||||
>
|
||||
<ButtonBase onClick={() => {
|
||||
setMode("search")
|
||||
}}>
|
||||
<SearchIcon />
|
||||
</ButtonBase>
|
||||
<ButtonBase onClick={() => {
|
||||
setMode("default")
|
||||
setSearchValue('')
|
||||
setSelectedMember(0)
|
||||
openQManager()
|
||||
}}>
|
||||
<InsertLinkIcon sx={{
|
||||
color: 'white'
|
||||
}} />
|
||||
</ButtonBase>
|
||||
<ContextMenuMentions getTimestampMention={getTimestampMention} groupId={selectedGroup}>
|
||||
<ButtonBase onClick={() => {
|
||||
setMode("mentions")
|
||||
setSearchValue('')
|
||||
setSelectedMember(0)
|
||||
}}>
|
||||
<AlternateEmailIcon sx={{
|
||||
color: mentionList?.length > 0 && (!lastMentionTimestamp || lastMentionTimestamp < mentionList[0]?.timestamp) ? 'var(--unread)' : 'white'
|
||||
}} />
|
||||
</ButtonBase>
|
||||
|
||||
</ContextMenuMentions>
|
||||
|
||||
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
@ -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
|
||||
<Box>
|
||||
<Typography>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</Typography>
|
||||
</Box>
|
||||
): (
|
||||
): isForceShowCreationKeyPopup ? null : (
|
||||
<Box>
|
||||
<Typography>The group member list has changed. Please re-encrypt the secret key.</Typography>
|
||||
</Box>
|
||||
@ -173,6 +176,7 @@ export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, sec
|
||||
}}>
|
||||
<Button onClick={()=> {
|
||||
setHideCommonKeyPopup(true)
|
||||
setIsForceShowCreationKeyPopup(false)
|
||||
}} size='small'>Hide</Button>
|
||||
</Box>
|
||||
<CustomizedSnackbars open={openSnack} setOpen={setOpenSnack} info={infoSnack} setInfo={setInfoSnack} />
|
||||
|
@ -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}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@ -641,7 +648,7 @@ export const GroupAnnouncements = ({
|
||||
marginTop: "auto",
|
||||
alignSelf: "center",
|
||||
cursor: isSending ? "default" : "pointer",
|
||||
background: "red",
|
||||
background: "var(--danger)",
|
||||
flexShrink: 0,
|
||||
padding: isMobile && "5px",
|
||||
fontSize: isMobile && "14px",
|
||||
|
@ -24,7 +24,8 @@ export const GroupForum = ({
|
||||
myAddress,
|
||||
hide,
|
||||
defaultThread,
|
||||
setDefaultThread
|
||||
setDefaultThread,
|
||||
isPrivate
|
||||
}) => {
|
||||
const { rootHeight } = useContext(MyContext);
|
||||
const [isMoved, setIsMoved] = useState(false);
|
||||
@ -50,7 +51,7 @@ export const GroupForum = ({
|
||||
left: hide && '-1000px'
|
||||
}}
|
||||
>
|
||||
<GroupMail hide={hide} getSecretKey={getSecretKey} selectedGroup={selectedGroup} userInfo={userInfo} secretKey={secretKey} defaultThread={defaultThread} setDefaultThread={setDefaultThread} />
|
||||
<GroupMail isPrivate={isPrivate} hide={hide} getSecretKey={getSecretKey} selectedGroup={selectedGroup} userInfo={userInfo} secretKey={secretKey} defaultThread={defaultThread} setDefaultThread={setDefaultThread} />
|
||||
|
||||
</div>
|
||||
);
|
||||
|
@ -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 `<a href="${DOMPurify.sanitize(href)}" class="auto-link">${DOMPurify.sanitize(url)}</a>`;
|
||||
});
|
||||
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(/<span[^>]*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 `<a href="${DOMPurify.sanitize(href)}" class="auto-link">${DOMPurify.sanitize(url)}</a>`;
|
||||
});
|
||||
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(/<span[^>]*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;
|
||||
|
||||
|
@ -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 && (
|
||||
<div className="unread-divider" id="unread-divider-id">
|
||||
Unread messages below
|
||||
</div>
|
||||
)}
|
||||
|
||||
<MessageWragger lastMessage={lastSignature === message?.signature} isLast={isLast} onSeen={onSeenFunc}>
|
||||
|
||||
|
||||
<div
|
||||
ref={lastSignature === message?.signature ? ref : null}
|
||||
style={{
|
||||
padding: "10px",
|
||||
backgroundColor: "#232428",
|
||||
@ -79,24 +161,43 @@ export const MessageItem = ({
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '20px',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<WrapperUserAction
|
||||
disabled={myAddress === message?.sender}
|
||||
address={message?.sender}
|
||||
name={message?.senderName}
|
||||
>
|
||||
|
||||
<Avatar
|
||||
sx={{
|
||||
backgroundColor: "#27282c",
|
||||
color: "white",
|
||||
height: '40px',
|
||||
width: '40px'
|
||||
}}
|
||||
alt={message?.senderName}
|
||||
src={message?.senderName ? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
message?.senderName
|
||||
}/qortal_avatar?async=true` : ''}
|
||||
src={userAvatarUrl}
|
||||
>
|
||||
{message?.senderName?.charAt(0)}
|
||||
</Avatar>
|
||||
|
||||
|
||||
</WrapperUserAction>
|
||||
<Tooltip disableFocusListener title={`level ${userInfo}`}>
|
||||
|
||||
|
||||
<img style={{
|
||||
visibility: userInfo !== undefined ? 'visible' : 'hidden',
|
||||
width: '30px',
|
||||
height: 'auto'
|
||||
}} src={getBadgeImg(userInfo)} />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box
|
||||
@ -136,7 +237,7 @@ export const MessageItem = ({
|
||||
gap: '10px',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
{message?.sender === myAddress && !message?.isNotEncrypted && (
|
||||
{message?.sender === myAddress && (!message?.isNotEncrypted || isPrivate === false) && (
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
onEdit(message);
|
||||
@ -201,11 +302,7 @@ export const MessageItem = ({
|
||||
}}>Replied to {reply?.senderName || reply?.senderAddress}</Typography>
|
||||
{reply?.messageText && (
|
||||
<MessageDisplay
|
||||
htmlContent={generateHTML(reply?.messageText, [
|
||||
StarterKit,
|
||||
Underline,
|
||||
Highlight,
|
||||
])}
|
||||
htmlContent={htmlReply}
|
||||
/>
|
||||
)}
|
||||
{reply?.decryptedData?.type === "notification" ? (
|
||||
@ -217,15 +314,13 @@ export const MessageItem = ({
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{message?.messageText && (
|
||||
{htmlText && (
|
||||
<MessageDisplay
|
||||
htmlContent={generateHTML(message?.messageText, [
|
||||
StarterKit,
|
||||
Underline,
|
||||
Highlight,
|
||||
])}
|
||||
/>
|
||||
htmlContent={htmlText}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
{message?.decryptedData?.type === "notification" ? (
|
||||
<MessageDisplay htmlContent={message.decryptedData?.data?.message} />
|
||||
) : (
|
||||
@ -341,7 +436,7 @@ export const MessageItem = ({
|
||||
alignItems: 'center',
|
||||
gap: '15px'
|
||||
}}>
|
||||
{message?.isNotEncrypted && (
|
||||
{message?.isNotEncrypted && isPrivate && (
|
||||
<KeyOffIcon sx={{
|
||||
color: 'white',
|
||||
marginLeft: '10px'
|
||||
@ -397,21 +492,12 @@ export const MessageItem = ({
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* <Message
|
||||
model={{
|
||||
direction: 'incoming',
|
||||
message: message.text,
|
||||
position: 'single',
|
||||
sender: message.senderName,
|
||||
sentTime: message.timestamp
|
||||
}}
|
||||
|
||||
></Message> */}
|
||||
{/* {!message.unread && <span style={{ color: 'green' }}> Seen</span>} */}
|
||||
|
||||
</div>
|
||||
</MessageWragger>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
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})=> {
|
||||
</Box>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
const MessageWragger = ({lastMessage, onSeen, isLast, children})=> {
|
||||
|
||||
if(lastMessage){
|
||||
return (
|
||||
<WatchComponent onSeen={onSeen} isLast={isLast}>{children}</WatchComponent>
|
||||
)
|
||||
}
|
||||
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 <div ref={ref} style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
}
|
@ -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 (
|
||||
<div className="control-group">
|
||||
<div className="button-group">
|
||||
<div className="button-group" style={{
|
||||
display: 'flex'
|
||||
}}>
|
||||
<IconButton
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
disabled={!editor.can().chain().focus().toggleBold().run()}
|
||||
@ -244,6 +249,43 @@ const MenuBar = ({ setEditorRef, isChat }) => {
|
||||
>
|
||||
<RedoIcon />
|
||||
</IconButton>
|
||||
{isChat && (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginLeft: '5px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={()=> {
|
||||
setIsDisabledEditorEnter(!isDisabledEditorEnter)
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
edge="start"
|
||||
tabIndex={-1}
|
||||
disableRipple
|
||||
|
||||
checked={isDisabledEditorEnter}
|
||||
sx={{
|
||||
"&.Mui-checked": {
|
||||
color: "gray", // Customize the color when checked
|
||||
},
|
||||
"& .MuiSvgIcon-root": {
|
||||
color: "gray",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
color: 'gray'
|
||||
}}
|
||||
>
|
||||
disable enter
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{!isChat && (
|
||||
<>
|
||||
<IconButton
|
||||
@ -300,6 +342,7 @@ export default ({
|
||||
membersWithNames,
|
||||
enableMentions
|
||||
}) => {
|
||||
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 (
|
||||
<div>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
height: '100%'
|
||||
}}>
|
||||
<EditorProvider
|
||||
slotBefore={
|
||||
(isFocusedParent || !isMobile || overrideMobile) && (
|
||||
<MenuBar setEditorRef={setEditorRefFunc} isChat={isChat} />
|
||||
<MenuBar setEditorRef={setEditorRefFunc} isChat={isChat} isDisabledEditorEnter={isDisabledEditorEnter} setIsDisabledEditorEnter={handleSetIsDisabledEditorEnter} />
|
||||
)
|
||||
}
|
||||
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(
|
||||
|
@ -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;
|
||||
|
@ -124,11 +124,19 @@ export const ContextMenuPinnedApps = ({ children, app, isMine }) => {
|
||||
<MenuItem onClick={(e) => {
|
||||
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;
|
||||
}
|
||||
});
|
||||
}}>
|
||||
<ListItemIcon sx={{ minWidth: '32px' }}>
|
||||
|
@ -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)"
|
||||
|
@ -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",
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
gap: '10px'
|
||||
}}>
|
||||
{isPrivate && (
|
||||
<LockIcon sx={{
|
||||
color: 'var(--green)'
|
||||
}} />
|
||||
)}
|
||||
{isPrivate === false && (
|
||||
<NoEncryptionGmailerrorredIcon sx={{
|
||||
color: 'var(--danger)'
|
||||
}} />
|
||||
)}
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "16px",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{selectedGroup?.groupName}
|
||||
{selectedGroup?.groupId === '0' ? 'General' :selectedGroup?.groupName}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
@ -110,76 +126,10 @@ export const DesktopHeader = ({
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
alignItems: "center",
|
||||
visibility: selectedGroup?.groupId === '0' ? 'hidden' : 'visibile'
|
||||
}}
|
||||
>
|
||||
{/* <ButtonBase
|
||||
onClick={() => {
|
||||
goToHome();
|
||||
}}
|
||||
>
|
||||
<IconWrapper
|
||||
color="rgba(250, 250, 250, 0.5)"
|
||||
label="Home"
|
||||
selected={isHome}
|
||||
>
|
||||
<HomeIcon
|
||||
height={25}
|
||||
color={isHome ? "white" : "rgba(250, 250, 250, 0.5)"}
|
||||
/>
|
||||
</IconWrapper>
|
||||
</ButtonBase>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setDesktopSideView("groups");
|
||||
}}
|
||||
>
|
||||
<IconWrapper
|
||||
color="rgba(250, 250, 250, 0.5)"
|
||||
label="Groups"
|
||||
selected={isGroups}
|
||||
>
|
||||
<HubsIcon
|
||||
height={25}
|
||||
color={
|
||||
hasUnreadGroups
|
||||
? "var(--unread)"
|
||||
: isGroups
|
||||
? "white"
|
||||
: "rgba(250, 250, 250, 0.5)"
|
||||
}
|
||||
/>
|
||||
</IconWrapper>
|
||||
</ButtonBase>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setDesktopSideView("directs");
|
||||
}}
|
||||
>
|
||||
<IconWrapper
|
||||
color="rgba(250, 250, 250, 0.5)"
|
||||
label="Messaging"
|
||||
selected={isDirects}
|
||||
>
|
||||
<MessagingIcon
|
||||
height={25}
|
||||
color={
|
||||
hasUnreadDirects
|
||||
? "var(--unread)"
|
||||
: isDirects
|
||||
? "white"
|
||||
: "rgba(250, 250, 250, 0.5)"
|
||||
}
|
||||
/>
|
||||
</IconWrapper>
|
||||
</ButtonBase> */}
|
||||
{/* <Box
|
||||
sx={{
|
||||
width: "1px",
|
||||
height: "50px",
|
||||
background: "white",
|
||||
borderRadius: "50px",
|
||||
}}
|
||||
/> */}
|
||||
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
goToAnnouncements()
|
||||
|
@ -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
|
||||
/>
|
||||
|
||||
</ButtonBase> */}
|
||||
<Save isDesktop disableWidth />
|
||||
<Save isDesktop disableWidth myName={myName} />
|
||||
{/* <CoreSyncStatus imageSize="30px" position="left" /> */}
|
||||
{isEnabledDevMode && (
|
||||
<ButtonBase
|
||||
|
@ -228,7 +228,7 @@ export const AttachmentCard = ({
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
color: "var(--unread)",
|
||||
color: "var(--danger)",
|
||||
}}
|
||||
>
|
||||
{errorMsg}
|
||||
@ -263,7 +263,7 @@ export const AttachmentCard = ({
|
||||
}}>
|
||||
|
||||
<FileAttachmentContainer >
|
||||
<Typography>{resourceDetails?.status?.status}</Typography>
|
||||
<Typography>{resourceDetails?.status?.status === 'DOWNLOADED' ? 'BUILDING' : resourceDetails?.status?.status}</Typography>
|
||||
{!resourceDetails && (
|
||||
<>
|
||||
<DownloadIcon />
|
||||
@ -271,7 +271,7 @@ export const AttachmentCard = ({
|
||||
|
||||
</>
|
||||
)}
|
||||
{resourceDetails && resourceDetails?.status?.status !== 'READY' && (
|
||||
{resourceDetails && resourceDetails?.status?.status !== 'READY' && resourceDetails?.status?.status !== 'FAILED_TO_DOWNLOAD' && (
|
||||
<>
|
||||
<CircularProgress sx={{
|
||||
color: 'white'
|
||||
|
@ -159,7 +159,7 @@ export const ImageCard = ({
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
color: "var(--unread)",
|
||||
color: "var(--danger)",
|
||||
}}
|
||||
>
|
||||
{errorMsg}
|
||||
|
@ -221,7 +221,7 @@ export const PollCard = ({
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
color: "var(--unread)",
|
||||
color: "var(--danger)",
|
||||
}}
|
||||
>
|
||||
{errorMsg}
|
||||
|