Merge branch 'Qortal:feature/initial-conversion' into feature/initial-conversion

This commit is contained in:
nico.benaz 2025-02-28 13:45:35 +01:00 committed by GitHub
commit e4eef77685
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
156 changed files with 13248 additions and 3643 deletions

View File

@ -11,7 +11,7 @@ module.exports = {
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
'off',
{ allowConstantExport: true },
],
},

1
.gitignore vendored
View File

@ -23,3 +23,4 @@ dist-ssr
*.sln
*.sw?
release-builds/
.env

View File

@ -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

View File

@ -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": {

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -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": {

View 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"
}
]
}

View File

@ -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"
}

View 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"
}
]
}

View 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"
}
]
}

View 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"
}
]
}

File diff suppressed because it is too large Load Diff

View File

@ -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"
]
}
}

View 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

View 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

View 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
})
}

View 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'

View File

@ -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

View File

@ -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
View File

@ -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",

View File

@ -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"
},

View File

@ -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)({

File diff suppressed because it is too large Load Diff

View File

@ -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>
</>
);
};

View File

@ -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 (

View File

@ -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"

View File

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
src/assets/QMailLogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View 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

View File

@ -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: [],
});

View File

@ -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
);
}
}

View File

@ -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) {

View File

@ -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
};

View 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;

View 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>
)
}

View 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% }
}

View 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;
}

View File

@ -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);
}

View File

@ -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',
}

View File

@ -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>

View File

@ -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%',

View File

@ -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>

View File

@ -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>
</>
)}

View File

@ -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}
/>
);
})}

View File

@ -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>
)}
</>
);

View File

@ -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>
)
}
);
};

View File

@ -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}

View File

@ -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

View File

@ -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,
};

View File

@ -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>
);

View 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>
)}
</>
);
};

View File

@ -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>

View File

@ -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>
)

View 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,
};
};

View File

@ -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 },

View File

@ -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>
);

View File

@ -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>
)
}
);
};

View File

@ -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={{

View File

@ -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()

View File

@ -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>

View File

@ -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>

View File

@ -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>
);
};
)
}

View File

@ -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} />

View File

@ -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",

View File

@ -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>
);

View File

@ -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;

View File

@ -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>
}

View File

@ -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(

View File

@ -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;

View File

@ -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' }}>

View File

@ -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)"

View File

@ -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()

View File

@ -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

View File

@ -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'

View File

@ -159,7 +159,7 @@ export const ImageCard = ({
<Typography
sx={{
fontSize: "14px",
color: "var(--unread)",
color: "var(--danger)",
}}
>
{errorMsg}

View File

@ -221,7 +221,7 @@ export const PollCard = ({
<Typography
sx={{
fontSize: "14px",
color: "var(--unread)",
color: "var(--danger)",
}}
>
{errorMsg}

Some files were not shown because too many files have changed in this diff Show More