diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 92d403a..6d98e04 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -2,17 +2,17 @@ module.exports = { root: true, env: { browser: true, es2020: true }, extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:react-hooks/recommended', + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react-hooks/recommended", ], - ignorePatterns: ['dist', '.eslintrc.cjs'], - parser: '@typescript-eslint/parser', - plugins: ['react-refresh'], + ignorePatterns: ["dist", ".eslintrc.cjs"], + parser: "@typescript-eslint/parser", + plugins: ["react-refresh"], rules: { - 'react-refresh/only-export-components': [ - 'off', + "react-refresh/only-export-components": [ + "off", { allowConstantExport: true }, ], }, -} +}; diff --git a/.gitignore b/.gitignore index e494b90..979a042 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ lerna-debug.log* node_modules dist +dist.zip dist-ssr *.local diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..e13c8ee --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +node_modules +build +dist \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..badfcf9 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,23 @@ +{ + "arrowParens": "always", + "bracketSameLine": false, + "bracketSpacing": true, + "embeddedLanguageFormatting": "auto", + "endOfLine": "lf", + "experimentalTernaries": false, + "htmlWhitespaceSensitivity": "css", + "insertPragma": false, + "jsxBracketSameLine": false, + "jsxSingleQuote": false, + "printWidth": 80, + "proseWrap": "preserve", + "quoteProps": "as-needed", + "requirePragma": false, + "semi": true, + "singleAttributePerLine": false, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false, + "vueIndentScriptAndStyle": false +} diff --git a/README.md b/README.md index 25f4e90..34cfbbb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Qortal Hub - Desktop Interface for Qortal -Qortal Hub is the newest interface for Qortal, part of the 'Qortal Trifecta' series of new User Interfaces for the platform/network. +Qortal Hub is the newest interface for Qortal, part of the 'Qortal Trifecta' series of new User Interfaces for the platform/network. 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. @@ -8,14 +8,20 @@ It is likely that Qortal Hub will become the new 'primary interface' for Qortal, 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. -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. +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. ## 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). +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. +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! +## Internationalization (i18n) +Qortal-Hub supports internationalization (i18n) using [i18next](https://www.i18next.com/), allowing seamless translation of UI text into multiple languages. +The setup includes modularized translation files, language detection, context and runtime language switching. +Files with translation are in `public/locales/` folder. + +See [guidelines](./docs/i18n_languages.md). diff --git a/android/.gitignore b/android/.gitignore deleted file mode 100644 index 48354a3..0000000 --- a/android/.gitignore +++ /dev/null @@ -1,101 +0,0 @@ -# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore - -# Built application files -*.apk -*.aar -*.ap_ -*.aab - -# Files for the ART/Dalvik VM -*.dex - -# Java class files -*.class - -# Generated files -bin/ -gen/ -out/ -# Uncomment the following line in case you need and you don't have the release build type files in your app -# release/ - -# Gradle files -.gradle/ -build/ - -# Local configuration file (sdk path, etc) -local.properties - -# Proguard folder generated by Eclipse -proguard/ - -# Log Files -*.log - -# Android Studio Navigation editor temp files -.navigation/ - -# Android Studio captures folder -captures/ - -# IntelliJ -*.iml -.idea/workspace.xml -.idea/tasks.xml -.idea/gradle.xml -.idea/assetWizardSettings.xml -.idea/dictionaries -.idea/libraries -# Android Studio 3 in .gitignore file. -.idea/caches -.idea/modules.xml -# Comment next line if keeping position of elements in Navigation Editor is relevant for you -.idea/navEditor.xml - -# Keystore files -# Uncomment the following lines if you do not want to check your keystore files in. -#*.jks -#*.keystore - -# External native build folder generated in Android Studio 2.2 and later -.externalNativeBuild -.cxx/ - -# Google Services (e.g. APIs or Firebase) -# google-services.json - -# Freeline -freeline.py -freeline/ -freeline_project_description.json - -# fastlane -fastlane/report.xml -fastlane/Preview.html -fastlane/screenshots -fastlane/test_output -fastlane/readme.md - -# Version control -vcs.xml - -# lint -lint/intermediates/ -lint/generated/ -lint/outputs/ -lint/tmp/ -# lint/reports/ - -# Android Profiling -*.hprof - -# Cordova plugins for Capacitor -capacitor-cordova-android-plugins - -# Copied web assets -app/src/main/assets/public - -# Generated Config files -app/src/main/assets/capacitor.config.json -app/src/main/assets/capacitor.plugins.json -app/src/main/res/xml/config.xml diff --git a/android/app/.gitignore b/android/app/.gitignore deleted file mode 100644 index 043df80..0000000 --- a/android/app/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/build/* -!/build/.npmkeep diff --git a/android/app/build.gradle b/android/app/build.gradle deleted file mode 100644 index 3f5e684..0000000 --- a/android/app/build.gradle +++ /dev/null @@ -1,54 +0,0 @@ -apply plugin: 'com.android.application' - -android { - namespace "com.example.app" - compileSdk rootProject.ext.compileSdkVersion - defaultConfig { - applicationId "com.example.app" - minSdkVersion rootProject.ext.minSdkVersion - targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 1 - versionName "1.0" - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - aaptOptions { - // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. - // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61 - ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' - } - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } -} - -repositories { - flatDir{ - dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs' - } -} - -dependencies { - implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" - implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" - implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" - implementation project(':capacitor-android') - testImplementation "junit:junit:$junitVersion" - androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" - androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" - implementation project(':capacitor-cordova-android-plugins') -} - -apply from: 'capacitor.build.gradle' - -try { - def servicesJSON = file('google-services.json') - if (servicesJSON.text) { - apply plugin: 'com.google.gms.google-services' - } -} catch(Exception e) { - logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work") -} diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle deleted file mode 100644 index d4f22b6..0000000 --- a/android/app/capacitor.build.gradle +++ /dev/null @@ -1,23 +0,0 @@ -// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN - -android { - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } -} - -apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" -dependencies { - implementation project(':capacitor-browser') - implementation project(':capacitor-filesystem') - implementation project(':capacitor-local-notifications') - implementation project(':evva-capacitor-secure-storage-plugin') - implementation project(':transistorsoft-capacitor-background-fetch') - implementation "androidx.webkit:webkit:1.4.0" -} - - -if (hasProperty('postBuildExtras')) { - postBuildExtras() -} diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro deleted file mode 100644 index f1b4245..0000000 --- a/android/app/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile diff --git a/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java b/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java deleted file mode 100644 index f2c2217..0000000 --- a/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.getcapacitor.myapp; - -import static org.junit.Assert.*; - -import android.content.Context; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.platform.app.InstrumentationRegistry; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - - @Test - public void useAppContext() throws Exception { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); - - assertEquals("com.getcapacitor.app", appContext.getPackageName()); - } -} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index 2b2013c..0000000 --- a/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/android/app/src/main/java/com/example/app/BackgroundFetchHeadlessTask.java b/android/app/src/main/java/com/example/app/BackgroundFetchHeadlessTask.java deleted file mode 100644 index fac2dc1..0000000 --- a/android/app/src/main/java/com/example/app/BackgroundFetchHeadlessTask.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.app; - - -import android.content.Context; -import android.util.Log; - -import com.transistorsoft.tsbackgroundfetch.BackgroundFetch; -import com.transistorsoft.tsbackgroundfetch.BGTask; - -public class BackgroundFetchHeadlessTask{ - public void onFetch(Context context, BGTask task) { - // Get a reference to the BackgroundFetch Android API. - BackgroundFetch backgroundFetch = BackgroundFetch.getInstance(context); - // Get the taskId. - String taskId = task.getTaskId(); - // Log a message to adb logcat. - Log.d("MyHeadlessTask", "BackgroundFetchHeadlessTask onFetch -- CUSTOM IMPLEMENTATION: " + taskId); - - boolean isTimeout = task.getTimedOut(); - // Is this a timeout? - if (isTimeout) { - backgroundFetch.finish(taskId); - return; - } - // Do your work here... - // - // - // Signal finish just like the Javascript API. - backgroundFetch.finish(taskId); - } -} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/app/MainActivity.java b/android/app/src/main/java/com/example/app/MainActivity.java deleted file mode 100644 index 966f32d..0000000 --- a/android/app/src/main/java/com/example/app/MainActivity.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.app; - -import com.getcapacitor.BridgeActivity; - -public class MainActivity extends BridgeActivity {} diff --git a/android/app/src/main/res/drawable-land-hdpi/splash.png b/android/app/src/main/res/drawable-land-hdpi/splash.png deleted file mode 100644 index e31573b..0000000 Binary files a/android/app/src/main/res/drawable-land-hdpi/splash.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-land-mdpi/splash.png b/android/app/src/main/res/drawable-land-mdpi/splash.png deleted file mode 100644 index f7a6492..0000000 Binary files a/android/app/src/main/res/drawable-land-mdpi/splash.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-land-xhdpi/splash.png b/android/app/src/main/res/drawable-land-xhdpi/splash.png deleted file mode 100644 index 8077255..0000000 Binary files a/android/app/src/main/res/drawable-land-xhdpi/splash.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-land-xxhdpi/splash.png b/android/app/src/main/res/drawable-land-xxhdpi/splash.png deleted file mode 100644 index 14c6c8f..0000000 Binary files a/android/app/src/main/res/drawable-land-xxhdpi/splash.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-land-xxxhdpi/splash.png b/android/app/src/main/res/drawable-land-xxxhdpi/splash.png deleted file mode 100644 index 244ca25..0000000 Binary files a/android/app/src/main/res/drawable-land-xxxhdpi/splash.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-port-hdpi/splash.png b/android/app/src/main/res/drawable-port-hdpi/splash.png deleted file mode 100644 index 74faaa5..0000000 Binary files a/android/app/src/main/res/drawable-port-hdpi/splash.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-port-mdpi/splash.png b/android/app/src/main/res/drawable-port-mdpi/splash.png deleted file mode 100644 index e944f4a..0000000 Binary files a/android/app/src/main/res/drawable-port-mdpi/splash.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-port-xhdpi/splash.png b/android/app/src/main/res/drawable-port-xhdpi/splash.png deleted file mode 100644 index 564a82f..0000000 Binary files a/android/app/src/main/res/drawable-port-xhdpi/splash.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-port-xxhdpi/splash.png b/android/app/src/main/res/drawable-port-xxhdpi/splash.png deleted file mode 100644 index bfabe68..0000000 Binary files a/android/app/src/main/res/drawable-port-xxhdpi/splash.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-port-xxxhdpi/splash.png b/android/app/src/main/res/drawable-port-xxxhdpi/splash.png deleted file mode 100644 index 6929071..0000000 Binary files a/android/app/src/main/res/drawable-port-xxxhdpi/splash.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index c7bd21d..0000000 --- a/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index d5fccc5..0000000 --- a/android/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/android/app/src/main/res/drawable/qort.png b/android/app/src/main/res/drawable/qort.png deleted file mode 100644 index 39d090f..0000000 Binary files a/android/app/src/main/res/drawable/qort.png and /dev/null differ diff --git a/android/app/src/main/res/drawable/splash.png b/android/app/src/main/res/drawable/splash.png deleted file mode 100644 index a760af2..0000000 Binary files a/android/app/src/main/res/drawable/splash.png and /dev/null differ diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index b5ad138..0000000 --- a/android/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index 90f9580..0000000 --- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 12a409b..0000000 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_back.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_back.png deleted file mode 100644 index a05f3fc..0000000 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_back.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_fore.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_fore.png deleted file mode 100644 index d6bdb01..0000000 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_fore.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 3966b05..0000000 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_back.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_back.png deleted file mode 100644 index 3814b23..0000000 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_back.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_fore.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_fore.png deleted file mode 100644 index 1907b24..0000000 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_fore.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index ff8e391..0000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_back.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_back.png deleted file mode 100644 index 2e1846f..0000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_back.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png deleted file mode 100644 index 123ff93..0000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index f1f3036..0000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png deleted file mode 100644 index fe154c8..0000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png deleted file mode 100644 index c4fdc57..0000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 7191d0c..0000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png deleted file mode 100644 index b640524..0000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png deleted file mode 100644 index 69a0eb8..0000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png and /dev/null differ diff --git a/android/app/src/main/res/values/ic_launcher_background.xml b/android/app/src/main/res/values/ic_launcher_background.xml deleted file mode 100644 index c5d5899..0000000 --- a/android/app/src/main/res/values/ic_launcher_background.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #FFFFFF - \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml deleted file mode 100644 index 624265c..0000000 --- a/android/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - Qortal - Qortal - com.example.app - com.example.app - diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml deleted file mode 100644 index be874e5..0000000 --- a/android/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/android/app/src/main/res/xml/file_paths.xml b/android/app/src/main/res/xml/file_paths.xml deleted file mode 100644 index bd0c4d8..0000000 --- a/android/app/src/main/res/xml/file_paths.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java b/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java deleted file mode 100644 index 0297327..0000000 --- a/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.getcapacitor.myapp; - -import static org.junit.Assert.*; - -import org.junit.Test; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - - @Test - public void addition_isCorrect() throws Exception { - assertEquals(4, 2 + 2); - } -} diff --git a/android/build.gradle b/android/build.gradle deleted file mode 100644 index 7e1c3f3..0000000 --- a/android/build.gradle +++ /dev/null @@ -1,33 +0,0 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. - -buildscript { - - repositories { - google() - mavenCentral() - } - dependencies { - classpath 'com.android.tools.build:gradle:8.2.1' - classpath 'com.google.gms:google-services:4.4.0' - - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files - } -} - -apply from: "variables.gradle" - -allprojects { - repositories { - google() - mavenCentral() - maven { - // capacitor-background-fetch - url("${project(':transistorsoft-capacitor-background-fetch').projectDir}/libs") - } - } -} - -task clean(type: Delete) { - delete rootProject.buildDir -} diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle deleted file mode 100644 index c72458d..0000000 --- a/android/capacitor.settings.gradle +++ /dev/null @@ -1,18 +0,0 @@ -// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN -include ':capacitor-android' -project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') - -include ':capacitor-browser' -project(':capacitor-browser').projectDir = new File('../node_modules/@capacitor/browser/android') - -include ':capacitor-filesystem' -project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android') - -include ':capacitor-local-notifications' -project(':capacitor-local-notifications').projectDir = new File('../node_modules/@capacitor/local-notifications/android') - -include ':evva-capacitor-secure-storage-plugin' -project(':evva-capacitor-secure-storage-plugin').projectDir = new File('../node_modules/@evva/capacitor-secure-storage-plugin/android') - -include ':transistorsoft-capacitor-background-fetch' -project(':transistorsoft-capacitor-background-fetch').projectDir = new File('../node_modules/@transistorsoft/capacitor-background-fetch/android') diff --git a/android/gradle.properties b/android/gradle.properties deleted file mode 100644 index 2e87c52..0000000 --- a/android/gradle.properties +++ /dev/null @@ -1,22 +0,0 @@ -# Project-wide Gradle settings. - -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. - -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html - -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx1536m - -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true - -# AndroidX package structure to make it clearer which packages are bundled with the -# Android operating system, and which are packaged with your app's APK -# https://developer.android.com/topic/libraries/support-library/androidx-rn -android.useAndroidX=true diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 033e24c..0000000 Binary files a/android/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index c747538..0000000 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew deleted file mode 100755 index fcb6fca..0000000 --- a/android/gradlew +++ /dev/null @@ -1,248 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat deleted file mode 100644 index 6689b85..0000000 --- a/android/gradlew.bat +++ /dev/null @@ -1,92 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/android/settings.gradle b/android/settings.gradle deleted file mode 100644 index 3b4431d..0000000 --- a/android/settings.gradle +++ /dev/null @@ -1,5 +0,0 @@ -include ':app' -include ':capacitor-cordova-android-plugins' -project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/') - -apply from: 'capacitor.settings.gradle' \ No newline at end of file diff --git a/android/variables.gradle b/android/variables.gradle deleted file mode 100644 index 8ef305d..0000000 --- a/android/variables.gradle +++ /dev/null @@ -1,16 +0,0 @@ -ext { - minSdkVersion = 22 - compileSdkVersion = 34 - targetSdkVersion = 34 - androidxActivityVersion = '1.8.0' - androidxAppCompatVersion = '1.6.1' - androidxCoordinatorLayoutVersion = '1.2.0' - androidxCoreVersion = '1.12.0' - androidxFragmentVersion = '1.6.2' - coreSplashScreenVersion = '1.0.1' - androidxWebkitVersion = '1.9.0' - junitVersion = '4.13.2' - androidxJunitVersion = '1.1.5' - androidxEspressoCoreVersion = '3.5.1' - cordovaAndroidVersion = '10.1.1' -} \ No newline at end of file diff --git a/capacitor.config.ts b/capacitor.config.ts index c7f641f..87d927b 100644 --- a/capacitor.config.ts +++ b/capacitor.config.ts @@ -1,15 +1,15 @@ -import type { CapacitorConfig } from '@capacitor/cli'; +import type { CapacitorConfig } from "@capacitor/cli"; const config: CapacitorConfig = { - appId: 'org.Qortal.Qortal-Hub', - appName: 'Qortal-Hub', - webDir: 'dist', - "plugins": { - "LocalNotifications": { - "smallIcon": "qort", - "iconColor": "#09b6e8" - } - } + appId: "org.Qortal.Qortal-Hub", + appName: "Qortal-Hub", + webDir: "dist", + plugins: { + LocalNotifications: { + smallIcon: "qort", + iconColor: "#09b6e8", + }, + }, }; export default config; diff --git a/dist.zip b/dist.zip deleted file mode 100644 index 8f6f422..0000000 Binary files a/dist.zip and /dev/null differ diff --git a/docs/contribution.md b/docs/contribution.md new file mode 100644 index 0000000..07d06fe --- /dev/null +++ b/docs/contribution.md @@ -0,0 +1,84 @@ + +# 🤝 Contributing Guide + +Thank you for your interest in contributing! We follow a structured Git workflow to keep the project clean, stable, and production-ready at all times. + +--- + +## 📦 Branch Overview + +| Branch | Purpose | +|------------------|----------------------------------------------------------| +| `master` | Stable, production-ready code. All releases are tagged from here. | +| `develop` | Active development branch. All new features go here first. | +| `release/x.y.z` | Pre-release branch for staging, QA, and final polish. | + +--- + +## 🌿 Creating a Feature or Fix + +1. **Start from `develop`:** + + ```bash + git checkout develop + git checkout -b feature/your-feature-name + ``` + +2. **Make your changes and commit them.** + +3. **Push your branch:** + + ```bash + git push origin feature/your-feature-name + ``` + +4. **Open a Pull Request into `develop`.** + +--- + +## 🚀 Releasing Code (Maintainers Only) + +A new `release/x.y.z` branch must be created for **every release**. + +1. **Create a `release/` branch from `master`:** + + ```bash + git checkout master + git checkout -b release/1.2.0 + ``` + +2. **Merge in `develop` or selected branches if `develop` is not ready:** + + ```bash + git merge develop + # or + git merge feature/finished-feature + git merge feature/another-complete-feature + ``` + +3. **Polish, test, and fix issues as needed.** + +4. **Merge back into `develop`:** + + ```bash + git checkout develop + git merge release/1.2.0 + git push origin develop + ``` + +5. **Finalize the release:** + + ```bash + git checkout master + git merge release/1.2.0 + git tag v1.2.0 + git push origin master --tags + ``` + +6. **Delete the release branch:** + + ```bash + git branch -d release/1.2.0 + git push origin --delete release/1.2.0 + ``` + diff --git a/docs/i18n_languages.md b/docs/i18n_languages.md new file mode 100644 index 0000000..7447193 --- /dev/null +++ b/docs/i18n_languages.md @@ -0,0 +1,10 @@ +# I18N Guidelines + +In JSON file: + +- Keep the file sorted +- Always write in lowercase + +In GUI: + +- If the first letter of the translation must be uppercase, use the postProcess, for example: `{t_auth('advanced_users', { postProcess: 'capitalize' })}` diff --git a/electron/capacitor.config.ts b/electron/capacitor.config.ts index c7f641f..87d927b 100644 --- a/electron/capacitor.config.ts +++ b/electron/capacitor.config.ts @@ -1,15 +1,15 @@ -import type { CapacitorConfig } from '@capacitor/cli'; +import type { CapacitorConfig } from "@capacitor/cli"; const config: CapacitorConfig = { - appId: 'org.Qortal.Qortal-Hub', - appName: 'Qortal-Hub', - webDir: 'dist', - "plugins": { - "LocalNotifications": { - "smallIcon": "qort", - "iconColor": "#09b6e8" - } - } + appId: "org.Qortal.Qortal-Hub", + appName: "Qortal-Hub", + webDir: "dist", + plugins: { + LocalNotifications: { + smallIcon: "qort", + iconColor: "#09b6e8", + }, + }, }; export default config; diff --git a/electron/package.json b/electron/package.json index 31f5372..05f8865 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,6 +1,6 @@ { "name": "qortal-hub", - "version": "0.5.3", + "version": "0.5.4-pre", "description": "A desktop app that gives you access to the Qortal network", "author": { "name": "", @@ -57,4 +57,4 @@ "capacitor", "electron" ] -} \ No newline at end of file +} diff --git a/i18n.js b/i18n.js new file mode 100644 index 0000000..5277e59 --- /dev/null +++ b/i18n.js @@ -0,0 +1,58 @@ +import { initReactI18next } from 'react-i18next'; +import HttpBackend from 'i18next-http-backend'; +import LocalStorageBackend from 'i18next-localstorage-backend'; +import HttpApi from 'i18next-http-backend'; +import i18n from 'i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; + +// Detect environment +const isDev = process.env.NODE_ENV === 'development'; + +// Register custom postProcessor: it capitalizes the first letter of a translation- +// Usage: +// t('greeting', { postProcess: 'capitalize' }) +const capitalize = { + type: 'postProcessor', + name: 'capitalize', + process: (value) => { + return value.charAt(0).toUpperCase() + value.slice(1); + }, +}; + +export const supportedLanguages = { + de: { name: 'Deutsch', flag: '🇩🇪' }, + en: { name: 'English', flag: '🇺🇸' }, + es: { name: 'Español', flag: '🇪🇸' }, + fr: { name: 'Français', flag: '🇫🇷' }, + it: { name: 'Italiano', flag: '🇮🇹' }, + ru: { name: 'Русский', flag: '🇷🇺' }, +}; + +i18n + .use(HttpApi) + .use(LanguageDetector) + .use(initReactI18next) + .use(capitalize) + .init({ + backend: { + backends: [LocalStorageBackend, HttpBackend], + backendOptions: [ + { + expirationTime: 7 * 24 * 60 * 60 * 1000, // 7 days + }, + { + loadPath: '/locales/{{lng}}/{{ns}}.json', + }, + ], + }, + debug: isDev, + fallbackLng: 'en', + interpolation: { + escapeValue: false, + }, + lng: navigator.language, + ns: ['auth', 'core', 'group', 'tutorial'], + supportedLngs: Object.keys(supportedLanguages), + }); + +export default i18n; diff --git a/index.html b/index.html index 43b8b56..74f19fd 100644 --- a/index.html +++ b/index.html @@ -1,12 +1,9 @@ - + - - + - - Qortal Hub diff --git a/package-lock.json b/package-lock.json index b43a338..fbba254 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,32 +15,31 @@ "@capacitor/core": "^6.1.2", "@capacitor/filesystem": "^6.0.1", "@capacitor/local-notifications": "^6.1.0", - "@chatscope/chat-ui-kit-react": "^2.0.3", - "@dnd-kit/core": "^6.1.0", - "@dnd-kit/sortable": "^8.0.0", + "@dnd-kit/core": "^6.3.0", + "@dnd-kit/sortable": "^10.0.0", "@electron/packager": "^18.3.6", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.0", "@evva/capacitor-secure-storage-plugin": "^3.0.1", - "@mui/icons-material": "^5.16.4", - "@mui/lab": "^5.0.0-alpha.173", - "@mui/material": "^5.16.7", - "@reduxjs/toolkit": "^2.2.7", - "@tanstack/react-virtual": "^3.10.8", + "@mui/icons-material": "^7.0.1", + "@mui/lab": "^7.0.0-beta.11", + "@mui/material": "^7.0.1", + "@tanstack/react-virtual": "^3.13.6", "@testing-library/jest-dom": "^6.4.6", - "@testing-library/user-event": "^14.5.2", - "@tiptap/extension-color": "^2.5.9", - "@tiptap/extension-highlight": "^2.6.6", - "@tiptap/extension-image": "^2.6.6", - "@tiptap/extension-mention": "^2.9.1", - "@tiptap/extension-placeholder": "^2.6.2", - "@tiptap/extension-text-style": "^2.5.9", - "@tiptap/extension-underline": "^2.6.6", - "@tiptap/pm": "^2.5.9", - "@tiptap/react": "^2.5.9", - "@tiptap/starter-kit": "^2.5.9", + "@testing-library/user-event": "^14.6.1", + "@tiptap/extension-color": "^2.11.7", + "@tiptap/extension-highlight": "^2.11.7", + "@tiptap/extension-image": "^2.11.7", + "@tiptap/extension-mention": "^2.11.7", + "@tiptap/extension-placeholder": "^2.11.7", + "@tiptap/extension-text-style": "^2.11.7", + "@tiptap/extension-underline": "^2.11.7", + "@tiptap/pm": "^2.11.7", + "@tiptap/react": "^2.11.7", + "@tiptap/starter-kit": "^2.11.7", "@transistorsoft/capacitor-background-fetch": "^6.0.1", "@types/chrome": "^0.0.263", + "@uiw/react-color": "^2.5.1", "adm-zip": "^0.5.16", "asmcrypto.js": "2.3.2", "axios": "^1.7.7", @@ -56,29 +55,29 @@ "emoji-picker-react": "^4.12.0", "file-saver": "^2.0.5", "html-to-text": "^9.0.5", + "i18next": "^25.0.1", + "i18next-browser-languagedetector": "^8.0.5", + "i18next-http-backend": "^3.0.2", + "i18next-localstorage-backend": "^4.2.0", + "jotai": "^2.12.3", "jssha": "3.3.1", "lit": "^3.2.1", "lodash": "^4.17.21", "mime": "^4.0.4", "moment": "^2.30.1", "npm": "^10.8.3", - "quill-image-resize-module-react": "^3.0.0", - "react": "^18.2.0", - "react-copy-to-clipboard": "^5.1.0", + "react": "^19.1.0", "react-countdown-circle-timer": "^3.2.1", - "react-dom": "^18.2.0", + "react-dom": "^19.1.0", "react-dropzone": "^14.2.3", "react-frame-component": "^5.2.7", - "react-infinite-scroller": "^1.2.6", - "react-intersection-observer": "^9.13.0", - "react-json-view-lite": "^2.0.1", + "react-i18next": "^15.4.1", + "react-intersection-observer": "^9.16.0", + "react-json-view-lite": "^2.4.1", "react-loader-spinner": "^6.1.6", "react-qr-code": "^2.0.15", - "react-quill": "^2.0.0", - "react-redux": "^9.1.2", - "react-virtualized": "^9.22.5", + "react-virtualized": "^9.22.6", "react-virtuoso": "^4.10.4", - "recoil": "^0.7.7", "short-unique-id": "^5.2.0", "slate": "^0.103.0", "slate-react": "^0.109.0", @@ -90,14 +89,12 @@ "vite-plugin-wasm": "^3.3.0" }, "devDependencies": { - "@testing-library/dom": "^10.3.0", - "@testing-library/react": "^16.0.0", + "@testing-library/dom": "^10.4.0", + "@testing-library/react": "^16.3.0", "@types/dompurify": "^3.0.5", "@types/lodash": "^4.17.7", - "@types/react": "^18.2.64", - "@types/react-copy-to-clipboard": "^5.0.7", - "@types/react-dom": "^18.2.21", - "@types/react-infinite-scroller": "^1.2.5", + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", "@types/react-virtualized": "^9.21.30", "@typescript-eslint/eslint-plugin": "^7.1.1", "@typescript-eslint/parser": "^7.1.1", @@ -106,10 +103,11 @@ "eslint": "^8.57.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", + "prettier": "^3.5.3", "rename-cli": "^6.2.1", "typescript": "^5.2.2", "vite": "^5.1.6", - "vitest": "^1.6.0" + "vitest": "^1.6.1" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1478,9 +1476,10 @@ } }, "node_modules/@babel/runtime": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", - "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", + "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -1771,30 +1770,6 @@ "@capacitor/core": "^6.0.0" } }, - "node_modules/@chatscope/chat-ui-kit-react": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@chatscope/chat-ui-kit-react/-/chat-ui-kit-react-2.0.3.tgz", - "integrity": "sha512-0IkjFskRec7SHrFivOQPiZMie5GLQL+ZnROiIbj4yptbC3aMEMFdHRAZrfqlid3uQx9kYhdtn34wMLh1vVNMLA==", - "dependencies": { - "@chatscope/chat-ui-kit-styles": "^1.2.0", - "@fortawesome/fontawesome-free": "^5.12.1", - "@fortawesome/fontawesome-svg-core": "^1.2.26", - "@fortawesome/free-solid-svg-icons": "^5.12.0", - "@fortawesome/react-fontawesome": "^0.1.8", - "classnames": "^2.2.6", - "prop-types": "^15.7.2" - }, - "peerDependencies": { - "prop-types": "^15.7.2", - "react": "^16.12.0 || ^17.0.0 || ^18.2.0", - "react-dom": "^16.12.0 || ^17.0.0 || ^18.2.0" - } - }, - "node_modules/@chatscope/chat-ui-kit-styles": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@chatscope/chat-ui-kit-styles/-/chat-ui-kit-styles-1.4.0.tgz", - "integrity": "sha512-016mBJD3DESw7Nh+lkKcPd22xG92ghA0VpIXIbjQtmXhC7Ve6wRazTy8z1Ahut+Tbv179+JxrftuMngsj/yV8Q==" - }, "node_modules/@develar/schema-utils": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", @@ -1812,9 +1787,10 @@ } }, "node_modules/@dnd-kit/accessibility": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.0.tgz", - "integrity": "sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", "dependencies": { "tslib": "^2.0.0" }, @@ -1823,11 +1799,12 @@ } }, "node_modules/@dnd-kit/core": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.1.0.tgz", - "integrity": "sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", "dependencies": { - "@dnd-kit/accessibility": "^3.1.0", + "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, @@ -1837,15 +1814,16 @@ } }, "node_modules/@dnd-kit/sortable": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-8.0.0.tgz", - "integrity": "sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { - "@dnd-kit/core": "^6.1.0", + "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, @@ -2425,21 +2403,35 @@ } }, "node_modules/@emotion/cache": { - "version": "11.11.0", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", - "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", "dependencies": { - "@emotion/memoize": "^0.8.1", - "@emotion/sheet": "^1.2.2", - "@emotion/utils": "^1.2.1", - "@emotion/weak-memoize": "^0.3.1", + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", "stylis": "4.2.0" } }, + "node_modules/@emotion/cache/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/cache/node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, "node_modules/@emotion/hash": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", - "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" }, "node_modules/@emotion/is-prop-valid": { "version": "1.2.2", @@ -2478,21 +2470,35 @@ } }, "node_modules/@emotion/serialize": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.3.tgz", - "integrity": "sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", "dependencies": { - "@emotion/hash": "^0.9.1", - "@emotion/memoize": "^0.8.1", - "@emotion/unitless": "^0.8.1", - "@emotion/utils": "^1.2.1", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", "csstype": "^3.0.2" } }, + "node_modules/@emotion/serialize/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/serialize/node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, "node_modules/@emotion/sheet": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", - "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" }, "node_modules/@emotion/styled": { "version": "11.11.0", @@ -2519,7 +2525,8 @@ "node_modules/@emotion/unitless": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", - "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", + "license": "MIT" }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { "version": "1.0.1", @@ -2530,9 +2537,10 @@ } }, "node_modules/@emotion/utils": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", - "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" }, "node_modules/@emotion/weak-memoize": { "version": "0.3.1", @@ -2985,94 +2993,6 @@ "@capacitor/core": "^6.1.2" } }, - "node_modules/@floating-ui/core": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", - "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", - "dependencies": { - "@floating-ui/utils": "^0.2.1" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz", - "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==", - "dependencies": { - "@floating-ui/core": "^1.0.0", - "@floating-ui/utils": "^0.2.0" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz", - "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==", - "dependencies": { - "@floating-ui/dom": "^1.6.1" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", - "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" - }, - "node_modules/@fortawesome/fontawesome-common-types": { - "version": "0.2.36", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz", - "integrity": "sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg==", - "hasInstallScript": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/@fortawesome/fontawesome-free": { - "version": "5.15.4", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz", - "integrity": "sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg==", - "hasInstallScript": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/@fortawesome/fontawesome-svg-core": { - "version": "1.2.36", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.36.tgz", - "integrity": "sha512-YUcsLQKYb6DmaJjIHdDWpBIGCcyE/W+p/LMGvjQem55Mm2XWVAP5kWTMKWLv9lwpCVjpLxPyOMOyUocP1GxrtA==", - "hasInstallScript": true, - "dependencies": { - "@fortawesome/fontawesome-common-types": "^0.2.36" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@fortawesome/free-solid-svg-icons": { - "version": "5.15.4", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.4.tgz", - "integrity": "sha512-JLmQfz6tdtwxoihXLg6lT78BorrFyCf59SAwBM6qV/0zXyVeDygJVb3fk+j5Qat+Yvcxp1buLTY5iDh1ZSAQ8w==", - "hasInstallScript": true, - "dependencies": { - "@fortawesome/fontawesome-common-types": "^0.2.36" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@fortawesome/react-fontawesome": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.19.tgz", - "integrity": "sha512-Hyb+lB8T18cvLNX0S3llz7PcSOAJMLwiVKBuuzwM/nI5uoBw+gQjnf9il0fR1C3DKOI5Kc79pkJ4/xB0Uw9aFQ==", - "dependencies": { - "prop-types": "^15.8.1" - }, - "peerDependencies": { - "@fortawesome/fontawesome-svg-core": "~1 || ~6", - "react": ">=16.x" - } - }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -3624,9 +3544,10 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -3723,64 +3644,35 @@ "node": ">= 10.0.0" } }, - "node_modules/@mui/base": { - "version": "5.0.0-beta.40", - "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", - "integrity": "sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==", - "dependencies": { - "@babel/runtime": "^7.23.9", - "@floating-ui/react-dom": "^2.0.8", - "@mui/types": "^7.2.14", - "@mui/utils": "^5.15.14", - "@popperjs/core": "^2.11.8", - "clsx": "^2.1.0", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@mui/core-downloads-tracker": { - "version": "5.16.7", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.7.tgz", - "integrity": "sha512-RtsCt4Geed2/v74sbihWzzRs+HsIQCfclHeORh5Ynu2fS4icIKozcSubwuG7vtzq2uW3fOR1zITSP84TNt2GoQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.0.2.tgz", + "integrity": "sha512-TfeFU9TgN1N06hyb/pV/63FfO34nijZRMqgHk0TJ3gkl4Fbd+wZ73+ZtOd7jag6hMmzO9HSrBc6Vdn591nhkAg==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" } }, "node_modules/@mui/icons-material": { - "version": "5.16.4", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.4.tgz", - "integrity": "sha512-j9/CWctv6TH6Dou2uR2EH7UOgu79CW/YcozxCYVLJ7l03pCsiOlJ5sBArnWJxJ+nGkFwyL/1d1k8JEPMDR125A==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.0.2.tgz", + "integrity": "sha512-Bo57PFLOqXOqPNrXjd8AhzH5s6TCsNUQbvnQ0VKZ8D+lIlteqKnrk/O1luMJUc/BXONK7BfIdTdc7qOnXYbMdw==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.23.9" + "@babel/runtime": "^7.27.0" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@mui/material": "^5.0.0", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" + "@mui/material": "^7.0.2", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -3789,20 +3681,20 @@ } }, "node_modules/@mui/lab": { - "version": "5.0.0-alpha.173", - "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.173.tgz", - "integrity": "sha512-Gt5zopIWwxDgGy/MXcp6GueD84xFFugFai4hYiXY0zowJpTVnIrTQCQXV004Q7rejJ7aaCntX9hpPJqCrioshA==", + "version": "7.0.0-beta.11", + "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-7.0.0-beta.11.tgz", + "integrity": "sha512-VJDEUgiRsjo8V2xDvBEE9Cfs1cSlwzp9UFmD3KzIg6emFMqz9BfYh+9cFI4iQWaoz3Agm3q6bVKlhlX6xw6sVQ==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/base": "5.0.0-beta.40", - "@mui/system": "^5.16.5", - "@mui/types": "^7.2.15", - "@mui/utils": "^5.16.5", - "clsx": "^2.1.0", + "@babel/runtime": "^7.27.0", + "@mui/system": "^7.0.2", + "@mui/types": "^7.4.1", + "@mui/utils": "^7.0.2", + "clsx": "^2.1.1", "prop-types": "^15.8.1" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" }, "funding": { "type": "opencollective", @@ -3811,10 +3703,11 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material": ">=5.15.0", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" + "@mui/material": "^7.0.2", + "@mui/material-pigment-css": "^7.0.2", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/react": { @@ -3823,31 +3716,35 @@ "@emotion/styled": { "optional": true }, + "@mui/material-pigment-css": { + "optional": true + }, "@types/react": { "optional": true } } }, "node_modules/@mui/material": { - "version": "5.16.7", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.7.tgz", - "integrity": "sha512-cwwVQxBhK60OIOqZOVLFt55t01zmarKJiJUWbk0+8s/Ix5IaUzAShqlJchxsIQ4mSrWqgcKCCXKtIlG5H+/Jmg==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.0.2.tgz", + "integrity": "sha512-rjJlJ13+3LdLfobRplkXbjIFEIkn6LgpetgU/Cs3Xd8qINCCQK9qXQIjjQ6P0FXFTPFzEVMj0VgBR1mN+FhOcA==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/core-downloads-tracker": "^5.16.7", - "@mui/system": "^5.16.7", - "@mui/types": "^7.2.15", - "@mui/utils": "^5.16.6", + "@babel/runtime": "^7.27.0", + "@mui/core-downloads-tracker": "^7.0.2", + "@mui/system": "^7.0.2", + "@mui/types": "^7.4.1", + "@mui/utils": "^7.0.2", "@popperjs/core": "^2.11.8", - "@types/react-transition-group": "^4.4.10", - "clsx": "^2.1.0", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1", - "react-is": "^18.3.1", + "react-is": "^19.1.0", "react-transition-group": "^4.4.5" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" }, "funding": { "type": "opencollective", @@ -3856,9 +3753,10 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" + "@mui/material-pigment-css": "^7.0.2", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/react": { @@ -3867,35 +3765,40 @@ "@emotion/styled": { "optional": true }, + "@mui/material-pigment-css": { + "optional": true + }, "@types/react": { "optional": true } } }, "node_modules/@mui/material/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==" + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz", + "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==", + "license": "MIT" }, "node_modules/@mui/private-theming": { - "version": "5.16.6", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.6.tgz", - "integrity": "sha512-rAk+Rh8Clg7Cd7shZhyt2HGTTE5wYKNSJ5sspf28Fqm/PZ69Er9o6KX25g03/FG2dfpg5GCwZh/xOojiTfm3hw==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.0.2.tgz", + "integrity": "sha512-6lt8heDC9wN8YaRqEdhqnm0cFCv08AMf4IlttFvOVn7ZdKd81PNpD/rEtPGLLwQAFyyKSxBG4/2XCgpbcdNKiA==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/utils": "^5.16.6", + "@babel/runtime": "^7.27.0", + "@mui/utils": "^7.0.2", "prop-types": "^15.8.1" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -3904,17 +3807,20 @@ } }, "node_modules/@mui/styled-engine": { - "version": "5.16.6", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.6.tgz", - "integrity": "sha512-zaThmS67ZmtHSWToTiHslbI8jwrmITcN93LQaR2lKArbvS7Z3iLkwRoiikNWutx9MBs8Q6okKvbZq1RQYB3v7g==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.0.2.tgz", + "integrity": "sha512-11Bt4YdHGlh7sB8P75S9mRCUxTlgv7HGbr0UKz6m6Z9KLeiw1Bm9y/t3iqLLVMvSHYB6zL8X8X+LmfTE++gyBw==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.23.9", - "@emotion/cache": "^11.11.0", + "@babel/runtime": "^7.27.0", + "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", "csstype": "^3.1.3", "prop-types": "^15.8.1" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" }, "funding": { "type": "opencollective", @@ -3923,7 +3829,7 @@ "peerDependencies": { "@emotion/react": "^11.4.1", "@emotion/styled": "^11.3.0", - "react": "^17.0.0 || ^18.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/react": { @@ -3935,21 +3841,22 @@ } }, "node_modules/@mui/system": { - "version": "5.16.7", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.7.tgz", - "integrity": "sha512-Jncvs/r/d/itkxh7O7opOunTqbbSSzMTHzZkNLM+FjAOg+cYAZHrPDlYe1ZGKUYORwwb2XexlWnpZp0kZ4AHuA==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.0.2.tgz", + "integrity": "sha512-yFUraAWYWuKIISPPEVPSQ1NLeqmTT4qiQ+ktmyS8LO/KwHxB+NNVOacEZaIofh5x1NxY8rzphvU5X2heRZ/RDA==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/private-theming": "^5.16.6", - "@mui/styled-engine": "^5.16.6", - "@mui/types": "^7.2.15", - "@mui/utils": "^5.16.6", - "clsx": "^2.1.0", + "@babel/runtime": "^7.27.0", + "@mui/private-theming": "^7.0.2", + "@mui/styled-engine": "^7.0.2", + "@mui/types": "^7.4.1", + "@mui/utils": "^7.0.2", + "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" }, "funding": { "type": "opencollective", @@ -3958,8 +3865,8 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/react": { @@ -3974,11 +3881,15 @@ } }, "node_modules/@mui/types": { - "version": "7.2.15", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.15.tgz", - "integrity": "sha512-nbo7yPhtKJkdf9kcVOF8JZHPZTmqXjJ/tI0bdWgHg5tp9AnIN4Y7f7wm9T+0SyGYJk76+GYZ8Q5XaTYAsUHN0Q==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.1.tgz", + "integrity": "sha512-gUL8IIAI52CRXP/MixT1tJKt3SI6tVv4U/9soFsTtAsHzaJQptZ42ffdHZV3niX1ei0aUgMvOxBBN0KYqdG39g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.0" + }, "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0" + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -3987,27 +3898,28 @@ } }, "node_modules/@mui/utils": { - "version": "5.16.6", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.6.tgz", - "integrity": "sha512-tWiQqlhxAt3KENNiSRL+DIn9H5xNVK6Jjf70x3PnfQPz1MPBdh7yyIcAyVBT9xiw7hP3SomRhPR7hzBMBCjqEA==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.0.2.tgz", + "integrity": "sha512-72gcuQjPzhj/MLmPHLCgZjy2VjOH4KniR/4qRtXTTXIEwbkgcN+Y5W/rC90rWtMmZbjt9svZev/z+QHUI4j74w==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/types": "^7.2.15", - "@types/prop-types": "^15.7.12", + "@babel/runtime": "^7.27.0", + "@mui/types": "^7.4.1", + "@types/prop-types": "^15.7.14", "clsx": "^2.1.1", "prop-types": "^15.8.1", - "react-is": "^18.3.1" + "react-is": "^19.1.0" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -4016,9 +3928,10 @@ } }, "node_modules/@mui/utils/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==" + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz", + "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==", + "license": "MIT" }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -4098,29 +4011,6 @@ "url": "https://opencollective.com/popperjs" } }, - "node_modules/@reduxjs/toolkit": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.7.tgz", - "integrity": "sha512-faI3cZbSdFb8yv9dhDTmGwclW0vk0z5o1cia+kf7gCbaCwHI5e+7tP57mJUv22pNcNbeA62GSrPpfrUfdXcQ6g==", - "dependencies": { - "immer": "^10.0.3", - "redux": "^5.0.1", - "redux-thunk": "^3.1.0", - "reselect": "^5.1.0" - }, - "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18", - "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-redux": { - "optional": true - } - } - }, "node_modules/@remirror/core-constants": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz", @@ -4640,34 +4530,36 @@ } }, "node_modules/@tanstack/react-virtual": { - "version": "3.10.8", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.10.8.tgz", - "integrity": "sha512-VbzbVGSsZlQktyLrP5nxE+vE1ZR+U0NFAWPbJLoG2+DKPwd2D7dVICTVIIaYlJqX1ZCEnYDbaOpmMwbsyhBoIA==", + "version": "3.13.6", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.6.tgz", + "integrity": "sha512-WT7nWs8ximoQ0CDx/ngoFP7HbQF9Q2wQe4nh2NB+u2486eX3nZRE40P9g6ccCVq7ZfTSH5gFOuCoVH5DLNS/aA==", + "license": "MIT", "dependencies": { - "@tanstack/virtual-core": "3.10.8" + "@tanstack/virtual-core": "3.13.6" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/@tanstack/virtual-core": { - "version": "3.10.8", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.8.tgz", - "integrity": "sha512-PBu00mtt95jbKFi6Llk9aik8bnR3tR/oQP1o3TSi+iG//+Q2RTIzCEgKkHG8BB86kxMNW6O8wku+Lmi+QFR6jA==", + "version": "3.13.6", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.6.tgz", + "integrity": "sha512-cnQUeWnhNP8tJ4WsGcYiX24Gjkc9ALstLbHcBj1t3E7EimN6n6kHH+DPV4PpDnuw00NApQp+ViojMj1GRdwYQg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" } }, "node_modules/@testing-library/dom": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.3.0.tgz", - "integrity": "sha512-pT/TYB2+IyMYkkB6lqpkzD7VFbsR0JBJtflK3cS68sCNWxmOhWwRm1XvVHlseNEorsNcxkYsb4sRDV3aNIpttg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.10.4", @@ -4904,9 +4796,9 @@ } }, "node_modules/@testing-library/react": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.0.0.tgz", - "integrity": "sha512-guuxUKRWQ+FgNX0h0NS0FIq3Q3uLtWVpBzcLOggmfMoUpgBnzBzvLLd4fbm6yS8ydJd94cIfY4yP9qUQjM2KwQ==", + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", "dev": true, "license": "MIT", "dependencies": { @@ -4917,10 +4809,10 @@ }, "peerDependencies": { "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0", - "@types/react-dom": "^18.0.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -4932,9 +4824,9 @@ } }, "node_modules/@testing-library/user-event": { - "version": "14.5.2", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", - "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", "license": "MIT", "engines": { "node": ">=12", @@ -4945,9 +4837,10 @@ } }, "node_modules/@tiptap/core": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.9.1.tgz", - "integrity": "sha512-tifnLL/ARzQ6/FGEJjVwj9UT3v+pENdWHdk9x6F3X0mB1y0SeCjV21wpFLYESzwNdBPAj8NMp8Behv7dBnhIfw==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.11.7.tgz", + "integrity": "sha512-zN+NFFxLsxNEL8Qioc+DL6b8+Tt2bmRbXH22Gk6F6nD30x83eaUSFlSv3wqvgyCq3I1i1NO394So+Agmayx6rQ==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -4957,33 +4850,36 @@ } }, "node_modules/@tiptap/extension-blockquote": { - "version": "2.5.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.5.9.tgz", - "integrity": "sha512-LhGyigmd/v1OjYPeoVK8UvFHbH6ffh175ZuNvseZY4PsBd7kZhrSUiuMG8xYdNX8FxamsxAzr2YpsYnOzu3W7A==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.11.7.tgz", + "integrity": "sha512-liD8kWowl3CcYCG9JQlVx1eSNc/aHlt6JpVsuWvzq6J8APWX693i3+zFqyK2eCDn0k+vW62muhSBe3u09hA3Zw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.9" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-bold": { - "version": "2.5.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.5.9.tgz", - "integrity": "sha512-XUJdzFb31t0+bwiRquJf0btBpqOB3axQNHTKM9XADuL4S+Z6OBPj0I5rYINeElw/Q7muvdWrHWHh/ovNJA1/5A==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.11.7.tgz", + "integrity": "sha512-VTR3JlldBixXbjpLTFme/Bxf1xeUgZZY3LTlt5JDlCW3CxO7k05CIa+kEZ8LXpog5annytZDUVtWqxrNjmsuHQ==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.9" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-bubble-menu": { - "version": "2.5.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.5.9.tgz", - "integrity": "sha512-NddZ8Qn5dgPPa1W4yk0jdhF4tDBh0FwzBpbnDu2Xz/0TUHrA36ugB2CvR5xS1we4zUKckgpVqOqgdelrmqqFVg==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.11.7.tgz", + "integrity": "sha512-0vYqSUSSap3kk3/VT4tFE1/6StX70I3/NKQ4J68ZSFgkgyB3ZVlYv7/dY3AkEukjsEp3yN7m8Gw8ei2eEwyzwg==", + "license": "MIT", "dependencies": { "tippy.js": "^6.3.7" }, @@ -4992,89 +4888,96 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.9", - "@tiptap/pm": "^2.5.9" + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" } }, "node_modules/@tiptap/extension-bullet-list": { - "version": "2.5.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.5.9.tgz", - "integrity": "sha512-hJTv1x4omFgaID4LMRT5tOZb/VKmi8Kc6jsf4JNq4Grxd2sANmr9qpmKtBZvviK+XD5PpTXHvL+1c8C1SQtuHQ==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.11.7.tgz", + "integrity": "sha512-WbPogE2/Q3e3/QYgbT1Sj4KQUfGAJNc5pvb7GrUbvRQsAh7HhtuO8hqdDwH8dEdD/cNUehgt17TO7u8qV6qeBw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.9" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-code": { - "version": "2.5.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.5.9.tgz", - "integrity": "sha512-Q1PL3DUXiEe5eYUwOug1haRjSaB0doAKwx7KFVI+kSGbDwCV6BdkKAeYf3us/O2pMP9D0im8RWX4dbSnatgwBA==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.11.7.tgz", + "integrity": "sha512-VpPO1Uy/eF4hYOpohS/yMOcE1C07xmMj0/D989D9aS1x95jWwUVrSkwC+PlWMUBx9PbY2NRsg1ZDwVvlNKZ6yQ==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.9" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-code-block": { - "version": "2.5.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.5.9.tgz", - "integrity": "sha512-+MUwp0VFFv2aFiZ/qN6q10vfIc6VhLoFFpfuETX10eIRks0Xuj2nGiqCDj7ca0/M44bRg2VvW8+tg/ZEHFNl9g==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.11.7.tgz", + "integrity": "sha512-To/y/2H04VWqiANy53aXjV7S6fA86c2759RsH1hTIe57jA1KyE7I5tlAofljOLZK/covkGmPeBddSPHGJbz++Q==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.9", - "@tiptap/pm": "^2.5.9" + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" } }, "node_modules/@tiptap/extension-color": { - "version": "2.5.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-color/-/extension-color-2.5.9.tgz", - "integrity": "sha512-VUGCT9iqD/Ni9arLIxkCbykAElRMFyew7uk2kbbNvttzdwzmZkbslEgCiaEZQTqKr8w4wjuQL14YOtXc6iwEww==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-color/-/extension-color-2.11.7.tgz", + "integrity": "sha512-2CWb0Qnh8Crf9OwnnWB+M1QJtWrbn6IMSwuOzk+tSzdWSazjN8h6XAZVemr0qMdAA/SyUigzorStiPxN6o3/vQ==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.9", - "@tiptap/extension-text-style": "^2.5.9" + "@tiptap/core": "^2.7.0", + "@tiptap/extension-text-style": "^2.7.0" } }, "node_modules/@tiptap/extension-document": { - "version": "2.5.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.5.9.tgz", - "integrity": "sha512-VdNZYDyCzC3W430UdeRXR9IZzPeODSbi5Xz/JEdV93THVp8AC9CrZR7/qjqdBTgbTB54VP8Yr6bKfCoIAF0BeQ==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.11.7.tgz", + "integrity": "sha512-95ouJXPjdAm9+VBRgFo4lhDoMcHovyl/awORDI8gyEn0Rdglt+ZRZYoySFzbVzer9h0cre+QdIwr9AIzFFbfdA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.9" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-dropcursor": { - "version": "2.5.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.5.9.tgz", - "integrity": "sha512-nEOb37UryG6bsU9JAs/HojE6Jg43LupNTAMISbnuB1CPAeAqNsFMwORd9eEPkyEwnQT7MkhsMOSJM44GoPGIFA==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.11.7.tgz", + "integrity": "sha512-63mL+nxQILizsr5NbmgDeOjFEWi34BLt7evwL6UUZEVM15K8V1G8pD9Y0kCXrZYpHWz0tqFRXdrhDz0Ppu8oVw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.9", - "@tiptap/pm": "^2.5.9" + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" } }, "node_modules/@tiptap/extension-floating-menu": { - "version": "2.5.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.5.9.tgz", - "integrity": "sha512-MWJIQQT6e5MgqHny8neeH2Dx926nVPF7sv4p84nX4E0dnkRbEYUP8mCsWYhSUvxxIif6e+yY+4654f2Q9qTx1w==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.11.7.tgz", + "integrity": "sha512-DG54WoUu2vxHRVzKZiR5I5RMOYj45IlxQMkBAx1wjS0ch41W8DUYEeipvMMjCeKtEI+emz03xYUcOAP9LRmg+w==", + "license": "MIT", "dependencies": { "tippy.js": "^6.3.7" }, @@ -5083,125 +4986,135 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.9", - "@tiptap/pm": "^2.5.9" + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" } }, "node_modules/@tiptap/extension-gapcursor": { - "version": "2.5.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.5.9.tgz", - "integrity": "sha512-yW7V2ebezsa7mWEDWCg4A1ZGsmSV5bEHKse9wzHCDkb7TutSVhLZxGo72U6hNN9PnAksv+FJQk03NuZNYvNyRQ==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.11.7.tgz", + "integrity": "sha512-EceesmPG7FyjXZ8EgeJPUov9G1mAf2AwdypxBNH275g6xd5dmU/KvjoFZjmQ0X1ve7mS+wNupVlGxAEUYoveew==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.9", - "@tiptap/pm": "^2.5.9" + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" } }, "node_modules/@tiptap/extension-hard-break": { - "version": "2.5.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.5.9.tgz", - "integrity": "sha512-8hQ63SgZRG4BqHOeSfeaowG2eMr2beced018pOGbpHbE3XSYoISkMVuFz4Z8UEVR3W9dTbKo4wxNufSTducocQ==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.11.7.tgz", + "integrity": "sha512-zTkZSA6q+F5sLOdCkiC2+RqJQN0zdsJqvFIOVFL/IDVOnq6PZO5THzwRRLvOSnJJl3edRQCl/hUgS0L5sTInGQ==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.9" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-heading": { - "version": "2.5.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.5.9.tgz", - "integrity": "sha512-HHowAlGUbFn1qvmY02ydM7qiPPMTGhAJn2A46enDRjNHW5UoqeMfkMpTEYaioOexyguRFSfDT3gpK68IHkQORQ==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.11.7.tgz", + "integrity": "sha512-8kWh7y4Rd2fwxfWOhFFWncHdkDkMC1Z60yzIZWjIu72+6yQxvo8w3yeb7LI7jER4kffbMmadgcfhCHC/fkObBA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.9" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-highlight": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-2.6.6.tgz", - "integrity": "sha512-Z02AYWm1AJAfhmfT4fGCI3YitijF4uNu+eiuq7OxhCiVf9IYaq8xlH2YMxa09QvMUo70ovklxk97+vQUUHeqfQ==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-2.11.7.tgz", + "integrity": "sha512-c/NH4kIpNOWCUQv8RkFNDyOcgt+2pYFpDf0QBJmzhAuv4BIeS2bDmDtuNS7VgoWRZH+xxCNXfvm2BG+kjtipEg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-history": { - "version": "2.5.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.5.9.tgz", - "integrity": "sha512-hGPtJgoZSwnVVqi/xipC2ET/9X2G2UI/Y+M3IYV1ZlM0tCYsv4spNi3uXlZqnXRwYcBXLk5u6e/dmsy5QFbL8g==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.11.7.tgz", + "integrity": "sha512-Cu5x3aS13I040QSRoLdd+w09G4OCVfU+azpUqxufZxeNs9BIJC+0jowPLeOxKDh6D5GGT2A8sQtxc6a/ssbs8g==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.9", - "@tiptap/pm": "^2.5.9" + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" } }, "node_modules/@tiptap/extension-horizontal-rule": { - "version": "2.5.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.5.9.tgz", - "integrity": "sha512-/ES5NdxCndBmZAgIXSpCJH8YzENcpxR0S8w34coSWyv+iW0Sq7rW/mksQw8ZIVsj8a7ntpoY5OoRFpSlqcvyGw==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.11.7.tgz", + "integrity": "sha512-uVmQwD2dzZ5xwmvUlciy0ItxOdOfQjH6VLmu80zyJf8Yu7mvwP8JyxoXUX0vd1xHpwAhgQ9/ozjIWYGIw79DPQ==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.9", - "@tiptap/pm": "^2.5.9" + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" } }, "node_modules/@tiptap/extension-image": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-2.6.6.tgz", - "integrity": "sha512-dwJKvoqsr72B4tcTH8hXhfBJzUMs/jXUEE9MnfzYnSXf+CYALLjF8r/IkGYbxce62GP/bMDoj8BgpF8saeHtqA==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-2.11.7.tgz", + "integrity": "sha512-YvCmTDB7Oo+A56tR4S/gcNaYpqU4DDlSQcRp5IQvmQV5EekSe0lnEazGDoqOCwsit9qQhj4MPQJhKrnaWrJUrg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-italic": { - "version": "2.5.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.5.9.tgz", - "integrity": "sha512-Bw+P139L4cy+B56zpUiRjP8BZSaAUl3JFMnr/FO+FG55QhCxFMXIc6XrC3vslNy5ef3B3zv4gCttS3ee8ByMiw==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.11.7.tgz", + "integrity": "sha512-r985bkQfG0HMpmCU0X0p/Xe7U1qgRm2mxvcp6iPCuts2FqxaCoyfNZ8YnMsgVK1mRhM7+CQ5SEg2NOmQNtHvPw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.9" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-list-item": { - "version": "2.5.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.5.9.tgz", - "integrity": "sha512-d9Eo+vBz74SMxP0r25aqiErV256C+lGz+VWMjOoqJa6xWLM1keYy12JtGQWJi8UDVZrDskJKCHq81A0uLt27WA==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.11.7.tgz", + "integrity": "sha512-6ikh7Y+qAbkSuIHXPIINqfzmWs5uIGrylihdZ9adaIyvrN1KSnWIqrZIk/NcZTg5YFIJlXrnGSRSjb/QM3WUhw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.9" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-mention": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-mention/-/extension-mention-2.9.1.tgz", - "integrity": "sha512-2IzunpivdNtDNdtAXwRiQbNhTm87zrbkhz1cCE+2y9pWiX1QLXyx0HQq/DIAjxp6v7y4sIh+5UTUTFlH7vD9wQ==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-mention/-/extension-mention-2.11.7.tgz", + "integrity": "sha512-Q/fkceDOug4VjiqrCRLzBnOL9Oj+XugWwDgwfucJJMBOJxZ3++3eZGZ54dri/xK39A4ZD+xuMBF7PrJIy+Z5dw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -5213,113 +5126,121 @@ } }, "node_modules/@tiptap/extension-ordered-list": { - "version": "2.5.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.5.9.tgz", - "integrity": "sha512-9MsWpvVvzILuEOd/GdroF7RI7uDuE1M6at9rzsaVGvCPVHZBvu1XR3MSVK5OdiJbbJuPGttlzEFLaN/rQdCGFg==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.11.7.tgz", + "integrity": "sha512-bLGCHDMB0vbJk7uu8bRg8vES3GsvxkX7Cgjgm/6xysHFbK98y0asDtNxkW1VvuRreNGz4tyB6vkcVCfrxl4jKw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.9" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-paragraph": { - "version": "2.5.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.5.9.tgz", - "integrity": "sha512-HDXGiHTJ/V85dbDMjcFj4XfqyTQZqry6V21ucMzgBZYX60X3gIn7VpQTQnnRjvULSgtfOASSJP6BELc5TyiK0w==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.11.7.tgz", + "integrity": "sha512-Pl3B4q6DJqTvvAdraqZaNP9Hh0UWEHL5nNdxhaRNuhKaUo7lq8wbDSIxIW3lvV0lyCs0NfyunkUvSm1CXb6d4Q==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.9" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-placeholder": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-2.6.2.tgz", - "integrity": "sha512-Aou6lH456j5mpry36jyAdZzINxFx6fjqvmapmmORJKV+9J889P7RN7laRRsosWHez0Oxg4KuWL3FuDexx6ZJOQ==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-2.11.7.tgz", + "integrity": "sha512-/06zXV4HIjYoiaUq1fVJo/RcU8pHbzx21evOpeG/foCfNpMI4xLU/vnxdUi6/SQqpZMY0eFutDqod1InkSOqsg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.2", - "@tiptap/pm": "^2.6.2" + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" } }, "node_modules/@tiptap/extension-strike": { - "version": "2.5.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.5.9.tgz", - "integrity": "sha512-QezkOZpczpl09S8lp5JL7sRkwREoPY16Y/lTvBcFKm3TZbVzYZZ/KwS0zpwK9HXTfXr8os4L9AGjQf0tHonX+w==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.11.7.tgz", + "integrity": "sha512-D6GYiW9F24bvAY7XMOARNZbC8YGPzdzWdXd8VOOJABhf4ynMi/oW4NNiko+kZ67jn3EGaKoz32VMJzNQgYi1HA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.9" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-text": { - "version": "2.5.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.5.9.tgz", - "integrity": "sha512-W0pfiQUPsMkwaV5Y/wKW4cFsyXAIkyOFt7uN5u6LrZ/iW9KZ/IsDODPJDikWp0aeQnXzT9NNQULTpCjbHzzS6g==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.11.7.tgz", + "integrity": "sha512-wObCn8qZkIFnXTLvBP+X8KgaEvTap/FJ/i4hBMfHBCKPGDx99KiJU6VIbDXG8d5ZcFZE0tOetK1pP5oI7qgMlQ==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.9" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-text-style": { - "version": "2.5.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-2.5.9.tgz", - "integrity": "sha512-1pNnl/a5EdY7g3IeFomm0B6eiTvAFOBeJGswoYxogzHmkWbLFhXFdgZ6qz7+k985w4qscsG1GpvtOW3IrJ9J6g==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-2.11.7.tgz", + "integrity": "sha512-LHO6DBg/9SkCQFdWlVfw9nolUmw+Cid94WkTY+7IwrpyG2+ZGQxnKpCJCKyeaFNbDoYAtvu0vuTsSXeCkgShcA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.9" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-underline": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-2.6.6.tgz", - "integrity": "sha512-3A4HqsDM/AFb2VaeWACpGexjgI257kz0yU4jNV8uyydDR2KhqeinuEnoSoOmx9T3pL006TWfPg4vaQYPO3qvrQ==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-2.11.7.tgz", + "integrity": "sha512-NtoQw6PGijOAtXC6G+0Aq0/Z5wwEjPhNHs8nsjXogfWIgaj/aI4/zfBnA06eI3WT+emMYQTl0fTc4CUPnLVU8g==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/pm": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.9.1.tgz", - "integrity": "sha512-mvV86fr7kEuDYEApQ2uMPCKL2uagUE0BsXiyyz3KOkY1zifyVm1fzdkscb24Qy1GmLzWAIIihA+3UHNRgYdOlQ==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.11.7.tgz", + "integrity": "sha512-7gEEfz2Q6bYKXM07vzLUD0vqXFhC5geWRA6LCozTiLdVFDdHWiBrvb2rtkL5T7mfLq03zc1QhH7rI3F6VntOEA==", + "license": "MIT", "dependencies": { "prosemirror-changeset": "^2.2.1", "prosemirror-collab": "^1.3.1", - "prosemirror-commands": "^1.6.0", + "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", - "prosemirror-markdown": "^1.13.0", + "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", - "prosemirror-model": "^1.22.3", + "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", - "prosemirror-tables": "^1.4.0", + "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", - "prosemirror-transform": "^1.10.0", - "prosemirror-view": "^1.34.3" + "prosemirror-transform": "^1.10.2", + "prosemirror-view": "^1.37.0" }, "funding": { "type": "github", @@ -5327,50 +5248,55 @@ } }, "node_modules/@tiptap/react": { - "version": "2.5.9", - "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-2.5.9.tgz", - "integrity": "sha512-NZYAslIb79oxIOFHx9T9ey5oX0aJ1uRbtT2vvrvvyRaO6fKWgAwMYN92bOu5/f2oUVGUp6l7wkYZGdjz/XP5bA==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-2.11.7.tgz", + "integrity": "sha512-gQZEUkAoPsBptnB4T2gAtiUxswjVGhfsM9vOElQco+b11DYmy110T2Zuhg+2YGvB/CG3RoWJx34808P0FX1ijA==", + "license": "MIT", "dependencies": { - "@tiptap/extension-bubble-menu": "^2.5.9", - "@tiptap/extension-floating-menu": "^2.5.9", + "@tiptap/extension-bubble-menu": "^2.11.7", + "@tiptap/extension-floating-menu": "^2.11.7", "@types/use-sync-external-store": "^0.0.6", - "use-sync-external-store": "^1.2.2" + "fast-deep-equal": "^3", + "use-sync-external-store": "^1" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.9", - "@tiptap/pm": "^2.5.9", - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/@tiptap/starter-kit": { - "version": "2.5.9", - "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.5.9.tgz", - "integrity": "sha512-nZ4V+vRayomjxUsajFMHv1iJ5SiSaEA65LAXze/CzyZXGMXfL2OLzY7wJoaVJ4BgwINuO0dOSAtpNDN6jI+6mQ==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.11.7.tgz", + "integrity": "sha512-K+q51KwNU/l0kqRuV5e1824yOLVftj6kGplGQLvJG56P7Rb2dPbM/JeaDbxQhnHT/KDGamG0s0Po0M3pPY163A==", + "license": "MIT", "dependencies": { - "@tiptap/core": "^2.5.9", - "@tiptap/extension-blockquote": "^2.5.9", - "@tiptap/extension-bold": "^2.5.9", - "@tiptap/extension-bullet-list": "^2.5.9", - "@tiptap/extension-code": "^2.5.9", - "@tiptap/extension-code-block": "^2.5.9", - "@tiptap/extension-document": "^2.5.9", - "@tiptap/extension-dropcursor": "^2.5.9", - "@tiptap/extension-gapcursor": "^2.5.9", - "@tiptap/extension-hard-break": "^2.5.9", - "@tiptap/extension-heading": "^2.5.9", - "@tiptap/extension-history": "^2.5.9", - "@tiptap/extension-horizontal-rule": "^2.5.9", - "@tiptap/extension-italic": "^2.5.9", - "@tiptap/extension-list-item": "^2.5.9", - "@tiptap/extension-ordered-list": "^2.5.9", - "@tiptap/extension-paragraph": "^2.5.9", - "@tiptap/extension-strike": "^2.5.9", - "@tiptap/extension-text": "^2.5.9" + "@tiptap/core": "^2.11.7", + "@tiptap/extension-blockquote": "^2.11.7", + "@tiptap/extension-bold": "^2.11.7", + "@tiptap/extension-bullet-list": "^2.11.7", + "@tiptap/extension-code": "^2.11.7", + "@tiptap/extension-code-block": "^2.11.7", + "@tiptap/extension-document": "^2.11.7", + "@tiptap/extension-dropcursor": "^2.11.7", + "@tiptap/extension-gapcursor": "^2.11.7", + "@tiptap/extension-hard-break": "^2.11.7", + "@tiptap/extension-heading": "^2.11.7", + "@tiptap/extension-history": "^2.11.7", + "@tiptap/extension-horizontal-rule": "^2.11.7", + "@tiptap/extension-italic": "^2.11.7", + "@tiptap/extension-list-item": "^2.11.7", + "@tiptap/extension-ordered-list": "^2.11.7", + "@tiptap/extension-paragraph": "^2.11.7", + "@tiptap/extension-strike": "^2.11.7", + "@tiptap/extension-text": "^2.11.7", + "@tiptap/extension-text-style": "^2.11.7", + "@tiptap/pm": "^2.11.7" }, "funding": { "type": "github", @@ -5541,12 +5467,34 @@ "@types/node": "*" } }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "license": "MIT" + }, "node_modules/@types/lodash": { "version": "4.17.7", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", "dev": true }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT" + }, "node_modules/@types/ms": { "version": "0.7.34", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", @@ -5576,60 +5524,36 @@ } }, "node_modules/@types/prop-types": { - "version": "15.7.12", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", - "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" - }, - "node_modules/@types/quill": { - "version": "1.3.10", - "resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz", - "integrity": "sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==", - "dependencies": { - "parchment": "^1.1.2" - } + "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "license": "MIT" }, "node_modules/@types/react": { - "version": "18.2.67", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.67.tgz", - "integrity": "sha512-vkIE2vTIMHQ/xL0rgmuoECBCkZFZeHr49HeWSc24AptMbNRo7pwSBvj73rlJJs9fGKj0koS+V7kQB1jHS0uCgw==", + "version": "19.1.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz", + "integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==", + "license": "MIT", "dependencies": { - "@types/prop-types": "*", - "@types/scheduler": "*", "csstype": "^3.0.2" } }, - "node_modules/@types/react-copy-to-clipboard": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.7.tgz", - "integrity": "sha512-Gft19D+as4M+9Whq1oglhmK49vqPhcLzk8WfvfLvaYMIPYanyfLy0+CwFucMJfdKoSFyySPmkkWn8/E6voQXjQ==", - "dev": true, - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/react-dom": { - "version": "18.2.22", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.22.tgz", - "integrity": "sha512-fHkBXPeNtfvri6gdsMYyW+dW7RXFo6Ad09nLFK0VQWR7yGLai/Cyvyj696gbwYvBnhGtevUG9cET0pmUbMtoPQ==", + "version": "19.1.2", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.2.tgz", + "integrity": "sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==", "dev": true, - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/react-infinite-scroller": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@types/react-infinite-scroller/-/react-infinite-scroller-1.2.5.tgz", - "integrity": "sha512-fJU1jhMgoL6NJFrqTM0Ob7tnd2sQWGxe2ESwiU6FZWbJK/VO/Er5+AOhc+e2zbT0dk5pLygqctsulOLJ8xnSzw==", - "dev": true, - "dependencies": { - "@types/react": "*" + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" } }, "node_modules/@types/react-transition-group": { - "version": "4.4.10", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", - "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", - "dependencies": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { "@types/react": "*" } }, @@ -5656,11 +5580,6 @@ "@types/node": "*" } }, - "node_modules/@types/scheduler": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", - "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" - }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", @@ -5675,7 +5594,8 @@ "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==" + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==", + "license": "MIT" }, "node_modules/@types/trusted-types": { "version": "2.0.7", @@ -5892,6 +5812,418 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@uiw/color-convert": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@uiw/color-convert/-/color-convert-2.5.1.tgz", + "integrity": "sha512-p+P8Ho0Z1AbUprES0hcLEDAaXbGH92TmjckkRQZ5S7HcyQ+9ZXlSsDFILjFbYu/okVjx5VG59T57Dx84lv9AWA==", + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0" + } + }, + "node_modules/@uiw/react-color": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@uiw/react-color/-/react-color-2.5.1.tgz", + "integrity": "sha512-u6Kj7rdhsMOls2KItpHLkG8WTghDS2jYBucLeOLLJXJDs25TuEBI9d1o939og8cUJtTwBrowWFFU63a1kGsciA==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.5.1", + "@uiw/react-color-alpha": "2.5.1", + "@uiw/react-color-block": "2.5.1", + "@uiw/react-color-chrome": "2.5.1", + "@uiw/react-color-circle": "2.5.1", + "@uiw/react-color-colorful": "2.5.1", + "@uiw/react-color-compact": "2.5.1", + "@uiw/react-color-editable-input": "2.5.1", + "@uiw/react-color-editable-input-hsla": "2.5.1", + "@uiw/react-color-editable-input-rgba": "2.5.1", + "@uiw/react-color-github": "2.5.1", + "@uiw/react-color-hue": "2.5.1", + "@uiw/react-color-material": "2.5.1", + "@uiw/react-color-name": "2.5.1", + "@uiw/react-color-saturation": "2.5.1", + "@uiw/react-color-shade-slider": "2.5.1", + "@uiw/react-color-sketch": "2.5.1", + "@uiw/react-color-slider": "2.5.1", + "@uiw/react-color-swatch": "2.5.1", + "@uiw/react-color-wheel": "2.5.1" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-alpha": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@uiw/react-color-alpha/-/react-color-alpha-2.5.1.tgz", + "integrity": "sha512-hPsIgsnuOQrqinXt3Gt+87fHudbUvvPW+TpvRY0HS9v4ptFu5UsCc/7DPTVKTaL+p+0oaA6eTbziLzPLRLzgsQ==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.5.1", + "@uiw/react-drag-event-interactive": "2.5.1" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-block": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@uiw/react-color-block/-/react-color-block-2.5.1.tgz", + "integrity": "sha512-qvubiV0z0P3OxpNt6o1UQ3CVsjVBY1/n/oz6Gzzxx9YPqSClI04AtFjwOQxF7M17SYqXv+88y77gfEfPIqk5+A==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.5.1", + "@uiw/react-color-editable-input": "2.5.1", + "@uiw/react-color-swatch": "2.5.1" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-chrome": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@uiw/react-color-chrome/-/react-color-chrome-2.5.1.tgz", + "integrity": "sha512-m/CyRaWgmkW5aQTQ8AZwyvopYm+bhvX06uS+ezQjXDYDtjLvq7RbM0JLLNIOyMXke964R58fhoX4G06ZWd8ycA==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.5.1", + "@uiw/react-color-alpha": "2.5.1", + "@uiw/react-color-editable-input": "2.5.1", + "@uiw/react-color-editable-input-hsla": "2.5.1", + "@uiw/react-color-editable-input-rgba": "2.5.1", + "@uiw/react-color-github": "2.5.1", + "@uiw/react-color-hue": "2.5.1", + "@uiw/react-color-saturation": "2.5.1" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-circle": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@uiw/react-color-circle/-/react-color-circle-2.5.1.tgz", + "integrity": "sha512-+8zb/Ork1Q5f2bq0jN+GF7OyqY+2ZDYGrdZovN3EBZLMmERbg6TM2+1gTweeFsdiEM/gpteupJpwKpO1aBCocg==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.5.1", + "@uiw/react-color-swatch": "2.5.1" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-colorful": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@uiw/react-color-colorful/-/react-color-colorful-2.5.1.tgz", + "integrity": "sha512-Y/8Y2Kman6IZQpgs4tPTGPuTNr3fJIJxf4f13jll6xuaOsVZeDq9q+DlMErggL+5ICtaBr8gG+w68nCiY+QqKg==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.5.1", + "@uiw/react-color-alpha": "2.5.1", + "@uiw/react-color-hue": "2.5.1", + "@uiw/react-color-saturation": "2.5.1" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-compact": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@uiw/react-color-compact/-/react-color-compact-2.5.1.tgz", + "integrity": "sha512-5jHJcXEkjMwcghzCgSBU2rPMVjuuaJ7B6IxypNkafRQ4FkW/6bP9WpPkzcNXCZ/gPvSJ1OMQ+Y600mdO78qG5Q==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.5.1", + "@uiw/react-color-editable-input": "2.5.1", + "@uiw/react-color-editable-input-rgba": "2.5.1", + "@uiw/react-color-swatch": "2.5.1" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-editable-input": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@uiw/react-color-editable-input/-/react-color-editable-input-2.5.1.tgz", + "integrity": "sha512-0kr5vQJGPln8LObXwfI2YLiHFz2DW3Atgi51JXlrZUyyaVujXRgMTAc1fz/1RQR6cU2A4bweFaCQljcTsv+Cdg==", + "license": "MIT", + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-editable-input-hsla": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@uiw/react-color-editable-input-hsla/-/react-color-editable-input-hsla-2.5.1.tgz", + "integrity": "sha512-gmnXB6JrYFAd8VN/EfNDJaTdkFHAnUxjzcsQjQyOEr046jDjWgEc/5o2uE1LwIvoJNg9Lo6LYsr37LnFWwsiLw==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.5.1", + "@uiw/react-color-editable-input-rgba": "2.5.1" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-editable-input-rgba": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@uiw/react-color-editable-input-rgba/-/react-color-editable-input-rgba-2.5.1.tgz", + "integrity": "sha512-rk6OxL9lTdRI45aNe3GbUghvaELk4knkEf0gvF/mPHxoeE+nNphSrO5gHm3HhoDOgaplp81VP3q4gUwcdjBzvw==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.5.1", + "@uiw/react-color-editable-input": "2.5.1" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-github": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@uiw/react-color-github/-/react-color-github-2.5.1.tgz", + "integrity": "sha512-t05rIy2ifReiVnjv3x+IVlJH7wvwtZugMeouDa/1Y7jIGZswO0zw3zMxz7qfHrzf5NVYWjmEF8QCj85ngv9brg==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.5.1", + "@uiw/react-color-swatch": "2.5.1" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-hue": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@uiw/react-color-hue/-/react-color-hue-2.5.1.tgz", + "integrity": "sha512-o7mjZhm+U4gHxaBXFxjPINeE3jWfiZAl7RUFqwn4PDZC8wvhU5hEKgJUvcXzErYro0ZYrE1fC/wUHRpI+vcEBg==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.5.1", + "@uiw/react-color-alpha": "2.5.1" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-material": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@uiw/react-color-material/-/react-color-material-2.5.1.tgz", + "integrity": "sha512-iPB4YfKVTNO1lSIQ16DMdDurDKvGTjv6Qwi/nq47yE3nnhB0YbOFwb/IZbWBS1sCTPx1an7dM2IZ+hYoYcjrXg==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.5.1", + "@uiw/react-color-editable-input": "2.5.1", + "@uiw/react-color-editable-input-rgba": "2.5.1" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-name": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@uiw/react-color-name/-/react-color-name-2.5.1.tgz", + "integrity": "sha512-JFb6DFz9kF2jI42MS/vtXZu1XzIrzcSIOqCwVkYWCQnSxOM9h+vd4pv2Yi1oy7IPgaadXUDkrGQSAvEkXU593Q==", + "dependencies": { + "colors-named": "^1.0.1", + "colors-named-hex": "^1.0.1" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0" + } + }, + "node_modules/@uiw/react-color-saturation": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@uiw/react-color-saturation/-/react-color-saturation-2.5.1.tgz", + "integrity": "sha512-mQ6eGmn6dUXfScQrb5tP0TBGCpZWzrQuYOAiwK9u31IJaxFwD1NNAzkiienWe4MQkA5zmgz7Ol6FEdLN8K+vGw==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.5.1", + "@uiw/react-drag-event-interactive": "2.5.1" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-shade-slider": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@uiw/react-color-shade-slider/-/react-color-shade-slider-2.5.1.tgz", + "integrity": "sha512-hrscAmqmy/Od/usUPETaEuvsNRhUGvNArl73d7HK6e6FjbRFPDBq40LkvjETe8BJMbxrBXTMo6dK7DO08lYq9g==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.5.1", + "@uiw/react-color-alpha": "2.5.1" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-sketch": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@uiw/react-color-sketch/-/react-color-sketch-2.5.1.tgz", + "integrity": "sha512-eQgAnlSZvqoTt6frZa/j+tFdaIBEFneIdxEUfidD8hwvyu5OR/WLHnDy/4fYAxhehDp9Ej8eS3ZsCgPACBMOtA==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.5.1", + "@uiw/react-color-alpha": "2.5.1", + "@uiw/react-color-editable-input": "2.5.1", + "@uiw/react-color-editable-input-rgba": "2.5.1", + "@uiw/react-color-hue": "2.5.1", + "@uiw/react-color-saturation": "2.5.1", + "@uiw/react-color-swatch": "2.5.1" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-slider": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@uiw/react-color-slider/-/react-color-slider-2.5.1.tgz", + "integrity": "sha512-2yluI0Akp6UMXTeAJ4CEjL8flhIFpn3xUPsFXbQmBSzMYJygleVFmwhMye8LSA2PCe3UdaqA2cWXxWsTL0FbIg==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.5.1", + "@uiw/react-color-alpha": "2.5.1" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-swatch": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@uiw/react-color-swatch/-/react-color-swatch-2.5.1.tgz", + "integrity": "sha512-EQ7UEzxdohfsdpXmcEWNmK/uiznZovEKo6+j3OLrSU5pZGO7pxjR9sQMlscikvd8Mu1Mm3U0E6bJseo2acD4Lg==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.5.1" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-wheel": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@uiw/react-color-wheel/-/react-color-wheel-2.5.1.tgz", + "integrity": "sha512-e3tDwDoC2T7zTapRRm/QxcOJ7IWJwNCoxZ/f97RL1Ib3gAN/k67H1bkR9TK7euRCUxGy031guxTgdKO9v19XFg==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.5.1", + "@uiw/react-drag-event-interactive": "2.5.1" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-drag-event-interactive": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@uiw/react-drag-event-interactive/-/react-drag-event-interactive-2.5.1.tgz", + "integrity": "sha512-GNxhxk5L4O5Gpi20A/BG5sO0GNBNwtNWJidJsJu3pgHUBErN4rhqTDXXu3BQTz5C8yOG5D02Y6Zq/6yu6ckImw==", + "license": "MIT", + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -5918,14 +6250,14 @@ } }, "node_modules/@vitest/expect": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", - "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", + "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", "devOptional": true, "license": "MIT", "dependencies": { - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", "chai": "^4.3.10" }, "funding": { @@ -5933,13 +6265,13 @@ } }, "node_modules/@vitest/runner": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz", - "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", + "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", "devOptional": true, "license": "MIT", "dependencies": { - "@vitest/utils": "1.6.0", + "@vitest/utils": "1.6.1", "p-limit": "^5.0.0", "pathe": "^1.1.1" }, @@ -5964,9 +6296,9 @@ } }, "node_modules/@vitest/runner/node_modules/yocto-queue": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", - "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", + "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", "devOptional": true, "license": "MIT", "engines": { @@ -5977,9 +6309,9 @@ } }, "node_modules/@vitest/snapshot": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", - "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", + "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -5992,9 +6324,9 @@ } }, "node_modules/@vitest/spy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", - "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", + "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", "devOptional": true, "license": "MIT", "dependencies": { @@ -6005,9 +6337,9 @@ } }, "node_modules/@vitest/utils": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", - "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", "devOptional": true, "license": "MIT", "dependencies": { @@ -7255,6 +7587,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7289,9 +7622,9 @@ } }, "node_modules/chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", "devOptional": true, "license": "MIT", "dependencies": { @@ -7301,7 +7634,7 @@ "get-func-name": "^2.0.2", "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.8" + "type-detect": "^4.1.0" }, "engines": { "node": ">=4" @@ -7441,11 +7774,6 @@ "node": ">=8" } }, - "node_modules/classnames": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" - }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -7706,14 +8034,6 @@ "wrap-ansi": "^6.2.0" } }, - "node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", - "engines": { - "node": ">=0.8" - } - }, "node_modules/clone-response": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", @@ -7766,6 +8086,28 @@ "color-support": "bin.js" } }, + "node_modules/colors-named": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/colors-named/-/colors-named-1.0.2.tgz", + "integrity": "sha512-2ANq2r393PV9njYUD66UdfBcxR1slMqRA3QRTWgCx49JoCJ+kOhyfbQYxKJbPZQIhZUcNjVOs5AlyY1WwXec3w==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/colors-named-hex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/colors-named-hex/-/colors-named-hex-1.0.2.tgz", + "integrity": "sha512-k6kq1e1pUCQvSVwIaGFq2l0LrkAPQZWyeuZn1Z8nOiYSEZiKoFj4qx690h2Kd34DFl9Me0gKS6MUwAMBJj8nuA==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -7903,14 +8245,6 @@ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, - "node_modules/copy-to-clipboard": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", - "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", - "dependencies": { - "toggle-selection": "^1.0.6" - } - }, "node_modules/cordova-plugin-android-permissions": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/cordova-plugin-android-permissions/-/cordova-plugin-android-permissions-1.1.5.tgz", @@ -8043,6 +8377,15 @@ "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==" }, + "node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -8068,6 +8411,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", "engines": { "node": ">=4" } @@ -8076,6 +8420,7 @@ "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==", + "license": "MIT", "dependencies": { "camelize": "^1.0.0", "css-color-keywords": "^1.0.0", @@ -8262,25 +8607,6 @@ "node": ">=6" } }, - "node_modules/deep-equal": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", - "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", - "dependencies": { - "is-arguments": "^1.1.1", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "regexp.prototype.flags": "^1.5.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -9660,11 +9986,6 @@ "node": ">=0.10.0" } }, - "node_modules/eventemitter3": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", - "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==" - }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -9705,7 +10026,8 @@ "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true }, "node_modules/external-editor": { "version": "3.1.0", @@ -9780,11 +10102,6 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, - "node_modules/fast-diff": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", - "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==" - }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -10560,11 +10877,6 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "node_modules/hamt_plus": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", - "integrity": "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA==" - }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -10679,6 +10991,15 @@ "node": ">=18" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/html-to-text": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", @@ -10773,6 +11094,64 @@ "ms": "^2.0.0" } }, + "node_modules/i18next": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.0.1.tgz", + "integrity": "sha512-8S8PyZbrymJZn3DaN70/34JYWNhsqrU6yA4MuzcygJBv+41dgNMocEA8h+kV1P7MCc1ll03lOTOIXE7mpNCicw==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.10" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.5.tgz", + "integrity": "sha512-OstebRKqKiQw8xEvQF5aRyUujsCatanj7Q9eo5iiH2gJpoXGZ7483ol3sVBwfqbobTQPNH1J+NAyJ1aCQoEC+w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-http-backend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz", + "integrity": "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==", + "license": "MIT", + "dependencies": { + "cross-fetch": "4.0.0" + } + }, + "node_modules/i18next-localstorage-backend": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/i18next-localstorage-backend/-/i18next-localstorage-backend-4.2.0.tgz", + "integrity": "sha512-vglEQF0AnLriX7dLA2drHnqAYzHxnLwWQzBDw8YxcIDjOvYZz5rvpal59Dq4In+IHNmGNM32YgF0TDjBT0fHmA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.22.15" + } + }, "node_modules/iconv-corefoundation": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", @@ -11054,21 +11433,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -11748,6 +12112,27 @@ "node": ">=8" } }, + "node_modules/jotai": { + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.12.3.tgz", + "integrity": "sha512-DpoddSkmPGXMFtdfnoIHfueFeGP643nqYUWC6REjUcME+PG2UkAtYnLbffRDw3OURI9ZUTcRWkRGLsOvxuWMCg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=17.0.0", + "react": ">=17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/jpeg-exif": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz", @@ -12005,6 +12390,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", "dependencies": { "uc.micro": "^2.0.0" } @@ -12286,13 +12672,13 @@ } }, "node_modules/magic-string": { - "version": "0.30.10", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", - "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", "devOptional": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "@jridgewell/sourcemap-codec": "^1.5.0" } }, "node_modules/make-fetch-happen": { @@ -12395,6 +12781,7 @@ "version": "14.1.0", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "license": "MIT", "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", @@ -12461,7 +12848,8 @@ "node_modules/mdurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==" + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" }, "node_modules/merge-stream": { "version": "2.0.0", @@ -12890,6 +13278,48 @@ "semver": "^7.3.5" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-gyp": { "version": "9.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", @@ -15382,21 +15812,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-is": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", - "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -15692,11 +16107,6 @@ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" }, - "node_modules/parchment": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz", - "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==" - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -15936,9 +16346,9 @@ } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "funding": [ { "type": "opencollective", @@ -15953,10 +16363,11 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -15965,7 +16376,8 @@ "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==" + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" }, "node_modules/postject": { "version": "1.0.0-alpha.6", @@ -15990,6 +16402,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-bytes": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", @@ -16144,13 +16572,14 @@ } }, "node_modules/prosemirror-commands": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.6.0.tgz", - "integrity": "sha512-xn1U/g36OqXn2tn5nGmvnnimAj/g1pUx2ypJJIe8WkVX83WyJVC5LTARaxZa2AtQRwntu9Jc5zXs9gL9svp/mg==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", + "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==", + "license": "MIT", "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", - "prosemirror-transform": "^1.0.0" + "prosemirror-transform": "^1.10.2" } }, "node_modules/prosemirror-dropcursor": { @@ -16204,12 +16633,14 @@ } }, "node_modules/prosemirror-markdown": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.0.tgz", - "integrity": "sha512-UziddX3ZYSYibgx8042hfGKmukq5Aljp2qoBiJRejD/8MH70siQNz5RB1TrdTPheqLMy4aCe4GYNF10/3lQS5g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.2.tgz", + "integrity": "sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==", + "license": "MIT", "dependencies": { + "@types/markdown-it": "^14.0.0", "markdown-it": "^14.0.0", - "prosemirror-model": "^1.20.0" + "prosemirror-model": "^1.25.0" } }, "node_modules/prosemirror-menu": { @@ -16224,9 +16655,10 @@ } }, "node_modules/prosemirror-model": { - "version": "1.22.3", - "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.22.3.tgz", - "integrity": "sha512-V4XCysitErI+i0rKFILGt/xClnFJaohe/wrrlT2NSZ+zk8ggQfDH4x2wNK7Gm0Hp4CIoWizvXFP7L9KMaCuI0Q==", + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.1.tgz", + "integrity": "sha512-AUvbm7qqmpZa5d9fPKMvH1Q5bqYQvAZWOGRvxsB6iFLyycvC9MwNemNVjHVrWgjaoxAfY8XVg7DbvQ/qxvI9Eg==", + "license": "MIT", "dependencies": { "orderedmap": "^2.0.0" } @@ -16260,15 +16692,16 @@ } }, "node_modules/prosemirror-tables": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.4.0.tgz", - "integrity": "sha512-fxryZZkQG12fSCNuZDrYx6Xvo2rLYZTbKLRd8rglOPgNJGMKIS8uvTt6gGC38m7UCu/ENnXIP9pEz5uDaPc+cA==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.7.1.tgz", + "integrity": "sha512-eRQ97Bf+i9Eby99QbyAiyov43iOKgWa7QCGly+lrDt7efZ1v8NWolhXiB43hSDGIXT1UXgbs4KJN3a06FGpr1Q==", + "license": "MIT", "dependencies": { - "prosemirror-keymap": "^1.1.2", - "prosemirror-model": "^1.8.1", - "prosemirror-state": "^1.3.1", - "prosemirror-transform": "^1.2.1", - "prosemirror-view": "^1.13.3" + "prosemirror-keymap": "^1.2.2", + "prosemirror-model": "^1.25.0", + "prosemirror-state": "^1.4.3", + "prosemirror-transform": "^1.10.3", + "prosemirror-view": "^1.39.1" } }, "node_modules/prosemirror-trailing-node": { @@ -16297,17 +16730,19 @@ } }, "node_modules/prosemirror-transform": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.2.tgz", - "integrity": "sha512-2iUq0wv2iRoJO/zj5mv8uDUriOHWzXRnOTVgCzSXnktS/2iQRa3UUQwVlkBlYZFtygw6Nh1+X4mGqoYBINn5KQ==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.4.tgz", + "integrity": "sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==", + "license": "MIT", "dependencies": { "prosemirror-model": "^1.21.0" } }, "node_modules/prosemirror-view": { - "version": "1.36.0", - "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.36.0.tgz", - "integrity": "sha512-U0GQd5yFvV5qUtT41X1zCQfbw14vkbbKwLlQXhdylEmgpYVHkefXYcC4HHwWOfZa3x6Y8wxDLUBv7dxN5XQ3nA==", + "version": "1.39.2", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.39.2.tgz", + "integrity": "sha512-BmOkml0QWNob165gyUxXi5K5CVUgVPpqMEAAml/qzgKn9boLUWVPzQ6LtzXw8Cn1GtRQX4ELumPxqtLTDaAKtg==", + "license": "MIT", "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", @@ -16349,6 +16784,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", "engines": { "node": ">=6" } @@ -16398,42 +16834,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/quill": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz", - "integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==", - "dependencies": { - "clone": "^2.1.1", - "deep-equal": "^1.0.1", - "eventemitter3": "^2.0.3", - "extend": "^3.0.2", - "parchment": "^1.1.4", - "quill-delta": "^3.6.2" - } - }, - "node_modules/quill-delta": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz", - "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==", - "dependencies": { - "deep-equal": "^1.0.1", - "extend": "^3.0.2", - "fast-diff": "1.1.2" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/quill-image-resize-module-react": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/quill-image-resize-module-react/-/quill-image-resize-module-react-3.0.0.tgz", - "integrity": "sha512-3jVChLoXh+fwEELx3OswOEEuF+1KU3r/B9RAqZ//s+d+UMduVZzUepU1g/XoxjKoBJvWD2lJwBIFBRUNb8ebCw==", - "dependencies": { - "lodash": "^4.17.4", - "quill": "^1.2.2", - "raw-loader": "^0.5.1" - } - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -16442,34 +16842,15 @@ "safe-buffer": "^5.1.0" } }, - "node_modules/raw-loader": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-0.5.1.tgz", - "integrity": "sha512-sf7oGoLuaYAScB4VGr0tzetsYlS8EJH6qnTCfQ/WVEa89hALQ4RQfCKt5xCyPQKPDUbVUAIP1QsxAwfAjlDp7Q==" - }, "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/react-copy-to-clipboard": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz", - "integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==", - "dependencies": { - "copy-to-clipboard": "^3.3.1", - "prop-types": "^15.8.1" - }, - "peerDependencies": { - "react": "^15.3.0 || 16 || 17 || 18" - } - }, "node_modules/react-countdown-circle-timer": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/react-countdown-circle-timer/-/react-countdown-circle-timer-3.2.1.tgz", @@ -16479,15 +16860,15 @@ } }, "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" + "scheduler": "^0.26.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^19.1.0" } }, "node_modules/react-dropzone": { @@ -16516,24 +16897,36 @@ "react-dom": ">= 16.3" } }, - "node_modules/react-infinite-scroller": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/react-infinite-scroller/-/react-infinite-scroller-1.2.6.tgz", - "integrity": "sha512-mGdMyOD00YArJ1S1F3TVU9y4fGSfVVl6p5gh/Vt4u99CJOptfVu/q5V/Wlle72TMgYlBwIhbxK5wF0C/R33PXQ==", + "node_modules/react-i18next": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.4.1.tgz", + "integrity": "sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw==", + "license": "MIT", "dependencies": { - "prop-types": "^15.5.8" + "@babel/runtime": "^7.25.0", + "html-parse-stringify": "^3.0.1" }, "peerDependencies": { - "react": "^0.14.9 || ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + "i18next": ">= 23.2.3", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } } }, "node_modules/react-intersection-observer": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.13.0.tgz", - "integrity": "sha512-y0UvBfjDiXqC8h0EWccyaj4dVBWMxgEx0t5RGNzQsvkfvZwugnKwxpu70StY4ivzYuMajavwUDjH4LJyIki9Lw==", + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz", + "integrity": "sha512-w9nJSEp+DrW9KmQmeWHQyfaP6b03v+TdXynaoA964Wxt7mdR3An11z4NNCQgL4gKSK7y1ver2Fq+JKH6CWEzUA==", + "license": "MIT", "peerDependencies": { - "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "react-dom": { @@ -16547,14 +16940,15 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/react-json-view-lite": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-2.0.1.tgz", - "integrity": "sha512-yElNMSzL7UJ9rMDQIbTiBemXbvfAoqpxM/0IQd3nr52CLLBC0HxOSKcta/bayct2QCq7ZVzLzI8CGfuf387hHw==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-2.4.1.tgz", + "integrity": "sha512-fwFYknRIBxjbFm0kBDrzgBy1xa5tDg2LyXXBepC5f1b+MY3BUClMCsvanMPn089JbV1Eg3nZcrp0VCuH43aXnA==", + "license": "MIT", "engines": { "node": ">=18" }, "peerDependencies": { - "react": "^18.0.0" + "react": "^18.0.0 || ^19.0.0" } }, "node_modules/react-lifecycles-compat": { @@ -16595,47 +16989,6 @@ "react": "*" } }, - "node_modules/react-quill": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0.tgz", - "integrity": "sha512-4qQtv1FtCfLgoD3PXAur5RyxuUbPXQGOHgTlFie3jtxp43mXDtzCKaOgQ3mLyZfi1PUlyjycfivKelFhy13QUg==", - "dependencies": { - "@types/quill": "^1.3.10", - "lodash": "^4.17.4", - "quill": "^1.3.7" - }, - "peerDependencies": { - "react": "^16 || ^17 || ^18", - "react-dom": "^16 || ^17 || ^18" - } - }, - "node_modules/react-redux": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", - "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==", - "dependencies": { - "@types/use-sync-external-store": "^0.0.3", - "use-sync-external-store": "^1.0.0" - }, - "peerDependencies": { - "@types/react": "^18.2.25", - "react": "^18.0", - "redux": "^5.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "redux": { - "optional": true - } - } - }, - "node_modules/react-redux/node_modules/@types/use-sync-external-store": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", - "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" - }, "node_modules/react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -16649,6 +17002,7 @@ "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", @@ -16661,9 +17015,10 @@ } }, "node_modules/react-virtualized": { - "version": "9.22.5", - "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.22.5.tgz", - "integrity": "sha512-YqQMRzlVANBv1L/7r63OHa2b0ZsAaDp1UhVNEdUaXI8A5u6hTpA5NYtUueLH2rFuY/27mTGIBl7ZhqFKzw18YQ==", + "version": "9.22.6", + "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.22.6.tgz", + "integrity": "sha512-U5j7KuUQt3AaMatlMJ0UJddqSiX+Km0YJxSqbAzIiGw5EmNz0khMyqP2hzgu4+QUtm+QPIrxzUX4raJxmVJnHg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.7.2", "clsx": "^1.0.4", @@ -16673,8 +17028,8 @@ "react-lifecycles-compat": "^3.0.4" }, "peerDependencies": { - "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0", - "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0" + "react": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/react-virtualized/node_modules/clsx": { @@ -16850,25 +17205,6 @@ "node": ">=8.10.0" } }, - "node_modules/recoil": { - "version": "0.7.7", - "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz", - "integrity": "sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==", - "dependencies": { - "hamt_plus": "1.0.2" - }, - "peerDependencies": { - "react": ">=16.13.1" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -16882,19 +17218,6 @@ "node": ">=8" } }, - "node_modules/redux": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" - }, - "node_modules/redux-thunk": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", - "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", - "peerDependencies": { - "redux": "^5.0.0" - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -17154,11 +17477,6 @@ "url": "https://github.com/sponsors/jet2jet" } }, - "node_modules/reselect": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", - "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==" - }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -17495,12 +17813,10 @@ } }, "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "dependencies": { - "loose-envify": "^1.1.0" - } + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" }, "node_modules/scroll-into-view-if-needed": { "version": "3.1.0", @@ -17643,7 +17959,8 @@ "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==" + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" }, "node_modules/shebang-command": { "version": "2.0.0", @@ -17920,9 +18237,10 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -18306,16 +18624,17 @@ } }, "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==", + "version": "6.1.17", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.17.tgz", + "integrity": "sha512-97D7DwWanI7nN24v0D4SvbfjLE9656umNSJZkBkDIWL37aZqG/wRQ+Y9pWtXyBIM/NSfcBzHLErEsqHmJNSVUg==", + "license": "MIT", "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", + "postcss": "8.4.49", "shallowequal": "1.1.0", "stylis": "4.3.2", "tslib": "2.6.2" @@ -18335,7 +18654,8 @@ "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==" + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==", + "license": "MIT" }, "node_modules/stylis": { "version": "4.2.0", @@ -18685,11 +19005,6 @@ "node": ">=8.0" } }, - "node_modules/toggle-selection": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", - "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" - }, "node_modules/tough-cookie": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", @@ -18812,9 +19127,9 @@ } }, "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", "devOptional": true, "license": "MIT", "engines": { @@ -18918,7 +19233,8 @@ "node_modules/uc.micro": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==" + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" }, "node_modules/ufo": { "version": "1.5.3", @@ -19169,11 +19485,12 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", - "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/utf8-byte-length": { @@ -19307,9 +19624,9 @@ } }, "node_modules/vite-node": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", - "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", + "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -19380,17 +19697,17 @@ } }, "node_modules/vitest": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", - "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", + "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", "devOptional": true, "license": "MIT", "dependencies": { - "@vitest/expect": "1.6.0", - "@vitest/runner": "1.6.0", - "@vitest/snapshot": "1.6.0", - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", + "@vitest/expect": "1.6.1", + "@vitest/runner": "1.6.1", + "@vitest/snapshot": "1.6.1", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", "acorn-walk": "^8.3.2", "chai": "^4.3.10", "debug": "^4.3.4", @@ -19404,7 +19721,7 @@ "tinybench": "^2.5.1", "tinypool": "^0.8.3", "vite": "^5.0.0", - "vite-node": "1.6.0", + "vite-node": "1.6.1", "why-is-node-running": "^2.2.2" }, "bin": { @@ -19419,8 +19736,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.6.0", - "@vitest/ui": "1.6.0", + "@vitest/browser": "1.6.1", + "@vitest/ui": "1.6.1", "happy-dom": "*", "jsdom": "*" }, @@ -19445,6 +19762,15 @@ } } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/w3c-keyname": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", diff --git a/package.json b/package.json index f5b6940..2869cb9 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "vite", "build": "vite build", + "format": "prettier --write .", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", "test": "vitest", @@ -19,32 +20,31 @@ "@capacitor/core": "^6.1.2", "@capacitor/filesystem": "^6.0.1", "@capacitor/local-notifications": "^6.1.0", - "@chatscope/chat-ui-kit-react": "^2.0.3", - "@dnd-kit/core": "^6.1.0", - "@dnd-kit/sortable": "^8.0.0", + "@dnd-kit/core": "^6.3.0", + "@dnd-kit/sortable": "^10.0.0", "@electron/packager": "^18.3.6", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.0", "@evva/capacitor-secure-storage-plugin": "^3.0.1", - "@mui/icons-material": "^5.16.4", - "@mui/lab": "^5.0.0-alpha.173", - "@mui/material": "^5.16.7", - "@reduxjs/toolkit": "^2.2.7", - "@tanstack/react-virtual": "^3.10.8", + "@mui/icons-material": "^7.0.1", + "@mui/lab": "^7.0.0-beta.11", + "@mui/material": "^7.0.1", + "@tanstack/react-virtual": "^3.13.6", "@testing-library/jest-dom": "^6.4.6", - "@testing-library/user-event": "^14.5.2", - "@tiptap/extension-color": "^2.5.9", - "@tiptap/extension-highlight": "^2.6.6", - "@tiptap/extension-image": "^2.6.6", - "@tiptap/extension-mention": "^2.9.1", - "@tiptap/extension-placeholder": "^2.6.2", - "@tiptap/extension-text-style": "^2.5.9", - "@tiptap/extension-underline": "^2.6.6", - "@tiptap/pm": "^2.5.9", - "@tiptap/react": "^2.5.9", - "@tiptap/starter-kit": "^2.5.9", + "@testing-library/user-event": "^14.6.1", + "@tiptap/extension-color": "^2.11.7", + "@tiptap/extension-highlight": "^2.11.7", + "@tiptap/extension-image": "^2.11.7", + "@tiptap/extension-mention": "^2.11.7", + "@tiptap/extension-placeholder": "^2.11.7", + "@tiptap/extension-text-style": "^2.11.7", + "@tiptap/extension-underline": "^2.11.7", + "@tiptap/pm": "^2.11.7", + "@tiptap/react": "^2.11.7", + "@tiptap/starter-kit": "^2.11.7", "@transistorsoft/capacitor-background-fetch": "^6.0.1", "@types/chrome": "^0.0.263", + "@uiw/react-color": "^2.5.1", "adm-zip": "^0.5.16", "asmcrypto.js": "2.3.2", "axios": "^1.7.7", @@ -60,29 +60,29 @@ "emoji-picker-react": "^4.12.0", "file-saver": "^2.0.5", "html-to-text": "^9.0.5", + "i18next": "^25.0.1", + "i18next-browser-languagedetector": "^8.0.5", + "i18next-http-backend": "^3.0.2", + "i18next-localstorage-backend": "^4.2.0", + "jotai": "^2.12.3", "jssha": "3.3.1", "lit": "^3.2.1", "lodash": "^4.17.21", "mime": "^4.0.4", "moment": "^2.30.1", "npm": "^10.8.3", - "quill-image-resize-module-react": "^3.0.0", - "react": "^18.2.0", - "react-copy-to-clipboard": "^5.1.0", + "react": "^19.1.0", "react-countdown-circle-timer": "^3.2.1", - "react-dom": "^18.2.0", + "react-dom": "^19.1.0", "react-dropzone": "^14.2.3", "react-frame-component": "^5.2.7", - "react-infinite-scroller": "^1.2.6", - "react-intersection-observer": "^9.13.0", - "react-json-view-lite": "^2.0.1", + "react-i18next": "^15.4.1", + "react-intersection-observer": "^9.16.0", + "react-json-view-lite": "^2.4.1", "react-loader-spinner": "^6.1.6", "react-qr-code": "^2.0.15", - "react-quill": "^2.0.0", - "react-redux": "^9.1.2", - "react-virtualized": "^9.22.5", + "react-virtualized": "^9.22.6", "react-virtuoso": "^4.10.4", - "recoil": "^0.7.7", "short-unique-id": "^5.2.0", "slate": "^0.103.0", "slate-react": "^0.109.0", @@ -94,14 +94,12 @@ "vite-plugin-wasm": "^3.3.0" }, "devDependencies": { - "@testing-library/dom": "^10.3.0", - "@testing-library/react": "^16.0.0", + "@testing-library/dom": "^10.4.0", + "@testing-library/react": "^16.3.0", "@types/dompurify": "^3.0.5", "@types/lodash": "^4.17.7", - "@types/react": "^18.2.64", - "@types/react-copy-to-clipboard": "^5.0.7", - "@types/react-dom": "^18.2.21", - "@types/react-infinite-scroller": "^1.2.5", + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", "@types/react-virtualized": "^9.21.30", "@typescript-eslint/eslint-plugin": "^7.1.1", "@typescript-eslint/parser": "^7.1.1", @@ -110,9 +108,16 @@ "eslint": "^8.57.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", + "prettier": "^3.5.3", "rename-cli": "^6.2.1", "typescript": "^5.2.2", "vite": "^5.1.6", - "vitest": "^1.6.0" + "vitest": "^1.6.1" + }, + "overrides": { + "react-loader-spinner": { + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + } } } diff --git a/public/locales/de/auth.json b/public/locales/de/auth.json new file mode 100644 index 0000000..5b14183 --- /dev/null +++ b/public/locales/de/auth.json @@ -0,0 +1,43 @@ +{ + "account": { + "your": "ihr Konto", + "account_many": "Konten", + "account_one": "Konto" + }, + "advanced_users": "für fortgeschrittene Benutzer", + "apikey": { + "alternative": "Alternative: Datei auswählen", + "change": "API-Schlüssel ändern", + "enter": "API-Schlüssel eingeben", + "import": "API-Schlüssel importieren", + "key": "API-Schlüssel", + "select_valid": "gültigen API-Schlüssel auswählen" + }, + "authenticate": "authentifizieren", + "build_version": "Build-Version", + "create_account": "Konto erstellen", + "download_account": "Konto herunterladen", + "keep_secure": "Bewahren Sie Ihre Kontodatei sicher auf", + "node": { + "choose": "benutzerdefinierten Node auswählen", + "custom_many": "benutzerdefinierte Nodes", + "use_custom": "benutzerdefinierten Node verwenden", + "use_local": "lokalen Node verwenden", + "using": "verwende Node", + "using_public": "öffentlichen Node verwenden" + }, + "password": "Passwort", + "password_confirmation": "Passwort bestätigen", + "return_to_list": "zurück zur Liste", + "wallet": { + "password_confirmation": "Wallet-Passwort bestätigen", + "password": "Wallet-Passwort", + "keep_password": "aktuelles Passwort beibehalten", + "new_password": "neues Passwort", + "error": { + "missing_new_password": "bitte neues Passwort eingeben", + "missing_password": "bitte Passwort eingeben" + } + }, + "welcome": "willkommen bei" +} diff --git a/public/locales/de/core.json b/public/locales/de/core.json new file mode 100644 index 0000000..c2b7a29 --- /dev/null +++ b/public/locales/de/core.json @@ -0,0 +1,71 @@ +{ + "add": "hinzufügen", + "cancel": "abbrechen", + "choose": "auswählen", + "close": "schließen", + "continue": "fortfahren", + "core": { + "block_height": "Blockhöhe", + "information": "Kerninformationen", + "peers": "verbundene Peers", + "version": "Kernversion" + }, + "description": "Beschreibung", + "edit": "bearbeiten", + "export": "exportieren", + "import": "importieren", + "last_height": "letzte Höhe", + "loading": "Lade...", + "logout": "abmelden", + "minting_status": "Präge-Status", + "payment_notification": "Zahlungsbenachrichtigung", + "price": "Preis", + "q_mail": "Q-Mail", + "result": { + "error": { + "generic": "Ein Fehler ist aufgetreten", + "incorrect_password": "Falsches Passwort", + "save_qdn": "Speichern in QDN nicht möglich" + }, + "status": { + "minting": "(Prägung)", + "not_minting": "(keine Prägung)", + "synchronized": "synchronisiert", + "synchronizing": "synchronisiere" + }, + "success": { + "publish_qdn": "Erfolgreich in QDN veröffentlicht" + } + }, + "save_options": { + "no_pinned_changes": "Derzeit keine Änderungen an Ihren angehefteten Apps", + "overwrite_changes": "Die App konnte Ihre vorhandenen in QDN gespeicherten angehefteten Apps nicht herunterladen. Möchten Sie diese Änderungen überschreiben?", + "overwrite_qdn": "In QDN überschreiben", + "publish_qdn": "Möchten Sie Ihre Einstellungen in QDN (verschlüsselt) veröffentlichen?", + "qdn": "QDN-Speicherung verwenden", + "register_name": "Sie benötigen einen registrierten Qortal-Namen, um Ihre angehefteten Apps in QDN zu speichern.", + "reset_pinned": "Gefällt Ihnen Ihre aktuellen lokalen Änderungen nicht? Möchten Sie zu den Standard-Anheftungen zurückkehren?", + "reset_qdn": "Gefällt Ihnen Ihre aktuellen lokalen Änderungen nicht? Möchten Sie zu Ihren in QDN gespeicherten Anheftungen zurückkehren?", + "revert_default": "Auf Standard zurücksetzen", + "revert_qdn": "Auf QDN zurücksetzen", + "save_qdn": "In QDN speichern", + "save": "speichern", + "settings": "Sie verwenden die Export/Import-Methode zum Speichern von Einstellungen.", + "unsaved_changes": "Sie haben nicht gespeicherte Änderungen an Ihren angehefteten Apps. Speichern Sie sie in QDN." + }, + "settings": "Einstellungen", + "supply": "Angebot", + "theme": { + "dark": "Dunkelmodus", + "light": "Hellmodus" + }, + "title": "Titel", + "tutorial": "Tutorial", + "user_lookup": "Benutzersuche", + "wallet": { + "backup_wallet": "Wallet sichern", + "wallet": "Wallet", + "wallet_other": "Wallets" + }, + "welcome": "Willkommen" +} diff --git a/public/locales/de/tutorial.json b/public/locales/de/tutorial.json new file mode 100644 index 0000000..14a7021 --- /dev/null +++ b/public/locales/de/tutorial.json @@ -0,0 +1,21 @@ +{ + "1_getting_started": "1. Erste Schritte", + "2_overview": "2. Überblick", + "3_groups": "3. Qortal-Gruppen", + "4_obtain_qort": "4. QORT erhalten", + "account_creation": "Kontoerstellung", + "important_info": "wichtige Informationen!", + "apps": { + "dashboard": "1. App-Dashboard", + "navigation": "2. App-Navigation" + }, + "initial": { + "6_qort": "mindestens 6 QORT im Wallet haben", + "explore": "erkunden", + "general_chat": "allgemeiner Chat", + "getting_started": "erste Schritte", + "register_name": "einen Namen registrieren", + "see_apps": "apps ansehen", + "trade_qort": "QORT handeln" + } +} diff --git a/public/locales/en/auth.json b/public/locales/en/auth.json new file mode 100644 index 0000000..6591afe --- /dev/null +++ b/public/locales/en/auth.json @@ -0,0 +1,48 @@ +{ + "account": { + "your": "your account", + "account_many": "accounts", + "account_one": "account" + }, + "advanced_users": "for advanced users", + "apikey": { + "alternative": "alternative: File select", + "change": "change APIkey", + "enter": "enter APIkey", + "import": "import APIkey", + "key": "API key", + "select_valid": "select a valid apikey" + }, + "authenticate": "authenticate", + "build_version": "build version", + "create_account": "create account", + "download_account": "download account", + "keep_secure": "keep your account file secure", + "node": { + "choose": "choose custom node", + "custom_many": "custom nodes", + "use_custom": "use custom node", + "use_local": "use local node", + "using": "using node", + "using_public": "using public node" + }, + "password": "password", + "password_confirmation": "confirm password", + "return_to_list": "return to list", + "tips": { + "digital_id": "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.", + "new_account": "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.", + "new_users": "new users start here!" + }, + "wallet": { + "password_confirmation": "confirm wallet password", + "password": "wallet password", + "keep_password": "keep current password", + "new_password": "new password", + "error": { + "missing_new_password": "please enter a new password", + "missing_password": "please enter your password" + } + }, + "welcome": "welcome to" +} diff --git a/public/locales/en/core.json b/public/locales/en/core.json new file mode 100644 index 0000000..5ba27e8 --- /dev/null +++ b/public/locales/en/core.json @@ -0,0 +1,111 @@ +{ + "action": { + "add": "add", + "accept": "accept", + "backup_account": "backup account", + "backup_wallet": "backup wallet", + "cancel": "cancel", + "change": "change", + "change_language": "change language", + "choose": "choose", + "close": "close", + "continue": "continue", + "continue_logout": "continue to logout", + "decline": "decline", + "edit": "edit", + "export": "export", + "import": "import", + "invite": "invite", + "join": "join", + "logout": "logout", + "notify": "notify" + }, + "core": { + "block_height": "block height", + "information": "core information", + "peers": "connected peers", + "version": "core version" + }, + "count": { + "none": "none", + "one": "one" + }, + "description": "description", + "fee": { + "payment": "payment fee", + "publish": "publish fee" + }, + "page": { + "last": "last", + "first": "first", + "next": "next", + "previous": "previous" + }, + "downloading_qdn": "downloading from QDN", + "last_height": "last height", + "loading": "loading...", + "loading_posts": "loading posts... please wait.", + "message_us": "please message us on Telegram or Discord if you need 4 QORT to start chatting without any limitations", + "minting_status": "minting status", + "new_user": "are you a new user?", + "payment_notification": "payment notification", + "price": "price", + "q_mail": "q-mail", + "message": { + "error": { + "generic": "an error occurred", + "incorrect_password": "incorrect password", + "save_qdn": "unable to save to QDN" + }, + "status": { + "minting": "(minting)", + "not_minting": "(not minting)", + "synchronized": "synchronized", + "synchronizing": "synchronizing" + }, + "success": { + "order_submitted": "your buy order was submitted", + "publish_qdn": "successfully published to QDN", + "request_read": "I have read this request", + "transfer": "the transfer was succesful!" + } + }, + "save_options": { + "no_pinned_changes": "you currently do not have any changes to your pinned apps", + "overwrite_changes": "the app was unable to download your existing QDN-saved pinned apps. Would you like to overwrite those changes?", + "overwrite_qdn": "overwrite to QDN", + "publish_qdn": "would you like to publish your settings to QDN (encrypted)?", + "qdn": "use QDN saving", + "register_name": "you need a registered Qortal name to save your pinned apps to QDN.", + "reset_pinned": "don't like your current local changes? Would you like to reset to the default pinned apps?", + "reset_qdn": "don't like your current local changes? Would you like to reset to your saved QDN pinned apps?", + "revert_default": "revert to default", + "revert_qdn": "revert to QDN", + "save_qdn": "save to QDN", + "save": "save", + "settings": "you are using the export/import way of saving settings.", + "unsaved_changes": " you have unsaved changes to your pinned apps. Save them to QDN." + }, + "settings": "settings", + "supply": "supply", + "theme": { + "dark": "dark mode", + "light": "light mode" + }, + "time": { + "day_one": "{{count}} day", + "day_other": "{{count}} days", + "hour_one": "{{count}} hour", + "hour_other": "{{count}} hours", + "minute_one": "{{count}} minute", + "minute_other": "{{count}} minutes" + }, + "title": "title", + "tutorial": "tutorial", + "user_lookup": "user lookup", + "wallet": { + "wallet": "wallet", + "wallet_other": "wallets" + }, + "welcome": "welcome" +} diff --git a/public/locales/en/group.json b/public/locales/en/group.json new file mode 100644 index 0000000..345c6d0 --- /dev/null +++ b/public/locales/en/group.json @@ -0,0 +1,65 @@ +{ + "action": { + "cancel_ban": "cancel ban", + "create_group": "create group", + "find_group": "find group", + "join_group": "join group", + "invite_member": "invite member", + "refetch_page": "refetch page", + "return_to_thread": "return to threads" + }, + "advanced_options": "advanced options", + "approval_threshold": "group Approval Threshold (number / percentage of Admins that must approve a transaction)", + "ban_list": "ban list", + "block_delay": { + "minimum": "minimum Block delay for Group Transaction Approvals", + "maximum": "maximum Block delay for Group Transaction Approvals" + }, + "group": { + "closed": "closed (private) - users need permission to join", + "description": "description of group", + "invites": "group invites", + "management": "group management", + "name": "name of group", + "open": "open (public)", + "type": "group type" + }, + "invitation_expiry": "invitation Expiry Time", + "join_requests": "join requests", + "question": { + "cancel_ban": "would you like to perform a CANCEL_GROUP_BAN transaction?", + "create_group": "would you like to perform an CREATE_GROUP transaction?", + "group_invite": "would you like to perform a GROUP_INVITE transaction?", + "join_group": "would you like to perform an JOIN_GROUP transaction?", + "provide_thread": "please provide a thread title" + }, + "message": { + "generic": { + "encryption_key": "the group's first common encryption key is in the process of creation. Please wait a few minutes for it to be retrieved by the network. Checking every 2 minutes...", + "group_invited_you": "{{group}} has invited you", + "no_display": "nothing to display", + "no_selection": "no group selected", + "not_part_group": "you are not part of the encrypted group of members. Wait until an admin re-encrypts the keys.", + "only_encrypted": "only unencrypted messages will be displayed.", + "setting_group": "setting up group... please wait." + }, + "error": { + "access_name": "cannot send a message without a access to your name", + "description_required": "please provide a description", + "group_info": "cannot access group information", + "name_required": "please provide a name", + "notify_admins": "try notifying an admin from the list of admins below:" + }, + "success": { + "group_creation": "successfully created group. It may take a couple of minutes for the changes to propagate", + "group_creation_name": "created group {{group_name}}: awaiting confirmation", + "group_creation_label": "created group {{name}}: success!", + "group_invite": "successfully invited {{value}}. It may take a couple of minutes for the changes to propagate", + "join_creation": "successfully requested to join group. It may take a couple of minutes for the changes to propagate", + "group_join_name": "joined group {{group_name}}: awaiting confirmation", + "group_join_label": "joined group {{name}}: success!", + "loading_threads": "loading threads... please wait.", + "unbanned_user": "successfully unbanned user. It may take a couple of minutes for the changes to propagate" + } + } +} diff --git a/public/locales/en/tutorial.json b/public/locales/en/tutorial.json new file mode 100644 index 0000000..08c7628 --- /dev/null +++ b/public/locales/en/tutorial.json @@ -0,0 +1,21 @@ +{ + "1_getting_started": "1. Getting Started", + "2_overview": "2. Overview", + "3_groups": "3. Qortal Groups", + "4_obtain_qort": "4. Obtaining Qort", + "account_creation": "account creation", + "important_info": "important information!", + "apps": { + "dashboard": "1. Apps Dashboard", + "navigation": "2. Apps Navigation" + }, + "initial": { + "6_qort": "have at least 6 QORT in your wallet", + "explore": "explore", + "general_chat": "general chat", + "getting_started": "getting started", + "register_name": "register a name", + "see_apps": "see apps", + "trade_qort": "trade QORT" + } +} diff --git a/public/locales/es/auth.json b/public/locales/es/auth.json new file mode 100644 index 0000000..5d4b8b6 --- /dev/null +++ b/public/locales/es/auth.json @@ -0,0 +1,43 @@ +{ + "account": { + "your": "tu cuenta", + "account_many": "cuentas", + "account_one": "cuenta" + }, + "advanced_users": "para usuarios avanzados", + "apikey": { + "alternative": "alternativa: Seleccionar archivo", + "change": "cambiar clave API", + "enter": "ingresar clave API", + "import": "importar clave API", + "key": "clave API", + "select_valid": "selecciona una clave API válida" + }, + "authenticate": "autenticar", + "build_version": "versión de compilación", + "create_account": "crear cuenta", + "download_account": "descargar cuenta", + "keep_secure": "mantén tu archivo de cuenta seguro", + "node": { + "choose": "elegir nodo personalizado", + "custom_many": "nodos personalizados", + "use_custom": "usar nodo personalizado", + "use_local": "usar nodo local", + "using": "usando nodo", + "using_public": "usando nodo público" + }, + "password": "contraseña", + "password_confirmation": "confirmar contraseña", + "return_to_list": "volver a la lista", + "wallet": { + "password_confirmation": "confirmar contraseña del monedero", + "password": "contraseña del monedero", + "keep_password": "mantener la contraseña actual", + "new_password": "nueva contraseña", + "error": { + "missing_new_password": "por favor ingresa una nueva contraseña", + "missing_password": "por favor ingresa tu contraseña" + } + }, + "welcome": "bienvenido a" +} diff --git a/public/locales/es/core.json b/public/locales/es/core.json new file mode 100644 index 0000000..78b06e9 --- /dev/null +++ b/public/locales/es/core.json @@ -0,0 +1,71 @@ +{ + "add": "agregar", + "cancel": "cancelar", + "choose": "elegir", + "close": "cerrar", + "continue": "continuar", + "core": { + "block_height": "altura de bloque", + "information": "información del núcleo", + "peers": "pares conectados", + "version": "versión del núcleo" + }, + "description": "descripción", + "edit": "editar", + "export": "exportar", + "import": "importar", + "last_height": "última altura", + "loading": "cargando...", + "logout": "cerrar sesión", + "minting_status": "estado de acuñación", + "payment_notification": "notificación de pago", + "price": "precio", + "q_mail": "q-mail", + "result": { + "error": { + "generic": "ocurrió un error", + "incorrect_password": "contraseña incorrecta", + "save_qdn": "no se pudo guardar en QDN" + }, + "status": { + "minting": "(acuñando)", + "not_minting": "(no acuñando)", + "synchronized": "sincronizado", + "synchronizing": "sincronizando" + }, + "success": { + "publish_qdn": "publicado exitosamente en QDN" + } + }, + "save_options": { + "no_pinned_changes": "actualmente no tienes cambios en tus aplicaciones fijadas", + "overwrite_changes": "la aplicación no pudo descargar tus aplicaciones fijadas existentes guardadas en QDN. ¿Deseas sobrescribir esos cambios?", + "overwrite_qdn": "sobrescribir en QDN", + "publish_qdn": "¿Deseas publicar tus configuraciones en QDN (cifrado)?", + "qdn": "usar guardado en QDN", + "register_name": "necesitas un nombre Qortal registrado para guardar tus aplicaciones fijadas en QDN.", + "reset_pinned": "¿No te gustan tus cambios locales actuales? ¿Deseas restablecer las aplicaciones fijadas predeterminadas?", + "reset_qdn": "¿No te gustan tus cambios locales actuales? ¿Deseas restablecer tus aplicaciones fijadas guardadas en QDN?", + "revert_default": "restablecer a predeterminado", + "revert_qdn": "restablecer a QDN", + "save_qdn": "guardar en QDN", + "save": "guardar", + "settings": "estás utilizando el método de exportación/importación para guardar configuraciones.", + "unsaved_changes": "tienes cambios no guardados en tus aplicaciones fijadas. Guárdalos en QDN." + }, + "settings": "configuraciones", + "supply": "suministro", + "theme": { + "dark": "modo oscuro", + "light": "modo claro" + }, + "title": "título", + "tutorial": "tutorial", + "user_lookup": "búsqueda de usuario", + "wallet": { + "backup_wallet": "respaldar billetera", + "wallet": "billetera", + "wallet_other": "billeteras" + }, + "welcome": "bienvenido" +} diff --git a/public/locales/es/tutorial.json b/public/locales/es/tutorial.json new file mode 100644 index 0000000..c60e829 --- /dev/null +++ b/public/locales/es/tutorial.json @@ -0,0 +1,21 @@ +{ + "1_getting_started": "1. Comenzando", + "2_overview": "2. Visión general", + "3_groups": "3. Grupos de Qortal", + "4_obtain_qort": "4. Obtener QORT", + "account_creation": "creación de cuenta", + "important_info": "¡información importante!", + "apps": { + "dashboard": "1. Panel de aplicaciones", + "navigation": "2. Navegación de aplicaciones" + }, + "initial": { + "6_qort": "tener al menos 6 QORT en tu monedero", + "explore": "explorar", + "general_chat": "chat general", + "getting_started": "comenzando", + "register_name": "registrar un nombre", + "see_apps": "ver aplicaciones", + "trade_qort": "intercambiar QORT" + } +} diff --git a/public/locales/fr/auth.json b/public/locales/fr/auth.json new file mode 100644 index 0000000..f314d3e --- /dev/null +++ b/public/locales/fr/auth.json @@ -0,0 +1,43 @@ +{ + "account": { + "your": "ton compte", + "account_many": "comptes", + "account_one": "compte" + }, + "advanced_users": "pour les utilisateurs avancés", + "apikey": { + "alternative": "alternative : Sélectionner un fichier", + "change": "changer la clé API", + "enter": "entrer la clé API", + "import": "importer la clé API", + "key": "clé API", + "select_valid": "sélectionnez une clé API valide" + }, + "authenticate": "authentifier", + "build_version": "version de build", + "create_account": "créer un compte", + "download_account": "télécharger le compte", + "keep_secure": "Gardez votre fichier de compte en sécurité", + "node": { + "choose": "choisir un nœud personnalisé", + "custom_many": "nœuds personnalisés", + "use_custom": "utiliser un nœud personnalisé", + "use_local": "utiliser un nœud local", + "using": "utilise le nœud", + "using_public": "utilise un nœud public" + }, + "password": "mot de passe", + "password_confirmation": "confirmer le mot de passe", + "return_to_list": "retour à la liste", + "wallet": { + "password_confirmation": "confirmer le mot de passe du portefeuille", + "password": "mot de passe du portefeuille", + "keep_password": "garder le mot de passe actuel", + "new_password": "nouveau mot de passe", + "error": { + "missing_new_password": "veuillez entrer un nouveau mot de passe", + "missing_password": "veuillez entrer votre mot de passe" + } + }, + "welcome": "bienvenue sur" +} diff --git a/public/locales/fr/core.json b/public/locales/fr/core.json new file mode 100644 index 0000000..61d231d --- /dev/null +++ b/public/locales/fr/core.json @@ -0,0 +1,71 @@ +{ + "add": "ajouter", + "cancel": "annuler", + "choose": "choisir", + "close": "fermer", + "continue": "continuer", + "core": { + "block_height": "hauteur de bloc", + "information": "informations du noyau", + "peers": "pairs connectés", + "version": "version du noyau" + }, + "description": "description", + "edit": "éditer", + "export": "exporter", + "import": "importer", + "last_height": "dernière hauteur", + "loading": "chargement...", + "logout": "se déconnecter", + "minting_status": "statut de frappe", + "payment_notification": "notification de paiement", + "price": "prix", + "q_mail": "q-mail", + "result": { + "error": { + "generic": "une erreur s'est produite", + "incorrect_password": "mot de passe incorrect", + "save_qdn": "impossible d'enregistrer dans QDN" + }, + "status": { + "minting": "(frappe en cours)", + "not_minting": "(pas de frappe)", + "synchronized": "synchronisé", + "synchronizing": "synchronisation en cours" + }, + "success": { + "publish_qdn": "publié avec succès dans QDN" + } + }, + "save_options": { + "no_pinned_changes": "vous n'avez actuellement aucune modification de vos applications épinglées", + "overwrite_changes": "l'application n'a pas pu télécharger vos applications épinglées existantes enregistrées dans QDN. Voulez-vous écraser ces modifications ?", + "overwrite_qdn": "écraser dans QDN", + "publish_qdn": "souhaitez-vous publier vos paramètres dans QDN (chiffré) ?", + "qdn": "utiliser l'enregistrement QDN", + "register_name": "vous devez avoir un nom Qortal enregistré pour enregistrer vos applications épinglées dans QDN.", + "reset_pinned": "vous n'aimez pas vos modifications locales actuelles ? Voulez-vous réinitialiser les applications épinglées par défaut ?", + "reset_qdn": "vous n'aimez pas vos modifications locales actuelles ? Voulez-vous réinitialiser vos applications épinglées enregistrées dans QDN ?", + "revert_default": "revenir aux paramètres par défaut", + "revert_qdn": "revenir à QDN", + "save_qdn": "enregistrer dans QDN", + "save": "enregistrer", + "settings": "vous utilisez la méthode d'exportation/importation pour enregistrer les paramètres.", + "unsaved_changes": "vous avez des modifications non enregistrées de vos applications épinglées. Enregistrez-les dans QDN." + }, + "settings": "paramètres", + "supply": "approvisionnement", + "theme": { + "dark": "mode sombre", + "light": "mode clair" + }, + "title": "titre", + "tutorial": "tutoriel", + "user_lookup": "recherche d'utilisateur", + "wallet": { + "backup_wallet": "sauvegarder le portefeuille", + "wallet": "portefeuille", + "wallet_other": "portefeuilles" + }, + "welcome": "bienvenue" +} diff --git a/public/locales/fr/tutorial.json b/public/locales/fr/tutorial.json new file mode 100644 index 0000000..0d3ef04 --- /dev/null +++ b/public/locales/fr/tutorial.json @@ -0,0 +1,21 @@ +{ + "1_getting_started": "1. Démarrer", + "2_overview": "2. Aperçu", + "3_groups": "3. Groupes Qortal", + "4_obtain_qort": "4. Obtenir des QORT", + "account_creation": "création de compte", + "important_info": "informations importantes !", + "apps": { + "dashboard": "1. Tableau de bord des applications", + "navigation": "2. Navigation des applications" + }, + "initial": { + "6_qort": "avoir au moins 6 QORT dans votre portefeuille", + "explore": "explorer", + "general_chat": "chat général", + "getting_started": "démarrer", + "register_name": "enregistrer un nom", + "see_apps": "voir les applications", + "trade_qort": "échanger des QORT" + } +} diff --git a/public/locales/it/auth.json b/public/locales/it/auth.json new file mode 100644 index 0000000..513378f --- /dev/null +++ b/public/locales/it/auth.json @@ -0,0 +1,48 @@ +{ + "account": { + "your": "il tuo account", + "account_many": "account", + "account_one": "account" + }, + "advanced_users": "per utenti avanzati", + "apikey": { + "alternative": "alternativa: selezione file", + "change": "cambia APIkey", + "enter": "inserisci APIkey", + "import": "importa APIkey", + "key": "chiave API", + "select_valid": "seleziona una APIkey valida" + }, + "authenticate": "autentica", + "build_version": "versione build", + "create_account": "crea account", + "download_account": "scarica account", + "keep_secure": "mantieni sicuro il file del tuo account", + "node": { + "choose": "scegli nodo personalizzato", + "custom_many": "nodi personalizzati", + "use_custom": "usa nodo personalizzato", + "use_local": "usa nodo locale", + "using": "utilizzo nodo", + "using_public": "utilizzo nodo pubblico" + }, + "password": "password", + "password_confirmation": "conferma password", + "return_to_list": "torna alla lista", + "tips": { + "digital_id": "il tuo wallet è come la tua identità digitale su Qortal ed è il modo in cui accederai all'interfaccia utente di Qortal. Contiene il tuo indirizzo pubblico e il nome Qortal che sceglierai. Ogni transazione che esegui è collegata alla tua identità ed è qui che gestisci tutti i tuoi QORT e altre criptovalute scambiabili su Qortal.", + "new_account": "creare un account significa creare un nuovo wallet e un'identità digitale per iniziare a usare Qortal. Una volta creato l'account, potrai iniziare a ottenere QORT, acquistare un nome e un avatar, pubblicare video e blog, e molto altro.", + "new_users": "i nuovi utenti iniziano qui!" + }, + "wallet": { + "password_confirmation": "conferma password del wallet", + "password": "password del wallet", + "keep_password": "mantieni password corrente", + "new_password": "nuova password", + "error": { + "missing_new_password": "per favore inserisci una nuova password", + "missing_password": "per favore inserisci la tua password" + } + }, + "welcome": "benvenuto in" +} diff --git a/public/locales/it/core.json b/public/locales/it/core.json new file mode 100644 index 0000000..8319b22 --- /dev/null +++ b/public/locales/it/core.json @@ -0,0 +1,111 @@ +{ + "action": { + "add": "aggiungi", + "accept": "accetta", + "backup_account": "backup account", + "backup_wallet": "backup wallet", + "cancel": "annulla", + "change": "cambia", + "change_language": "cambia lingua", + "choose": "scegli", + "close": "chiudi", + "continue": "continua", + "continue_logout": "continua con il logout", + "decline": "rifiuta", + "edit": "modifica", + "export": "esporta", + "import": "importa", + "invite": "invita", + "join": "unisciti", + "logout": "esci", + "notify": "notifica" + }, + "core": { + "block_height": "altezza blocco", + "information": "informazioni core", + "peers": "peer connessi", + "version": "versione core" + }, + "count": { + "none": "nessuno", + "one": "uno" + }, + "description": "descrizione", + "fee": { + "payment": "commissione di pagamento", + "publish": "commissione di pubblicazione" + }, + "page": { + "last": "ultimo", + "first": "primo", + "next": "successivo", + "previous": "precedente" + }, + "downloading_qdn": "scaricamento da QDN", + "last_height": "ultima altezza", + "loading": "caricamento...", + "loading_posts": "caricamento post... attendere prego.", + "message_us": "per favore scrivici su Telegram o Discord se hai bisogno di 4 QORT per iniziare a chattare senza limitazioni", + "minting_status": "stato minting", + "new_user": "sei un nuovo utente?", + "payment_notification": "notifica di pagamento", + "price": "prezzo", + "q_mail": "q-mail", + "message": { + "error": { + "generic": "si è verificato un errore", + "incorrect_password": "password errata", + "save_qdn": "impossibile salvare su QDN" + }, + "status": { + "minting": "(minting)", + "not_minting": "(non minting)", + "synchronized": "sincronizzato", + "synchronizing": "sincronizzazione in corso" + }, + "success": { + "order_submitted": "il tuo ordine di acquisto è stato inviato", + "publish_qdn": "pubblicato su QDN con successo", + "request_read": "ho letto questa richiesta", + "transfer": "il trasferimento è stato effettuato con successo!" + } + }, + "save_options": { + "no_pinned_changes": "attualmente non hai modifiche alle tue app appuntate", + "overwrite_changes": "l'app non è riuscita a scaricare le tue app appuntate salvate su QDN. Vuoi sovrascrivere le modifiche?", + "overwrite_qdn": "sovrascrivi su QDN", + "publish_qdn": "vuoi pubblicare le tue impostazioni su QDN (crittografato)?", + "qdn": "usa il salvataggio su QDN", + "register_name": "devi avere un nome Qortal registrato per salvare le tue app appuntate su QDN.", + "reset_pinned": "non ti piacciono le modifiche locali attuali? Vuoi ripristinare le app appuntate predefinite?", + "reset_qdn": "non ti piacciono le modifiche locali attuali? Vuoi ripristinare le app appuntate salvate su QDN?", + "revert_default": "ripristina predefinito", + "revert_qdn": "ripristina da QDN", + "save_qdn": "salva su QDN", + "save": "salva", + "settings": "stai usando il metodo di esportazione/importazione per salvare le impostazioni.", + "unsaved_changes": "hai modifiche non salvate alle tue app appuntate. Salvale su QDN." + }, + "settings": "impostazioni", + "supply": "disponibilità", + "theme": { + "dark": "modalità scura", + "light": "modalità chiara" + }, + "time": { + "day_one": "{{count}} giorno", + "day_other": "{{count}} giorni", + "hour_one": "{{count}} ora", + "hour_other": "{{count}} ore", + "minute_one": "{{count}} minuto", + "minute_other": "{{count}} minuti" + }, + "title": "titolo", + "tutorial": "tutorial", + "user_lookup": "ricerca utente", + "wallet": { + "wallet": "wallet", + "wallet_other": "wallet" + }, + "welcome": "benvenuto" +} diff --git a/public/locales/it/group.json b/public/locales/it/group.json new file mode 100644 index 0000000..862496d --- /dev/null +++ b/public/locales/it/group.json @@ -0,0 +1,65 @@ +{ + "action": { + "cancel_ban": "annulla ban", + "create_group": "crea gruppo", + "find_group": "trova gruppo", + "join_group": "unisciti al gruppo", + "invite_member": "invita membro", + "refetch_page": "ricarica pagina", + "return_to_thread": "torna ai thread" + }, + "advanced_options": "opzioni avanzate", + "approval_threshold": "soglia di Approvazione del gruppo (numero/percentuale di Admin che devono approvare una transazione)", + "ban_list": "lista ban", + "block_delay": { + "minimum": "ritardo minimo dei blocchi per Approvazione di Transazioni di Gruppo", + "maximum": "ritardo massimo dei blocchi per Approvazione di Transazioni di Gruppo" + }, + "group": { + "closed": "chiuso (privato) - gli utenti necessitano di permesso per unirsi", + "description": "descrizione del gruppo", + "invites": "inviti del gruppo", + "management": "gestione del gruppo", + "name": "nome del gruppo", + "open": "aperto (pubblico)", + "type": "tipo di gruppo" + }, + "invitation_expiry": "tempo di scadenza dell'invito", + "join_requests": "richieste di adesione", + "question": { + "cancel_ban": "vuoi eseguire una transazione CANCEL_GROUP_BAN?", + "create_group": "vuoi eseguire una transazione CREATE_GROUP?", + "group_invite": "vuoi eseguire una transazione GROUP_INVITE?", + "join_group": "vuoi eseguire una transazione JOIN_GROUP?", + "provide_thread": "per favore fornisci un titolo per il thread" + }, + "message": { + "generic": { + "encryption_key": "la prima chiave di cifratura comune del gruppo è in fase di creazione. Attendere alcuni minuti affinché venga recuperata dalla rete. Controllo ogni 2 minuti...", + "group_invited_you": "{{group}} ti ha invitato", + "no_display": "niente da visualizzare", + "no_selection": "nessun gruppo selezionato", + "not_part_group": "non fai parte del gruppo cifrato dei membri. Attendi che un amministratore ri-codifichi le chiavi.", + "only_encrypted": "verranno visualizzati solo i messaggi non cifrati.", + "setting_group": "configurazione del gruppo... attendere prego." + }, + "error": { + "access_name": "impossibile inviare un messaggio senza accesso al tuo nome", + "description_required": "per favore fornisci una descrizione", + "group_info": "impossibile accedere alle informazioni del gruppo", + "name_required": "per favore fornisci un nome", + "notify_admins": "prova a notificare un admin dalla lista di amministratori qui sotto:" + }, + "success": { + "group_creation": "gruppo creato con successo. Potrebbero volerci alcuni minuti affinché le modifiche si propaghino", + "group_creation_name": "gruppo {{group_name}} creato: in attesa di conferma", + "group_creation_label": "gruppo {{name}} creato: successo!", + "group_invite": "invito inviato con successo a {{value}}. Potrebbero volerci alcuni minuti affinché le modifiche si propaghino", + "join_creation": "richiesta di adesione al gruppo inviata con successo. Potrebbero volerci alcuni minuti affinché le modifiche si propaghino", + "group_join_name": "entrato nel gruppo {{group_name}}: in attesa di conferma", + "group_join_label": "entrato nel gruppo {{name}}: successo!", + "loading_threads": "caricamento thread... attendere prego.", + "unbanned_user": "utente sbannato con successo. Potrebbero volerci alcuni minuti affinché le modifiche si propaghino" + } + } +} diff --git a/public/locales/it/tutorial.json b/public/locales/it/tutorial.json new file mode 100644 index 0000000..511b7cb --- /dev/null +++ b/public/locales/it/tutorial.json @@ -0,0 +1,21 @@ +{ + "1_getting_started": "1. Come iniziare", + "2_overview": "2. Overview", + "3_groups": "3. I gruppi in Qortal", + "4_obtain_qort": "4. Ottenere Qort", + "account_creation": "creazione account", + "important_info": "important information!", + "apps": { + "dashboard": "1. Dashboard delle app", + "navigation": "2. Navigation tra le app" + }, + "initial": { + "6_qort": "avere almeno 6 QORT nel proprio wallet", + "explore": "esplora", + "general_chat": "chat generale", + "getting_started": "come iniziare", + "register_name": "registra un nome", + "see_apps": "vedi le apps", + "trade_qort": "scambia i QORT" + } +} diff --git a/public/locales/ru/auth.json b/public/locales/ru/auth.json new file mode 100644 index 0000000..797220b --- /dev/null +++ b/public/locales/ru/auth.json @@ -0,0 +1,43 @@ +{ + "account": { + "your": "Ваш аккаунт", + "account_many": "аккаунты", + "account_one": "аккаунт" + }, + "advanced_users": "для продвинутых пользователей", + "apikey": { + "alternative": "альтернатива: выбрать файл", + "change": "изменить API-ключ", + "enter": "введите API-ключ", + "import": "импортировать API-ключ", + "key": "API-ключ", + "select_valid": "выберите действительный API-ключ" + }, + "authenticate": "аутентификация", + "build_version": "версия сборки", + "create_account": "создать аккаунт", + "download_account": "скачать аккаунт", + "keep_secure": "Храните файл аккаунта в безопасности", + "node": { + "choose": "выбрать пользовательский узел", + "custom_many": "пользовательские узлы", + "use_custom": "использовать пользовательский узел", + "use_local": "использовать локальный узел", + "using": "используется узел", + "using_public": "используется публичный узел" + }, + "password": "пароль", + "password_confirmation": "подтвердите пароль", + "return_to_list": "вернуться к списку", + "wallet": { + "password_confirmation": "подтвердите пароль кошелька", + "password": "пароль кошелька", + "keep_password": "сохранить текущий пароль", + "new_password": "новый пароль", + "error": { + "missing_new_password": "пожалуйста, введите новый пароль", + "missing_password": "пожалуйста, введите ваш пароль" + } + }, + "welcome": "добро пожаловать в" +} diff --git a/public/locales/ru/core.json b/public/locales/ru/core.json new file mode 100644 index 0000000..04bf898 --- /dev/null +++ b/public/locales/ru/core.json @@ -0,0 +1,71 @@ +{ + "add": "добавить", + "cancel": "отмена", + "choose": "выбрать", + "close": "закрыть", + "continue": "продолжить", + "core": { + "block_height": "высота блока", + "information": "информация ядра", + "peers": "подключенные узлы", + "version": "версия ядра" + }, + "description": "описание", + "edit": "редактировать", + "export": "экспорт", + "import": "импорт", + "last_height": "последняя высота", + "loading": "загрузка...", + "logout": "выйти", + "minting_status": "статус чеканки", + "payment_notification": "уведомление о платеже", + "price": "цена", + "q_mail": "q-mail", + "result": { + "error": { + "generic": "произошла ошибка", + "incorrect_password": "неверный пароль", + "save_qdn": "не удалось сохранить в QDN" + }, + "status": { + "minting": "(чеканка)", + "not_minting": "(не чеканится)", + "synchronized": "синхронизировано", + "synchronizing": "синхронизация" + }, + "success": { + "publish_qdn": "успешно опубликовано в QDN" + } + }, + "save_options": { + "no_pinned_changes": "у вас нет изменений в закреплённых приложениях", + "overwrite_changes": "приложению не удалось загрузить ваши закреплённые приложения, сохранённые в QDN. Хотите перезаписать эти изменения?", + "overwrite_qdn": "перезаписать в QDN", + "publish_qdn": "хотите опубликовать свои настройки в QDN (зашифровано)?", + "qdn": "использовать сохранение в QDN", + "register_name": "вам необходимо зарегистрированное имя Qortal для сохранения закреплённых приложений в QDN.", + "reset_pinned": "не устраивают текущие локальные изменения? Хотите сбросить до приложений по умолчанию?", + "reset_qdn": "не устраивают текущие локальные изменения? Хотите сбросить до сохранённых в QDN приложений?", + "revert_default": "сбросить до стандартных", + "revert_qdn": "сбросить до QDN", + "save_qdn": "сохранить в QDN", + "save": "сохранить", + "settings": "вы используете метод экспорта/импорта для сохранения настроек.", + "unsaved_changes": "у вас есть несохранённые изменения в закреплённых приложениях. Сохраните их в QDN." + }, + "settings": "настройки", + "supply": "предложение", + "theme": { + "dark": "тёмная тема", + "light": "светлая тема" + }, + "title": "заголовок", + "tutorial": "учебник", + "user_lookup": "поиск пользователя", + "wallet": { + "backup_wallet": "резервная копия кошелька", + "wallet": "кошелёк", + "wallet_other": "кошельки" + }, + "welcome": "добро пожаловать" +} diff --git a/public/locales/ru/tutorial.json b/public/locales/ru/tutorial.json new file mode 100644 index 0000000..405fdd0 --- /dev/null +++ b/public/locales/ru/tutorial.json @@ -0,0 +1,21 @@ +{ + "1_getting_started": "1. Начало работы", + "2_overview": "2. Обзор", + "3_groups": "3. Группы Qortal", + "4_obtain_qort": "4. Получение QORT", + "account_creation": "создание аккаунта", + "important_info": "важная информация!", + "apps": { + "dashboard": "1. Панель приложений", + "navigation": "2. Навигация по приложениям" + }, + "initial": { + "6_qort": "иметь не менее 6 QORT в кошельке", + "explore": "исследовать", + "general_chat": "общий чат", + "getting_started": "начало работы", + "register_name": "зарегистрировать имя", + "see_apps": "посмотреть приложения", + "trade_qort": "обмен QORT" + } +} diff --git a/public/pdfjs/web/viewer.css b/public/pdfjs/web/viewer.css index 4639f99..96df5d0 100644 --- a/public/pdfjs/web/viewer.css +++ b/public/pdfjs/web/viewer.css @@ -13,5181 +13,6426 @@ * limitations under the License. */ -.messageBar{ - --closing-button-icon:url(images/messageBar_closingButton.svg); - --message-bar-close-button-color:var(--text-primary-color); - --message-bar-close-button-color-hover:var(--text-primary-color); - --message-bar-close-button-border-radius:4px; - --message-bar-close-button-border:none; - --message-bar-close-button-hover-bg-color:rgb(21 20 26 / 0.14); - --message-bar-close-button-active-bg-color:rgb(21 20 26 / 0.21); - --message-bar-close-button-focus-bg-color:rgb(21 20 26 / 0.07); +.messageBar { + --closing-button-icon: url(images/messageBar_closingButton.svg); + --message-bar-close-button-color: var(--text-primary-color); + --message-bar-close-button-color-hover: var(--text-primary-color); + --message-bar-close-button-border-radius: 4px; + --message-bar-close-button-border: none; + --message-bar-close-button-hover-bg-color: rgb(21 20 26 / 0.14); + --message-bar-close-button-active-bg-color: rgb(21 20 26 / 0.21); + --message-bar-close-button-focus-bg-color: rgb(21 20 26 / 0.07); } -@media (prefers-color-scheme: dark){ - -:where(html:not(.is-light)) .messageBar{ - --message-bar-close-button-hover-bg-color:rgb(251 251 254 / 0.14); - --message-bar-close-button-active-bg-color:rgb(251 251 254 / 0.21); - --message-bar-close-button-focus-bg-color:rgb(251 251 254 / 0.07); -} - } - -:where(html.is-dark) .messageBar{ - --message-bar-close-button-hover-bg-color:rgb(251 251 254 / 0.14); - --message-bar-close-button-active-bg-color:rgb(251 251 254 / 0.21); - --message-bar-close-button-focus-bg-color:rgb(251 251 254 / 0.07); -} - -@media screen and (forced-colors: active){ - -.messageBar{ - --message-bar-close-button-color:ButtonText; - --message-bar-close-button-border:1px solid ButtonText; - --message-bar-close-button-hover-bg-color:ButtonText; - --message-bar-close-button-active-bg-color:ButtonText; - --message-bar-close-button-focus-bg-color:ButtonText; - --message-bar-close-button-color-hover:HighlightText; -} - } - -.messageBar{ - - display:flex; - position:relative; - padding:8px 8px 8px 16px; - flex-direction:column; - justify-content:center; - align-items:center; - gap:8px; - -webkit-user-select:none; - -moz-user-select:none; - user-select:none; - - border-radius:4px; - - border:1px solid var(--message-bar-border-color); - background:var(--message-bar-bg-color); - color:var(--message-bar-fg-color); -} - -.messageBar > div{ - display:flex; - align-items:flex-start; - gap:8px; - align-self:stretch; - } - -:is(.messageBar > div)::before{ - content:""; - display:inline-block; - width:16px; - height:16px; - -webkit-mask-image:var(--message-bar-icon); - mask-image:var(--message-bar-icon); - -webkit-mask-size:cover; - mask-size:cover; - background-color:var(--message-bar-icon-color); - flex-shrink:0; - } - -.messageBar button{ - cursor:pointer; - } - -:is(.messageBar button):focus-visible{ - outline:var(--focus-ring-outline); - outline-offset:2px; - } - -.messageBar .closeButton{ - width:32px; - height:32px; - background:none; - border-radius:var(--message-bar-close-button-border-radius); - border:var(--message-bar-close-button-border); - - display:flex; - align-items:center; - justify-content:center; - } - -:is(.messageBar .closeButton)::before{ - content:""; - display:inline-block; - width:16px; - height:16px; - -webkit-mask-image:var(--closing-button-icon); - mask-image:var(--closing-button-icon); - -webkit-mask-size:cover; - mask-size:cover; - background-color:var(--message-bar-close-button-color); - } - -:is(.messageBar .closeButton):is(:hover,:active,:focus)::before{ - background-color:var(--message-bar-close-button-color-hover); - } - -:is(.messageBar .closeButton):hover{ - background-color:var(--message-bar-close-button-hover-bg-color); - } - -:is(.messageBar .closeButton):active{ - background-color:var(--message-bar-close-button-active-bg-color); - } - -:is(.messageBar .closeButton):focus{ - background-color:var(--message-bar-close-button-focus-bg-color); - } - -:is(.messageBar .closeButton) > span{ - display:inline-block; - width:0; - height:0; - overflow:hidden; - } - -#editorUndoBar{ - --text-primary-color:#15141a; - - --message-bar-icon:url(images/secondaryToolbarButton-documentProperties.svg); - --message-bar-icon-color:#0060df; - --message-bar-bg-color:#deeafc; - --message-bar-fg-color:var(--text-primary-color); - --message-bar-border-color:rgb(0 0 0 / 0.08); - - --undo-button-bg-color:rgb(21 20 26 / 0.07); - --undo-button-bg-color-hover:rgb(21 20 26 / 0.14); - --undo-button-bg-color-active:rgb(21 20 26 / 0.21); - - --undo-button-fg-color:var(--message-bar-fg-color); - --undo-button-fg-color-hover:var(--undo-button-fg-color); - --undo-button-fg-color-active:var(--undo-button-fg-color); - - --focus-ring-color:#0060df; - --focus-ring-outline:2px solid var(--focus-ring-color); -} - -@media (prefers-color-scheme: dark){ - -:where(html:not(.is-light)) #editorUndoBar{ - --text-primary-color:#fbfbfe; - - --message-bar-icon-color:#73a7f3; - --message-bar-bg-color:#003070; - --message-bar-border-color:rgb(255 255 255 / 0.08); - - --undo-button-bg-color:rgb(255 255 255 / 0.08); - --undo-button-bg-color-hover:rgb(255 255 255 / 0.14); - --undo-button-bg-color-active:rgb(255 255 255 / 0.21); -} - } - -:where(html.is-dark) #editorUndoBar{ - --text-primary-color:#fbfbfe; - - --message-bar-icon-color:#73a7f3; - --message-bar-bg-color:#003070; - --message-bar-border-color:rgb(255 255 255 / 0.08); - - --undo-button-bg-color:rgb(255 255 255 / 0.08); - --undo-button-bg-color-hover:rgb(255 255 255 / 0.14); - --undo-button-bg-color-active:rgb(255 255 255 / 0.21); -} - -@media screen and (forced-colors: active){ - -#editorUndoBar{ - --text-primary-color:CanvasText; - - --message-bar-icon-color:CanvasText; - --message-bar-bg-color:Canvas; - --message-bar-border-color:CanvasText; - - --undo-button-bg-color:ButtonText; - --undo-button-bg-color-hover:SelectedItem; - --undo-button-bg-color-active:SelectedItem; - - --undo-button-fg-color:ButtonFace; - --undo-button-fg-color-hover:SelectedItemText; - --undo-button-fg-color-active:SelectedItemText; - - --focus-ring-color:CanvasText; -} - } - -#editorUndoBar{ - - position:fixed; - top:50px; - left:50%; - transform:translateX(-50%); - z-index:10; - - padding-block:8px; - padding-inline:16px 8px; - - font:menu; - font-size:15px; - - cursor:default; -} - -#editorUndoBar button{ - cursor:pointer; - } - -#editorUndoBar #editorUndoBarUndoButton{ - border-radius:4px; - font-weight:590; - line-height:19.5px; - color:var(--undo-button-fg-color); - border:none; - padding:4px 16px; - margin-inline-start:8px; - height:32px; - - background-color:var(--undo-button-bg-color); - } - -:is(#editorUndoBar #editorUndoBarUndoButton):hover{ - background-color:var(--undo-button-bg-color-hover); - color:var(--undo-button-fg-color-hover); - } - -:is(#editorUndoBar #editorUndoBarUndoButton):active{ - background-color:var(--undo-button-bg-color-active); - color:var(--undo-button-fg-color-active); - } - -#editorUndoBar > div{ - align-items:center; - } - -.dialog{ - --dialog-bg-color:white; - --dialog-border-color:white; - --dialog-shadow:0 2px 14px 0 rgb(58 57 68 / 0.2); - --text-primary-color:#15141a; - --text-secondary-color:#5b5b66; - --hover-filter:brightness(0.9); - --focus-ring-color:#0060df; - --focus-ring-outline:2px solid var(--focus-ring-color); - --link-fg-color:#0060df; - --link-hover-fg-color:#0250bb; - --separator-color:#f0f0f4; - - --textarea-border-color:#8f8f9d; - --textarea-bg-color:white; - --textarea-fg-color:var(--text-secondary-color); - - --radio-bg-color:#f0f0f4; - --radio-checked-bg-color:#fbfbfe; - --radio-border-color:#8f8f9d; - --radio-checked-border-color:#0060df; - - --button-secondary-bg-color:#f0f0f4; - --button-secondary-fg-color:var(--text-primary-color); - --button-secondary-border-color:var(--button-secondary-bg-color); - --button-secondary-hover-bg-color:var(--button-secondary-bg-color); - --button-secondary-hover-fg-color:var(--button-secondary-fg-color); - --button-secondary-hover-border-color:var(--button-secondary-hover-bg-color); - - --button-primary-bg-color:#0060df; - --button-primary-fg-color:#fbfbfe; - --button-primary-border-color:var(--button-primary-bg-color); - --button-primary-hover-bg-color:var(--button-primary-bg-color); - --button-primary-hover-fg-color:var(--button-primary-fg-color); - --button-primary-hover-border-color:var(--button-primary-hover-bg-color); -} - -@media (prefers-color-scheme: dark){ - -:where(html:not(.is-light)) .dialog{ - --dialog-bg-color:#1c1b22; - --dialog-border-color:#1c1b22; - --dialog-shadow:0 2px 14px 0 #15141a; - --text-primary-color:#fbfbfe; - --text-secondary-color:#cfcfd8; - --focus-ring-color:#0df; - --hover-filter:brightness(1.4); - --link-fg-color:#0df; - --link-hover-fg-color:#80ebff; - --separator-color:#52525e; - - --textarea-bg-color:#42414d; - - --radio-bg-color:#2b2a33; - --radio-checked-bg-color:#15141a; - --radio-checked-border-color:#0df; - - --button-secondary-bg-color:#2b2a33; - --button-primary-bg-color:#0df; - --button-primary-fg-color:#15141a; -} - } - -:where(html.is-dark) .dialog{ - --dialog-bg-color:#1c1b22; - --dialog-border-color:#1c1b22; - --dialog-shadow:0 2px 14px 0 #15141a; - --text-primary-color:#fbfbfe; - --text-secondary-color:#cfcfd8; - --focus-ring-color:#0df; - --hover-filter:brightness(1.4); - --link-fg-color:#0df; - --link-hover-fg-color:#80ebff; - --separator-color:#52525e; - - --textarea-bg-color:#42414d; - - --radio-bg-color:#2b2a33; - --radio-checked-bg-color:#15141a; - --radio-checked-border-color:#0df; - - --button-secondary-bg-color:#2b2a33; - --button-primary-bg-color:#0df; - --button-primary-fg-color:#15141a; -} - -@media screen and (forced-colors: active){ - -.dialog{ - --dialog-bg-color:Canvas; - --dialog-border-color:CanvasText; - --dialog-shadow:none; - --text-primary-color:CanvasText; - --text-secondary-color:CanvasText; - --hover-filter:none; - --focus-ring-color:ButtonBorder; - --link-fg-color:LinkText; - --link-hover-fg-color:LinkText; - --separator-color:CanvasText; - - --textarea-border-color:ButtonBorder; - --textarea-bg-color:Field; - --textarea-fg-color:ButtonText; - - --radio-bg-color:ButtonFace; - --radio-checked-bg-color:ButtonFace; - --radio-border-color:ButtonText; - --radio-checked-border-color:ButtonText; - - --button-secondary-bg-color:ButtonFace; - --button-secondary-fg-color:ButtonText; - --button-secondary-border-color:ButtonText; - --button-secondary-hover-bg-color:AccentColor; - --button-secondary-hover-fg-color:AccentColorText; - - --button-primary-bg-color:ButtonText; - --button-primary-fg-color:ButtonFace; - --button-primary-hover-bg-color:AccentColor; - --button-primary-hover-fg-color:AccentColorText; -} - } - -.dialog{ - - font:message-box; - font-size:13px; - font-weight:400; - line-height:150%; - border-radius:4px; - padding:12px 16px; - border:1px solid var(--dialog-border-color); - background:var(--dialog-bg-color); - color:var(--text-primary-color); - box-shadow:var(--dialog-shadow); -} - -:is(.dialog .mainContainer) *:focus-visible{ - outline:var(--focus-ring-outline); - outline-offset:2px; - } - -:is(.dialog .mainContainer) .title{ - display:flex; - width:auto; - flex-direction:column; - justify-content:flex-end; - align-items:flex-start; - gap:12px; - } - -:is(:is(.dialog .mainContainer) .title) > span{ - font-size:13px; - font-style:normal; - font-weight:590; - line-height:150%; - } - -:is(.dialog .mainContainer) .dialogSeparator{ - width:100%; - height:0; - margin-block:4px; - border-top:1px solid var(--separator-color); - border-bottom:none; - } - -:is(.dialog .mainContainer) .dialogButtonsGroup{ - display:flex; - gap:12px; - align-self:flex-end; - } - -:is(.dialog .mainContainer) .radio{ - display:flex; - flex-direction:column; - align-items:flex-start; - gap:4px; - } - -:is(:is(.dialog .mainContainer) .radio) > .radioButton{ - display:flex; - gap:8px; - align-self:stretch; - align-items:center; - } - -:is(:is(:is(.dialog .mainContainer) .radio) > .radioButton) input{ - -webkit-appearance:none; - -moz-appearance:none; - appearance:none; - box-sizing:border-box; - width:16px; - height:16px; - border-radius:50%; - background-color:var(--radio-bg-color); - border:1px solid var(--radio-border-color); - } - -:is(:is(:is(:is(.dialog .mainContainer) .radio) > .radioButton) input):hover{ - filter:var(--hover-filter); - } - -:is(:is(:is(:is(.dialog .mainContainer) .radio) > .radioButton) input):checked{ - background-color:var(--radio-checked-bg-color); - border:4px solid var(--radio-checked-border-color); - } - -:is(:is(.dialog .mainContainer) .radio) > .radioLabel{ - display:flex; - padding-inline-start:24px; - align-items:flex-start; - gap:10px; - align-self:stretch; - } - -:is(:is(:is(.dialog .mainContainer) .radio) > .radioLabel) > span{ - flex:1 0 0; - font-size:11px; - color:var(--text-secondary-color); - } - -:is(.dialog .mainContainer) button:not(:is(.toggle-button,.closeButton)){ - border-radius:4px; - border:1px solid; - font:menu; - font-weight:600; - padding:4px 16px; - width:auto; - height:32px; - } - -:is(:is(.dialog .mainContainer) button:not(:is(.toggle-button,.closeButton))):hover{ - cursor:pointer; - filter:var(--hover-filter); - } - -.secondaryButton:is(:is(.dialog .mainContainer) button:not(:is(.toggle-button,.closeButton))){ - color:var(--button-secondary-fg-color); - background-color:var(--button-secondary-bg-color); - border-color:var(--button-secondary-border-color); - } - -.secondaryButton:is(:is(.dialog .mainContainer) button:not(:is(.toggle-button,.closeButton))):hover{ - color:var(--button-secondary-hover-fg-color); - background-color:var(--button-secondary-hover-bg-color); - border-color:var(--button-secondary-hover-border-color); - } - -.primaryButton:is(:is(.dialog .mainContainer) button:not(:is(.toggle-button,.closeButton))){ - color:var(--button-primary-fg-color); - background-color:var(--button-primary-bg-color); - border-color:var(--button-primary-border-color); - opacity:1; - } - -.primaryButton:is(:is(.dialog .mainContainer) button:not(:is(.toggle-button,.closeButton))):hover{ - color:var(--button-primary-hover-fg-color); - background-color:var(--button-primary-hover-bg-color); - border-color:var(--button-primary-hover-border-color); - } - -:is(.dialog .mainContainer) a{ - color:var(--link-fg-color); - } - -:is(:is(.dialog .mainContainer) a):hover{ - color:var(--link-hover-fg-color); - } - -:is(.dialog .mainContainer) textarea{ - font:inherit; - padding:8px; - resize:none; - margin:0; - box-sizing:border-box; - border-radius:4px; - border:1px solid var(--textarea-border-color); - background:var(--textarea-bg-color); - color:var(--textarea-fg-color); - } - -:is(:is(.dialog .mainContainer) textarea):focus{ - outline-offset:0; - border-color:transparent; - } - -:is(:is(.dialog .mainContainer) textarea):disabled{ - pointer-events:none; - opacity:0.4; - } - -:is(.dialog .mainContainer) .messageBar{ - --message-bar-bg-color:#ffebcd; - --message-bar-fg-color:#15141a; - --message-bar-border-color:rgb(0 0 0 / 0.08); - --message-bar-icon:url(images/messageBar_warning.svg); - --message-bar-icon-color:#cd411e; - } - -@media (prefers-color-scheme: dark){ - -:where(html:not(.is-light)) :is(.dialog .mainContainer) .messageBar{ - --message-bar-bg-color:#5a3100; - --message-bar-fg-color:#fbfbfe; - --message-bar-border-color:rgb(255 255 255 / 0.08); - --message-bar-icon-color:#e49c49; - } - } - -:where(html.is-dark) :is(.dialog .mainContainer) .messageBar{ - --message-bar-bg-color:#5a3100; - --message-bar-fg-color:#fbfbfe; - --message-bar-border-color:rgb(255 255 255 / 0.08); - --message-bar-icon-color:#e49c49; - } - -@media screen and (forced-colors: active){ - -:is(.dialog .mainContainer) .messageBar{ - --message-bar-bg-color:HighlightText; - --message-bar-fg-color:CanvasText; - --message-bar-border-color:CanvasText; - --message-bar-icon-color:CanvasText; - } - } - -:is(.dialog .mainContainer) .messageBar{ - - align-self:stretch; - } - -:is(:is(:is(.dialog .mainContainer) .messageBar) > div)::before,:is(:is(:is(.dialog .mainContainer) .messageBar) > div) > div{ - margin-block:4px; - } - -:is(:is(:is(.dialog .mainContainer) .messageBar) > div) > div{ - display:flex; - flex-direction:column; - align-items:flex-start; - gap:8px; - flex:1 0 0; - } - -:is(:is(:is(:is(.dialog .mainContainer) .messageBar) > div) > div) .title{ - font-size:13px; - font-weight:590; - } - -:is(:is(:is(:is(.dialog .mainContainer) .messageBar) > div) > div) .description{ - font-size:13px; - } - -:is(.dialog .mainContainer) .toggler{ - display:flex; - align-items:center; - gap:8px; - align-self:stretch; - } - -:is(:is(.dialog .mainContainer) .toggler) > .togglerLabel{ - -webkit-user-select:none; - -moz-user-select:none; - user-select:none; - } - -.textLayer{ - position:absolute; - text-align:initial; - inset:0; - overflow:clip; - opacity:1; - line-height:1; - -webkit-text-size-adjust:none; - -moz-text-size-adjust:none; - text-size-adjust:none; - forced-color-adjust:none; - transform-origin:0 0; - caret-color:CanvasText; - z-index:0; -} - -.textLayer.highlighting{ - touch-action:none; - } - -.textLayer :is(span,br){ - color:transparent; - position:absolute; - white-space:pre; - cursor:text; - transform-origin:0% 0%; - } - -.textLayer > :not(.markedContent),.textLayer .markedContent span:not(.markedContent){ - z-index:1; - } - -.textLayer span.markedContent{ - top:0; - height:0; - } - -.textLayer span[role="img"]{ - -webkit-user-select:none; - -moz-user-select:none; - user-select:none; - cursor:default; - } - -.textLayer .highlight{ - --highlight-bg-color:rgb(180 0 170 / 0.25); - --highlight-selected-bg-color:rgb(0 100 0 / 0.25); - --highlight-backdrop-filter:none; - --highlight-selected-backdrop-filter:none; - } - -@media screen and (forced-colors: active){ - -.textLayer .highlight{ - --highlight-bg-color:transparent; - --highlight-selected-bg-color:transparent; - --highlight-backdrop-filter:var(--hcm-highlight-filter); - --highlight-selected-backdrop-filter:var( - --hcm-highlight-selected-filter - ); - } - } - -.textLayer .highlight{ - - margin:-1px; - padding:1px; - background-color:var(--highlight-bg-color); - -webkit-backdrop-filter:var(--highlight-backdrop-filter); - backdrop-filter:var(--highlight-backdrop-filter); - border-radius:4px; - } - -.appended:is(.textLayer .highlight){ - position:initial; - } - -.begin:is(.textLayer .highlight){ - border-radius:4px 0 0 4px; - } - -.end:is(.textLayer .highlight){ - border-radius:0 4px 4px 0; - } - -.middle:is(.textLayer .highlight){ - border-radius:0; - } - -.selected:is(.textLayer .highlight){ - background-color:var(--highlight-selected-bg-color); - -webkit-backdrop-filter:var(--highlight-selected-backdrop-filter); - backdrop-filter:var(--highlight-selected-backdrop-filter); - } - -.textLayer ::-moz-selection{ - background:rgba(0 0 255 / 0.25); - background:color-mix(in srgb, AccentColor, transparent 75%); - } - -.textLayer ::selection{ - background:rgba(0 0 255 / 0.25); - background:color-mix(in srgb, AccentColor, transparent 75%); - } - -.textLayer br::-moz-selection{ - background:transparent; - } - -.textLayer br::selection{ - background:transparent; - } - -.textLayer .endOfContent{ - display:block; - position:absolute; - inset:100% 0 0; - z-index:0; - cursor:default; - -webkit-user-select:none; - -moz-user-select:none; - user-select:none; - } - -.textLayer.selecting .endOfContent{ - top:0; - } - -.annotationLayer{ - --annotation-unfocused-field-background:url("data:image/svg+xml;charset=UTF-8,"); - --input-focus-border-color:Highlight; - --input-focus-outline:1px solid Canvas; - --input-unfocused-border-color:transparent; - --input-disabled-border-color:transparent; - --input-hover-border-color:black; - --link-outline:none; -} - -@media screen and (forced-colors: active){ - -.annotationLayer{ - --input-focus-border-color:CanvasText; - --input-unfocused-border-color:ActiveText; - --input-disabled-border-color:GrayText; - --input-hover-border-color:Highlight; - --link-outline:1.5px solid LinkText; -} - - .annotationLayer .textWidgetAnnotation :is(input,textarea):required,.annotationLayer .choiceWidgetAnnotation select:required,.annotationLayer .buttonWidgetAnnotation:is(.checkBox,.radioButton) input:required{ - outline:1.5px solid selectedItem; - } - - .annotationLayer .linkAnnotation{ - outline:var(--link-outline); - } - - :is(.annotationLayer .linkAnnotation):hover{ - -webkit-backdrop-filter:var(--hcm-highlight-filter); - backdrop-filter:var(--hcm-highlight-filter); - } - - :is(.annotationLayer .linkAnnotation) > a:hover{ - opacity:0 !important; - background:none !important; - box-shadow:none; - } - - .annotationLayer .popupAnnotation .popup{ - outline:calc(1.5px * var(--scale-factor)) solid CanvasText !important; - background-color:ButtonFace !important; - color:ButtonText !important; - } - - .annotationLayer .highlightArea:hover::after{ - position:absolute; - top:0; - left:0; - width:100%; - height:100%; - -webkit-backdrop-filter:var(--hcm-highlight-filter); - backdrop-filter:var(--hcm-highlight-filter); - content:""; - pointer-events:none; - } - - .annotationLayer .popupAnnotation.focused .popup{ - outline:calc(3px * var(--scale-factor)) solid Highlight !important; - } - } - -.annotationLayer{ - - position:absolute; - top:0; - left:0; - pointer-events:none; - transform-origin:0 0; -} - -.annotationLayer[data-main-rotation="90"] .norotate{ - transform:rotate(270deg) translateX(-100%); - } - -.annotationLayer[data-main-rotation="180"] .norotate{ - transform:rotate(180deg) translate(-100%, -100%); - } - -.annotationLayer[data-main-rotation="270"] .norotate{ - transform:rotate(90deg) translateY(-100%); - } - -.annotationLayer.disabled section,.annotationLayer.disabled .popup{ - pointer-events:none; - } - -.annotationLayer .annotationContent{ - position:absolute; - width:100%; - height:100%; - pointer-events:none; - } - -.freetext:is(.annotationLayer .annotationContent){ - background:transparent; - border:none; - inset:0; - overflow:visible; - white-space:nowrap; - font:10px sans-serif; - line-height:1.35; - -webkit-user-select:none; - -moz-user-select:none; - user-select:none; - } - -.annotationLayer section{ - position:absolute; - text-align:initial; - pointer-events:auto; - box-sizing:border-box; - transform-origin:0 0; - } - -:is(.annotationLayer section):has(div.annotationContent) canvas.annotationContent{ - display:none; - } - -.textLayer.selecting ~ .annotationLayer section{ - pointer-events:none; - } - -.annotationLayer :is(.linkAnnotation,.buttonWidgetAnnotation.pushButton) > a{ - position:absolute; - font-size:1em; - top:0; - left:0; - width:100%; - height:100%; - } - -.annotationLayer :is(.linkAnnotation,.buttonWidgetAnnotation.pushButton):not(.hasBorder) > a:hover{ - opacity:0.2; - background-color:rgb(255 255 0); - box-shadow:0 2px 10px rgb(255 255 0); - } - -.annotationLayer .linkAnnotation.hasBorder:hover{ - background-color:rgb(255 255 0 / 0.2); - } - -.annotationLayer .hasBorder{ - background-size:100% 100%; - } - -.annotationLayer .textAnnotation img{ - position:absolute; - cursor:pointer; - width:100%; - height:100%; - top:0; - left:0; - } - -.annotationLayer .textWidgetAnnotation :is(input,textarea),.annotationLayer .choiceWidgetAnnotation select,.annotationLayer .buttonWidgetAnnotation:is(.checkBox,.radioButton) input{ - background-image:var(--annotation-unfocused-field-background); - border:2px solid var(--input-unfocused-border-color); - box-sizing:border-box; - font:calc(9px * var(--scale-factor)) sans-serif; - height:100%; - margin:0; - vertical-align:top; - width:100%; - } - -.annotationLayer .textWidgetAnnotation :is(input,textarea):required,.annotationLayer .choiceWidgetAnnotation select:required,.annotationLayer .buttonWidgetAnnotation:is(.checkBox,.radioButton) input:required{ - outline:1.5px solid red; - } - -.annotationLayer .choiceWidgetAnnotation select option{ - padding:0; - } - -.annotationLayer .buttonWidgetAnnotation.radioButton input{ - border-radius:50%; - } - -.annotationLayer .textWidgetAnnotation textarea{ - resize:none; - } - -.annotationLayer .textWidgetAnnotation [disabled]:is(input,textarea),.annotationLayer .choiceWidgetAnnotation select[disabled],.annotationLayer .buttonWidgetAnnotation:is(.checkBox,.radioButton) input[disabled]{ - background:none; - border:2px solid var(--input-disabled-border-color); - cursor:not-allowed; - } - -.annotationLayer .textWidgetAnnotation :is(input,textarea):hover,.annotationLayer .choiceWidgetAnnotation select:hover,.annotationLayer .buttonWidgetAnnotation:is(.checkBox,.radioButton) input:hover{ - border:2px solid var(--input-hover-border-color); - } - -.annotationLayer .textWidgetAnnotation :is(input,textarea):hover,.annotationLayer .choiceWidgetAnnotation select:hover,.annotationLayer .buttonWidgetAnnotation.checkBox input:hover{ - border-radius:2px; - } - -.annotationLayer .textWidgetAnnotation :is(input,textarea):focus,.annotationLayer .choiceWidgetAnnotation select:focus{ - background:none; - border:2px solid var(--input-focus-border-color); - border-radius:2px; - outline:var(--input-focus-outline); - } - -.annotationLayer .buttonWidgetAnnotation:is(.checkBox,.radioButton) :focus{ - background-image:none; - background-color:transparent; - } - -.annotationLayer .buttonWidgetAnnotation.checkBox :focus{ - border:2px solid var(--input-focus-border-color); - border-radius:2px; - outline:var(--input-focus-outline); - } - -.annotationLayer .buttonWidgetAnnotation.radioButton :focus{ - border:2px solid var(--input-focus-border-color); - outline:var(--input-focus-outline); - } - -.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::before,.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::after,.annotationLayer .buttonWidgetAnnotation.radioButton input:checked::before{ - background-color:CanvasText; - content:""; - display:block; - position:absolute; - } - -.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::before,.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::after{ - height:80%; - left:45%; - width:1px; - } - -.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::before{ - transform:rotate(45deg); - } - -.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::after{ - transform:rotate(-45deg); - } - -.annotationLayer .buttonWidgetAnnotation.radioButton input:checked::before{ - border-radius:50%; - height:50%; - left:25%; - top:25%; - width:50%; - } - -.annotationLayer .textWidgetAnnotation input.comb{ - font-family:monospace; - padding-left:2px; - padding-right:0; - } - -.annotationLayer .textWidgetAnnotation input.comb:focus{ - width:103%; - } - -.annotationLayer .buttonWidgetAnnotation:is(.checkBox,.radioButton) input{ - -webkit-appearance:none; - -moz-appearance:none; - appearance:none; - } - -.annotationLayer .fileAttachmentAnnotation .popupTriggerArea{ - height:100%; - width:100%; - } - -.annotationLayer .popupAnnotation{ - position:absolute; - font-size:calc(9px * var(--scale-factor)); - pointer-events:none; - width:-moz-max-content; - width:max-content; - max-width:45%; - height:auto; - } - -.annotationLayer .popup{ - background-color:rgb(255 255 153); - box-shadow:0 calc(2px * var(--scale-factor)) calc(5px * var(--scale-factor)) rgb(136 136 136); - border-radius:calc(2px * var(--scale-factor)); - outline:1.5px solid rgb(255 255 74); - padding:calc(6px * var(--scale-factor)); - cursor:pointer; - font:message-box; - white-space:normal; - word-wrap:break-word; - pointer-events:auto; - } - -.annotationLayer .popupAnnotation.focused .popup{ - outline-width:3px; - } - -.annotationLayer .popup *{ - font-size:calc(9px * var(--scale-factor)); - } - -.annotationLayer .popup > .header{ - display:inline-block; - } - -.annotationLayer .popup > .header h1{ - display:inline; - } - -.annotationLayer .popup > .header .popupDate{ - display:inline-block; - margin-left:calc(5px * var(--scale-factor)); - width:-moz-fit-content; - width:fit-content; - } - -.annotationLayer .popupContent{ - border-top:1px solid rgb(51 51 51); - margin-top:calc(2px * var(--scale-factor)); - padding-top:calc(2px * var(--scale-factor)); - } - -.annotationLayer .richText > *{ - white-space:pre-wrap; - font-size:calc(9px * var(--scale-factor)); - } - -.annotationLayer .popupTriggerArea{ - cursor:pointer; - } - -.annotationLayer section svg{ - position:absolute; - width:100%; - height:100%; - top:0; - left:0; - } - -.annotationLayer .annotationTextContent{ - position:absolute; - width:100%; - height:100%; - opacity:0; - color:transparent; - -webkit-user-select:none; - -moz-user-select:none; - user-select:none; - pointer-events:none; - } - -:is(.annotationLayer .annotationTextContent) span{ - width:100%; - display:inline-block; - } - -.annotationLayer svg.quadrilateralsContainer{ - contain:strict; - width:0; - height:0; - position:absolute; - top:0; - left:0; - z-index:-1; - } - -:root{ - --xfa-unfocused-field-background:url("data:image/svg+xml;charset=UTF-8,"); - --xfa-focus-outline:auto; -} - -@media screen and (forced-colors: active){ - :root{ - --xfa-focus-outline:2px solid CanvasText; - } - .xfaLayer *:required{ - outline:1.5px solid selectedItem; +@media (prefers-color-scheme: dark) { + :where(html:not(.is-light)) .messageBar { + --message-bar-close-button-hover-bg-color: rgb(251 251 254 / 0.14); + --message-bar-close-button-active-bg-color: rgb(251 251 254 / 0.21); + --message-bar-close-button-focus-bg-color: rgb(251 251 254 / 0.07); } } -.xfaLayer{ - background-color:transparent; +:where(html.is-dark) .messageBar { + --message-bar-close-button-hover-bg-color: rgb(251 251 254 / 0.14); + --message-bar-close-button-active-bg-color: rgb(251 251 254 / 0.21); + --message-bar-close-button-focus-bg-color: rgb(251 251 254 / 0.07); } -.xfaLayer .highlight{ - margin:-1px; - padding:1px; - background-color:rgb(239 203 237); - border-radius:4px; +@media screen and (forced-colors: active) { + .messageBar { + --message-bar-close-button-color: ButtonText; + --message-bar-close-button-border: 1px solid ButtonText; + --message-bar-close-button-hover-bg-color: ButtonText; + --message-bar-close-button-active-bg-color: ButtonText; + --message-bar-close-button-focus-bg-color: ButtonText; + --message-bar-close-button-color-hover: HighlightText; + } } -.xfaLayer .highlight.appended{ - position:initial; +.messageBar { + display: flex; + position: relative; + padding: 8px 8px 8px 16px; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 8px; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + + border-radius: 4px; + + border: 1px solid var(--message-bar-border-color); + background: var(--message-bar-bg-color); + color: var(--message-bar-fg-color); } -.xfaLayer .highlight.begin{ - border-radius:4px 0 0 4px; +.messageBar > div { + display: flex; + align-items: flex-start; + gap: 8px; + align-self: stretch; } -.xfaLayer .highlight.end{ - border-radius:0 4px 4px 0; +:is(.messageBar > div)::before { + content: ''; + display: inline-block; + width: 16px; + height: 16px; + -webkit-mask-image: var(--message-bar-icon); + mask-image: var(--message-bar-icon); + -webkit-mask-size: cover; + mask-size: cover; + background-color: var(--message-bar-icon-color); + flex-shrink: 0; } -.xfaLayer .highlight.middle{ - border-radius:0; +.messageBar button { + cursor: pointer; } -.xfaLayer .highlight.selected{ - background-color:rgb(203 223 203); +:is(.messageBar button):focus-visible { + outline: var(--focus-ring-outline); + outline-offset: 2px; } -.xfaPage{ - overflow:hidden; - position:relative; +.messageBar .closeButton { + width: 32px; + height: 32px; + background: none; + border-radius: var(--message-bar-close-button-border-radius); + border: var(--message-bar-close-button-border); + + display: flex; + align-items: center; + justify-content: center; } -.xfaContentarea{ - position:absolute; +:is(.messageBar .closeButton)::before { + content: ''; + display: inline-block; + width: 16px; + height: 16px; + -webkit-mask-image: var(--closing-button-icon); + mask-image: var(--closing-button-icon); + -webkit-mask-size: cover; + mask-size: cover; + background-color: var(--message-bar-close-button-color); } -.xfaPrintOnly{ - display:none; +:is(.messageBar .closeButton):is(:hover, :active, :focus)::before { + background-color: var(--message-bar-close-button-color-hover); } -.xfaLayer{ - position:absolute; - text-align:initial; - top:0; - left:0; - transform-origin:0 0; - line-height:1.2; +:is(.messageBar .closeButton):hover { + background-color: var(--message-bar-close-button-hover-bg-color); } -.xfaLayer *{ - color:inherit; - font:inherit; - font-style:inherit; - font-weight:inherit; - font-kerning:inherit; - letter-spacing:-0.01px; - text-align:inherit; - text-decoration:inherit; - box-sizing:border-box; - background-color:transparent; - padding:0; - margin:0; - pointer-events:auto; - line-height:inherit; +:is(.messageBar .closeButton):active { + background-color: var(--message-bar-close-button-active-bg-color); } -.xfaLayer *:required{ - outline:1.5px solid red; +:is(.messageBar .closeButton):focus { + background-color: var(--message-bar-close-button-focus-bg-color); +} + +:is(.messageBar .closeButton) > span { + display: inline-block; + width: 0; + height: 0; + overflow: hidden; +} + +#editorUndoBar { + --text-primary-color: #15141a; + + --message-bar-icon: url(images/secondaryToolbarButton-documentProperties.svg); + --message-bar-icon-color: #0060df; + --message-bar-bg-color: #deeafc; + --message-bar-fg-color: var(--text-primary-color); + --message-bar-border-color: rgb(0 0 0 / 0.08); + + --undo-button-bg-color: rgb(21 20 26 / 0.07); + --undo-button-bg-color-hover: rgb(21 20 26 / 0.14); + --undo-button-bg-color-active: rgb(21 20 26 / 0.21); + + --undo-button-fg-color: var(--message-bar-fg-color); + --undo-button-fg-color-hover: var(--undo-button-fg-color); + --undo-button-fg-color-active: var(--undo-button-fg-color); + + --focus-ring-color: #0060df; + --focus-ring-outline: 2px solid var(--focus-ring-color); +} + +@media (prefers-color-scheme: dark) { + :where(html:not(.is-light)) #editorUndoBar { + --text-primary-color: #fbfbfe; + + --message-bar-icon-color: #73a7f3; + --message-bar-bg-color: #003070; + --message-bar-border-color: rgb(255 255 255 / 0.08); + + --undo-button-bg-color: rgb(255 255 255 / 0.08); + --undo-button-bg-color-hover: rgb(255 255 255 / 0.14); + --undo-button-bg-color-active: rgb(255 255 255 / 0.21); + } +} + +:where(html.is-dark) #editorUndoBar { + --text-primary-color: #fbfbfe; + + --message-bar-icon-color: #73a7f3; + --message-bar-bg-color: #003070; + --message-bar-border-color: rgb(255 255 255 / 0.08); + + --undo-button-bg-color: rgb(255 255 255 / 0.08); + --undo-button-bg-color-hover: rgb(255 255 255 / 0.14); + --undo-button-bg-color-active: rgb(255 255 255 / 0.21); +} + +@media screen and (forced-colors: active) { + #editorUndoBar { + --text-primary-color: CanvasText; + + --message-bar-icon-color: CanvasText; + --message-bar-bg-color: Canvas; + --message-bar-border-color: CanvasText; + + --undo-button-bg-color: ButtonText; + --undo-button-bg-color-hover: SelectedItem; + --undo-button-bg-color-active: SelectedItem; + + --undo-button-fg-color: ButtonFace; + --undo-button-fg-color-hover: SelectedItemText; + --undo-button-fg-color-active: SelectedItemText; + + --focus-ring-color: CanvasText; + } +} + +#editorUndoBar { + position: fixed; + top: 50px; + left: 50%; + transform: translateX(-50%); + z-index: 10; + + padding-block: 8px; + padding-inline: 16px 8px; + + font: menu; + font-size: 15px; + + cursor: default; +} + +#editorUndoBar button { + cursor: pointer; +} + +#editorUndoBar #editorUndoBarUndoButton { + border-radius: 4px; + font-weight: 590; + line-height: 19.5px; + color: var(--undo-button-fg-color); + border: none; + padding: 4px 16px; + margin-inline-start: 8px; + height: 32px; + + background-color: var(--undo-button-bg-color); +} + +:is(#editorUndoBar #editorUndoBarUndoButton):hover { + background-color: var(--undo-button-bg-color-hover); + color: var(--undo-button-fg-color-hover); +} + +:is(#editorUndoBar #editorUndoBarUndoButton):active { + background-color: var(--undo-button-bg-color-active); + color: var(--undo-button-fg-color-active); +} + +#editorUndoBar > div { + align-items: center; +} + +.dialog { + --dialog-bg-color: white; + --dialog-border-color: white; + --dialog-shadow: 0 2px 14px 0 rgb(58 57 68 / 0.2); + --text-primary-color: #15141a; + --text-secondary-color: #5b5b66; + --hover-filter: brightness(0.9); + --focus-ring-color: #0060df; + --focus-ring-outline: 2px solid var(--focus-ring-color); + --link-fg-color: #0060df; + --link-hover-fg-color: #0250bb; + --separator-color: #f0f0f4; + + --textarea-border-color: #8f8f9d; + --textarea-bg-color: white; + --textarea-fg-color: var(--text-secondary-color); + + --radio-bg-color: #f0f0f4; + --radio-checked-bg-color: #fbfbfe; + --radio-border-color: #8f8f9d; + --radio-checked-border-color: #0060df; + + --button-secondary-bg-color: #f0f0f4; + --button-secondary-fg-color: var(--text-primary-color); + --button-secondary-border-color: var(--button-secondary-bg-color); + --button-secondary-hover-bg-color: var(--button-secondary-bg-color); + --button-secondary-hover-fg-color: var(--button-secondary-fg-color); + --button-secondary-hover-border-color: var(--button-secondary-hover-bg-color); + + --button-primary-bg-color: #0060df; + --button-primary-fg-color: #fbfbfe; + --button-primary-border-color: var(--button-primary-bg-color); + --button-primary-hover-bg-color: var(--button-primary-bg-color); + --button-primary-hover-fg-color: var(--button-primary-fg-color); + --button-primary-hover-border-color: var(--button-primary-hover-bg-color); +} + +@media (prefers-color-scheme: dark) { + :where(html:not(.is-light)) .dialog { + --dialog-bg-color: #1c1b22; + --dialog-border-color: #1c1b22; + --dialog-shadow: 0 2px 14px 0 #15141a; + --text-primary-color: #fbfbfe; + --text-secondary-color: #cfcfd8; + --focus-ring-color: #0df; + --hover-filter: brightness(1.4); + --link-fg-color: #0df; + --link-hover-fg-color: #80ebff; + --separator-color: #52525e; + + --textarea-bg-color: #42414d; + + --radio-bg-color: #2b2a33; + --radio-checked-bg-color: #15141a; + --radio-checked-border-color: #0df; + + --button-secondary-bg-color: #2b2a33; + --button-primary-bg-color: #0df; + --button-primary-fg-color: #15141a; + } +} + +:where(html.is-dark) .dialog { + --dialog-bg-color: #1c1b22; + --dialog-border-color: #1c1b22; + --dialog-shadow: 0 2px 14px 0 #15141a; + --text-primary-color: #fbfbfe; + --text-secondary-color: #cfcfd8; + --focus-ring-color: #0df; + --hover-filter: brightness(1.4); + --link-fg-color: #0df; + --link-hover-fg-color: #80ebff; + --separator-color: #52525e; + + --textarea-bg-color: #42414d; + + --radio-bg-color: #2b2a33; + --radio-checked-bg-color: #15141a; + --radio-checked-border-color: #0df; + + --button-secondary-bg-color: #2b2a33; + --button-primary-bg-color: #0df; + --button-primary-fg-color: #15141a; +} + +@media screen and (forced-colors: active) { + .dialog { + --dialog-bg-color: Canvas; + --dialog-border-color: CanvasText; + --dialog-shadow: none; + --text-primary-color: CanvasText; + --text-secondary-color: CanvasText; + --hover-filter: none; + --focus-ring-color: ButtonBorder; + --link-fg-color: LinkText; + --link-hover-fg-color: LinkText; + --separator-color: CanvasText; + + --textarea-border-color: ButtonBorder; + --textarea-bg-color: Field; + --textarea-fg-color: ButtonText; + + --radio-bg-color: ButtonFace; + --radio-checked-bg-color: ButtonFace; + --radio-border-color: ButtonText; + --radio-checked-border-color: ButtonText; + + --button-secondary-bg-color: ButtonFace; + --button-secondary-fg-color: ButtonText; + --button-secondary-border-color: ButtonText; + --button-secondary-hover-bg-color: AccentColor; + --button-secondary-hover-fg-color: AccentColorText; + + --button-primary-bg-color: ButtonText; + --button-primary-fg-color: ButtonFace; + --button-primary-hover-bg-color: AccentColor; + --button-primary-hover-fg-color: AccentColorText; + } +} + +.dialog { + font: message-box; + font-size: 13px; + font-weight: 400; + line-height: 150%; + border-radius: 4px; + padding: 12px 16px; + border: 1px solid var(--dialog-border-color); + background: var(--dialog-bg-color); + color: var(--text-primary-color); + box-shadow: var(--dialog-shadow); +} + +:is(.dialog .mainContainer) *:focus-visible { + outline: var(--focus-ring-outline); + outline-offset: 2px; +} + +:is(.dialog .mainContainer) .title { + display: flex; + width: auto; + flex-direction: column; + justify-content: flex-end; + align-items: flex-start; + gap: 12px; +} + +:is(:is(.dialog .mainContainer) .title) > span { + font-size: 13px; + font-style: normal; + font-weight: 590; + line-height: 150%; +} + +:is(.dialog .mainContainer) .dialogSeparator { + width: 100%; + height: 0; + margin-block: 4px; + border-top: 1px solid var(--separator-color); + border-bottom: none; +} + +:is(.dialog .mainContainer) .dialogButtonsGroup { + display: flex; + gap: 12px; + align-self: flex-end; +} + +:is(.dialog .mainContainer) .radio { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; +} + +:is(:is(.dialog .mainContainer) .radio) > .radioButton { + display: flex; + gap: 8px; + align-self: stretch; + align-items: center; +} + +:is(:is(:is(.dialog .mainContainer) .radio) > .radioButton) input { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + box-sizing: border-box; + width: 16px; + height: 16px; + border-radius: 50%; + background-color: var(--radio-bg-color); + border: 1px solid var(--radio-border-color); +} + +:is(:is(:is(:is(.dialog .mainContainer) .radio) > .radioButton) input):hover { + filter: var(--hover-filter); +} + +:is(:is(:is(:is(.dialog .mainContainer) .radio) > .radioButton) input):checked { + background-color: var(--radio-checked-bg-color); + border: 4px solid var(--radio-checked-border-color); +} + +:is(:is(.dialog .mainContainer) .radio) > .radioLabel { + display: flex; + padding-inline-start: 24px; + align-items: flex-start; + gap: 10px; + align-self: stretch; +} + +:is(:is(:is(.dialog .mainContainer) .radio) > .radioLabel) > span { + flex: 1 0 0; + font-size: 11px; + color: var(--text-secondary-color); +} + +:is(.dialog .mainContainer) button:not(:is(.toggle-button, .closeButton)) { + border-radius: 4px; + border: 1px solid; + font: menu; + font-weight: 600; + padding: 4px 16px; + width: auto; + height: 32px; +} + +:is( + :is(.dialog .mainContainer) button:not(:is(.toggle-button, .closeButton)) + ):hover { + cursor: pointer; + filter: var(--hover-filter); +} + +.secondaryButton:is( + :is(.dialog .mainContainer) button:not(:is(.toggle-button, .closeButton)) + ) { + color: var(--button-secondary-fg-color); + background-color: var(--button-secondary-bg-color); + border-color: var(--button-secondary-border-color); +} + +.secondaryButton:is( + :is(.dialog .mainContainer) button:not(:is(.toggle-button, .closeButton)) + ):hover { + color: var(--button-secondary-hover-fg-color); + background-color: var(--button-secondary-hover-bg-color); + border-color: var(--button-secondary-hover-border-color); +} + +.primaryButton:is( + :is(.dialog .mainContainer) button:not(:is(.toggle-button, .closeButton)) + ) { + color: var(--button-primary-fg-color); + background-color: var(--button-primary-bg-color); + border-color: var(--button-primary-border-color); + opacity: 1; +} + +.primaryButton:is( + :is(.dialog .mainContainer) button:not(:is(.toggle-button, .closeButton)) + ):hover { + color: var(--button-primary-hover-fg-color); + background-color: var(--button-primary-hover-bg-color); + border-color: var(--button-primary-hover-border-color); +} + +:is(.dialog .mainContainer) a { + color: var(--link-fg-color); +} + +:is(:is(.dialog .mainContainer) a):hover { + color: var(--link-hover-fg-color); +} + +:is(.dialog .mainContainer) textarea { + font: inherit; + padding: 8px; + resize: none; + margin: 0; + box-sizing: border-box; + border-radius: 4px; + border: 1px solid var(--textarea-border-color); + background: var(--textarea-bg-color); + color: var(--textarea-fg-color); +} + +:is(:is(.dialog .mainContainer) textarea):focus { + outline-offset: 0; + border-color: transparent; +} + +:is(:is(.dialog .mainContainer) textarea):disabled { + pointer-events: none; + opacity: 0.4; +} + +:is(.dialog .mainContainer) .messageBar { + --message-bar-bg-color: #ffebcd; + --message-bar-fg-color: #15141a; + --message-bar-border-color: rgb(0 0 0 / 0.08); + --message-bar-icon: url(images/messageBar_warning.svg); + --message-bar-icon-color: #cd411e; +} + +@media (prefers-color-scheme: dark) { + :where(html:not(.is-light)) :is(.dialog .mainContainer) .messageBar { + --message-bar-bg-color: #5a3100; + --message-bar-fg-color: #fbfbfe; + --message-bar-border-color: rgb(255 255 255 / 0.08); + --message-bar-icon-color: #e49c49; + } +} + +:where(html.is-dark) :is(.dialog .mainContainer) .messageBar { + --message-bar-bg-color: #5a3100; + --message-bar-fg-color: #fbfbfe; + --message-bar-border-color: rgb(255 255 255 / 0.08); + --message-bar-icon-color: #e49c49; +} + +@media screen and (forced-colors: active) { + :is(.dialog .mainContainer) .messageBar { + --message-bar-bg-color: HighlightText; + --message-bar-fg-color: CanvasText; + --message-bar-border-color: CanvasText; + --message-bar-icon-color: CanvasText; + } +} + +:is(.dialog .mainContainer) .messageBar { + align-self: stretch; +} + +:is(:is(:is(.dialog .mainContainer) .messageBar) > div)::before, +:is(:is(:is(.dialog .mainContainer) .messageBar) > div) > div { + margin-block: 4px; +} + +:is(:is(:is(.dialog .mainContainer) .messageBar) > div) > div { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + flex: 1 0 0; +} + +:is(:is(:is(:is(.dialog .mainContainer) .messageBar) > div) > div) .title { + font-size: 13px; + font-weight: 590; +} + +:is(:is(:is(:is(.dialog .mainContainer) .messageBar) > div) > div) + .description { + font-size: 13px; +} + +:is(.dialog .mainContainer) .toggler { + display: flex; + align-items: center; + gap: 8px; + align-self: stretch; +} + +:is(:is(.dialog .mainContainer) .toggler) > .togglerLabel { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.textLayer { + position: absolute; + text-align: initial; + inset: 0; + overflow: clip; + opacity: 1; + line-height: 1; + -webkit-text-size-adjust: none; + -moz-text-size-adjust: none; + text-size-adjust: none; + forced-color-adjust: none; + transform-origin: 0 0; + caret-color: CanvasText; + z-index: 0; +} + +.textLayer.highlighting { + touch-action: none; +} + +.textLayer :is(span, br) { + color: transparent; + position: absolute; + white-space: pre; + cursor: text; + transform-origin: 0% 0%; +} + +.textLayer > :not(.markedContent), +.textLayer .markedContent span:not(.markedContent) { + z-index: 1; +} + +.textLayer span.markedContent { + top: 0; + height: 0; +} + +.textLayer span[role='img'] { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + cursor: default; +} + +.textLayer .highlight { + --highlight-bg-color: rgb(180 0 170 / 0.25); + --highlight-selected-bg-color: rgb(0 100 0 / 0.25); + --highlight-backdrop-filter: none; + --highlight-selected-backdrop-filter: none; +} + +@media screen and (forced-colors: active) { + .textLayer .highlight { + --highlight-bg-color: transparent; + --highlight-selected-bg-color: transparent; + --highlight-backdrop-filter: var(--hcm-highlight-filter); + --highlight-selected-backdrop-filter: var(--hcm-highlight-selected-filter); + } +} + +.textLayer .highlight { + margin: -1px; + padding: 1px; + background-color: var(--highlight-bg-color); + -webkit-backdrop-filter: var(--highlight-backdrop-filter); + backdrop-filter: var(--highlight-backdrop-filter); + border-radius: 4px; +} + +.appended:is(.textLayer .highlight) { + position: initial; +} + +.begin:is(.textLayer .highlight) { + border-radius: 4px 0 0 4px; +} + +.end:is(.textLayer .highlight) { + border-radius: 0 4px 4px 0; +} + +.middle:is(.textLayer .highlight) { + border-radius: 0; +} + +.selected:is(.textLayer .highlight) { + background-color: var(--highlight-selected-bg-color); + -webkit-backdrop-filter: var(--highlight-selected-backdrop-filter); + backdrop-filter: var(--highlight-selected-backdrop-filter); +} + +.textLayer ::-moz-selection { + background: rgba(0 0 255 / 0.25); + background: color-mix(in srgb, AccentColor, transparent 75%); +} + +.textLayer ::selection { + background: rgba(0 0 255 / 0.25); + background: color-mix(in srgb, AccentColor, transparent 75%); +} + +.textLayer br::-moz-selection { + background: transparent; +} + +.textLayer br::selection { + background: transparent; +} + +.textLayer .endOfContent { + display: block; + position: absolute; + inset: 100% 0 0; + z-index: 0; + cursor: default; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.textLayer.selecting .endOfContent { + top: 0; +} + +.annotationLayer { + --annotation-unfocused-field-background: url("data:image/svg+xml;charset=UTF-8,"); + --input-focus-border-color: Highlight; + --input-focus-outline: 1px solid Canvas; + --input-unfocused-border-color: transparent; + --input-disabled-border-color: transparent; + --input-hover-border-color: black; + --link-outline: none; +} + +@media screen and (forced-colors: active) { + .annotationLayer { + --input-focus-border-color: CanvasText; + --input-unfocused-border-color: ActiveText; + --input-disabled-border-color: GrayText; + --input-hover-border-color: Highlight; + --link-outline: 1.5px solid LinkText; + } + + .annotationLayer .textWidgetAnnotation :is(input, textarea):required, + .annotationLayer .choiceWidgetAnnotation select:required, + .annotationLayer + .buttonWidgetAnnotation:is(.checkBox, .radioButton) + input:required { + outline: 1.5px solid selectedItem; + } + + .annotationLayer .linkAnnotation { + outline: var(--link-outline); + } + + :is(.annotationLayer .linkAnnotation):hover { + -webkit-backdrop-filter: var(--hcm-highlight-filter); + backdrop-filter: var(--hcm-highlight-filter); + } + + :is(.annotationLayer .linkAnnotation) > a:hover { + opacity: 0 !important; + background: none !important; + box-shadow: none; + } + + .annotationLayer .popupAnnotation .popup { + outline: calc(1.5px * var(--scale-factor)) solid CanvasText !important; + background-color: ButtonFace !important; + color: ButtonText !important; + } + + .annotationLayer .highlightArea:hover::after { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + -webkit-backdrop-filter: var(--hcm-highlight-filter); + backdrop-filter: var(--hcm-highlight-filter); + content: ''; + pointer-events: none; + } + + .annotationLayer .popupAnnotation.focused .popup { + outline: calc(3px * var(--scale-factor)) solid Highlight !important; + } +} + +.annotationLayer { + position: absolute; + top: 0; + left: 0; + pointer-events: none; + transform-origin: 0 0; +} + +.annotationLayer[data-main-rotation='90'] .norotate { + transform: rotate(270deg) translateX(-100%); +} + +.annotationLayer[data-main-rotation='180'] .norotate { + transform: rotate(180deg) translate(-100%, -100%); +} + +.annotationLayer[data-main-rotation='270'] .norotate { + transform: rotate(90deg) translateY(-100%); +} + +.annotationLayer.disabled section, +.annotationLayer.disabled .popup { + pointer-events: none; +} + +.annotationLayer .annotationContent { + position: absolute; + width: 100%; + height: 100%; + pointer-events: none; +} + +.freetext:is(.annotationLayer .annotationContent) { + background: transparent; + border: none; + inset: 0; + overflow: visible; + white-space: nowrap; + font: 10px sans-serif; + line-height: 1.35; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.annotationLayer section { + position: absolute; + text-align: initial; + pointer-events: auto; + box-sizing: border-box; + transform-origin: 0 0; +} + +:is(.annotationLayer section):has(div.annotationContent) + canvas.annotationContent { + display: none; +} + +.textLayer.selecting ~ .annotationLayer section { + pointer-events: none; +} + +.annotationLayer :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton) > a { + position: absolute; + font-size: 1em; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.annotationLayer + :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton):not(.hasBorder) + > a:hover { + opacity: 0.2; + background-color: rgb(255 255 0); + box-shadow: 0 2px 10px rgb(255 255 0); +} + +.annotationLayer .linkAnnotation.hasBorder:hover { + background-color: rgb(255 255 0 / 0.2); +} + +.annotationLayer .hasBorder { + background-size: 100% 100%; +} + +.annotationLayer .textAnnotation img { + position: absolute; + cursor: pointer; + width: 100%; + height: 100%; + top: 0; + left: 0; +} + +.annotationLayer .textWidgetAnnotation :is(input, textarea), +.annotationLayer .choiceWidgetAnnotation select, +.annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) input { + background-image: var(--annotation-unfocused-field-background); + border: 2px solid var(--input-unfocused-border-color); + box-sizing: border-box; + font: calc(9px * var(--scale-factor)) sans-serif; + height: 100%; + margin: 0; + vertical-align: top; + width: 100%; +} + +.annotationLayer .textWidgetAnnotation :is(input, textarea):required, +.annotationLayer .choiceWidgetAnnotation select:required, +.annotationLayer + .buttonWidgetAnnotation:is(.checkBox, .radioButton) + input:required { + outline: 1.5px solid red; +} + +.annotationLayer .choiceWidgetAnnotation select option { + padding: 0; +} + +.annotationLayer .buttonWidgetAnnotation.radioButton input { + border-radius: 50%; +} + +.annotationLayer .textWidgetAnnotation textarea { + resize: none; +} + +.annotationLayer .textWidgetAnnotation [disabled]:is(input, textarea), +.annotationLayer .choiceWidgetAnnotation select[disabled], +.annotationLayer + .buttonWidgetAnnotation:is(.checkBox, .radioButton) + input[disabled] { + background: none; + border: 2px solid var(--input-disabled-border-color); + cursor: not-allowed; +} + +.annotationLayer .textWidgetAnnotation :is(input, textarea):hover, +.annotationLayer .choiceWidgetAnnotation select:hover, +.annotationLayer + .buttonWidgetAnnotation:is(.checkBox, .radioButton) + input:hover { + border: 2px solid var(--input-hover-border-color); +} + +.annotationLayer .textWidgetAnnotation :is(input, textarea):hover, +.annotationLayer .choiceWidgetAnnotation select:hover, +.annotationLayer .buttonWidgetAnnotation.checkBox input:hover { + border-radius: 2px; +} + +.annotationLayer .textWidgetAnnotation :is(input, textarea):focus, +.annotationLayer .choiceWidgetAnnotation select:focus { + background: none; + border: 2px solid var(--input-focus-border-color); + border-radius: 2px; + outline: var(--input-focus-outline); +} + +.annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) :focus { + background-image: none; + background-color: transparent; +} + +.annotationLayer .buttonWidgetAnnotation.checkBox :focus { + border: 2px solid var(--input-focus-border-color); + border-radius: 2px; + outline: var(--input-focus-outline); +} + +.annotationLayer .buttonWidgetAnnotation.radioButton :focus { + border: 2px solid var(--input-focus-border-color); + outline: var(--input-focus-outline); +} + +.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::before, +.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::after, +.annotationLayer .buttonWidgetAnnotation.radioButton input:checked::before { + background-color: CanvasText; + content: ''; + display: block; + position: absolute; +} + +.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::before, +.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::after { + height: 80%; + left: 45%; + width: 1px; +} + +.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::before { + transform: rotate(45deg); +} + +.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::after { + transform: rotate(-45deg); +} + +.annotationLayer .buttonWidgetAnnotation.radioButton input:checked::before { + border-radius: 50%; + height: 50%; + left: 25%; + top: 25%; + width: 50%; +} + +.annotationLayer .textWidgetAnnotation input.comb { + font-family: monospace; + padding-left: 2px; + padding-right: 0; +} + +.annotationLayer .textWidgetAnnotation input.comb:focus { + width: 103%; +} + +.annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) input { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.annotationLayer .fileAttachmentAnnotation .popupTriggerArea { + height: 100%; + width: 100%; +} + +.annotationLayer .popupAnnotation { + position: absolute; + font-size: calc(9px * var(--scale-factor)); + pointer-events: none; + width: -moz-max-content; + width: max-content; + max-width: 45%; + height: auto; +} + +.annotationLayer .popup { + background-color: rgb(255 255 153); + box-shadow: 0 calc(2px * var(--scale-factor)) calc(5px * var(--scale-factor)) + rgb(136 136 136); + border-radius: calc(2px * var(--scale-factor)); + outline: 1.5px solid rgb(255 255 74); + padding: calc(6px * var(--scale-factor)); + cursor: pointer; + font: message-box; + white-space: normal; + word-wrap: break-word; + pointer-events: auto; +} + +.annotationLayer .popupAnnotation.focused .popup { + outline-width: 3px; +} + +.annotationLayer .popup * { + font-size: calc(9px * var(--scale-factor)); +} + +.annotationLayer .popup > .header { + display: inline-block; +} + +.annotationLayer .popup > .header h1 { + display: inline; +} + +.annotationLayer .popup > .header .popupDate { + display: inline-block; + margin-left: calc(5px * var(--scale-factor)); + width: -moz-fit-content; + width: fit-content; +} + +.annotationLayer .popupContent { + border-top: 1px solid rgb(51 51 51); + margin-top: calc(2px * var(--scale-factor)); + padding-top: calc(2px * var(--scale-factor)); +} + +.annotationLayer .richText > * { + white-space: pre-wrap; + font-size: calc(9px * var(--scale-factor)); +} + +.annotationLayer .popupTriggerArea { + cursor: pointer; +} + +.annotationLayer section svg { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; +} + +.annotationLayer .annotationTextContent { + position: absolute; + width: 100%; + height: 100%; + opacity: 0; + color: transparent; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + pointer-events: none; +} + +:is(.annotationLayer .annotationTextContent) span { + width: 100%; + display: inline-block; +} + +.annotationLayer svg.quadrilateralsContainer { + contain: strict; + width: 0; + height: 0; + position: absolute; + top: 0; + left: 0; + z-index: -1; +} + +:root { + --xfa-unfocused-field-background: url("data:image/svg+xml;charset=UTF-8,"); + --xfa-focus-outline: auto; +} + +@media screen and (forced-colors: active) { + :root { + --xfa-focus-outline: 2px solid CanvasText; + } + .xfaLayer *:required { + outline: 1.5px solid selectedItem; + } +} + +.xfaLayer { + background-color: transparent; +} + +.xfaLayer .highlight { + margin: -1px; + padding: 1px; + background-color: rgb(239 203 237); + border-radius: 4px; +} + +.xfaLayer .highlight.appended { + position: initial; +} + +.xfaLayer .highlight.begin { + border-radius: 4px 0 0 4px; +} + +.xfaLayer .highlight.end { + border-radius: 0 4px 4px 0; +} + +.xfaLayer .highlight.middle { + border-radius: 0; +} + +.xfaLayer .highlight.selected { + background-color: rgb(203 223 203); +} + +.xfaPage { + overflow: hidden; + position: relative; +} + +.xfaContentarea { + position: absolute; +} + +.xfaPrintOnly { + display: none; +} + +.xfaLayer { + position: absolute; + text-align: initial; + top: 0; + left: 0; + transform-origin: 0 0; + line-height: 1.2; +} + +.xfaLayer * { + color: inherit; + font: inherit; + font-style: inherit; + font-weight: inherit; + font-kerning: inherit; + letter-spacing: -0.01px; + text-align: inherit; + text-decoration: inherit; + box-sizing: border-box; + background-color: transparent; + padding: 0; + margin: 0; + pointer-events: auto; + line-height: inherit; +} + +.xfaLayer *:required { + outline: 1.5px solid red; } .xfaLayer div, .xfaLayer svg, -.xfaLayer svg *{ - pointer-events:none; +.xfaLayer svg * { + pointer-events: none; } -.xfaLayer a{ - color:blue; +.xfaLayer a { + color: blue; } -.xfaRich li{ - margin-left:3em; +.xfaRich li { + margin-left: 3em; } -.xfaFont{ - color:black; - font-weight:normal; - font-kerning:none; - font-size:10px; - font-style:normal; - letter-spacing:0; - text-decoration:none; - vertical-align:0; +.xfaFont { + color: black; + font-weight: normal; + font-kerning: none; + font-size: 10px; + font-style: normal; + letter-spacing: 0; + text-decoration: none; + vertical-align: 0; } -.xfaCaption{ - overflow:hidden; - flex:0 0 auto; +.xfaCaption { + overflow: hidden; + flex: 0 0 auto; } -.xfaCaptionForCheckButton{ - overflow:hidden; - flex:1 1 auto; +.xfaCaptionForCheckButton { + overflow: hidden; + flex: 1 1 auto; } -.xfaLabel{ - height:100%; - width:100%; +.xfaLabel { + height: 100%; + width: 100%; } -.xfaLeft{ - display:flex; - flex-direction:row; - align-items:center; +.xfaLeft { + display: flex; + flex-direction: row; + align-items: center; } -.xfaRight{ - display:flex; - flex-direction:row-reverse; - align-items:center; +.xfaRight { + display: flex; + flex-direction: row-reverse; + align-items: center; } -:is(.xfaLeft, .xfaRight) > :is(.xfaCaption, .xfaCaptionForCheckButton){ - max-height:100%; +:is(.xfaLeft, .xfaRight) > :is(.xfaCaption, .xfaCaptionForCheckButton) { + max-height: 100%; } -.xfaTop{ - display:flex; - flex-direction:column; - align-items:flex-start; +.xfaTop { + display: flex; + flex-direction: column; + align-items: flex-start; } -.xfaBottom{ - display:flex; - flex-direction:column-reverse; - align-items:flex-start; +.xfaBottom { + display: flex; + flex-direction: column-reverse; + align-items: flex-start; } -:is(.xfaTop, .xfaBottom) > :is(.xfaCaption, .xfaCaptionForCheckButton){ - width:100%; +:is(.xfaTop, .xfaBottom) > :is(.xfaCaption, .xfaCaptionForCheckButton) { + width: 100%; } -.xfaBorder{ - background-color:transparent; - position:absolute; - pointer-events:none; +.xfaBorder { + background-color: transparent; + position: absolute; + pointer-events: none; } -.xfaWrapped{ - width:100%; - height:100%; +.xfaWrapped { + width: 100%; + height: 100%; } -:is(.xfaTextfield, .xfaSelect):focus{ - background-image:none; - background-color:transparent; - outline:var(--xfa-focus-outline); - outline-offset:-1px; +:is(.xfaTextfield, .xfaSelect):focus { + background-image: none; + background-color: transparent; + outline: var(--xfa-focus-outline); + outline-offset: -1px; } -:is(.xfaCheckbox, .xfaRadio):focus{ - outline:var(--xfa-focus-outline); +:is(.xfaCheckbox, .xfaRadio):focus { + outline: var(--xfa-focus-outline); } .xfaTextfield, -.xfaSelect{ - height:100%; - width:100%; - flex:1 1 auto; - border:none; - resize:none; - background-image:var(--xfa-unfocused-field-background); +.xfaSelect { + height: 100%; + width: 100%; + flex: 1 1 auto; + border: none; + resize: none; + background-image: var(--xfa-unfocused-field-background); } -.xfaSelect{ - padding-inline:2px; +.xfaSelect { + padding-inline: 2px; } -:is(.xfaTop, .xfaBottom) > :is(.xfaTextfield, .xfaSelect){ - flex:0 1 auto; +:is(.xfaTop, .xfaBottom) > :is(.xfaTextfield, .xfaSelect) { + flex: 0 1 auto; } -.xfaButton{ - cursor:pointer; - width:100%; - height:100%; - border:none; - text-align:center; +.xfaButton { + cursor: pointer; + width: 100%; + height: 100%; + border: none; + text-align: center; } -.xfaLink{ - width:100%; - height:100%; - position:absolute; - top:0; - left:0; +.xfaLink { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; } .xfaCheckbox, -.xfaRadio{ - width:100%; - height:100%; - flex:0 0 auto; - border:none; +.xfaRadio { + width: 100%; + height: 100%; + flex: 0 0 auto; + border: none; } -.xfaRich{ - white-space:pre-wrap; - width:100%; - height:100%; +.xfaRich { + white-space: pre-wrap; + width: 100%; + height: 100%; } -.xfaImage{ - -o-object-position:left top; - object-position:left top; - -o-object-fit:contain; - object-fit:contain; - width:100%; - height:100%; +.xfaImage { + -o-object-position: left top; + object-position: left top; + -o-object-fit: contain; + object-fit: contain; + width: 100%; + height: 100%; } .xfaLrTb, .xfaRlTb, -.xfaTb{ - display:flex; - flex-direction:column; - align-items:stretch; +.xfaTb { + display: flex; + flex-direction: column; + align-items: stretch; } -.xfaLr{ - display:flex; - flex-direction:row; - align-items:stretch; +.xfaLr { + display: flex; + flex-direction: row; + align-items: stretch; } -.xfaRl{ - display:flex; - flex-direction:row-reverse; - align-items:stretch; +.xfaRl { + display: flex; + flex-direction: row-reverse; + align-items: stretch; } -.xfaTb > div{ - justify-content:left; +.xfaTb > div { + justify-content: left; } -.xfaPosition{ - position:relative; +.xfaPosition { + position: relative; } -.xfaArea{ - position:relative; +.xfaArea { + position: relative; } -.xfaValignMiddle{ - display:flex; - align-items:center; +.xfaValignMiddle { + display: flex; + align-items: center; } -.xfaTable{ - display:flex; - flex-direction:column; - align-items:stretch; +.xfaTable { + display: flex; + flex-direction: column; + align-items: stretch; } -.xfaTable .xfaRow{ - display:flex; - flex-direction:row; - align-items:stretch; +.xfaTable .xfaRow { + display: flex; + flex-direction: row; + align-items: stretch; } -.xfaTable .xfaRlRow{ - display:flex; - flex-direction:row-reverse; - align-items:stretch; - flex:1; +.xfaTable .xfaRlRow { + display: flex; + flex-direction: row-reverse; + align-items: stretch; + flex: 1; } -.xfaTable .xfaRlRow > div{ - flex:1; +.xfaTable .xfaRlRow > div { + flex: 1; } -:is(.xfaNonInteractive, .xfaDisabled, .xfaReadOnly) :is(input, textarea){ - background:initial; +:is(.xfaNonInteractive, .xfaDisabled, .xfaReadOnly) :is(input, textarea) { + background: initial; } -@media print{ +@media print { .xfaTextfield, - .xfaSelect{ - background:transparent; + .xfaSelect { + background: transparent; } - .xfaSelect{ - -webkit-appearance:none; - -moz-appearance:none; - appearance:none; - text-indent:1px; - text-overflow:""; + .xfaSelect { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + text-indent: 1px; + text-overflow: ''; } } -.canvasWrapper svg{ - transform:none; - } - -.moving:is(.canvasWrapper svg){ - z-index:100000; - } - -[data-main-rotation="90"]:is(.highlight:is(.canvasWrapper svg),.highlightOutline:is(.canvasWrapper svg)) mask,[data-main-rotation="90"]:is(.highlight:is(.canvasWrapper svg),.highlightOutline:is(.canvasWrapper svg)) use:not(.clip,.mask){ - transform:matrix(0, 1, -1, 0, 1, 0); - } - -[data-main-rotation="180"]:is(.highlight:is(.canvasWrapper svg),.highlightOutline:is(.canvasWrapper svg)) mask,[data-main-rotation="180"]:is(.highlight:is(.canvasWrapper svg),.highlightOutline:is(.canvasWrapper svg)) use:not(.clip,.mask){ - transform:matrix(-1, 0, 0, -1, 1, 1); - } - -[data-main-rotation="270"]:is(.highlight:is(.canvasWrapper svg),.highlightOutline:is(.canvasWrapper svg)) mask,[data-main-rotation="270"]:is(.highlight:is(.canvasWrapper svg),.highlightOutline:is(.canvasWrapper svg)) use:not(.clip,.mask){ - transform:matrix(0, -1, 1, 0, 0, 1); - } - -.draw:is(.canvasWrapper svg){ - position:absolute; - mix-blend-mode:normal; - } - -.draw[data-draw-rotation="90"]:is(.canvasWrapper svg){ - transform:rotate(90deg); - } - -.draw[data-draw-rotation="180"]:is(.canvasWrapper svg){ - transform:rotate(180deg); - } - -.draw[data-draw-rotation="270"]:is(.canvasWrapper svg){ - transform:rotate(270deg); - } - -.highlight:is(.canvasWrapper svg){ - --blend-mode:multiply; - } - -@media screen and (forced-colors: active){ - -.highlight:is(.canvasWrapper svg){ - --blend-mode:difference; - } - } - -.highlight:is(.canvasWrapper svg){ - - position:absolute; - mix-blend-mode:var(--blend-mode); - } - -.highlight:is(.canvasWrapper svg):not(.free){ - fill-rule:evenodd; - } - -.highlightOutline:is(.canvasWrapper svg){ - position:absolute; - mix-blend-mode:normal; - fill-rule:evenodd; - fill:none; - } - -.highlightOutline.hovered:is(.canvasWrapper svg):not(.free):not(.selected){ - stroke:var(--hover-outline-color); - stroke-width:var(--outline-width); - } - -.highlightOutline.selected:is(.canvasWrapper svg):not(.free) .mainOutline{ - stroke:var(--outline-around-color); - stroke-width:calc( - var(--outline-width) + 2 * var(--outline-around-width) - ); - } - -.highlightOutline.selected:is(.canvasWrapper svg):not(.free) .secondaryOutline{ - stroke:var(--outline-color); - stroke-width:var(--outline-width); - } - -.highlightOutline.free.hovered:is(.canvasWrapper svg):not(.selected){ - stroke:var(--hover-outline-color); - stroke-width:calc(2 * var(--outline-width)); - } - -.highlightOutline.free.selected:is(.canvasWrapper svg) .mainOutline{ - stroke:var(--outline-around-color); - stroke-width:calc( - 2 * (var(--outline-width) + var(--outline-around-width)) - ); - } - -.highlightOutline.free.selected:is(.canvasWrapper svg) .secondaryOutline{ - stroke:var(--outline-color); - stroke-width:calc(2 * var(--outline-width)); - } - -.toggle-button{ - --button-background-color:#f0f0f4; - --button-background-color-hover:#e0e0e6; - --button-background-color-active:#cfcfd8; - --color-accent-primary:#0060df; - --color-accent-primary-hover:#0250bb; - --color-accent-primary-active:#054096; - --border-interactive-color:#8f8f9d; - --border-radius-circle:9999px; - --border-width:1px; - --size-item-small:16px; - --size-item-large:32px; - --color-canvas:white; +.canvasWrapper svg { + transform: none; } -@media (prefers-color-scheme: dark){ +.moving:is(.canvasWrapper svg) { + z-index: 100000; +} -:where(html:not(.is-light)) .toggle-button{ - --button-background-color:color-mix(in srgb, currentColor 7%, transparent); - --button-background-color-hover:color-mix( +[data-main-rotation='90']:is( + .highlight:is(.canvasWrapper svg), + .highlightOutline:is(.canvasWrapper svg) + ) + mask, +[data-main-rotation='90']:is( + .highlight:is(.canvasWrapper svg), + .highlightOutline:is(.canvasWrapper svg) + ) + use:not(.clip, .mask) { + transform: matrix(0, 1, -1, 0, 1, 0); +} + +[data-main-rotation='180']:is( + .highlight:is(.canvasWrapper svg), + .highlightOutline:is(.canvasWrapper svg) + ) + mask, +[data-main-rotation='180']:is( + .highlight:is(.canvasWrapper svg), + .highlightOutline:is(.canvasWrapper svg) + ) + use:not(.clip, .mask) { + transform: matrix(-1, 0, 0, -1, 1, 1); +} + +[data-main-rotation='270']:is( + .highlight:is(.canvasWrapper svg), + .highlightOutline:is(.canvasWrapper svg) + ) + mask, +[data-main-rotation='270']:is( + .highlight:is(.canvasWrapper svg), + .highlightOutline:is(.canvasWrapper svg) + ) + use:not(.clip, .mask) { + transform: matrix(0, -1, 1, 0, 0, 1); +} + +.draw:is(.canvasWrapper svg) { + position: absolute; + mix-blend-mode: normal; +} + +.draw[data-draw-rotation='90']:is(.canvasWrapper svg) { + transform: rotate(90deg); +} + +.draw[data-draw-rotation='180']:is(.canvasWrapper svg) { + transform: rotate(180deg); +} + +.draw[data-draw-rotation='270']:is(.canvasWrapper svg) { + transform: rotate(270deg); +} + +.highlight:is(.canvasWrapper svg) { + --blend-mode: multiply; +} + +@media screen and (forced-colors: active) { + .highlight:is(.canvasWrapper svg) { + --blend-mode: difference; + } +} + +.highlight:is(.canvasWrapper svg) { + position: absolute; + mix-blend-mode: var(--blend-mode); +} + +.highlight:is(.canvasWrapper svg):not(.free) { + fill-rule: evenodd; +} + +.highlightOutline:is(.canvasWrapper svg) { + position: absolute; + mix-blend-mode: normal; + fill-rule: evenodd; + fill: none; +} + +.highlightOutline.hovered:is(.canvasWrapper svg):not(.free):not(.selected) { + stroke: var(--hover-outline-color); + stroke-width: var(--outline-width); +} + +.highlightOutline.selected:is(.canvasWrapper svg):not(.free) .mainOutline { + stroke: var(--outline-around-color); + stroke-width: calc(var(--outline-width) + 2 * var(--outline-around-width)); +} + +.highlightOutline.selected:is(.canvasWrapper svg):not(.free) .secondaryOutline { + stroke: var(--outline-color); + stroke-width: var(--outline-width); +} + +.highlightOutline.free.hovered:is(.canvasWrapper svg):not(.selected) { + stroke: var(--hover-outline-color); + stroke-width: calc(2 * var(--outline-width)); +} + +.highlightOutline.free.selected:is(.canvasWrapper svg) .mainOutline { + stroke: var(--outline-around-color); + stroke-width: calc(2 * (var(--outline-width) + var(--outline-around-width))); +} + +.highlightOutline.free.selected:is(.canvasWrapper svg) .secondaryOutline { + stroke: var(--outline-color); + stroke-width: calc(2 * var(--outline-width)); +} + +.toggle-button { + --button-background-color: #f0f0f4; + --button-background-color-hover: #e0e0e6; + --button-background-color-active: #cfcfd8; + --color-accent-primary: #0060df; + --color-accent-primary-hover: #0250bb; + --color-accent-primary-active: #054096; + --border-interactive-color: #8f8f9d; + --border-radius-circle: 9999px; + --border-width: 1px; + --size-item-small: 16px; + --size-item-large: 32px; + --color-canvas: white; +} + +@media (prefers-color-scheme: dark) { + :where(html:not(.is-light)) .toggle-button { + --button-background-color: color-mix(in srgb, currentColor 7%, transparent); + --button-background-color-hover: color-mix( in srgb, currentColor 14%, transparent ); - --button-background-color-active:color-mix( + --button-background-color-active: color-mix( in srgb, currentColor 21%, transparent ); - --color-accent-primary:#0df; - --color-accent-primary-hover:#80ebff; - --color-accent-primary-active:#aaf2ff; - --border-interactive-color:#bfbfc9; - --color-canvas:#1c1b22; -} + --color-accent-primary: #0df; + --color-accent-primary-hover: #80ebff; + --color-accent-primary-active: #aaf2ff; + --border-interactive-color: #bfbfc9; + --color-canvas: #1c1b22; } - -:where(html.is-dark) .toggle-button{ - --button-background-color:color-mix(in srgb, currentColor 7%, transparent); - --button-background-color-hover:color-mix( - in srgb, - currentColor 14%, - transparent - ); - --button-background-color-active:color-mix( - in srgb, - currentColor 21%, - transparent - ); - --color-accent-primary:#0df; - --color-accent-primary-hover:#80ebff; - --color-accent-primary-active:#aaf2ff; - --border-interactive-color:#bfbfc9; - --color-canvas:#1c1b22; } -@media (forced-colors: active){ - -.toggle-button{ - --color-accent-primary:ButtonText; - --color-accent-primary-hover:SelectedItem; - --color-accent-primary-active:SelectedItem; - --border-interactive-color:ButtonText; - --button-background-color:ButtonFace; - --border-interactive-color-hover:SelectedItem; - --border-interactive-color-active:SelectedItem; - --border-interactive-color-disabled:GrayText; - --color-canvas:ButtonText; +:where(html.is-dark) .toggle-button { + --button-background-color: color-mix(in srgb, currentColor 7%, transparent); + --button-background-color-hover: color-mix( + in srgb, + currentColor 14%, + transparent + ); + --button-background-color-active: color-mix( + in srgb, + currentColor 21%, + transparent + ); + --color-accent-primary: #0df; + --color-accent-primary-hover: #80ebff; + --color-accent-primary-active: #aaf2ff; + --border-interactive-color: #bfbfc9; + --color-canvas: #1c1b22; } + +@media (forced-colors: active) { + .toggle-button { + --color-accent-primary: ButtonText; + --color-accent-primary-hover: SelectedItem; + --color-accent-primary-active: SelectedItem; + --border-interactive-color: ButtonText; + --button-background-color: ButtonFace; + --border-interactive-color-hover: SelectedItem; + --border-interactive-color-active: SelectedItem; + --border-interactive-color-disabled: GrayText; + --color-canvas: ButtonText; } +} -.toggle-button{ - - --toggle-background-color:var(--button-background-color); - --toggle-background-color-hover:var(--button-background-color-hover); - --toggle-background-color-active:var(--button-background-color-active); - --toggle-background-color-pressed:var(--color-accent-primary); - --toggle-background-color-pressed-hover:var(--color-accent-primary-hover); - --toggle-background-color-pressed-active:var(--color-accent-primary-active); - --toggle-border-color:var(--border-interactive-color); - --toggle-border-color-hover:var(--toggle-border-color); - --toggle-border-color-active:var(--toggle-border-color); - --toggle-border-radius:var(--border-radius-circle); - --toggle-border-width:var(--border-width); - --toggle-height:var(--size-item-small); - --toggle-width:var(--size-item-large); - --toggle-dot-background-color:var(--toggle-border-color); - --toggle-dot-background-color-hover:var(--toggle-dot-background-color); - --toggle-dot-background-color-active:var(--toggle-dot-background-color); - --toggle-dot-background-color-on-pressed:var(--color-canvas); - --toggle-dot-margin:1px; - --toggle-dot-height:calc( +.toggle-button { + --toggle-background-color: var(--button-background-color); + --toggle-background-color-hover: var(--button-background-color-hover); + --toggle-background-color-active: var(--button-background-color-active); + --toggle-background-color-pressed: var(--color-accent-primary); + --toggle-background-color-pressed-hover: var(--color-accent-primary-hover); + --toggle-background-color-pressed-active: var(--color-accent-primary-active); + --toggle-border-color: var(--border-interactive-color); + --toggle-border-color-hover: var(--toggle-border-color); + --toggle-border-color-active: var(--toggle-border-color); + --toggle-border-radius: var(--border-radius-circle); + --toggle-border-width: var(--border-width); + --toggle-height: var(--size-item-small); + --toggle-width: var(--size-item-large); + --toggle-dot-background-color: var(--toggle-border-color); + --toggle-dot-background-color-hover: var(--toggle-dot-background-color); + --toggle-dot-background-color-active: var(--toggle-dot-background-color); + --toggle-dot-background-color-on-pressed: var(--color-canvas); + --toggle-dot-margin: 1px; + --toggle-dot-height: calc( var(--toggle-height) - 2 * var(--toggle-dot-margin) - 2 * var(--toggle-border-width) ); - --toggle-dot-width:var(--toggle-dot-height); - --toggle-dot-transform-x:calc( + --toggle-dot-width: var(--toggle-dot-height); + --toggle-dot-transform-x: calc( var(--toggle-width) - 4 * var(--toggle-dot-margin) - var(--toggle-dot-width) ); - -webkit-appearance:none; + -webkit-appearance: none; - -moz-appearance:none; + -moz-appearance: none; - appearance:none; - padding:0; - margin:0; - border:var(--toggle-border-width) solid var(--toggle-border-color); - height:var(--toggle-height); - width:var(--toggle-width); - border-radius:var(--toggle-border-radius); - background:var(--toggle-background-color); - box-sizing:border-box; - flex-shrink:0; + appearance: none; + padding: 0; + margin: 0; + border: var(--toggle-border-width) solid var(--toggle-border-color); + height: var(--toggle-height); + width: var(--toggle-width); + border-radius: var(--toggle-border-radius); + background: var(--toggle-background-color); + box-sizing: border-box; + flex-shrink: 0; } -.toggle-button:focus-visible{ - outline:var(--focus-outline); - outline-offset:var(--focus-outline-offset); - } - -.toggle-button:enabled:hover{ - background:var(--toggle-background-color-hover); - border-color:var(--toggle-border-color); - } - -.toggle-button:enabled:active{ - background:var(--toggle-background-color-active); - border-color:var(--toggle-border-color); - } - -.toggle-button[aria-pressed="true"]{ - background:var(--toggle-background-color-pressed); - border-color:transparent; - } - -.toggle-button[aria-pressed="true"]:enabled:hover{ - background:var(--toggle-background-color-pressed-hover); - border-color:transparent; - } - -.toggle-button[aria-pressed="true"]:enabled:active{ - background:var(--toggle-background-color-pressed-active); - border-color:transparent; - } - -.toggle-button::before{ - display:block; - content:""; - background-color:var(--toggle-dot-background-color); - height:var(--toggle-dot-height); - width:var(--toggle-dot-width); - margin:var(--toggle-dot-margin); - border-radius:var(--toggle-border-radius); - translate:0; - } - -.toggle-button[aria-pressed="true"]::before{ - translate:var(--toggle-dot-transform-x); - background-color:var(--toggle-dot-background-color-on-pressed); - } - -.toggle-button[aria-pressed="true"]:enabled:hover::before,.toggle-button[aria-pressed="true"]:enabled:active::before{ - background-color:var(--toggle-dot-background-color-on-pressed); - } - -[dir="rtl"] .toggle-button[aria-pressed="true"]::before{ - translate:calc(-1 * var(--toggle-dot-transform-x)); - } - -@media (prefers-reduced-motion: no-preference){ - .toggle-button::before{ - transition:translate 100ms; - } - } - -@media (prefers-contrast){ - .toggle-button:enabled:hover{ - border-color:var(--toggle-border-color-hover); - } - - .toggle-button:enabled:active{ - border-color:var(--toggle-border-color-active); - } - - .toggle-button[aria-pressed="true"]:enabled{ - border-color:var(--toggle-border-color); - position:relative; - } - - .toggle-button[aria-pressed="true"]:enabled:hover,.toggle-button[aria-pressed="true"]:enabled:hover:active{ - border-color:var(--toggle-border-color-hover); - } - - .toggle-button[aria-pressed="true"]:enabled:active{ - background-color:var(--toggle-dot-background-color-active); - border-color:var(--toggle-dot-background-color-hover); - } - - .toggle-button:hover::before,.toggle-button:active::before{ - background-color:var(--toggle-dot-background-color-hover); - } - } - -@media (forced-colors){ - -.toggle-button{ - --toggle-dot-background-color:var(--color-accent-primary); - --toggle-dot-background-color-hover:var(--color-accent-primary-hover); - --toggle-dot-background-color-active:var(--color-accent-primary-active); - --toggle-dot-background-color-on-pressed:var(--button-background-color); - --toggle-background-color-disabled:var(--button-background-color-disabled); - --toggle-border-color-hover:var(--border-interactive-color-hover); - --toggle-border-color-active:var(--border-interactive-color-active); - --toggle-border-color-disabled:var(--border-interactive-color-disabled); +.toggle-button:focus-visible { + outline: var(--focus-outline); + outline-offset: var(--focus-outline-offset); } - .toggle-button[aria-pressed="true"]:enabled::after{ - border:1px solid var(--button-background-color); - content:""; - position:absolute; - height:var(--toggle-height); - width:var(--toggle-width); - display:block; - border-radius:var(--toggle-border-radius); - inset:-2px; - } +.toggle-button:enabled:hover { + background: var(--toggle-background-color-hover); + border-color: var(--toggle-border-color); +} - .toggle-button[aria-pressed="true"]:enabled:active::after{ - border-color:var(--toggle-border-color-active); - } +.toggle-button:enabled:active { + background: var(--toggle-background-color-active); + border-color: var(--toggle-border-color); +} + +.toggle-button[aria-pressed='true'] { + background: var(--toggle-background-color-pressed); + border-color: transparent; +} + +.toggle-button[aria-pressed='true']:enabled:hover { + background: var(--toggle-background-color-pressed-hover); + border-color: transparent; +} + +.toggle-button[aria-pressed='true']:enabled:active { + background: var(--toggle-background-color-pressed-active); + border-color: transparent; +} + +.toggle-button::before { + display: block; + content: ''; + background-color: var(--toggle-dot-background-color); + height: var(--toggle-dot-height); + width: var(--toggle-dot-width); + margin: var(--toggle-dot-margin); + border-radius: var(--toggle-border-radius); + translate: 0; +} + +.toggle-button[aria-pressed='true']::before { + translate: var(--toggle-dot-transform-x); + background-color: var(--toggle-dot-background-color-on-pressed); +} + +.toggle-button[aria-pressed='true']:enabled:hover::before, +.toggle-button[aria-pressed='true']:enabled:active::before { + background-color: var(--toggle-dot-background-color-on-pressed); +} + +[dir='rtl'] .toggle-button[aria-pressed='true']::before { + translate: calc(-1 * var(--toggle-dot-transform-x)); +} + +@media (prefers-reduced-motion: no-preference) { + .toggle-button::before { + transition: translate 100ms; + } +} + +@media (prefers-contrast) { + .toggle-button:enabled:hover { + border-color: var(--toggle-border-color-hover); } -:root{ - --outline-width:2px; - --outline-color:#0060df; - --outline-around-width:1px; - --outline-around-color:#f0f0f4; - --hover-outline-around-color:var(--outline-around-color); - --focus-outline:solid var(--outline-width) var(--outline-color); - --unfocus-outline:solid var(--outline-width) transparent; - --focus-outline-around:solid var(--outline-around-width) var(--outline-around-color); - --hover-outline-color:#8f8f9d; - --hover-outline:solid var(--outline-width) var(--hover-outline-color); - --hover-outline-around:solid var(--outline-around-width) var(--hover-outline-around-color); - --freetext-line-height:1.35; - --freetext-padding:2px; - --resizer-bg-color:var(--outline-color); - --resizer-size:6px; - --resizer-shift:calc( + .toggle-button:enabled:active { + border-color: var(--toggle-border-color-active); + } + + .toggle-button[aria-pressed='true']:enabled { + border-color: var(--toggle-border-color); + position: relative; + } + + .toggle-button[aria-pressed='true']:enabled:hover, + .toggle-button[aria-pressed='true']:enabled:hover:active { + border-color: var(--toggle-border-color-hover); + } + + .toggle-button[aria-pressed='true']:enabled:active { + background-color: var(--toggle-dot-background-color-active); + border-color: var(--toggle-dot-background-color-hover); + } + + .toggle-button:hover::before, + .toggle-button:active::before { + background-color: var(--toggle-dot-background-color-hover); + } +} + +@media (forced-colors) { + .toggle-button { + --toggle-dot-background-color: var(--color-accent-primary); + --toggle-dot-background-color-hover: var(--color-accent-primary-hover); + --toggle-dot-background-color-active: var(--color-accent-primary-active); + --toggle-dot-background-color-on-pressed: var(--button-background-color); + --toggle-background-color-disabled: var(--button-background-color-disabled); + --toggle-border-color-hover: var(--border-interactive-color-hover); + --toggle-border-color-active: var(--border-interactive-color-active); + --toggle-border-color-disabled: var(--border-interactive-color-disabled); + } + + .toggle-button[aria-pressed='true']:enabled::after { + border: 1px solid var(--button-background-color); + content: ''; + position: absolute; + height: var(--toggle-height); + width: var(--toggle-width); + display: block; + border-radius: var(--toggle-border-radius); + inset: -2px; + } + + .toggle-button[aria-pressed='true']:enabled:active::after { + border-color: var(--toggle-border-color-active); + } +} + +:root { + --outline-width: 2px; + --outline-color: #0060df; + --outline-around-width: 1px; + --outline-around-color: #f0f0f4; + --hover-outline-around-color: var(--outline-around-color); + --focus-outline: solid var(--outline-width) var(--outline-color); + --unfocus-outline: solid var(--outline-width) transparent; + --focus-outline-around: solid var(--outline-around-width) + var(--outline-around-color); + --hover-outline-color: #8f8f9d; + --hover-outline: solid var(--outline-width) var(--hover-outline-color); + --hover-outline-around: solid var(--outline-around-width) + var(--hover-outline-around-color); + --freetext-line-height: 1.35; + --freetext-padding: 2px; + --resizer-bg-color: var(--outline-color); + --resizer-size: 6px; + --resizer-shift: calc( 0px - (var(--outline-width) + var(--resizer-size)) / 2 - var(--outline-around-width) ); - --editorFreeText-editing-cursor:text; - --editorInk-editing-cursor:url(images/cursor-editorInk.svg) 0 16, pointer; - --editorHighlight-editing-cursor:url(images/cursor-editorTextHighlight.svg) 24 24, text; - --editorFreeHighlight-editing-cursor:url(images/cursor-editorFreeHighlight.svg) 1 18, pointer; + --editorFreeText-editing-cursor: text; + --editorInk-editing-cursor: url(images/cursor-editorInk.svg) 0 16, pointer; + --editorHighlight-editing-cursor: + url(images/cursor-editorTextHighlight.svg) 24 24, text; + --editorFreeHighlight-editing-cursor: + url(images/cursor-editorFreeHighlight.svg) 1 18, pointer; - --new-alt-text-warning-image:url(images/altText_warning.svg); + --new-alt-text-warning-image: url(images/altText_warning.svg); } -.visuallyHidden{ - position:absolute; - top:0; - left:0; - border:0; - margin:0; - padding:0; - width:0; - height:0; - overflow:hidden; - white-space:nowrap; - font-size:0; +.visuallyHidden { + position: absolute; + top: 0; + left: 0; + border: 0; + margin: 0; + padding: 0; + width: 0; + height: 0; + overflow: hidden; + white-space: nowrap; + font-size: 0; } -.textLayer.highlighting{ - cursor:var(--editorFreeHighlight-editing-cursor); - } +.textLayer.highlighting { + cursor: var(--editorFreeHighlight-editing-cursor); +} -.textLayer.highlighting:not(.free) span{ - cursor:var(--editorHighlight-editing-cursor); - } +.textLayer.highlighting:not(.free) span { + cursor: var(--editorHighlight-editing-cursor); +} -[role="img"]:is(.textLayer.highlighting:not(.free) span){ - cursor:var(--editorFreeHighlight-editing-cursor); - } +[role='img']:is(.textLayer.highlighting:not(.free) span) { + cursor: var(--editorFreeHighlight-editing-cursor); +} -.textLayer.highlighting.free span{ - cursor:var(--editorFreeHighlight-editing-cursor); - } +.textLayer.highlighting.free span { + cursor: var(--editorFreeHighlight-editing-cursor); +} -:is(#viewerContainer.pdfPresentationMode:fullscreen,.annotationEditorLayer.disabled) .noAltTextBadge{ - display:none !important; - } +:is( + #viewerContainer.pdfPresentationMode:fullscreen, + .annotationEditorLayer.disabled + ) + .noAltTextBadge { + display: none !important; +} -@media (min-resolution: 1.1dppx){ - :root{ - --editorFreeText-editing-cursor:url(images/cursor-editorFreeText.svg) 0 16, text; +@media (min-resolution: 1.1dppx) { + :root { + --editorFreeText-editing-cursor: + url(images/cursor-editorFreeText.svg) 0 16, text; } } -@media screen and (forced-colors: active){ - :root{ - --outline-color:CanvasText; - --outline-around-color:ButtonFace; - --resizer-bg-color:ButtonText; - --hover-outline-color:Highlight; - --hover-outline-around-color:SelectedItemText; +@media screen and (forced-colors: active) { + :root { + --outline-color: CanvasText; + --outline-around-color: ButtonFace; + --resizer-bg-color: ButtonText; + --hover-outline-color: Highlight; + --hover-outline-around-color: SelectedItemText; } } -[data-editor-rotation="90"]{ - transform:rotate(90deg); +[data-editor-rotation='90'] { + transform: rotate(90deg); } -[data-editor-rotation="180"]{ - transform:rotate(180deg); +[data-editor-rotation='180'] { + transform: rotate(180deg); } -[data-editor-rotation="270"]{ - transform:rotate(270deg); +[data-editor-rotation='270'] { + transform: rotate(270deg); } -.annotationEditorLayer{ - background:transparent; - position:absolute; - inset:0; - font-size:calc(100px * var(--scale-factor)); - transform-origin:0 0; - cursor:auto; +.annotationEditorLayer { + background: transparent; + position: absolute; + inset: 0; + font-size: calc(100px * var(--scale-factor)); + transform-origin: 0 0; + cursor: auto; } -.annotationEditorLayer .selectedEditor{ - z-index:100000 !important; - } - -.annotationEditorLayer.drawing *{ - pointer-events:none !important; - } - -.annotationEditorLayer.waiting{ - content:""; - cursor:wait; - position:absolute; - inset:0; - width:100%; - height:100%; +.annotationEditorLayer .selectedEditor { + z-index: 100000 !important; } -.annotationEditorLayer.disabled{ - pointer-events:none; +.annotationEditorLayer.drawing * { + pointer-events: none !important; } -.annotationEditorLayer.freetextEditing{ - cursor:var(--editorFreeText-editing-cursor); +.annotationEditorLayer.waiting { + content: ''; + cursor: wait; + position: absolute; + inset: 0; + width: 100%; + height: 100%; } -.annotationEditorLayer.inkEditing{ - cursor:var(--editorInk-editing-cursor); +.annotationEditorLayer.disabled { + pointer-events: none; } -.annotationEditorLayer .draw{ - box-sizing:border-box; +.annotationEditorLayer.freetextEditing { + cursor: var(--editorFreeText-editing-cursor); } -.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor){ - position:absolute; - background:transparent; - z-index:1; - transform-origin:0 0; - cursor:auto; - max-width:100%; - max-height:100%; - border:var(--unfocus-outline); +.annotationEditorLayer.inkEditing { + cursor: var(--editorInk-editing-cursor); } -.draggable.selectedEditor:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)){ - cursor:move; - } - -.selectedEditor:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)){ - border:var(--focus-outline); - outline:var(--focus-outline-around); - } - -.selectedEditor:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor))::before{ - content:""; - position:absolute; - inset:0; - border:var(--focus-outline-around); - pointer-events:none; - } - -:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)):hover:not(.selectedEditor){ - border:var(--hover-outline); - outline:var(--hover-outline-around); - } - -:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)):hover:not(.selectedEditor)::before{ - content:""; - position:absolute; - inset:0; - border:var(--focus-outline-around); - } - -:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar{ - --editor-toolbar-delete-image:url(images/editor-toolbar-delete.svg); - --editor-toolbar-bg-color:#f0f0f4; - --editor-toolbar-highlight-image:url(images/toolbarButton-editorHighlight.svg); - --editor-toolbar-fg-color:#2e2e56; - --editor-toolbar-border-color:#8f8f9d; - --editor-toolbar-hover-border-color:var(--editor-toolbar-border-color); - --editor-toolbar-hover-bg-color:#e0e0e6; - --editor-toolbar-hover-fg-color:var(--editor-toolbar-fg-color); - --editor-toolbar-hover-outline:none; - --editor-toolbar-focus-outline-color:#0060df; - --editor-toolbar-shadow:0 2px 6px 0 rgb(58 57 68 / 0.2); - --editor-toolbar-vert-offset:6px; - --editor-toolbar-height:28px; - --editor-toolbar-padding:2px; - --alt-text-done-color:#2ac3a2; - --alt-text-warning-color:#0090ed; - --alt-text-hover-done-color:var(--alt-text-done-color); - --alt-text-hover-warning-color:var(--alt-text-warning-color); - } - -@media (prefers-color-scheme: dark){ - -:where(html:not(.is-light)) :is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar{ - --editor-toolbar-bg-color:#2b2a33; - --editor-toolbar-fg-color:#fbfbfe; - --editor-toolbar-hover-bg-color:#52525e; - --editor-toolbar-focus-outline-color:#0df; - --alt-text-done-color:#54ffbd; - --alt-text-warning-color:#80ebff; - } - } - -:where(html.is-dark) :is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar{ - --editor-toolbar-bg-color:#2b2a33; - --editor-toolbar-fg-color:#fbfbfe; - --editor-toolbar-hover-bg-color:#52525e; - --editor-toolbar-focus-outline-color:#0df; - --alt-text-done-color:#54ffbd; - --alt-text-warning-color:#80ebff; - } - -@media screen and (forced-colors: active){ - -:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar{ - --editor-toolbar-bg-color:ButtonFace; - --editor-toolbar-fg-color:ButtonText; - --editor-toolbar-border-color:ButtonText; - --editor-toolbar-hover-border-color:AccentColor; - --editor-toolbar-hover-bg-color:ButtonFace; - --editor-toolbar-hover-fg-color:AccentColor; - --editor-toolbar-hover-outline:2px solid var(--editor-toolbar-hover-border-color); - --editor-toolbar-focus-outline-color:ButtonBorder; - --editor-toolbar-shadow:none; - --alt-text-done-color:var(--editor-toolbar-fg-color); - --alt-text-warning-color:var(--editor-toolbar-fg-color); - --alt-text-hover-done-color:var(--editor-toolbar-hover-fg-color); - --alt-text-hover-warning-color:var(--editor-toolbar-hover-fg-color); - } - } - -:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar{ - - display:flex; - width:-moz-fit-content; - width:fit-content; - height:var(--editor-toolbar-height); - flex-direction:column; - justify-content:center; - align-items:center; - cursor:default; - pointer-events:auto; - box-sizing:content-box; - padding:var(--editor-toolbar-padding); - - position:absolute; - inset-inline-end:0; - inset-block-start:calc(100% + var(--editor-toolbar-vert-offset)); - - border-radius:6px; - background-color:var(--editor-toolbar-bg-color); - border:1px solid var(--editor-toolbar-border-color); - box-shadow:var(--editor-toolbar-shadow); - } - -.hidden:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar){ - display:none; - } - -:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar):has(:focus-visible){ - border-color:transparent; - } - -[dir="ltr"] :is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar){ - transform-origin:100% 0; - } - -[dir="rtl"] :is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar){ - transform-origin:0 0; - } - -:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons{ - display:flex; - justify-content:center; - align-items:center; - gap:0; - height:100%; - } - -:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) button{ - padding:0; - } - -:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .divider{ - width:0; - height:calc( - 2 * var(--editor-toolbar-padding) + var(--editor-toolbar-height) - ); - border-left:1px solid var(--editor-toolbar-border-color); - border-right:none; - display:inline-block; - margin-inline:2px; - } - -:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .highlightButton{ - width:var(--editor-toolbar-height); - } - -:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .highlightButton)::before{ - content:""; - -webkit-mask-image:var(--editor-toolbar-highlight-image); - mask-image:var(--editor-toolbar-highlight-image); - -webkit-mask-repeat:no-repeat; - mask-repeat:no-repeat; - -webkit-mask-position:center; - mask-position:center; - display:inline-block; - background-color:var(--editor-toolbar-fg-color); - width:100%; - height:100%; - } - -:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .highlightButton):hover::before{ - background-color:var(--editor-toolbar-hover-fg-color); - } - -:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .delete{ - width:var(--editor-toolbar-height); - } - -:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .delete)::before{ - content:""; - -webkit-mask-image:var(--editor-toolbar-delete-image); - mask-image:var(--editor-toolbar-delete-image); - -webkit-mask-repeat:no-repeat; - mask-repeat:no-repeat; - -webkit-mask-position:center; - mask-position:center; - display:inline-block; - background-color:var(--editor-toolbar-fg-color); - width:100%; - height:100%; - } - -:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .delete):hover::before{ - background-color:var(--editor-toolbar-hover-fg-color); - } - -:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) > *{ - height:var(--editor-toolbar-height); - } - -:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) > :not(.divider){ - border:none; - background-color:transparent; - cursor:pointer; - } - -:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) > :not(.divider)):hover{ - border-radius:2px; - background-color:var(--editor-toolbar-hover-bg-color); - color:var(--editor-toolbar-hover-fg-color); - outline:var(--editor-toolbar-hover-outline); - outline-offset:1px; - } - -:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) > :not(.divider)):hover:active{ - outline:none; - } - -:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) > :not(.divider)):focus-visible{ - border-radius:2px; - outline:2px solid var(--editor-toolbar-focus-outline-color); - } - -:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText{ - --alt-text-add-image:url(images/altText_add.svg); - --alt-text-done-image:url(images/altText_done.svg); - - display:flex; - align-items:center; - justify-content:center; - width:-moz-max-content; - width:max-content; - padding-inline:8px; - pointer-events:all; - font:menu; - font-weight:590; - font-size:12px; - color:var(--editor-toolbar-fg-color); - } - -:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText):disabled{ - pointer-events:none; - } - -:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText)::before{ - content:""; - -webkit-mask-image:var(--alt-text-add-image); - mask-image:var(--alt-text-add-image); - -webkit-mask-repeat:no-repeat; - mask-repeat:no-repeat; - -webkit-mask-position:center; - mask-position:center; - display:inline-block; - width:12px; - height:13px; - background-color:var(--editor-toolbar-fg-color); - margin-inline-end:4px; - } - -:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText):hover::before{ - background-color:var(--editor-toolbar-hover-fg-color); - } - -.done:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText)::before{ - -webkit-mask-image:var(--alt-text-done-image); - mask-image:var(--alt-text-done-image); - } - -.new:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText)::before{ - width:16px; - height:16px; - -webkit-mask-image:var(--new-alt-text-warning-image); - mask-image:var(--new-alt-text-warning-image); - background-color:var(--alt-text-warning-color); - -webkit-mask-size:cover; - mask-size:cover; - } - -.new:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText):hover::before{ - background-color:var(--alt-text-hover-warning-color); - } - -.new.done:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText)::before{ - -webkit-mask-image:var(--alt-text-done-image); - mask-image:var(--alt-text-done-image); - background-color:var(--alt-text-done-color); - } - -.new.done:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText):hover::before{ - background-color:var(--alt-text-hover-done-color); - } - -:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText) .tooltip{ - display:none; - word-wrap:anywhere; - } - -.show:is(:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText) .tooltip){ - --alt-text-tooltip-bg:#f0f0f4; - --alt-text-tooltip-fg:#15141a; - --alt-text-tooltip-border:#8f8f9d; - --alt-text-tooltip-shadow:0px 2px 6px 0px rgb(58 57 68 / 0.2); - } - -@media (prefers-color-scheme: dark){ - -:where(html:not(.is-light)) .show:is(:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText) .tooltip){ - --alt-text-tooltip-bg:#1c1b22; - --alt-text-tooltip-fg:#fbfbfe; - --alt-text-tooltip-shadow:0px 2px 6px 0px #15141a; - } - } - -:where(html.is-dark) .show:is(:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText) .tooltip){ - --alt-text-tooltip-bg:#1c1b22; - --alt-text-tooltip-fg:#fbfbfe; - --alt-text-tooltip-shadow:0px 2px 6px 0px #15141a; - } - -@media screen and (forced-colors: active){ - -.show:is(:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText) .tooltip){ - --alt-text-tooltip-bg:Canvas; - --alt-text-tooltip-fg:CanvasText; - --alt-text-tooltip-border:CanvasText; - --alt-text-tooltip-shadow:none; - } - } - -.show:is(:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText) .tooltip){ - - display:inline-flex; - flex-direction:column; - align-items:center; - justify-content:center; - position:absolute; - top:calc(100% + 2px); - inset-inline-start:0; - padding-block:2px 3px; - padding-inline:3px; - max-width:300px; - width:-moz-max-content; - width:max-content; - height:auto; - font-size:12px; - - border:0.5px solid var(--alt-text-tooltip-border); - background:var(--alt-text-tooltip-bg); - box-shadow:var(--alt-text-tooltip-shadow); - color:var(--alt-text-tooltip-fg); - - pointer-events:none; - } - -.annotationEditorLayer .freeTextEditor{ - padding:calc(var(--freetext-padding) * var(--scale-factor)); - width:auto; - height:auto; - touch-action:none; +.annotationEditorLayer .draw { + box-sizing: border-box; } -.annotationEditorLayer .freeTextEditor .internal{ - background:transparent; - border:none; - inset:0; - overflow:visible; - white-space:nowrap; - font:10px sans-serif; - line-height:var(--freetext-line-height); - -webkit-user-select:none; - -moz-user-select:none; - user-select:none; +.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor) { + position: absolute; + background: transparent; + z-index: 1; + transform-origin: 0 0; + cursor: auto; + max-width: 100%; + max-height: 100%; + border: var(--unfocus-outline); } -.annotationEditorLayer .freeTextEditor .overlay{ - position:absolute; - display:none; - background:transparent; - inset:0; - width:100%; - height:100%; +.draggable.selectedEditor:is( + .annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor) + ) { + cursor: move; } -.annotationEditorLayer freeTextEditor .overlay.enabled{ - display:block; +.selectedEditor:is( + .annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor) + ) { + border: var(--focus-outline); + outline: var(--focus-outline-around); } -.annotationEditorLayer .freeTextEditor .internal:empty::before{ - content:attr(default-content); - color:gray; +.selectedEditor:is( + .annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor) + )::before { + content: ''; + position: absolute; + inset: 0; + border: var(--focus-outline-around); + pointer-events: none; } -.annotationEditorLayer .freeTextEditor .internal:focus{ - outline:none; - -webkit-user-select:auto; - -moz-user-select:auto; - user-select:auto; +:is( + .annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor) + ):hover:not(.selectedEditor) { + border: var(--hover-outline); + outline: var(--hover-outline-around); } -.annotationEditorLayer .inkEditor{ - width:100%; - height:100%; +:is( + .annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor) + ):hover:not(.selectedEditor)::before { + content: ''; + position: absolute; + inset: 0; + border: var(--focus-outline-around); } -.annotationEditorLayer .inkEditor.editing{ - cursor:inherit; +:is( + .annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), + .textLayer + ) + .editToolbar { + --editor-toolbar-delete-image: url(images/editor-toolbar-delete.svg); + --editor-toolbar-bg-color: #f0f0f4; + --editor-toolbar-highlight-image: url(images/toolbarButton-editorHighlight.svg); + --editor-toolbar-fg-color: #2e2e56; + --editor-toolbar-border-color: #8f8f9d; + --editor-toolbar-hover-border-color: var(--editor-toolbar-border-color); + --editor-toolbar-hover-bg-color: #e0e0e6; + --editor-toolbar-hover-fg-color: var(--editor-toolbar-fg-color); + --editor-toolbar-hover-outline: none; + --editor-toolbar-focus-outline-color: #0060df; + --editor-toolbar-shadow: 0 2px 6px 0 rgb(58 57 68 / 0.2); + --editor-toolbar-vert-offset: 6px; + --editor-toolbar-height: 28px; + --editor-toolbar-padding: 2px; + --alt-text-done-color: #2ac3a2; + --alt-text-warning-color: #0090ed; + --alt-text-hover-done-color: var(--alt-text-done-color); + --alt-text-hover-warning-color: var(--alt-text-warning-color); } -.annotationEditorLayer .inkEditor .inkEditorCanvas{ - position:absolute; - inset:0; - width:100%; - height:100%; - touch-action:none; -} - -.annotationEditorLayer .stampEditor{ - width:auto; - height:auto; -} - -:is(.annotationEditorLayer .stampEditor) canvas{ - position:absolute; - width:100%; - height:100%; - margin:0; - top:0; - left:0; - } - -:is(.annotationEditorLayer .stampEditor) .noAltTextBadge{ - --no-alt-text-badge-border-color:#f0f0f4; - --no-alt-text-badge-bg-color:#cfcfd8; - --no-alt-text-badge-fg-color:#5b5b66; - } - -@media (prefers-color-scheme: dark){ - -:where(html:not(.is-light)) :is(.annotationEditorLayer .stampEditor) .noAltTextBadge{ - --no-alt-text-badge-border-color:#52525e; - --no-alt-text-badge-bg-color:#fbfbfe; - --no-alt-text-badge-fg-color:#15141a; - } - } - -:where(html.is-dark) :is(.annotationEditorLayer .stampEditor) .noAltTextBadge{ - --no-alt-text-badge-border-color:#52525e; - --no-alt-text-badge-bg-color:#fbfbfe; - --no-alt-text-badge-fg-color:#15141a; - } - -@media screen and (forced-colors: active){ - -:is(.annotationEditorLayer .stampEditor) .noAltTextBadge{ - --no-alt-text-badge-border-color:ButtonText; - --no-alt-text-badge-bg-color:ButtonFace; - --no-alt-text-badge-fg-color:ButtonText; - } - } - -:is(.annotationEditorLayer .stampEditor) .noAltTextBadge{ - - position:absolute; - inset-inline-end:5px; - inset-block-end:5px; - display:inline-flex; - width:32px; - height:32px; - padding:3px; - justify-content:center; - align-items:center; - pointer-events:none; - z-index:1; - - border-radius:2px; - border:1px solid var(--no-alt-text-badge-border-color); - background:var(--no-alt-text-badge-bg-color); - } - -:is(:is(.annotationEditorLayer .stampEditor) .noAltTextBadge)::before{ - content:""; - display:inline-block; - width:16px; - height:16px; - -webkit-mask-image:var(--new-alt-text-warning-image); - mask-image:var(--new-alt-text-warning-image); - -webkit-mask-size:cover; - mask-size:cover; - background-color:var(--no-alt-text-badge-fg-color); - } - -:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers{ - position:absolute; - inset:0; - } - -.hidden:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers){ - display:none; - } - -:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers) > .resizer{ - width:var(--resizer-size); - height:var(--resizer-size); - background:content-box var(--resizer-bg-color); - border:var(--focus-outline-around); - border-radius:2px; - position:absolute; - } - -.topLeft:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers) > .resizer){ - top:var(--resizer-shift); - left:var(--resizer-shift); - } - -.topMiddle:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers) > .resizer){ - top:var(--resizer-shift); - left:calc(50% + var(--resizer-shift)); - } - -.topRight:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers) > .resizer){ - top:var(--resizer-shift); - right:var(--resizer-shift); - } - -.middleRight:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers) > .resizer){ - top:calc(50% + var(--resizer-shift)); - right:var(--resizer-shift); - } - -.bottomRight:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers) > .resizer){ - bottom:var(--resizer-shift); - right:var(--resizer-shift); - } - -.bottomMiddle:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers) > .resizer){ - bottom:var(--resizer-shift); - left:calc(50% + var(--resizer-shift)); - } - -.bottomLeft:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers) > .resizer){ - bottom:var(--resizer-shift); - left:var(--resizer-shift); - } - -.middleLeft:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers) > .resizer){ - top:calc(50% + var(--resizer-shift)); - left:var(--resizer-shift); - } - -.topLeft:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer),.bottomRight:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer){ - cursor:nwse-resize; - } - -.topMiddle:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer),.bottomMiddle:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer){ - cursor:ns-resize; - } - -.topRight:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer),.bottomLeft:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer){ - cursor:nesw-resize; - } - -.middleRight:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer),.middleLeft:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer){ - cursor:ew-resize; - } - -.topLeft:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer),.bottomRight:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer){ - cursor:nesw-resize; - } - -.topMiddle:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer),.bottomMiddle:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer){ - cursor:ew-resize; - } - -.topRight:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer),.bottomLeft:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer){ - cursor:nwse-resize; - } - -.middleRight:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer),.middleLeft:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer){ - cursor:ns-resize; - } - -:is(.annotationEditorLayer :is([data-main-rotation="0"] [data-editor-rotation="90"],[data-main-rotation="90"] [data-editor-rotation="0"],[data-main-rotation="180"] [data-editor-rotation="270"],[data-main-rotation="270"] [data-editor-rotation="180"])) .editToolbar{ - rotate:270deg; - } - -[dir="ltr"] :is(:is(.annotationEditorLayer :is([data-main-rotation="0"] [data-editor-rotation="90"],[data-main-rotation="90"] [data-editor-rotation="0"],[data-main-rotation="180"] [data-editor-rotation="270"],[data-main-rotation="270"] [data-editor-rotation="180"])) .editToolbar){ - inset-inline-end:calc(0px - var(--editor-toolbar-vert-offset)); - inset-block-start:0; - } - -[dir="rtl"] :is(:is(.annotationEditorLayer :is([data-main-rotation="0"] [data-editor-rotation="90"],[data-main-rotation="90"] [data-editor-rotation="0"],[data-main-rotation="180"] [data-editor-rotation="270"],[data-main-rotation="270"] [data-editor-rotation="180"])) .editToolbar){ - inset-inline-end:calc(100% + var(--editor-toolbar-vert-offset)); - inset-block-start:0; - } - -:is(.annotationEditorLayer :is([data-main-rotation="0"] [data-editor-rotation="180"],[data-main-rotation="90"] [data-editor-rotation="90"],[data-main-rotation="180"] [data-editor-rotation="0"],[data-main-rotation="270"] [data-editor-rotation="270"])) .editToolbar{ - rotate:180deg; - inset-inline-end:100%; - inset-block-start:calc(0pc - var(--editor-toolbar-vert-offset)); - } - -:is(.annotationEditorLayer :is([data-main-rotation="0"] [data-editor-rotation="270"],[data-main-rotation="90"] [data-editor-rotation="180"],[data-main-rotation="180"] [data-editor-rotation="90"],[data-main-rotation="270"] [data-editor-rotation="0"])) .editToolbar{ - rotate:90deg; - } - -[dir="ltr"] :is(:is(.annotationEditorLayer :is([data-main-rotation="0"] [data-editor-rotation="270"],[data-main-rotation="90"] [data-editor-rotation="180"],[data-main-rotation="180"] [data-editor-rotation="90"],[data-main-rotation="270"] [data-editor-rotation="0"])) .editToolbar){ - inset-inline-end:calc(100% + var(--editor-toolbar-vert-offset)); - inset-block-start:100%; - } - -[dir="rtl"] :is(:is(.annotationEditorLayer :is([data-main-rotation="0"] [data-editor-rotation="270"],[data-main-rotation="90"] [data-editor-rotation="180"],[data-main-rotation="180"] [data-editor-rotation="90"],[data-main-rotation="270"] [data-editor-rotation="0"])) .editToolbar){ - inset-inline-start:calc(0px - var(--editor-toolbar-vert-offset)); - inset-block-start:0; - } - -.dialog.altText::backdrop{ - -webkit-mask:url(#alttext-manager-mask); - mask:url(#alttext-manager-mask); - } - -.dialog.altText.positioned{ - margin:0; - } - -.dialog.altText #altTextContainer{ - width:300px; - height:-moz-fit-content; - height:fit-content; - display:inline-flex; - flex-direction:column; - align-items:flex-start; - gap:16px; - } - -:is(.dialog.altText #altTextContainer) #overallDescription{ - display:flex; - flex-direction:column; - align-items:flex-start; - gap:4px; - align-self:stretch; - } - -:is(:is(.dialog.altText #altTextContainer) #overallDescription) span{ - align-self:stretch; - } - -:is(:is(.dialog.altText #altTextContainer) #overallDescription) .title{ - font-size:13px; - font-style:normal; - font-weight:590; - } - -:is(.dialog.altText #altTextContainer) #addDescription{ - display:flex; - flex-direction:column; - align-items:stretch; - gap:8px; - } - -:is(:is(.dialog.altText #altTextContainer) #addDescription) .descriptionArea{ - flex:1; - padding-inline:24px 10px; - } - -:is(:is(:is(.dialog.altText #altTextContainer) #addDescription) .descriptionArea) textarea{ - width:100%; - min-height:75px; - } - -:is(.dialog.altText #altTextContainer) #buttons{ - display:flex; - justify-content:flex-end; - align-items:flex-start; - gap:8px; - align-self:stretch; - } - -.dialog.newAltText{ - --new-alt-text-ai-disclaimer-icon:url(images/altText_disclaimer.svg); - --new-alt-text-spinner-icon:url(images/altText_spinner.svg); - --preview-image-bg-color:#f0f0f4; - --preview-image-border:none; -} - -@media (prefers-color-scheme: dark){ - -:where(html:not(.is-light)) .dialog.newAltText{ - --preview-image-bg-color:#2b2a33; -} - } - -:where(html.is-dark) .dialog.newAltText{ - --preview-image-bg-color:#2b2a33; -} - -@media screen and (forced-colors: active){ - -.dialog.newAltText{ - --preview-image-bg-color:ButtonFace; - --preview-image-border:1px solid ButtonText; -} - } - -.dialog.newAltText{ - - width:80%; - max-width:570px; - min-width:300px; - padding:0; -} - -.dialog.newAltText.noAi #newAltTextDisclaimer,.dialog.newAltText.noAi #newAltTextCreateAutomatically{ - display:none !important; - } - -.dialog.newAltText.aiInstalling #newAltTextCreateAutomatically{ - display:none !important; - } - -.dialog.newAltText.aiInstalling #newAltTextDownloadModel{ - display:flex !important; - } - -.dialog.newAltText.error #newAltTextNotNow{ - display:none !important; - } - -.dialog.newAltText.error #newAltTextCancel{ - display:inline-block !important; - } - -.dialog.newAltText:not(.error) #newAltTextError{ - display:none !important; - } - -.dialog.newAltText #newAltTextContainer{ - display:flex; - width:auto; - padding:16px; - flex-direction:column; - justify-content:flex-end; - align-items:flex-start; - gap:12px; - flex:0 1 auto; - line-height:normal; - } - -:is(.dialog.newAltText #newAltTextContainer) #mainContent{ - display:flex; - justify-content:flex-end; - align-items:flex-start; - gap:12px; - align-self:stretch; - flex:1 1 auto; - } - -:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionAndSettings{ - display:flex; - flex-direction:column; - align-items:flex-start; - gap:16px; - flex:1 0 0; - align-self:stretch; - } - -:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction{ - display:flex; - flex-direction:column; - align-items:flex-start; - gap:8px; - align-self:stretch; - flex:1 1 auto; - } - -:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer{ - width:100%; - height:70px; - position:relative; - } - -:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer) textarea{ - width:100%; - height:100%; - padding:8px; - } - -:is(:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer) textarea)::-moz-placeholder{ - color:var(--text-secondary-color); - } - -:is(:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer) textarea)::placeholder{ - color:var(--text-secondary-color); - } - -:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer) .altTextSpinner{ - display:none; - position:absolute; - width:16px; - height:16px; - inset-inline-start:8px; - inset-block-start:8px; - -webkit-mask-size:cover; - mask-size:cover; - background-color:var(--text-secondary-color); - pointer-events:none; - } - -.loading:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer) textarea::-moz-placeholder{ - color:transparent; - } - -.loading:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer) textarea::placeholder{ - color:transparent; - } - -.loading:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer) .altTextSpinner{ - display:inline-block; - -webkit-mask-image:var(--new-alt-text-spinner-icon); - mask-image:var(--new-alt-text-spinner-icon); - } - -:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescription{ - font-size:11px; - } - -:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDisclaimer{ - display:flex; - flex-direction:row; - align-items:flex-start; - gap:4px; - font-size:11px; - } - -:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDisclaimer)::before{ - content:""; - display:inline-block; - width:17px; - height:16px; - -webkit-mask-image:var(--new-alt-text-ai-disclaimer-icon); - mask-image:var(--new-alt-text-ai-disclaimer-icon); - -webkit-mask-size:cover; - mask-size:cover; - background-color:var(--text-secondary-color); - flex:1 0 auto; - } - -:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #newAltTextDownloadModel{ - display:flex; - align-items:center; - gap:4px; - align-self:stretch; - } - -:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #newAltTextDownloadModel)::before{ - content:""; - display:inline-block; - width:16px; - height:16px; - -webkit-mask-image:var(--new-alt-text-spinner-icon); - mask-image:var(--new-alt-text-spinner-icon); - -webkit-mask-size:cover; - mask-size:cover; - background-color:var(--text-secondary-color); - } - -:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #newAltTextImagePreview{ - width:180px; - aspect-ratio:1; - display:flex; - justify-content:center; - align-items:center; - flex:0 0 auto; - background-color:var(--preview-image-bg-color); - border:var(--preview-image-border); - } - -:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #newAltTextImagePreview) > canvas{ - max-width:100%; - max-height:100%; - } - -.colorPicker{ - --hover-outline-color:#0250bb; - --selected-outline-color:#0060df; - --swatch-border-color:#cfcfd8; -} - -@media (prefers-color-scheme: dark){ - -:where(html:not(.is-light)) .colorPicker{ - --hover-outline-color:#80ebff; - --selected-outline-color:#aaf2ff; - --swatch-border-color:#52525e; -} - } - -:where(html.is-dark) .colorPicker{ - --hover-outline-color:#80ebff; - --selected-outline-color:#aaf2ff; - --swatch-border-color:#52525e; -} - -@media screen and (forced-colors: active){ - -.colorPicker{ - --hover-outline-color:Highlight; - --selected-outline-color:var(--hover-outline-color); - --swatch-border-color:ButtonText; -} - } - -.colorPicker .swatch{ - width:16px; - height:16px; - border:1px solid var(--swatch-border-color); - border-radius:100%; - outline-offset:2px; - box-sizing:border-box; - forced-color-adjust:none; - } - -.colorPicker button:is(:hover,.selected) > .swatch{ - border:none; - } - -.annotationEditorLayer[data-main-rotation="0"] .highlightEditor:not(.free) > .editToolbar{ - rotate:0deg; - } - -.annotationEditorLayer[data-main-rotation="90"] .highlightEditor:not(.free) > .editToolbar{ - rotate:270deg; - } - -.annotationEditorLayer[data-main-rotation="180"] .highlightEditor:not(.free) > .editToolbar{ - rotate:180deg; - } - -.annotationEditorLayer[data-main-rotation="270"] .highlightEditor:not(.free) > .editToolbar{ - rotate:90deg; - } - -.annotationEditorLayer .highlightEditor{ - position:absolute; - background:transparent; - z-index:1; - cursor:auto; - max-width:100%; - max-height:100%; - border:none; - outline:none; - pointer-events:none; - transform-origin:0 0; - } - -:is(.annotationEditorLayer .highlightEditor):not(.free){ - transform:none; - } - -:is(.annotationEditorLayer .highlightEditor) .internal{ - position:absolute; - top:0; - left:0; - width:100%; - height:100%; - pointer-events:auto; - } - -.disabled:is(.annotationEditorLayer .highlightEditor) .internal{ - pointer-events:none; - } - -.selectedEditor:is(.annotationEditorLayer .highlightEditor) .internal{ - cursor:pointer; - } - -:is(.annotationEditorLayer .highlightEditor) .editToolbar{ - --editor-toolbar-colorpicker-arrow-image:url(images/toolbarButton-menuArrow.svg); - - transform-origin:center !important; - } - -:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker{ - position:relative; - width:auto; - display:flex; - justify-content:center; - align-items:center; - gap:4px; - padding:4px; - } - -:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker)::after{ - content:""; - -webkit-mask-image:var(--editor-toolbar-colorpicker-arrow-image); - mask-image:var(--editor-toolbar-colorpicker-arrow-image); - -webkit-mask-repeat:no-repeat; - mask-repeat:no-repeat; - -webkit-mask-position:center; - mask-position:center; - display:inline-block; - background-color:var(--editor-toolbar-fg-color); - width:12px; - height:12px; - } - -:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker):hover::after{ - background-color:var(--editor-toolbar-hover-fg-color); - } - -:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker):has(.dropdown:not(.hidden)){ - background-color:var(--editor-toolbar-hover-bg-color); - } - -:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker):has(.dropdown:not(.hidden))::after{ - scale:-1; - } - -:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker) .dropdown{ - position:absolute; - display:flex; - justify-content:center; - align-items:center; - flex-direction:column; - gap:11px; - padding-block:8px; - border-radius:6px; - background-color:var(--editor-toolbar-bg-color); - border:1px solid var(--editor-toolbar-border-color); - box-shadow:var(--editor-toolbar-shadow); - inset-block-start:calc(100% + 4px); - width:calc(100% + 2 * var(--editor-toolbar-padding)); - } - -:is(:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker) .dropdown) button{ - width:100%; - height:auto; - border:none; - cursor:pointer; - display:flex; - justify-content:center; - align-items:center; - background:none; - } - -:is(:is(:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker) .dropdown) button):is(:active,:focus-visible){ - outline:none; - } - -:is(:is(:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker) .dropdown) button) > .swatch{ - outline-offset:2px; - } - -[aria-selected="true"]:is(:is(:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker) .dropdown) button) > .swatch{ - outline:2px solid var(--selected-outline-color); - } - -:is(:is(:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker) .dropdown) button):is(:hover,:active,:focus-visible) > .swatch{ - outline:2px solid var(--hover-outline-color); - } - -.editorParamsToolbar:has(#highlightParamsToolbarContainer){ - padding:unset; -} - -#highlightParamsToolbarContainer{ - gap:16px; - padding-inline:10px; - padding-block-end:12px; -} - -#highlightParamsToolbarContainer .colorPicker{ - display:flex; - flex-direction:column; - gap:8px; - } - -:is(#highlightParamsToolbarContainer .colorPicker) .dropdown{ - display:flex; - justify-content:space-between; - align-items:center; - flex-direction:row; - height:auto; - } - -:is(:is(#highlightParamsToolbarContainer .colorPicker) .dropdown) button{ - width:auto; - height:auto; - border:none; - cursor:pointer; - display:flex; - justify-content:center; - align-items:center; - background:none; - flex:0 0 auto; - padding:0; - } - -:is(:is(:is(#highlightParamsToolbarContainer .colorPicker) .dropdown) button) .swatch{ - width:24px; - height:24px; - } - -:is(:is(:is(#highlightParamsToolbarContainer .colorPicker) .dropdown) button):is(:active,:focus-visible){ - outline:none; - } - -[aria-selected="true"]:is(:is(:is(#highlightParamsToolbarContainer .colorPicker) .dropdown) button) > .swatch{ - outline:2px solid var(--selected-outline-color); - } - -:is(:is(:is(#highlightParamsToolbarContainer .colorPicker) .dropdown) button):is(:hover,:active,:focus-visible) > .swatch{ - outline:2px solid var(--hover-outline-color); - } - -#highlightParamsToolbarContainer #editorHighlightThickness{ - display:flex; - flex-direction:column; - align-items:center; - gap:4px; - align-self:stretch; - } - -:is(#highlightParamsToolbarContainer #editorHighlightThickness) .editorParamsLabel{ - height:auto; - align-self:stretch; - } - -:is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker{ - display:flex; - justify-content:space-between; - align-items:center; - align-self:stretch; - - --example-color:#bfbfc9; - } - -@media (prefers-color-scheme: dark){ - -:where(html:not(.is-light)) :is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker{ - --example-color:#80808e; - } - } - -:where(html.is-dark) :is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker{ - --example-color:#80808e; - } - -@media screen and (forced-colors: active){ - -:is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker{ - --example-color:CanvasText; - } - } - -:is(:is(:is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker) > .editorParamsSlider[disabled]){ - opacity:0.4; - } - -:is(:is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker)::before,:is(:is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker)::after{ - content:""; - width:8px; - aspect-ratio:1; - display:block; - border-radius:100%; - background-color:var(--example-color); - } - -:is(:is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker)::after{ - width:24px; - } - -:is(:is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker) .editorParamsSlider{ - width:unset; - height:14px; - } - -#highlightParamsToolbarContainer #editorHighlightVisibility{ - display:flex; - flex-direction:column; - align-items:flex-start; - gap:8px; - align-self:stretch; - } - -:is(#highlightParamsToolbarContainer #editorHighlightVisibility) .divider{ - --divider-color:#d7d7db; - } - -@media (prefers-color-scheme: dark){ - -:where(html:not(.is-light)) :is(#highlightParamsToolbarContainer #editorHighlightVisibility) .divider{ - --divider-color:#8f8f9d; - } - } - -:where(html.is-dark) :is(#highlightParamsToolbarContainer #editorHighlightVisibility) .divider{ - --divider-color:#8f8f9d; - } - -@media screen and (forced-colors: active){ - -:is(#highlightParamsToolbarContainer #editorHighlightVisibility) .divider{ - --divider-color:CanvasText; - } - } - -:is(#highlightParamsToolbarContainer #editorHighlightVisibility) .divider{ - - margin-block:4px; - width:100%; - height:1px; - background-color:var(--divider-color); - } - -:is(#highlightParamsToolbarContainer #editorHighlightVisibility) .toggler{ - display:flex; - justify-content:space-between; - align-items:center; - align-self:stretch; - } - -#altTextSettingsDialog{ - padding:16px; -} - -#altTextSettingsDialog #altTextSettingsContainer{ - display:flex; - width:573px; - flex-direction:column; - gap:16px; - } - -:is(#altTextSettingsDialog #altTextSettingsContainer) .mainContainer{ - gap:16px; - } - -:is(#altTextSettingsDialog #altTextSettingsContainer) .description{ - color:var(--text-secondary-color); - } - -:is(#altTextSettingsDialog #altTextSettingsContainer) #aiModelSettings{ - display:flex; - flex-direction:column; - gap:12px; - } - -:is(:is(#altTextSettingsDialog #altTextSettingsContainer) #aiModelSettings) button{ - width:-moz-fit-content; - width:fit-content; - } - -.download:is(:is(#altTextSettingsDialog #altTextSettingsContainer) #aiModelSettings) #deleteModelButton{ - display:none; - } - -:is(:is(#altTextSettingsDialog #altTextSettingsContainer) #aiModelSettings):not(.download) #downloadModelButton{ - display:none; - } - -:is(#altTextSettingsDialog #altTextSettingsContainer) #automaticAltText,:is(#altTextSettingsDialog #altTextSettingsContainer) #altTextEditor{ - display:flex; - flex-direction:column; - gap:8px; - } - -:is(#altTextSettingsDialog #altTextSettingsContainer) #createModelDescription,:is(#altTextSettingsDialog #altTextSettingsContainer) #aiModelSettings,:is(#altTextSettingsDialog #altTextSettingsContainer) #showAltTextDialogDescription{ - padding-inline-start:40px; - } - -:is(#altTextSettingsDialog #altTextSettingsContainer) #automaticSettings{ - display:flex; - flex-direction:column; - gap:16px; - } - -:root{ - --viewer-container-height:0; - --pdfViewer-padding-bottom:0; - --page-margin:1px auto -8px; - --page-border:9px solid transparent; - --spreadHorizontalWrapped-margin-LR:-3.5px; - --loading-icon-delay:400ms; -} - -@media screen and (forced-colors: active){ - :root{ - --pdfViewer-padding-bottom:9px; - --page-margin:8px auto -1px; - --page-border:1px solid CanvasText; - --spreadHorizontalWrapped-margin-LR:3.5px; +@media (prefers-color-scheme: dark) { + :where(html:not(.is-light)) + :is( + .annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), + .textLayer + ) + .editToolbar { + --editor-toolbar-bg-color: #2b2a33; + --editor-toolbar-fg-color: #fbfbfe; + --editor-toolbar-hover-bg-color: #52525e; + --editor-toolbar-focus-outline-color: #0df; + --alt-text-done-color: #54ffbd; + --alt-text-warning-color: #80ebff; } } -[data-main-rotation="90"]{ - transform:rotate(90deg) translateY(-100%); +:where(html.is-dark) + :is( + .annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), + .textLayer + ) + .editToolbar { + --editor-toolbar-bg-color: #2b2a33; + --editor-toolbar-fg-color: #fbfbfe; + --editor-toolbar-hover-bg-color: #52525e; + --editor-toolbar-focus-outline-color: #0df; + --alt-text-done-color: #54ffbd; + --alt-text-warning-color: #80ebff; } -[data-main-rotation="180"]{ - transform:rotate(180deg) translate(-100%, -100%); + +@media screen and (forced-colors: active) { + :is( + .annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), + .textLayer + ) + .editToolbar { + --editor-toolbar-bg-color: ButtonFace; + --editor-toolbar-fg-color: ButtonText; + --editor-toolbar-border-color: ButtonText; + --editor-toolbar-hover-border-color: AccentColor; + --editor-toolbar-hover-bg-color: ButtonFace; + --editor-toolbar-hover-fg-color: AccentColor; + --editor-toolbar-hover-outline: 2px solid + var(--editor-toolbar-hover-border-color); + --editor-toolbar-focus-outline-color: ButtonBorder; + --editor-toolbar-shadow: none; + --alt-text-done-color: var(--editor-toolbar-fg-color); + --alt-text-warning-color: var(--editor-toolbar-fg-color); + --alt-text-hover-done-color: var(--editor-toolbar-hover-fg-color); + --alt-text-hover-warning-color: var(--editor-toolbar-hover-fg-color); + } } -[data-main-rotation="270"]{ - transform:rotate(270deg) translateX(-100%); + +:is( + .annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), + .textLayer + ) + .editToolbar { + display: flex; + width: -moz-fit-content; + width: fit-content; + height: var(--editor-toolbar-height); + flex-direction: column; + justify-content: center; + align-items: center; + cursor: default; + pointer-events: auto; + box-sizing: content-box; + padding: var(--editor-toolbar-padding); + + position: absolute; + inset-inline-end: 0; + inset-block-start: calc(100% + var(--editor-toolbar-vert-offset)); + + border-radius: 6px; + background-color: var(--editor-toolbar-bg-color); + border: 1px solid var(--editor-toolbar-border-color); + box-shadow: var(--editor-toolbar-shadow); +} + +.hidden:is( + :is( + .annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), + .textLayer + ) + .editToolbar + ) { + display: none; +} + +:is( + :is( + .annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), + .textLayer + ) + .editToolbar + ):has(:focus-visible) { + border-color: transparent; +} + +[dir='ltr'] + :is( + :is( + .annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), + .textLayer + ) + .editToolbar + ) { + transform-origin: 100% 0; +} + +[dir='rtl'] + :is( + :is( + .annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), + .textLayer + ) + .editToolbar + ) { + transform-origin: 0 0; +} + +:is( + :is( + .annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), + .textLayer + ) + .editToolbar + ) + .buttons { + display: flex; + justify-content: center; + align-items: center; + gap: 0; + height: 100%; +} + +:is( + :is( + :is( + .annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), + .textLayer + ) + .editToolbar + ) + .buttons + ) + button { + padding: 0; +} + +:is( + :is( + :is( + .annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), + .textLayer + ) + .editToolbar + ) + .buttons + ) + .divider { + width: 0; + height: calc( + 2 * var(--editor-toolbar-padding) + var(--editor-toolbar-height) + ); + border-left: 1px solid var(--editor-toolbar-border-color); + border-right: none; + display: inline-block; + margin-inline: 2px; +} + +:is( + :is( + :is( + .annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), + .textLayer + ) + .editToolbar + ) + .buttons + ) + .highlightButton { + width: var(--editor-toolbar-height); +} + +:is( + :is( + :is( + :is( + .annotationEditorLayer + :is( + .freeTextEditor, + .inkEditor, + .stampEditor, + .highlightEditor + ), + .textLayer + ) + .editToolbar + ) + .buttons + ) + .highlightButton + )::before { + content: ''; + -webkit-mask-image: var(--editor-toolbar-highlight-image); + mask-image: var(--editor-toolbar-highlight-image); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: center; + mask-position: center; + display: inline-block; + background-color: var(--editor-toolbar-fg-color); + width: 100%; + height: 100%; +} + +:is( + :is( + :is( + :is( + .annotationEditorLayer + :is( + .freeTextEditor, + .inkEditor, + .stampEditor, + .highlightEditor + ), + .textLayer + ) + .editToolbar + ) + .buttons + ) + .highlightButton + ):hover::before { + background-color: var(--editor-toolbar-hover-fg-color); +} + +:is( + :is( + :is( + .annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), + .textLayer + ) + .editToolbar + ) + .buttons + ) + .delete { + width: var(--editor-toolbar-height); +} + +:is( + :is( + :is( + :is( + .annotationEditorLayer + :is( + .freeTextEditor, + .inkEditor, + .stampEditor, + .highlightEditor + ), + .textLayer + ) + .editToolbar + ) + .buttons + ) + .delete + )::before { + content: ''; + -webkit-mask-image: var(--editor-toolbar-delete-image); + mask-image: var(--editor-toolbar-delete-image); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: center; + mask-position: center; + display: inline-block; + background-color: var(--editor-toolbar-fg-color); + width: 100%; + height: 100%; +} + +:is( + :is( + :is( + :is( + .annotationEditorLayer + :is( + .freeTextEditor, + .inkEditor, + .stampEditor, + .highlightEditor + ), + .textLayer + ) + .editToolbar + ) + .buttons + ) + .delete + ):hover::before { + background-color: var(--editor-toolbar-hover-fg-color); +} + +:is( + :is( + :is( + .annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), + .textLayer + ) + .editToolbar + ) + .buttons + ) + > * { + height: var(--editor-toolbar-height); +} + +:is( + :is( + :is( + .annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), + .textLayer + ) + .editToolbar + ) + .buttons + ) + > :not(.divider) { + border: none; + background-color: transparent; + cursor: pointer; +} + +:is( + :is( + :is( + :is( + .annotationEditorLayer + :is( + .freeTextEditor, + .inkEditor, + .stampEditor, + .highlightEditor + ), + .textLayer + ) + .editToolbar + ) + .buttons + ) + > :not(.divider) + ):hover { + border-radius: 2px; + background-color: var(--editor-toolbar-hover-bg-color); + color: var(--editor-toolbar-hover-fg-color); + outline: var(--editor-toolbar-hover-outline); + outline-offset: 1px; +} + +:is( + :is( + :is( + :is( + .annotationEditorLayer + :is( + .freeTextEditor, + .inkEditor, + .stampEditor, + .highlightEditor + ), + .textLayer + ) + .editToolbar + ) + .buttons + ) + > :not(.divider) + ):hover:active { + outline: none; +} + +:is( + :is( + :is( + :is( + .annotationEditorLayer + :is( + .freeTextEditor, + .inkEditor, + .stampEditor, + .highlightEditor + ), + .textLayer + ) + .editToolbar + ) + .buttons + ) + > :not(.divider) + ):focus-visible { + border-radius: 2px; + outline: 2px solid var(--editor-toolbar-focus-outline-color); +} + +:is( + :is( + :is( + .annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), + .textLayer + ) + .editToolbar + ) + .buttons + ) + .altText { + --alt-text-add-image: url(images/altText_add.svg); + --alt-text-done-image: url(images/altText_done.svg); + + display: flex; + align-items: center; + justify-content: center; + width: -moz-max-content; + width: max-content; + padding-inline: 8px; + pointer-events: all; + font: menu; + font-weight: 590; + font-size: 12px; + color: var(--editor-toolbar-fg-color); +} + +:is( + :is( + :is( + :is( + .annotationEditorLayer + :is( + .freeTextEditor, + .inkEditor, + .stampEditor, + .highlightEditor + ), + .textLayer + ) + .editToolbar + ) + .buttons + ) + .altText + ):disabled { + pointer-events: none; +} + +:is( + :is( + :is( + :is( + .annotationEditorLayer + :is( + .freeTextEditor, + .inkEditor, + .stampEditor, + .highlightEditor + ), + .textLayer + ) + .editToolbar + ) + .buttons + ) + .altText + )::before { + content: ''; + -webkit-mask-image: var(--alt-text-add-image); + mask-image: var(--alt-text-add-image); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: center; + mask-position: center; + display: inline-block; + width: 12px; + height: 13px; + background-color: var(--editor-toolbar-fg-color); + margin-inline-end: 4px; +} + +:is( + :is( + :is( + :is( + .annotationEditorLayer + :is( + .freeTextEditor, + .inkEditor, + .stampEditor, + .highlightEditor + ), + .textLayer + ) + .editToolbar + ) + .buttons + ) + .altText + ):hover::before { + background-color: var(--editor-toolbar-hover-fg-color); +} + +.done:is( + :is( + :is( + :is( + .annotationEditorLayer + :is( + .freeTextEditor, + .inkEditor, + .stampEditor, + .highlightEditor + ), + .textLayer + ) + .editToolbar + ) + .buttons + ) + .altText + )::before { + -webkit-mask-image: var(--alt-text-done-image); + mask-image: var(--alt-text-done-image); +} + +.new:is( + :is( + :is( + :is( + .annotationEditorLayer + :is( + .freeTextEditor, + .inkEditor, + .stampEditor, + .highlightEditor + ), + .textLayer + ) + .editToolbar + ) + .buttons + ) + .altText + )::before { + width: 16px; + height: 16px; + -webkit-mask-image: var(--new-alt-text-warning-image); + mask-image: var(--new-alt-text-warning-image); + background-color: var(--alt-text-warning-color); + -webkit-mask-size: cover; + mask-size: cover; +} + +.new:is( + :is( + :is( + :is( + .annotationEditorLayer + :is( + .freeTextEditor, + .inkEditor, + .stampEditor, + .highlightEditor + ), + .textLayer + ) + .editToolbar + ) + .buttons + ) + .altText + ):hover::before { + background-color: var(--alt-text-hover-warning-color); +} + +.new.done:is( + :is( + :is( + :is( + .annotationEditorLayer + :is( + .freeTextEditor, + .inkEditor, + .stampEditor, + .highlightEditor + ), + .textLayer + ) + .editToolbar + ) + .buttons + ) + .altText + )::before { + -webkit-mask-image: var(--alt-text-done-image); + mask-image: var(--alt-text-done-image); + background-color: var(--alt-text-done-color); +} + +.new.done:is( + :is( + :is( + :is( + .annotationEditorLayer + :is( + .freeTextEditor, + .inkEditor, + .stampEditor, + .highlightEditor + ), + .textLayer + ) + .editToolbar + ) + .buttons + ) + .altText + ):hover::before { + background-color: var(--alt-text-hover-done-color); +} + +:is( + :is( + :is( + :is( + .annotationEditorLayer + :is( + .freeTextEditor, + .inkEditor, + .stampEditor, + .highlightEditor + ), + .textLayer + ) + .editToolbar + ) + .buttons + ) + .altText + ) + .tooltip { + display: none; + word-wrap: anywhere; +} + +.show:is( + :is( + :is( + :is( + :is( + .annotationEditorLayer + :is( + .freeTextEditor, + .inkEditor, + .stampEditor, + .highlightEditor + ), + .textLayer + ) + .editToolbar + ) + .buttons + ) + .altText + ) + .tooltip + ) { + --alt-text-tooltip-bg: #f0f0f4; + --alt-text-tooltip-fg: #15141a; + --alt-text-tooltip-border: #8f8f9d; + --alt-text-tooltip-shadow: 0px 2px 6px 0px rgb(58 57 68 / 0.2); +} + +@media (prefers-color-scheme: dark) { + :where(html:not(.is-light)) + .show:is( + :is( + :is( + :is( + :is( + .annotationEditorLayer + :is( + .freeTextEditor, + .inkEditor, + .stampEditor, + .highlightEditor + ), + .textLayer + ) + .editToolbar + ) + .buttons + ) + .altText + ) + .tooltip + ) { + --alt-text-tooltip-bg: #1c1b22; + --alt-text-tooltip-fg: #fbfbfe; + --alt-text-tooltip-shadow: 0px 2px 6px 0px #15141a; + } +} + +:where(html.is-dark) + .show:is( + :is( + :is( + :is( + :is( + .annotationEditorLayer + :is( + .freeTextEditor, + .inkEditor, + .stampEditor, + .highlightEditor + ), + .textLayer + ) + .editToolbar + ) + .buttons + ) + .altText + ) + .tooltip + ) { + --alt-text-tooltip-bg: #1c1b22; + --alt-text-tooltip-fg: #fbfbfe; + --alt-text-tooltip-shadow: 0px 2px 6px 0px #15141a; +} + +@media screen and (forced-colors: active) { + .show:is( + :is( + :is( + :is( + :is( + .annotationEditorLayer + :is( + .freeTextEditor, + .inkEditor, + .stampEditor, + .highlightEditor + ), + .textLayer + ) + .editToolbar + ) + .buttons + ) + .altText + ) + .tooltip + ) { + --alt-text-tooltip-bg: Canvas; + --alt-text-tooltip-fg: CanvasText; + --alt-text-tooltip-border: CanvasText; + --alt-text-tooltip-shadow: none; + } +} + +.show:is( + :is( + :is( + :is( + :is( + .annotationEditorLayer + :is( + .freeTextEditor, + .inkEditor, + .stampEditor, + .highlightEditor + ), + .textLayer + ) + .editToolbar + ) + .buttons + ) + .altText + ) + .tooltip + ) { + display: inline-flex; + flex-direction: column; + align-items: center; + justify-content: center; + position: absolute; + top: calc(100% + 2px); + inset-inline-start: 0; + padding-block: 2px 3px; + padding-inline: 3px; + max-width: 300px; + width: -moz-max-content; + width: max-content; + height: auto; + font-size: 12px; + + border: 0.5px solid var(--alt-text-tooltip-border); + background: var(--alt-text-tooltip-bg); + box-shadow: var(--alt-text-tooltip-shadow); + color: var(--alt-text-tooltip-fg); + + pointer-events: none; +} + +.annotationEditorLayer .freeTextEditor { + padding: calc(var(--freetext-padding) * var(--scale-factor)); + width: auto; + height: auto; + touch-action: none; +} + +.annotationEditorLayer .freeTextEditor .internal { + background: transparent; + border: none; + inset: 0; + overflow: visible; + white-space: nowrap; + font: 10px sans-serif; + line-height: var(--freetext-line-height); + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.annotationEditorLayer .freeTextEditor .overlay { + position: absolute; + display: none; + background: transparent; + inset: 0; + width: 100%; + height: 100%; +} + +.annotationEditorLayer freeTextEditor .overlay.enabled { + display: block; +} + +.annotationEditorLayer .freeTextEditor .internal:empty::before { + content: attr(default-content); + color: gray; +} + +.annotationEditorLayer .freeTextEditor .internal:focus { + outline: none; + -webkit-user-select: auto; + -moz-user-select: auto; + user-select: auto; +} + +.annotationEditorLayer .inkEditor { + width: 100%; + height: 100%; +} + +.annotationEditorLayer .inkEditor.editing { + cursor: inherit; +} + +.annotationEditorLayer .inkEditor .inkEditorCanvas { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + touch-action: none; +} + +.annotationEditorLayer .stampEditor { + width: auto; + height: auto; +} + +:is(.annotationEditorLayer .stampEditor) canvas { + position: absolute; + width: 100%; + height: 100%; + margin: 0; + top: 0; + left: 0; +} + +:is(.annotationEditorLayer .stampEditor) .noAltTextBadge { + --no-alt-text-badge-border-color: #f0f0f4; + --no-alt-text-badge-bg-color: #cfcfd8; + --no-alt-text-badge-fg-color: #5b5b66; +} + +@media (prefers-color-scheme: dark) { + :where(html:not(.is-light)) + :is(.annotationEditorLayer .stampEditor) + .noAltTextBadge { + --no-alt-text-badge-border-color: #52525e; + --no-alt-text-badge-bg-color: #fbfbfe; + --no-alt-text-badge-fg-color: #15141a; + } +} + +:where(html.is-dark) :is(.annotationEditorLayer .stampEditor) .noAltTextBadge { + --no-alt-text-badge-border-color: #52525e; + --no-alt-text-badge-bg-color: #fbfbfe; + --no-alt-text-badge-fg-color: #15141a; +} + +@media screen and (forced-colors: active) { + :is(.annotationEditorLayer .stampEditor) .noAltTextBadge { + --no-alt-text-badge-border-color: ButtonText; + --no-alt-text-badge-bg-color: ButtonFace; + --no-alt-text-badge-fg-color: ButtonText; + } +} + +:is(.annotationEditorLayer .stampEditor) .noAltTextBadge { + position: absolute; + inset-inline-end: 5px; + inset-block-end: 5px; + display: inline-flex; + width: 32px; + height: 32px; + padding: 3px; + justify-content: center; + align-items: center; + pointer-events: none; + z-index: 1; + + border-radius: 2px; + border: 1px solid var(--no-alt-text-badge-border-color); + background: var(--no-alt-text-badge-bg-color); +} + +:is(:is(.annotationEditorLayer .stampEditor) .noAltTextBadge)::before { + content: ''; + display: inline-block; + width: 16px; + height: 16px; + -webkit-mask-image: var(--new-alt-text-warning-image); + mask-image: var(--new-alt-text-warning-image); + -webkit-mask-size: cover; + mask-size: cover; + background-color: var(--no-alt-text-badge-fg-color); +} + +:is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor)) + > .resizers { + position: absolute; + inset: 0; +} + +.hidden:is( + :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor)) + > .resizers + ) { + display: none; +} + +:is( + :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor)) + > .resizers + ) + > .resizer { + width: var(--resizer-size); + height: var(--resizer-size); + background: content-box var(--resizer-bg-color); + border: var(--focus-outline-around); + border-radius: 2px; + position: absolute; +} + +.topLeft:is( + :is( + :is( + .annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor) + ) + > .resizers + ) + > .resizer + ) { + top: var(--resizer-shift); + left: var(--resizer-shift); +} + +.topMiddle:is( + :is( + :is( + .annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor) + ) + > .resizers + ) + > .resizer + ) { + top: var(--resizer-shift); + left: calc(50% + var(--resizer-shift)); +} + +.topRight:is( + :is( + :is( + .annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor) + ) + > .resizers + ) + > .resizer + ) { + top: var(--resizer-shift); + right: var(--resizer-shift); +} + +.middleRight:is( + :is( + :is( + .annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor) + ) + > .resizers + ) + > .resizer + ) { + top: calc(50% + var(--resizer-shift)); + right: var(--resizer-shift); +} + +.bottomRight:is( + :is( + :is( + .annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor) + ) + > .resizers + ) + > .resizer + ) { + bottom: var(--resizer-shift); + right: var(--resizer-shift); +} + +.bottomMiddle:is( + :is( + :is( + .annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor) + ) + > .resizers + ) + > .resizer + ) { + bottom: var(--resizer-shift); + left: calc(50% + var(--resizer-shift)); +} + +.bottomLeft:is( + :is( + :is( + .annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor) + ) + > .resizers + ) + > .resizer + ) { + bottom: var(--resizer-shift); + left: var(--resizer-shift); +} + +.middleLeft:is( + :is( + :is( + .annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor) + ) + > .resizers + ) + > .resizer + ) { + top: calc(50% + var(--resizer-shift)); + left: var(--resizer-shift); +} + +.topLeft:is( + :is( + .annotationEditorLayer[data-main-rotation='0'] + :is([data-editor-rotation='0'], [data-editor-rotation='180']), + .annotationEditorLayer[data-main-rotation='90'] + :is([data-editor-rotation='270'], [data-editor-rotation='90']), + .annotationEditorLayer[data-main-rotation='180'] + :is([data-editor-rotation='180'], [data-editor-rotation='0']), + .annotationEditorLayer[data-main-rotation='270'] + :is([data-editor-rotation='90'], [data-editor-rotation='270']) + ) + > .resizers + > .resizer + ), +.bottomRight:is( + :is( + .annotationEditorLayer[data-main-rotation='0'] + :is([data-editor-rotation='0'], [data-editor-rotation='180']), + .annotationEditorLayer[data-main-rotation='90'] + :is([data-editor-rotation='270'], [data-editor-rotation='90']), + .annotationEditorLayer[data-main-rotation='180'] + :is([data-editor-rotation='180'], [data-editor-rotation='0']), + .annotationEditorLayer[data-main-rotation='270'] + :is([data-editor-rotation='90'], [data-editor-rotation='270']) + ) + > .resizers + > .resizer + ) { + cursor: nwse-resize; +} + +.topMiddle:is( + :is( + .annotationEditorLayer[data-main-rotation='0'] + :is([data-editor-rotation='0'], [data-editor-rotation='180']), + .annotationEditorLayer[data-main-rotation='90'] + :is([data-editor-rotation='270'], [data-editor-rotation='90']), + .annotationEditorLayer[data-main-rotation='180'] + :is([data-editor-rotation='180'], [data-editor-rotation='0']), + .annotationEditorLayer[data-main-rotation='270'] + :is([data-editor-rotation='90'], [data-editor-rotation='270']) + ) + > .resizers + > .resizer + ), +.bottomMiddle:is( + :is( + .annotationEditorLayer[data-main-rotation='0'] + :is([data-editor-rotation='0'], [data-editor-rotation='180']), + .annotationEditorLayer[data-main-rotation='90'] + :is([data-editor-rotation='270'], [data-editor-rotation='90']), + .annotationEditorLayer[data-main-rotation='180'] + :is([data-editor-rotation='180'], [data-editor-rotation='0']), + .annotationEditorLayer[data-main-rotation='270'] + :is([data-editor-rotation='90'], [data-editor-rotation='270']) + ) + > .resizers + > .resizer + ) { + cursor: ns-resize; +} + +.topRight:is( + :is( + .annotationEditorLayer[data-main-rotation='0'] + :is([data-editor-rotation='0'], [data-editor-rotation='180']), + .annotationEditorLayer[data-main-rotation='90'] + :is([data-editor-rotation='270'], [data-editor-rotation='90']), + .annotationEditorLayer[data-main-rotation='180'] + :is([data-editor-rotation='180'], [data-editor-rotation='0']), + .annotationEditorLayer[data-main-rotation='270'] + :is([data-editor-rotation='90'], [data-editor-rotation='270']) + ) + > .resizers + > .resizer + ), +.bottomLeft:is( + :is( + .annotationEditorLayer[data-main-rotation='0'] + :is([data-editor-rotation='0'], [data-editor-rotation='180']), + .annotationEditorLayer[data-main-rotation='90'] + :is([data-editor-rotation='270'], [data-editor-rotation='90']), + .annotationEditorLayer[data-main-rotation='180'] + :is([data-editor-rotation='180'], [data-editor-rotation='0']), + .annotationEditorLayer[data-main-rotation='270'] + :is([data-editor-rotation='90'], [data-editor-rotation='270']) + ) + > .resizers + > .resizer + ) { + cursor: nesw-resize; +} + +.middleRight:is( + :is( + .annotationEditorLayer[data-main-rotation='0'] + :is([data-editor-rotation='0'], [data-editor-rotation='180']), + .annotationEditorLayer[data-main-rotation='90'] + :is([data-editor-rotation='270'], [data-editor-rotation='90']), + .annotationEditorLayer[data-main-rotation='180'] + :is([data-editor-rotation='180'], [data-editor-rotation='0']), + .annotationEditorLayer[data-main-rotation='270'] + :is([data-editor-rotation='90'], [data-editor-rotation='270']) + ) + > .resizers + > .resizer + ), +.middleLeft:is( + :is( + .annotationEditorLayer[data-main-rotation='0'] + :is([data-editor-rotation='0'], [data-editor-rotation='180']), + .annotationEditorLayer[data-main-rotation='90'] + :is([data-editor-rotation='270'], [data-editor-rotation='90']), + .annotationEditorLayer[data-main-rotation='180'] + :is([data-editor-rotation='180'], [data-editor-rotation='0']), + .annotationEditorLayer[data-main-rotation='270'] + :is([data-editor-rotation='90'], [data-editor-rotation='270']) + ) + > .resizers + > .resizer + ) { + cursor: ew-resize; +} + +.topLeft:is( + :is( + .annotationEditorLayer[data-main-rotation='0'] + :is([data-editor-rotation='90'], [data-editor-rotation='270']), + .annotationEditorLayer[data-main-rotation='90'] + :is([data-editor-rotation='0'], [data-editor-rotation='180']), + .annotationEditorLayer[data-main-rotation='180'] + :is([data-editor-rotation='270'], [data-editor-rotation='90']), + .annotationEditorLayer[data-main-rotation='270'] + :is([data-editor-rotation='180'], [data-editor-rotation='0']) + ) + > .resizers + > .resizer + ), +.bottomRight:is( + :is( + .annotationEditorLayer[data-main-rotation='0'] + :is([data-editor-rotation='90'], [data-editor-rotation='270']), + .annotationEditorLayer[data-main-rotation='90'] + :is([data-editor-rotation='0'], [data-editor-rotation='180']), + .annotationEditorLayer[data-main-rotation='180'] + :is([data-editor-rotation='270'], [data-editor-rotation='90']), + .annotationEditorLayer[data-main-rotation='270'] + :is([data-editor-rotation='180'], [data-editor-rotation='0']) + ) + > .resizers + > .resizer + ) { + cursor: nesw-resize; +} + +.topMiddle:is( + :is( + .annotationEditorLayer[data-main-rotation='0'] + :is([data-editor-rotation='90'], [data-editor-rotation='270']), + .annotationEditorLayer[data-main-rotation='90'] + :is([data-editor-rotation='0'], [data-editor-rotation='180']), + .annotationEditorLayer[data-main-rotation='180'] + :is([data-editor-rotation='270'], [data-editor-rotation='90']), + .annotationEditorLayer[data-main-rotation='270'] + :is([data-editor-rotation='180'], [data-editor-rotation='0']) + ) + > .resizers + > .resizer + ), +.bottomMiddle:is( + :is( + .annotationEditorLayer[data-main-rotation='0'] + :is([data-editor-rotation='90'], [data-editor-rotation='270']), + .annotationEditorLayer[data-main-rotation='90'] + :is([data-editor-rotation='0'], [data-editor-rotation='180']), + .annotationEditorLayer[data-main-rotation='180'] + :is([data-editor-rotation='270'], [data-editor-rotation='90']), + .annotationEditorLayer[data-main-rotation='270'] + :is([data-editor-rotation='180'], [data-editor-rotation='0']) + ) + > .resizers + > .resizer + ) { + cursor: ew-resize; +} + +.topRight:is( + :is( + .annotationEditorLayer[data-main-rotation='0'] + :is([data-editor-rotation='90'], [data-editor-rotation='270']), + .annotationEditorLayer[data-main-rotation='90'] + :is([data-editor-rotation='0'], [data-editor-rotation='180']), + .annotationEditorLayer[data-main-rotation='180'] + :is([data-editor-rotation='270'], [data-editor-rotation='90']), + .annotationEditorLayer[data-main-rotation='270'] + :is([data-editor-rotation='180'], [data-editor-rotation='0']) + ) + > .resizers + > .resizer + ), +.bottomLeft:is( + :is( + .annotationEditorLayer[data-main-rotation='0'] + :is([data-editor-rotation='90'], [data-editor-rotation='270']), + .annotationEditorLayer[data-main-rotation='90'] + :is([data-editor-rotation='0'], [data-editor-rotation='180']), + .annotationEditorLayer[data-main-rotation='180'] + :is([data-editor-rotation='270'], [data-editor-rotation='90']), + .annotationEditorLayer[data-main-rotation='270'] + :is([data-editor-rotation='180'], [data-editor-rotation='0']) + ) + > .resizers + > .resizer + ) { + cursor: nwse-resize; +} + +.middleRight:is( + :is( + .annotationEditorLayer[data-main-rotation='0'] + :is([data-editor-rotation='90'], [data-editor-rotation='270']), + .annotationEditorLayer[data-main-rotation='90'] + :is([data-editor-rotation='0'], [data-editor-rotation='180']), + .annotationEditorLayer[data-main-rotation='180'] + :is([data-editor-rotation='270'], [data-editor-rotation='90']), + .annotationEditorLayer[data-main-rotation='270'] + :is([data-editor-rotation='180'], [data-editor-rotation='0']) + ) + > .resizers + > .resizer + ), +.middleLeft:is( + :is( + .annotationEditorLayer[data-main-rotation='0'] + :is([data-editor-rotation='90'], [data-editor-rotation='270']), + .annotationEditorLayer[data-main-rotation='90'] + :is([data-editor-rotation='0'], [data-editor-rotation='180']), + .annotationEditorLayer[data-main-rotation='180'] + :is([data-editor-rotation='270'], [data-editor-rotation='90']), + .annotationEditorLayer[data-main-rotation='270'] + :is([data-editor-rotation='180'], [data-editor-rotation='0']) + ) + > .resizers + > .resizer + ) { + cursor: ns-resize; +} + +:is( + .annotationEditorLayer + :is( + [data-main-rotation='0'] [data-editor-rotation='90'], + [data-main-rotation='90'] [data-editor-rotation='0'], + [data-main-rotation='180'] [data-editor-rotation='270'], + [data-main-rotation='270'] [data-editor-rotation='180'] + ) + ) + .editToolbar { + rotate: 270deg; +} + +[dir='ltr'] + :is( + :is( + .annotationEditorLayer + :is( + [data-main-rotation='0'] [data-editor-rotation='90'], + [data-main-rotation='90'] [data-editor-rotation='0'], + [data-main-rotation='180'] [data-editor-rotation='270'], + [data-main-rotation='270'] [data-editor-rotation='180'] + ) + ) + .editToolbar + ) { + inset-inline-end: calc(0px - var(--editor-toolbar-vert-offset)); + inset-block-start: 0; +} + +[dir='rtl'] + :is( + :is( + .annotationEditorLayer + :is( + [data-main-rotation='0'] [data-editor-rotation='90'], + [data-main-rotation='90'] [data-editor-rotation='0'], + [data-main-rotation='180'] [data-editor-rotation='270'], + [data-main-rotation='270'] [data-editor-rotation='180'] + ) + ) + .editToolbar + ) { + inset-inline-end: calc(100% + var(--editor-toolbar-vert-offset)); + inset-block-start: 0; +} + +:is( + .annotationEditorLayer + :is( + [data-main-rotation='0'] [data-editor-rotation='180'], + [data-main-rotation='90'] [data-editor-rotation='90'], + [data-main-rotation='180'] [data-editor-rotation='0'], + [data-main-rotation='270'] [data-editor-rotation='270'] + ) + ) + .editToolbar { + rotate: 180deg; + inset-inline-end: 100%; + inset-block-start: calc(0pc - var(--editor-toolbar-vert-offset)); +} + +:is( + .annotationEditorLayer + :is( + [data-main-rotation='0'] [data-editor-rotation='270'], + [data-main-rotation='90'] [data-editor-rotation='180'], + [data-main-rotation='180'] [data-editor-rotation='90'], + [data-main-rotation='270'] [data-editor-rotation='0'] + ) + ) + .editToolbar { + rotate: 90deg; +} + +[dir='ltr'] + :is( + :is( + .annotationEditorLayer + :is( + [data-main-rotation='0'] [data-editor-rotation='270'], + [data-main-rotation='90'] [data-editor-rotation='180'], + [data-main-rotation='180'] [data-editor-rotation='90'], + [data-main-rotation='270'] [data-editor-rotation='0'] + ) + ) + .editToolbar + ) { + inset-inline-end: calc(100% + var(--editor-toolbar-vert-offset)); + inset-block-start: 100%; +} + +[dir='rtl'] + :is( + :is( + .annotationEditorLayer + :is( + [data-main-rotation='0'] [data-editor-rotation='270'], + [data-main-rotation='90'] [data-editor-rotation='180'], + [data-main-rotation='180'] [data-editor-rotation='90'], + [data-main-rotation='270'] [data-editor-rotation='0'] + ) + ) + .editToolbar + ) { + inset-inline-start: calc(0px - var(--editor-toolbar-vert-offset)); + inset-block-start: 0; +} + +.dialog.altText::backdrop { + -webkit-mask: url(#alttext-manager-mask); + mask: url(#alttext-manager-mask); +} + +.dialog.altText.positioned { + margin: 0; +} + +.dialog.altText #altTextContainer { + width: 300px; + height: -moz-fit-content; + height: fit-content; + display: inline-flex; + flex-direction: column; + align-items: flex-start; + gap: 16px; +} + +:is(.dialog.altText #altTextContainer) #overallDescription { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; + align-self: stretch; +} + +:is(:is(.dialog.altText #altTextContainer) #overallDescription) span { + align-self: stretch; +} + +:is(:is(.dialog.altText #altTextContainer) #overallDescription) .title { + font-size: 13px; + font-style: normal; + font-weight: 590; +} + +:is(.dialog.altText #altTextContainer) #addDescription { + display: flex; + flex-direction: column; + align-items: stretch; + gap: 8px; +} + +:is(:is(.dialog.altText #altTextContainer) #addDescription) .descriptionArea { + flex: 1; + padding-inline: 24px 10px; +} + +:is( + :is(:is(.dialog.altText #altTextContainer) #addDescription) .descriptionArea + ) + textarea { + width: 100%; + min-height: 75px; +} + +:is(.dialog.altText #altTextContainer) #buttons { + display: flex; + justify-content: flex-end; + align-items: flex-start; + gap: 8px; + align-self: stretch; +} + +.dialog.newAltText { + --new-alt-text-ai-disclaimer-icon: url(images/altText_disclaimer.svg); + --new-alt-text-spinner-icon: url(images/altText_spinner.svg); + --preview-image-bg-color: #f0f0f4; + --preview-image-border: none; +} + +@media (prefers-color-scheme: dark) { + :where(html:not(.is-light)) .dialog.newAltText { + --preview-image-bg-color: #2b2a33; + } +} + +:where(html.is-dark) .dialog.newAltText { + --preview-image-bg-color: #2b2a33; +} + +@media screen and (forced-colors: active) { + .dialog.newAltText { + --preview-image-bg-color: ButtonFace; + --preview-image-border: 1px solid ButtonText; + } +} + +.dialog.newAltText { + width: 80%; + max-width: 570px; + min-width: 300px; + padding: 0; +} + +.dialog.newAltText.noAi #newAltTextDisclaimer, +.dialog.newAltText.noAi #newAltTextCreateAutomatically { + display: none !important; +} + +.dialog.newAltText.aiInstalling #newAltTextCreateAutomatically { + display: none !important; +} + +.dialog.newAltText.aiInstalling #newAltTextDownloadModel { + display: flex !important; +} + +.dialog.newAltText.error #newAltTextNotNow { + display: none !important; +} + +.dialog.newAltText.error #newAltTextCancel { + display: inline-block !important; +} + +.dialog.newAltText:not(.error) #newAltTextError { + display: none !important; +} + +.dialog.newAltText #newAltTextContainer { + display: flex; + width: auto; + padding: 16px; + flex-direction: column; + justify-content: flex-end; + align-items: flex-start; + gap: 12px; + flex: 0 1 auto; + line-height: normal; +} + +:is(.dialog.newAltText #newAltTextContainer) #mainContent { + display: flex; + justify-content: flex-end; + align-items: flex-start; + gap: 12px; + align-self: stretch; + flex: 1 1 auto; +} + +:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) + #descriptionAndSettings { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 16px; + flex: 1 0 0; + align-self: stretch; +} + +:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) + #descriptionInstruction { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + align-self: stretch; + flex: 1 1 auto; +} + +:is( + :is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) + #descriptionInstruction + ) + #newAltTextDescriptionContainer { + width: 100%; + height: 70px; + position: relative; +} + +:is( + :is( + :is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) + #descriptionInstruction + ) + #newAltTextDescriptionContainer + ) + textarea { + width: 100%; + height: 100%; + padding: 8px; +} + +:is( + :is( + :is( + :is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) + #descriptionInstruction + ) + #newAltTextDescriptionContainer + ) + textarea + )::-moz-placeholder { + color: var(--text-secondary-color); +} + +:is( + :is( + :is( + :is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) + #descriptionInstruction + ) + #newAltTextDescriptionContainer + ) + textarea + )::placeholder { + color: var(--text-secondary-color); +} + +:is( + :is( + :is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) + #descriptionInstruction + ) + #newAltTextDescriptionContainer + ) + .altTextSpinner { + display: none; + position: absolute; + width: 16px; + height: 16px; + inset-inline-start: 8px; + inset-block-start: 8px; + -webkit-mask-size: cover; + mask-size: cover; + background-color: var(--text-secondary-color); + pointer-events: none; +} + +.loading:is( + :is( + :is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) + #descriptionInstruction + ) + #newAltTextDescriptionContainer + ) + textarea::-moz-placeholder { + color: transparent; +} + +.loading:is( + :is( + :is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) + #descriptionInstruction + ) + #newAltTextDescriptionContainer + ) + textarea::placeholder { + color: transparent; +} + +.loading:is( + :is( + :is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) + #descriptionInstruction + ) + #newAltTextDescriptionContainer + ) + .altTextSpinner { + display: inline-block; + -webkit-mask-image: var(--new-alt-text-spinner-icon); + mask-image: var(--new-alt-text-spinner-icon); +} + +:is( + :is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) + #descriptionInstruction + ) + #newAltTextDescription { + font-size: 11px; +} + +:is( + :is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) + #descriptionInstruction + ) + #newAltTextDisclaimer { + display: flex; + flex-direction: row; + align-items: flex-start; + gap: 4px; + font-size: 11px; +} + +:is( + :is( + :is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) + #descriptionInstruction + ) + #newAltTextDisclaimer + )::before { + content: ''; + display: inline-block; + width: 17px; + height: 16px; + -webkit-mask-image: var(--new-alt-text-ai-disclaimer-icon); + mask-image: var(--new-alt-text-ai-disclaimer-icon); + -webkit-mask-size: cover; + mask-size: cover; + background-color: var(--text-secondary-color); + flex: 1 0 auto; +} + +:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) + #newAltTextDownloadModel { + display: flex; + align-items: center; + gap: 4px; + align-self: stretch; +} + +:is( + :is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) + #newAltTextDownloadModel + )::before { + content: ''; + display: inline-block; + width: 16px; + height: 16px; + -webkit-mask-image: var(--new-alt-text-spinner-icon); + mask-image: var(--new-alt-text-spinner-icon); + -webkit-mask-size: cover; + mask-size: cover; + background-color: var(--text-secondary-color); +} + +:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) + #newAltTextImagePreview { + width: 180px; + aspect-ratio: 1; + display: flex; + justify-content: center; + align-items: center; + flex: 0 0 auto; + background-color: var(--preview-image-bg-color); + border: var(--preview-image-border); +} + +:is( + :is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) + #newAltTextImagePreview + ) + > canvas { + max-width: 100%; + max-height: 100%; +} + +.colorPicker { + --hover-outline-color: #0250bb; + --selected-outline-color: #0060df; + --swatch-border-color: #cfcfd8; +} + +@media (prefers-color-scheme: dark) { + :where(html:not(.is-light)) .colorPicker { + --hover-outline-color: #80ebff; + --selected-outline-color: #aaf2ff; + --swatch-border-color: #52525e; + } +} + +:where(html.is-dark) .colorPicker { + --hover-outline-color: #80ebff; + --selected-outline-color: #aaf2ff; + --swatch-border-color: #52525e; +} + +@media screen and (forced-colors: active) { + .colorPicker { + --hover-outline-color: Highlight; + --selected-outline-color: var(--hover-outline-color); + --swatch-border-color: ButtonText; + } +} + +.colorPicker .swatch { + width: 16px; + height: 16px; + border: 1px solid var(--swatch-border-color); + border-radius: 100%; + outline-offset: 2px; + box-sizing: border-box; + forced-color-adjust: none; +} + +.colorPicker button:is(:hover, .selected) > .swatch { + border: none; +} + +.annotationEditorLayer[data-main-rotation='0'] + .highlightEditor:not(.free) + > .editToolbar { + rotate: 0deg; +} + +.annotationEditorLayer[data-main-rotation='90'] + .highlightEditor:not(.free) + > .editToolbar { + rotate: 270deg; +} + +.annotationEditorLayer[data-main-rotation='180'] + .highlightEditor:not(.free) + > .editToolbar { + rotate: 180deg; +} + +.annotationEditorLayer[data-main-rotation='270'] + .highlightEditor:not(.free) + > .editToolbar { + rotate: 90deg; +} + +.annotationEditorLayer .highlightEditor { + position: absolute; + background: transparent; + z-index: 1; + cursor: auto; + max-width: 100%; + max-height: 100%; + border: none; + outline: none; + pointer-events: none; + transform-origin: 0 0; +} + +:is(.annotationEditorLayer .highlightEditor):not(.free) { + transform: none; +} + +:is(.annotationEditorLayer .highlightEditor) .internal { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: auto; +} + +.disabled:is(.annotationEditorLayer .highlightEditor) .internal { + pointer-events: none; +} + +.selectedEditor:is(.annotationEditorLayer .highlightEditor) .internal { + cursor: pointer; +} + +:is(.annotationEditorLayer .highlightEditor) .editToolbar { + --editor-toolbar-colorpicker-arrow-image: url(images/toolbarButton-menuArrow.svg); + + transform-origin: center !important; +} + +:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) + .colorPicker { + position: relative; + width: auto; + display: flex; + justify-content: center; + align-items: center; + gap: 4px; + padding: 4px; +} + +:is( + :is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) + .colorPicker + )::after { + content: ''; + -webkit-mask-image: var(--editor-toolbar-colorpicker-arrow-image); + mask-image: var(--editor-toolbar-colorpicker-arrow-image); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: center; + mask-position: center; + display: inline-block; + background-color: var(--editor-toolbar-fg-color); + width: 12px; + height: 12px; +} + +:is( + :is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) + .colorPicker + ):hover::after { + background-color: var(--editor-toolbar-hover-fg-color); +} + +:is( + :is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) + .colorPicker + ):has(.dropdown:not(.hidden)) { + background-color: var(--editor-toolbar-hover-bg-color); +} + +:is( + :is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) + .colorPicker + ):has(.dropdown:not(.hidden))::after { + scale: -1; +} + +:is( + :is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) + .colorPicker + ) + .dropdown { + position: absolute; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + gap: 11px; + padding-block: 8px; + border-radius: 6px; + background-color: var(--editor-toolbar-bg-color); + border: 1px solid var(--editor-toolbar-border-color); + box-shadow: var(--editor-toolbar-shadow); + inset-block-start: calc(100% + 4px); + width: calc(100% + 2 * var(--editor-toolbar-padding)); +} + +:is( + :is( + :is( + :is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) + .buttons + ) + .colorPicker + ) + .dropdown + ) + button { + width: 100%; + height: auto; + border: none; + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + background: none; +} + +:is( + :is( + :is( + :is( + :is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) + .buttons + ) + .colorPicker + ) + .dropdown + ) + button + ):is(:active, :focus-visible) { + outline: none; +} + +:is( + :is( + :is( + :is( + :is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) + .buttons + ) + .colorPicker + ) + .dropdown + ) + button + ) + > .swatch { + outline-offset: 2px; +} + +[aria-selected='true']:is( + :is( + :is( + :is( + :is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) + .buttons + ) + .colorPicker + ) + .dropdown + ) + button + ) + > .swatch { + outline: 2px solid var(--selected-outline-color); +} + +:is( + :is( + :is( + :is( + :is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) + .buttons + ) + .colorPicker + ) + .dropdown + ) + button + ):is(:hover, :active, :focus-visible) + > .swatch { + outline: 2px solid var(--hover-outline-color); +} + +.editorParamsToolbar:has(#highlightParamsToolbarContainer) { + padding: unset; +} + +#highlightParamsToolbarContainer { + gap: 16px; + padding-inline: 10px; + padding-block-end: 12px; +} + +#highlightParamsToolbarContainer .colorPicker { + display: flex; + flex-direction: column; + gap: 8px; +} + +:is(#highlightParamsToolbarContainer .colorPicker) .dropdown { + display: flex; + justify-content: space-between; + align-items: center; + flex-direction: row; + height: auto; +} + +:is(:is(#highlightParamsToolbarContainer .colorPicker) .dropdown) button { + width: auto; + height: auto; + border: none; + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + background: none; + flex: 0 0 auto; + padding: 0; +} + +:is(:is(:is(#highlightParamsToolbarContainer .colorPicker) .dropdown) button) + .swatch { + width: 24px; + height: 24px; +} + +:is( + :is(:is(#highlightParamsToolbarContainer .colorPicker) .dropdown) button + ):is(:active, :focus-visible) { + outline: none; +} + +[aria-selected='true']:is( + :is(:is(#highlightParamsToolbarContainer .colorPicker) .dropdown) button + ) + > .swatch { + outline: 2px solid var(--selected-outline-color); +} + +:is( + :is(:is(#highlightParamsToolbarContainer .colorPicker) .dropdown) button + ):is(:hover, :active, :focus-visible) + > .swatch { + outline: 2px solid var(--hover-outline-color); +} + +#highlightParamsToolbarContainer #editorHighlightThickness { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + align-self: stretch; +} + +:is(#highlightParamsToolbarContainer #editorHighlightThickness) + .editorParamsLabel { + height: auto; + align-self: stretch; +} + +:is(#highlightParamsToolbarContainer #editorHighlightThickness) + .thicknessPicker { + display: flex; + justify-content: space-between; + align-items: center; + align-self: stretch; + + --example-color: #bfbfc9; +} + +@media (prefers-color-scheme: dark) { + :where(html:not(.is-light)) + :is(#highlightParamsToolbarContainer #editorHighlightThickness) + .thicknessPicker { + --example-color: #80808e; + } +} + +:where(html.is-dark) + :is(#highlightParamsToolbarContainer #editorHighlightThickness) + .thicknessPicker { + --example-color: #80808e; +} + +@media screen and (forced-colors: active) { + :is(#highlightParamsToolbarContainer #editorHighlightThickness) + .thicknessPicker { + --example-color: CanvasText; + } +} + +:is( + :is( + :is(#highlightParamsToolbarContainer #editorHighlightThickness) + .thicknessPicker + ) + > .editorParamsSlider[disabled] +) { + opacity: 0.4; +} + +:is( + :is(#highlightParamsToolbarContainer #editorHighlightThickness) + .thicknessPicker + )::before, +:is( + :is(#highlightParamsToolbarContainer #editorHighlightThickness) + .thicknessPicker + )::after { + content: ''; + width: 8px; + aspect-ratio: 1; + display: block; + border-radius: 100%; + background-color: var(--example-color); +} + +:is( + :is(#highlightParamsToolbarContainer #editorHighlightThickness) + .thicknessPicker + )::after { + width: 24px; +} + +:is( + :is(#highlightParamsToolbarContainer #editorHighlightThickness) + .thicknessPicker + ) + .editorParamsSlider { + width: unset; + height: 14px; +} + +#highlightParamsToolbarContainer #editorHighlightVisibility { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + align-self: stretch; +} + +:is(#highlightParamsToolbarContainer #editorHighlightVisibility) .divider { + --divider-color: #d7d7db; +} + +@media (prefers-color-scheme: dark) { + :where(html:not(.is-light)) + :is(#highlightParamsToolbarContainer #editorHighlightVisibility) + .divider { + --divider-color: #8f8f9d; + } +} + +:where(html.is-dark) + :is(#highlightParamsToolbarContainer #editorHighlightVisibility) + .divider { + --divider-color: #8f8f9d; +} + +@media screen and (forced-colors: active) { + :is(#highlightParamsToolbarContainer #editorHighlightVisibility) .divider { + --divider-color: CanvasText; + } +} + +:is(#highlightParamsToolbarContainer #editorHighlightVisibility) .divider { + margin-block: 4px; + width: 100%; + height: 1px; + background-color: var(--divider-color); +} + +:is(#highlightParamsToolbarContainer #editorHighlightVisibility) .toggler { + display: flex; + justify-content: space-between; + align-items: center; + align-self: stretch; +} + +#altTextSettingsDialog { + padding: 16px; +} + +#altTextSettingsDialog #altTextSettingsContainer { + display: flex; + width: 573px; + flex-direction: column; + gap: 16px; +} + +:is(#altTextSettingsDialog #altTextSettingsContainer) .mainContainer { + gap: 16px; +} + +:is(#altTextSettingsDialog #altTextSettingsContainer) .description { + color: var(--text-secondary-color); +} + +:is(#altTextSettingsDialog #altTextSettingsContainer) #aiModelSettings { + display: flex; + flex-direction: column; + gap: 12px; +} + +:is(:is(#altTextSettingsDialog #altTextSettingsContainer) #aiModelSettings) + button { + width: -moz-fit-content; + width: fit-content; +} + +.download:is( + :is(#altTextSettingsDialog #altTextSettingsContainer) #aiModelSettings + ) + #deleteModelButton { + display: none; +} + +:is(:is(#altTextSettingsDialog #altTextSettingsContainer) #aiModelSettings):not( + .download + ) + #downloadModelButton { + display: none; +} + +:is(#altTextSettingsDialog #altTextSettingsContainer) #automaticAltText, +:is(#altTextSettingsDialog #altTextSettingsContainer) #altTextEditor { + display: flex; + flex-direction: column; + gap: 8px; +} + +:is(#altTextSettingsDialog #altTextSettingsContainer) #createModelDescription, +:is(#altTextSettingsDialog #altTextSettingsContainer) #aiModelSettings, +:is(#altTextSettingsDialog #altTextSettingsContainer) + #showAltTextDialogDescription { + padding-inline-start: 40px; +} + +:is(#altTextSettingsDialog #altTextSettingsContainer) #automaticSettings { + display: flex; + flex-direction: column; + gap: 16px; +} + +:root { + --viewer-container-height: 0; + --pdfViewer-padding-bottom: 0; + --page-margin: 1px auto -8px; + --page-border: 9px solid transparent; + --spreadHorizontalWrapped-margin-LR: -3.5px; + --loading-icon-delay: 400ms; +} + +@media screen and (forced-colors: active) { + :root { + --pdfViewer-padding-bottom: 9px; + --page-margin: 8px auto -1px; + --page-border: 1px solid CanvasText; + --spreadHorizontalWrapped-margin-LR: 3.5px; + } +} + +[data-main-rotation='90'] { + transform: rotate(90deg) translateY(-100%); +} +[data-main-rotation='180'] { + transform: rotate(180deg) translate(-100%, -100%); +} +[data-main-rotation='270'] { + transform: rotate(270deg) translateX(-100%); } #hiddenCopyElement, -.hiddenCanvasElement{ - position:absolute; - top:0; - left:0; - width:0; - height:0; - display:none; +.hiddenCanvasElement { + position: absolute; + top: 0; + left: 0; + width: 0; + height: 0; + display: none; } -.pdfViewer{ - --scale-factor:1; - --page-bg-color:unset; +.pdfViewer { + --scale-factor: 1; + --page-bg-color: unset; - padding-bottom:var(--pdfViewer-padding-bottom); + padding-bottom: var(--pdfViewer-padding-bottom); - --hcm-highlight-filter:none; - --hcm-highlight-selected-filter:none; + --hcm-highlight-filter: none; + --hcm-highlight-selected-filter: none; } -@media screen and (forced-colors: active){ - -.pdfViewer{ - --hcm-highlight-filter:invert(100%); -} +@media screen and (forced-colors: active) { + .pdfViewer { + --hcm-highlight-filter: invert(100%); } - -.pdfViewer.copyAll{ - cursor:wait; - } - -.pdfViewer .canvasWrapper{ - overflow:hidden; - width:100%; - height:100%; - } - -:is(.pdfViewer .canvasWrapper) canvas{ - position:absolute; - top:0; - left:0; - margin:0; - display:block; - width:100%; - height:100%; - contain:content; - } - -:is(:is(.pdfViewer .canvasWrapper) canvas) .structTree{ - contain:strict; - } - -.pdfViewer .page{ - --scale-round-x:1px; - --scale-round-y:1px; - - direction:ltr; - width:816px; - height:1056px; - margin:var(--page-margin); - position:relative; - overflow:visible; - border:var(--page-border); - background-clip:content-box; - background-color:var(--page-bg-color, rgb(255 255 255)); } -.pdfViewer .dummyPage{ - position:relative; - width:0; - height:var(--viewer-container-height); +.pdfViewer.copyAll { + cursor: wait; } -.pdfViewer.noUserSelect{ - -webkit-user-select:none; - -moz-user-select:none; - user-select:none; +.pdfViewer .canvasWrapper { + overflow: hidden; + width: 100%; + height: 100%; } -.pdfViewer.removePageBorders .page{ - margin:0 auto 10px; - border:none; +:is(.pdfViewer .canvasWrapper) canvas { + position: absolute; + top: 0; + left: 0; + margin: 0; + display: block; + width: 100%; + height: 100%; + contain: content; +} + +:is(:is(.pdfViewer .canvasWrapper) canvas) .structTree { + contain: strict; +} + +.pdfViewer .page { + --scale-round-x: 1px; + --scale-round-y: 1px; + + direction: ltr; + width: 816px; + height: 1056px; + margin: var(--page-margin); + position: relative; + overflow: visible; + border: var(--page-border); + background-clip: content-box; + background-color: var(--page-bg-color, rgb(255 255 255)); +} + +.pdfViewer .dummyPage { + position: relative; + width: 0; + height: var(--viewer-container-height); +} + +.pdfViewer.noUserSelect { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.pdfViewer.removePageBorders .page { + margin: 0 auto 10px; + border: none; } .pdfViewer:is(.scrollHorizontal, .scrollWrapped), -.spread{ - margin-inline:3.5px; - text-align:center; +.spread { + margin-inline: 3.5px; + text-align: center; } .pdfViewer.scrollHorizontal, -.spread{ - white-space:nowrap; +.spread { + white-space: nowrap; } .pdfViewer.removePageBorders, -.pdfViewer:is(.scrollHorizontal, .scrollWrapped) .spread{ - margin-inline:0; +.pdfViewer:is(.scrollHorizontal, .scrollWrapped) .spread { + margin-inline: 0; } .spread :is(.page, .dummyPage), -.pdfViewer:is(.scrollHorizontal, .scrollWrapped) :is(.page, .spread){ - display:inline-block; - vertical-align:middle; +.pdfViewer:is(.scrollHorizontal, .scrollWrapped) :is(.page, .spread) { + display: inline-block; + vertical-align: middle; } .spread .page, -.pdfViewer:is(.scrollHorizontal, .scrollWrapped) .page{ - margin-inline:var(--spreadHorizontalWrapped-margin-LR); +.pdfViewer:is(.scrollHorizontal, .scrollWrapped) .page { + margin-inline: var(--spreadHorizontalWrapped-margin-LR); } .pdfViewer.removePageBorders .spread .page, -.pdfViewer.removePageBorders:is(.scrollHorizontal, .scrollWrapped) .page{ - margin-inline:5px; +.pdfViewer.removePageBorders:is(.scrollHorizontal, .scrollWrapped) .page { + margin-inline: 5px; } -.pdfViewer .page.loadingIcon::after{ - position:absolute; - top:0; - left:0; - content:""; - width:100%; - height:100%; - background:url("images/loading-icon.gif") center no-repeat; - display:none; - transition-property:display; - transition-delay:var(--loading-icon-delay); - z-index:5; - contain:strict; +.pdfViewer .page.loadingIcon::after { + position: absolute; + top: 0; + left: 0; + content: ''; + width: 100%; + height: 100%; + background: url('images/loading-icon.gif') center no-repeat; + display: none; + transition-property: display; + transition-delay: var(--loading-icon-delay); + z-index: 5; + contain: strict; } -.pdfViewer .page.loading::after{ - display:block; +.pdfViewer .page.loading::after { + display: block; } -.pdfViewer .page:not(.loading)::after{ - transition-property:none; - display:none; +.pdfViewer .page:not(.loading)::after { + transition-property: none; + display: none; } -.pdfPresentationMode .pdfViewer{ - padding-bottom:0; +.pdfPresentationMode .pdfViewer { + padding-bottom: 0; } -.pdfPresentationMode .spread{ - margin:0; +.pdfPresentationMode .spread { + margin: 0; } -.pdfPresentationMode .pdfViewer .page{ - margin:0 auto; - border:2px solid transparent; +.pdfPresentationMode .pdfViewer .page { + margin: 0 auto; + border: 2px solid transparent; } -:root{ - --dir-factor:1; - --inline-start:left; - --inline-end:right; +:root { + --dir-factor: 1; + --inline-start: left; + --inline-end: right; - --sidebar-width:200px; - --sidebar-transition-duration:200ms; - --sidebar-transition-timing-function:ease; + --sidebar-width: 200px; + --sidebar-transition-duration: 200ms; + --sidebar-transition-timing-function: ease; - --toolbar-height:32px; - --toolbar-horizontal-padding:1px; - --toolbar-vertical-padding:2px; - --icon-size:16px; + --toolbar-height: 32px; + --toolbar-horizontal-padding: 1px; + --toolbar-vertical-padding: 2px; + --icon-size: 16px; - --toolbar-icon-opacity:0.7; - --doorhanger-icon-opacity:0.9; - --doorhanger-height:8px; + --toolbar-icon-opacity: 0.7; + --doorhanger-icon-opacity: 0.9; + --doorhanger-height: 8px; - --main-color:rgb(12 12 13); - --body-bg-color:rgb(212 212 215); - --progressBar-color:rgb(10 132 255); - --progressBar-bg-color:rgb(221 221 222); - --progressBar-blend-color:rgb(116 177 239); - --scrollbar-color:auto; - --scrollbar-bg-color:auto; - --toolbar-icon-bg-color:rgb(0 0 0); - --toolbar-icon-hover-bg-color:rgb(0 0 0); + --main-color: rgb(12 12 13); + --body-bg-color: rgb(212 212 215); + --progressBar-color: rgb(10 132 255); + --progressBar-bg-color: rgb(221 221 222); + --progressBar-blend-color: rgb(116 177 239); + --scrollbar-color: auto; + --scrollbar-bg-color: auto; + --toolbar-icon-bg-color: rgb(0 0 0); + --toolbar-icon-hover-bg-color: rgb(0 0 0); - --sidebar-narrow-bg-color:rgb(212 212 215 / 0.9); - --sidebar-toolbar-bg-color:rgb(245 246 247); - --toolbar-bg-color:rgb(249 249 250); - --toolbar-border-color:rgb(184 184 184); - --toolbar-box-shadow:0 1px 0 var(--toolbar-border-color); - --toolbar-border-bottom:none; - --toolbarSidebar-box-shadow:inset calc(-1px * var(--dir-factor)) 0 0 rgb(0 0 0 / 0.25), 0 1px 0 rgb(0 0 0 / 0.15), 0 0 1px rgb(0 0 0 / 0.1); - --toolbarSidebar-border-bottom:none; - --button-hover-color:rgb(221 222 223); - --toggled-btn-color:rgb(0 0 0); - --toggled-btn-bg-color:rgb(0 0 0 / 0.3); - --toggled-hover-active-btn-color:rgb(0 0 0 / 0.4); - --toggled-hover-btn-outline:none; - --dropdown-btn-bg-color:rgb(215 215 219); - --dropdown-btn-border:none; - --separator-color:rgb(0 0 0 / 0.3); - --field-color:rgb(6 6 6); - --field-bg-color:rgb(255 255 255); - --field-border-color:rgb(187 187 188); - --treeitem-color:rgb(0 0 0 / 0.8); - --treeitem-bg-color:rgb(0 0 0 / 0.15); - --treeitem-hover-color:rgb(0 0 0 / 0.9); - --treeitem-selected-color:rgb(0 0 0 / 0.9); - --treeitem-selected-bg-color:rgb(0 0 0 / 0.25); - --thumbnail-hover-color:rgb(0 0 0 / 0.1); - --thumbnail-selected-color:rgb(0 0 0 / 0.2); - --doorhanger-bg-color:rgb(255 255 255); - --doorhanger-border-color:rgb(12 12 13 / 0.2); - --doorhanger-hover-color:rgb(12 12 13); - --doorhanger-hover-bg-color:rgb(237 237 237); - --doorhanger-separator-color:rgb(222 222 222); - --dialog-button-border:none; - --dialog-button-bg-color:rgb(12 12 13 / 0.1); - --dialog-button-hover-bg-color:rgb(12 12 13 / 0.3); + --sidebar-narrow-bg-color: rgb(212 212 215 / 0.9); + --sidebar-toolbar-bg-color: rgb(245 246 247); + --toolbar-bg-color: rgb(249 249 250); + --toolbar-border-color: rgb(184 184 184); + --toolbar-box-shadow: 0 1px 0 var(--toolbar-border-color); + --toolbar-border-bottom: none; + --toolbarSidebar-box-shadow: + inset calc(-1px * var(--dir-factor)) 0 0 rgb(0 0 0 / 0.25), + 0 1px 0 rgb(0 0 0 / 0.15), 0 0 1px rgb(0 0 0 / 0.1); + --toolbarSidebar-border-bottom: none; + --button-hover-color: rgb(221 222 223); + --toggled-btn-color: rgb(0 0 0); + --toggled-btn-bg-color: rgb(0 0 0 / 0.3); + --toggled-hover-active-btn-color: rgb(0 0 0 / 0.4); + --toggled-hover-btn-outline: none; + --dropdown-btn-bg-color: rgb(215 215 219); + --dropdown-btn-border: none; + --separator-color: rgb(0 0 0 / 0.3); + --field-color: rgb(6 6 6); + --field-bg-color: rgb(255 255 255); + --field-border-color: rgb(187 187 188); + --treeitem-color: rgb(0 0 0 / 0.8); + --treeitem-bg-color: rgb(0 0 0 / 0.15); + --treeitem-hover-color: rgb(0 0 0 / 0.9); + --treeitem-selected-color: rgb(0 0 0 / 0.9); + --treeitem-selected-bg-color: rgb(0 0 0 / 0.25); + --thumbnail-hover-color: rgb(0 0 0 / 0.1); + --thumbnail-selected-color: rgb(0 0 0 / 0.2); + --doorhanger-bg-color: rgb(255 255 255); + --doorhanger-border-color: rgb(12 12 13 / 0.2); + --doorhanger-hover-color: rgb(12 12 13); + --doorhanger-hover-bg-color: rgb(237 237 237); + --doorhanger-separator-color: rgb(222 222 222); + --dialog-button-border: none; + --dialog-button-bg-color: rgb(12 12 13 / 0.1); + --dialog-button-hover-bg-color: rgb(12 12 13 / 0.3); - --loading-icon:url(images/loading.svg); - --treeitem-expanded-icon:url(images/treeitem-expanded.svg); - --treeitem-collapsed-icon:url(images/treeitem-collapsed.svg); - --toolbarButton-editorFreeText-icon:url(images/toolbarButton-editorFreeText.svg); - --toolbarButton-editorHighlight-icon:url(images/toolbarButton-editorHighlight.svg); - --toolbarButton-editorInk-icon:url(images/toolbarButton-editorInk.svg); - --toolbarButton-editorStamp-icon:url(images/toolbarButton-editorStamp.svg); - --toolbarButton-menuArrow-icon:url(images/toolbarButton-menuArrow.svg); - --toolbarButton-sidebarToggle-icon:url(images/toolbarButton-sidebarToggle.svg); - --toolbarButton-secondaryToolbarToggle-icon:url(images/toolbarButton-secondaryToolbarToggle.svg); - --toolbarButton-pageUp-icon:url(images/toolbarButton-pageUp.svg); - --toolbarButton-pageDown-icon:url(images/toolbarButton-pageDown.svg); - --toolbarButton-zoomOut-icon:url(images/toolbarButton-zoomOut.svg); - --toolbarButton-zoomIn-icon:url(images/toolbarButton-zoomIn.svg); - --toolbarButton-presentationMode-icon:url(images/toolbarButton-presentationMode.svg); - --toolbarButton-print-icon:url(images/toolbarButton-print.svg); - --toolbarButton-openFile-icon:url(images/toolbarButton-openFile.svg); - --toolbarButton-download-icon:url(images/toolbarButton-download.svg); - --toolbarButton-bookmark-icon:url(images/toolbarButton-bookmark.svg); - --toolbarButton-viewThumbnail-icon:url(images/toolbarButton-viewThumbnail.svg); - --toolbarButton-viewOutline-icon:url(images/toolbarButton-viewOutline.svg); - --toolbarButton-viewAttachments-icon:url(images/toolbarButton-viewAttachments.svg); - --toolbarButton-viewLayers-icon:url(images/toolbarButton-viewLayers.svg); - --toolbarButton-currentOutlineItem-icon:url(images/toolbarButton-currentOutlineItem.svg); - --toolbarButton-search-icon:url(images/toolbarButton-search.svg); - --findbarButton-previous-icon:url(images/findbarButton-previous.svg); - --findbarButton-next-icon:url(images/findbarButton-next.svg); - --secondaryToolbarButton-firstPage-icon:url(images/secondaryToolbarButton-firstPage.svg); - --secondaryToolbarButton-lastPage-icon:url(images/secondaryToolbarButton-lastPage.svg); - --secondaryToolbarButton-rotateCcw-icon:url(images/secondaryToolbarButton-rotateCcw.svg); - --secondaryToolbarButton-rotateCw-icon:url(images/secondaryToolbarButton-rotateCw.svg); - --secondaryToolbarButton-selectTool-icon:url(images/secondaryToolbarButton-selectTool.svg); - --secondaryToolbarButton-handTool-icon:url(images/secondaryToolbarButton-handTool.svg); - --secondaryToolbarButton-scrollPage-icon:url(images/secondaryToolbarButton-scrollPage.svg); - --secondaryToolbarButton-scrollVertical-icon:url(images/secondaryToolbarButton-scrollVertical.svg); - --secondaryToolbarButton-scrollHorizontal-icon:url(images/secondaryToolbarButton-scrollHorizontal.svg); - --secondaryToolbarButton-scrollWrapped-icon:url(images/secondaryToolbarButton-scrollWrapped.svg); - --secondaryToolbarButton-spreadNone-icon:url(images/secondaryToolbarButton-spreadNone.svg); - --secondaryToolbarButton-spreadOdd-icon:url(images/secondaryToolbarButton-spreadOdd.svg); - --secondaryToolbarButton-spreadEven-icon:url(images/secondaryToolbarButton-spreadEven.svg); - --secondaryToolbarButton-imageAltTextSettings-icon:var( + --loading-icon: url(images/loading.svg); + --treeitem-expanded-icon: url(images/treeitem-expanded.svg); + --treeitem-collapsed-icon: url(images/treeitem-collapsed.svg); + --toolbarButton-editorFreeText-icon: url(images/toolbarButton-editorFreeText.svg); + --toolbarButton-editorHighlight-icon: url(images/toolbarButton-editorHighlight.svg); + --toolbarButton-editorInk-icon: url(images/toolbarButton-editorInk.svg); + --toolbarButton-editorStamp-icon: url(images/toolbarButton-editorStamp.svg); + --toolbarButton-menuArrow-icon: url(images/toolbarButton-menuArrow.svg); + --toolbarButton-sidebarToggle-icon: url(images/toolbarButton-sidebarToggle.svg); + --toolbarButton-secondaryToolbarToggle-icon: url(images/toolbarButton-secondaryToolbarToggle.svg); + --toolbarButton-pageUp-icon: url(images/toolbarButton-pageUp.svg); + --toolbarButton-pageDown-icon: url(images/toolbarButton-pageDown.svg); + --toolbarButton-zoomOut-icon: url(images/toolbarButton-zoomOut.svg); + --toolbarButton-zoomIn-icon: url(images/toolbarButton-zoomIn.svg); + --toolbarButton-presentationMode-icon: url(images/toolbarButton-presentationMode.svg); + --toolbarButton-print-icon: url(images/toolbarButton-print.svg); + --toolbarButton-openFile-icon: url(images/toolbarButton-openFile.svg); + --toolbarButton-download-icon: url(images/toolbarButton-download.svg); + --toolbarButton-bookmark-icon: url(images/toolbarButton-bookmark.svg); + --toolbarButton-viewThumbnail-icon: url(images/toolbarButton-viewThumbnail.svg); + --toolbarButton-viewOutline-icon: url(images/toolbarButton-viewOutline.svg); + --toolbarButton-viewAttachments-icon: url(images/toolbarButton-viewAttachments.svg); + --toolbarButton-viewLayers-icon: url(images/toolbarButton-viewLayers.svg); + --toolbarButton-currentOutlineItem-icon: url(images/toolbarButton-currentOutlineItem.svg); + --toolbarButton-search-icon: url(images/toolbarButton-search.svg); + --findbarButton-previous-icon: url(images/findbarButton-previous.svg); + --findbarButton-next-icon: url(images/findbarButton-next.svg); + --secondaryToolbarButton-firstPage-icon: url(images/secondaryToolbarButton-firstPage.svg); + --secondaryToolbarButton-lastPage-icon: url(images/secondaryToolbarButton-lastPage.svg); + --secondaryToolbarButton-rotateCcw-icon: url(images/secondaryToolbarButton-rotateCcw.svg); + --secondaryToolbarButton-rotateCw-icon: url(images/secondaryToolbarButton-rotateCw.svg); + --secondaryToolbarButton-selectTool-icon: url(images/secondaryToolbarButton-selectTool.svg); + --secondaryToolbarButton-handTool-icon: url(images/secondaryToolbarButton-handTool.svg); + --secondaryToolbarButton-scrollPage-icon: url(images/secondaryToolbarButton-scrollPage.svg); + --secondaryToolbarButton-scrollVertical-icon: url(images/secondaryToolbarButton-scrollVertical.svg); + --secondaryToolbarButton-scrollHorizontal-icon: url(images/secondaryToolbarButton-scrollHorizontal.svg); + --secondaryToolbarButton-scrollWrapped-icon: url(images/secondaryToolbarButton-scrollWrapped.svg); + --secondaryToolbarButton-spreadNone-icon: url(images/secondaryToolbarButton-spreadNone.svg); + --secondaryToolbarButton-spreadOdd-icon: url(images/secondaryToolbarButton-spreadOdd.svg); + --secondaryToolbarButton-spreadEven-icon: url(images/secondaryToolbarButton-spreadEven.svg); + --secondaryToolbarButton-imageAltTextSettings-icon: var( --toolbarButton-editorStamp-icon ); - --secondaryToolbarButton-documentProperties-icon:url(images/secondaryToolbarButton-documentProperties.svg); - --editorParams-stampAddImage-icon:url(images/toolbarButton-zoomIn.svg); + --secondaryToolbarButton-documentProperties-icon: url(images/secondaryToolbarButton-documentProperties.svg); + --editorParams-stampAddImage-icon: url(images/toolbarButton-zoomIn.svg); } -[dir="rtl"]:root{ - --dir-factor:-1; - --inline-start:right; - --inline-end:left; +[dir='rtl']:root { + --dir-factor: -1; + --inline-start: right; + --inline-end: left; } -@media (prefers-color-scheme: dark){ - :root:where(:not(.is-light)){ - --main-color:rgb(249 249 250); - --body-bg-color:rgb(42 42 46); - --progressBar-color:rgb(0 96 223); - --progressBar-bg-color:rgb(40 40 43); - --progressBar-blend-color:rgb(20 68 133); - --scrollbar-color:rgb(121 121 123); - --scrollbar-bg-color:rgb(35 35 39); - --toolbar-icon-bg-color:rgb(255 255 255); - --toolbar-icon-hover-bg-color:rgb(255 255 255); +@media (prefers-color-scheme: dark) { + :root:where(:not(.is-light)) { + --main-color: rgb(249 249 250); + --body-bg-color: rgb(42 42 46); + --progressBar-color: rgb(0 96 223); + --progressBar-bg-color: rgb(40 40 43); + --progressBar-blend-color: rgb(20 68 133); + --scrollbar-color: rgb(121 121 123); + --scrollbar-bg-color: rgb(35 35 39); + --toolbar-icon-bg-color: rgb(255 255 255); + --toolbar-icon-hover-bg-color: rgb(255 255 255); - --sidebar-narrow-bg-color:rgb(42 42 46 / 0.9); - --sidebar-toolbar-bg-color:rgb(50 50 52); - --toolbar-bg-color:rgb(56 56 61); - --toolbar-border-color:rgb(12 12 13); - --button-hover-color:rgb(102 102 103); - --toggled-btn-color:rgb(255 255 255); - --toggled-btn-bg-color:rgb(0 0 0 / 0.3); - --toggled-hover-active-btn-color:rgb(0 0 0 / 0.4); - --dropdown-btn-bg-color:rgb(74 74 79); - --separator-color:rgb(0 0 0 / 0.3); - --field-color:rgb(250 250 250); - --field-bg-color:rgb(64 64 68); - --field-border-color:rgb(115 115 115); - --treeitem-color:rgb(255 255 255 / 0.8); - --treeitem-bg-color:rgb(255 255 255 / 0.15); - --treeitem-hover-color:rgb(255 255 255 / 0.9); - --treeitem-selected-color:rgb(255 255 255 / 0.9); - --treeitem-selected-bg-color:rgb(255 255 255 / 0.25); - --thumbnail-hover-color:rgb(255 255 255 / 0.1); - --thumbnail-selected-color:rgb(255 255 255 / 0.2); - --doorhanger-bg-color:rgb(74 74 79); - --doorhanger-border-color:rgb(39 39 43); - --doorhanger-hover-color:rgb(249 249 250); - --doorhanger-hover-bg-color:rgb(93 94 98); - --doorhanger-separator-color:rgb(92 92 97); - --dialog-button-bg-color:rgb(92 92 97); - --dialog-button-hover-bg-color:rgb(115 115 115); + --sidebar-narrow-bg-color: rgb(42 42 46 / 0.9); + --sidebar-toolbar-bg-color: rgb(50 50 52); + --toolbar-bg-color: rgb(56 56 61); + --toolbar-border-color: rgb(12 12 13); + --button-hover-color: rgb(102 102 103); + --toggled-btn-color: rgb(255 255 255); + --toggled-btn-bg-color: rgb(0 0 0 / 0.3); + --toggled-hover-active-btn-color: rgb(0 0 0 / 0.4); + --dropdown-btn-bg-color: rgb(74 74 79); + --separator-color: rgb(0 0 0 / 0.3); + --field-color: rgb(250 250 250); + --field-bg-color: rgb(64 64 68); + --field-border-color: rgb(115 115 115); + --treeitem-color: rgb(255 255 255 / 0.8); + --treeitem-bg-color: rgb(255 255 255 / 0.15); + --treeitem-hover-color: rgb(255 255 255 / 0.9); + --treeitem-selected-color: rgb(255 255 255 / 0.9); + --treeitem-selected-bg-color: rgb(255 255 255 / 0.25); + --thumbnail-hover-color: rgb(255 255 255 / 0.1); + --thumbnail-selected-color: rgb(255 255 255 / 0.2); + --doorhanger-bg-color: rgb(74 74 79); + --doorhanger-border-color: rgb(39 39 43); + --doorhanger-hover-color: rgb(249 249 250); + --doorhanger-hover-bg-color: rgb(93 94 98); + --doorhanger-separator-color: rgb(92 92 97); + --dialog-button-bg-color: rgb(92 92 97); + --dialog-button-hover-bg-color: rgb(115 115 115); } } -:root:where(.is-dark){ - --main-color:rgb(249 249 250); - --body-bg-color:rgb(42 42 46); - --progressBar-color:rgb(0 96 223); - --progressBar-bg-color:rgb(40 40 43); - --progressBar-blend-color:rgb(20 68 133); - --scrollbar-color:rgb(121 121 123); - --scrollbar-bg-color:rgb(35 35 39); - --toolbar-icon-bg-color:rgb(255 255 255); - --toolbar-icon-hover-bg-color:rgb(255 255 255); +:root:where(.is-dark) { + --main-color: rgb(249 249 250); + --body-bg-color: rgb(42 42 46); + --progressBar-color: rgb(0 96 223); + --progressBar-bg-color: rgb(40 40 43); + --progressBar-blend-color: rgb(20 68 133); + --scrollbar-color: rgb(121 121 123); + --scrollbar-bg-color: rgb(35 35 39); + --toolbar-icon-bg-color: rgb(255 255 255); + --toolbar-icon-hover-bg-color: rgb(255 255 255); - --sidebar-narrow-bg-color:rgb(42 42 46 / 0.9); - --sidebar-toolbar-bg-color:rgb(50 50 52); - --toolbar-bg-color:rgb(56 56 61); - --toolbar-border-color:rgb(12 12 13); - --button-hover-color:rgb(102 102 103); - --toggled-btn-color:rgb(255 255 255); - --toggled-btn-bg-color:rgb(0 0 0 / 0.3); - --toggled-hover-active-btn-color:rgb(0 0 0 / 0.4); - --dropdown-btn-bg-color:rgb(74 74 79); - --separator-color:rgb(0 0 0 / 0.3); - --field-color:rgb(250 250 250); - --field-bg-color:rgb(64 64 68); - --field-border-color:rgb(115 115 115); - --treeitem-color:rgb(255 255 255 / 0.8); - --treeitem-bg-color:rgb(255 255 255 / 0.15); - --treeitem-hover-color:rgb(255 255 255 / 0.9); - --treeitem-selected-color:rgb(255 255 255 / 0.9); - --treeitem-selected-bg-color:rgb(255 255 255 / 0.25); - --thumbnail-hover-color:rgb(255 255 255 / 0.1); - --thumbnail-selected-color:rgb(255 255 255 / 0.2); - --doorhanger-bg-color:rgb(74 74 79); - --doorhanger-border-color:rgb(39 39 43); - --doorhanger-hover-color:rgb(249 249 250); - --doorhanger-hover-bg-color:rgb(93 94 98); - --doorhanger-separator-color:rgb(92 92 97); - --dialog-button-bg-color:rgb(92 92 97); - --dialog-button-hover-bg-color:rgb(115 115 115); - } + --sidebar-narrow-bg-color: rgb(42 42 46 / 0.9); + --sidebar-toolbar-bg-color: rgb(50 50 52); + --toolbar-bg-color: rgb(56 56 61); + --toolbar-border-color: rgb(12 12 13); + --button-hover-color: rgb(102 102 103); + --toggled-btn-color: rgb(255 255 255); + --toggled-btn-bg-color: rgb(0 0 0 / 0.3); + --toggled-hover-active-btn-color: rgb(0 0 0 / 0.4); + --dropdown-btn-bg-color: rgb(74 74 79); + --separator-color: rgb(0 0 0 / 0.3); + --field-color: rgb(250 250 250); + --field-bg-color: rgb(64 64 68); + --field-border-color: rgb(115 115 115); + --treeitem-color: rgb(255 255 255 / 0.8); + --treeitem-bg-color: rgb(255 255 255 / 0.15); + --treeitem-hover-color: rgb(255 255 255 / 0.9); + --treeitem-selected-color: rgb(255 255 255 / 0.9); + --treeitem-selected-bg-color: rgb(255 255 255 / 0.25); + --thumbnail-hover-color: rgb(255 255 255 / 0.1); + --thumbnail-selected-color: rgb(255 255 255 / 0.2); + --doorhanger-bg-color: rgb(74 74 79); + --doorhanger-border-color: rgb(39 39 43); + --doorhanger-hover-color: rgb(249 249 250); + --doorhanger-hover-bg-color: rgb(93 94 98); + --doorhanger-separator-color: rgb(92 92 97); + --dialog-button-bg-color: rgb(92 92 97); + --dialog-button-hover-bg-color: rgb(115 115 115); +} -@media screen and (forced-colors: active){ - :root{ - --button-hover-color:Highlight; - --doorhanger-hover-bg-color:Highlight; - --toolbar-icon-opacity:1; - --toolbar-icon-bg-color:ButtonText; - --toolbar-icon-hover-bg-color:ButtonFace; - --toggled-hover-active-btn-color:ButtonText; - --toggled-hover-btn-outline:2px solid ButtonBorder; - --toolbar-border-color:CanvasText; - --toolbar-border-bottom:1px solid var(--toolbar-border-color); - --toolbar-box-shadow:none; - --toggled-btn-color:HighlightText; - --toggled-btn-bg-color:LinkText; - --doorhanger-hover-color:ButtonFace; - --doorhanger-border-color-whcm:1px solid ButtonText; - --doorhanger-triangle-opacity-whcm:0; - --dialog-button-border:1px solid Highlight; - --dialog-button-hover-bg-color:Highlight; - --dialog-button-hover-color:ButtonFace; - --dropdown-btn-border:1px solid ButtonText; - --field-border-color:ButtonText; - --main-color:CanvasText; - --separator-color:GrayText; - --doorhanger-separator-color:GrayText; - --toolbarSidebar-box-shadow:none; - --toolbarSidebar-border-bottom:1px solid var(--toolbar-border-color); +@media screen and (forced-colors: active) { + :root { + --button-hover-color: Highlight; + --doorhanger-hover-bg-color: Highlight; + --toolbar-icon-opacity: 1; + --toolbar-icon-bg-color: ButtonText; + --toolbar-icon-hover-bg-color: ButtonFace; + --toggled-hover-active-btn-color: ButtonText; + --toggled-hover-btn-outline: 2px solid ButtonBorder; + --toolbar-border-color: CanvasText; + --toolbar-border-bottom: 1px solid var(--toolbar-border-color); + --toolbar-box-shadow: none; + --toggled-btn-color: HighlightText; + --toggled-btn-bg-color: LinkText; + --doorhanger-hover-color: ButtonFace; + --doorhanger-border-color-whcm: 1px solid ButtonText; + --doorhanger-triangle-opacity-whcm: 0; + --dialog-button-border: 1px solid Highlight; + --dialog-button-hover-bg-color: Highlight; + --dialog-button-hover-color: ButtonFace; + --dropdown-btn-border: 1px solid ButtonText; + --field-border-color: ButtonText; + --main-color: CanvasText; + --separator-color: GrayText; + --doorhanger-separator-color: GrayText; + --toolbarSidebar-box-shadow: none; + --toolbarSidebar-border-bottom: 1px solid var(--toolbar-border-color); } } -@media screen and (prefers-reduced-motion: reduce){ - :root{ - --sidebar-transition-duration:0; +@media screen and (prefers-reduced-motion: reduce) { + :root { + --sidebar-transition-duration: 0; } } -@keyframes progressIndeterminate{ - 0%{ - transform:translateX(calc(-142px * var(--dir-factor))); +@keyframes progressIndeterminate { + 0% { + transform: translateX(calc(-142px * var(--dir-factor))); } - 100%{ - transform:translateX(0); + 100% { + transform: translateX(0); } } -html[data-toolbar-density="compact"]{ - --toolbar-height:30px; - } +html[data-toolbar-density='compact'] { + --toolbar-height: 30px; +} -html[data-toolbar-density="touch"]{ - --toolbar-height:44px; - } +html[data-toolbar-density='touch'] { + --toolbar-height: 44px; +} html, -body{ - height:100%; - width:100%; +body { + height: 100%; + width: 100%; } -body{ - margin:0; - background-color:var(--body-bg-color); - scrollbar-color:var(--scrollbar-color) var(--scrollbar-bg-color); +body { + margin: 0; + background-color: var(--body-bg-color); + scrollbar-color: var(--scrollbar-color) var(--scrollbar-bg-color); } -body.wait::before{ - content:""; - position:fixed; - width:100%; - height:100%; - z-index:100000; - cursor:wait; - } +body.wait::before { + content: ''; + position: fixed; + width: 100%; + height: 100%; + z-index: 100000; + cursor: wait; +} .hidden, -[hidden]{ - display:none !important; +[hidden] { + display: none !important; } -#viewerContainer.pdfPresentationMode:fullscreen{ - top:0; - background-color:rgb(0 0 0); - width:100%; - height:100%; - overflow:hidden; - cursor:none; - -webkit-user-select:none; - -moz-user-select:none; - user-select:none; +#viewerContainer.pdfPresentationMode:fullscreen { + top: 0; + background-color: rgb(0 0 0); + width: 100%; + height: 100%; + overflow: hidden; + cursor: none; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; } -.pdfPresentationMode:fullscreen section:not([data-internal-link]){ - pointer-events:none; +.pdfPresentationMode:fullscreen section:not([data-internal-link]) { + pointer-events: none; } -.pdfPresentationMode:fullscreen .textLayer span{ - cursor:none; +.pdfPresentationMode:fullscreen .textLayer span { + cursor: none; } .pdfPresentationMode.pdfPresentationModeControls > *, -.pdfPresentationMode.pdfPresentationModeControls .textLayer span{ - cursor:default; +.pdfPresentationMode.pdfPresentationModeControls .textLayer span { + cursor: default; } -#outerContainer{ - width:100%; - height:100%; - position:relative; - margin:0; +#outerContainer { + width: 100%; + height: 100%; + position: relative; + margin: 0; } -#sidebarContainer{ - position:absolute; - inset-block:var(--toolbar-height) 0; - inset-inline-start:calc(-1 * var(--sidebar-width)); - width:var(--sidebar-width); - visibility:hidden; - z-index:1; - font:message-box; - border-top:1px solid transparent; - border-inline-end:var(--doorhanger-border-color-whcm); - transition-property:inset-inline-start; - transition-duration:var(--sidebar-transition-duration); - transition-timing-function:var(--sidebar-transition-timing-function); +#sidebarContainer { + position: absolute; + inset-block: var(--toolbar-height) 0; + inset-inline-start: calc(-1 * var(--sidebar-width)); + width: var(--sidebar-width); + visibility: hidden; + z-index: 1; + font: message-box; + border-top: 1px solid transparent; + border-inline-end: var(--doorhanger-border-color-whcm); + transition-property: inset-inline-start; + transition-duration: var(--sidebar-transition-duration); + transition-timing-function: var(--sidebar-transition-timing-function); } -#outerContainer:is(.sidebarMoving, .sidebarOpen) #sidebarContainer{ - visibility:visible; +#outerContainer:is(.sidebarMoving, .sidebarOpen) #sidebarContainer { + visibility: visible; } -#outerContainer.sidebarOpen #sidebarContainer{ - inset-inline-start:0; +#outerContainer.sidebarOpen #sidebarContainer { + inset-inline-start: 0; } -#mainContainer{ - position:absolute; - inset:0; - min-width:350px; - margin:0; - display:flex; - flex-direction:column; +#mainContainer { + position: absolute; + inset: 0; + min-width: 350px; + margin: 0; + display: flex; + flex-direction: column; } -#sidebarContent{ - inset-block:var(--toolbar-height) 0; - inset-inline-start:0; - overflow:auto; - position:absolute; - width:100%; - box-shadow:inset calc(-1px * var(--dir-factor)) 0 0 rgb(0 0 0 / 0.25); +#sidebarContent { + inset-block: var(--toolbar-height) 0; + inset-inline-start: 0; + overflow: auto; + position: absolute; + width: 100%; + box-shadow: inset calc(-1px * var(--dir-factor)) 0 0 rgb(0 0 0 / 0.25); } -#viewerContainer{ - overflow:auto; - position:absolute; - inset:var(--toolbar-height) 0 0; - outline:none; - z-index:0; +#viewerContainer { + overflow: auto; + position: absolute; + inset: var(--toolbar-height) 0 0; + outline: none; + z-index: 0; } -#viewerContainer:not(.pdfPresentationMode){ - transition-duration:var(--sidebar-transition-duration); - transition-timing-function:var(--sidebar-transition-timing-function); +#viewerContainer:not(.pdfPresentationMode) { + transition-duration: var(--sidebar-transition-duration); + transition-timing-function: var(--sidebar-transition-timing-function); } -#outerContainer.sidebarOpen #viewerContainer:not(.pdfPresentationMode){ - inset-inline-start:var(--sidebar-width); - transition-property:inset-inline-start; +#outerContainer.sidebarOpen #viewerContainer:not(.pdfPresentationMode) { + inset-inline-start: var(--sidebar-width); + transition-property: inset-inline-start; } -#sidebarContainer :is(input, button, select){ - font:message-box; +#sidebarContainer :is(input, button, select) { + font: message-box; } -.toolbar{ - z-index:2; +.toolbar { + z-index: 2; } -#toolbarSidebar{ - width:100%; - height:var(--toolbar-height); - background-color:var(--sidebar-toolbar-bg-color); - box-shadow:var(--toolbarSidebar-box-shadow); - border-bottom:var(--toolbarSidebar-border-bottom); - padding:var(--toolbar-vertical-padding) var(--toolbar-horizontal-padding); - justify-content:space-between; +#toolbarSidebar { + width: 100%; + height: var(--toolbar-height); + background-color: var(--sidebar-toolbar-bg-color); + box-shadow: var(--toolbarSidebar-box-shadow); + border-bottom: var(--toolbarSidebar-border-bottom); + padding: var(--toolbar-vertical-padding) var(--toolbar-horizontal-padding); + justify-content: space-between; } -#toolbarSidebar #toolbarSidebarLeft{ - width:auto; - height:100%; - } - -:is(#toolbarSidebar #toolbarSidebarLeft) #viewThumbnail::before{ - -webkit-mask-image:var(--toolbarButton-viewThumbnail-icon); - mask-image:var(--toolbarButton-viewThumbnail-icon); - } - -:is(#toolbarSidebar #toolbarSidebarLeft) #viewOutline::before{ - -webkit-mask-image:var(--toolbarButton-viewOutline-icon); - mask-image:var(--toolbarButton-viewOutline-icon); - transform:scaleX(var(--dir-factor)); - } - -:is(#toolbarSidebar #toolbarSidebarLeft) #viewAttachments::before{ - -webkit-mask-image:var(--toolbarButton-viewAttachments-icon); - mask-image:var(--toolbarButton-viewAttachments-icon); - } - -:is(#toolbarSidebar #toolbarSidebarLeft) #viewLayers::before{ - -webkit-mask-image:var(--toolbarButton-viewLayers-icon); - mask-image:var(--toolbarButton-viewLayers-icon); - } - -#toolbarSidebar #toolbarSidebarRight{ - width:auto; - height:100%; - padding-inline-end:2px; - } - -#sidebarResizer{ - position:absolute; - inset-block:0; - inset-inline-end:-6px; - width:6px; - z-index:200; - cursor:ew-resize; +#toolbarSidebar #toolbarSidebarLeft { + width: auto; + height: 100%; } -#outerContainer.sidebarOpen #loadingBar{ - inset-inline-start:var(--sidebar-width); +:is(#toolbarSidebar #toolbarSidebarLeft) #viewThumbnail::before { + -webkit-mask-image: var(--toolbarButton-viewThumbnail-icon); + mask-image: var(--toolbarButton-viewThumbnail-icon); +} + +:is(#toolbarSidebar #toolbarSidebarLeft) #viewOutline::before { + -webkit-mask-image: var(--toolbarButton-viewOutline-icon); + mask-image: var(--toolbarButton-viewOutline-icon); + transform: scaleX(var(--dir-factor)); +} + +:is(#toolbarSidebar #toolbarSidebarLeft) #viewAttachments::before { + -webkit-mask-image: var(--toolbarButton-viewAttachments-icon); + mask-image: var(--toolbarButton-viewAttachments-icon); +} + +:is(#toolbarSidebar #toolbarSidebarLeft) #viewLayers::before { + -webkit-mask-image: var(--toolbarButton-viewLayers-icon); + mask-image: var(--toolbarButton-viewLayers-icon); +} + +#toolbarSidebar #toolbarSidebarRight { + width: auto; + height: 100%; + padding-inline-end: 2px; +} + +#sidebarResizer { + position: absolute; + inset-block: 0; + inset-inline-end: -6px; + width: 6px; + z-index: 200; + cursor: ew-resize; +} + +#outerContainer.sidebarOpen #loadingBar { + inset-inline-start: var(--sidebar-width); } #outerContainer.sidebarResizing - :is(#sidebarContainer, #viewerContainer, #loadingBar){ - transition-duration:0s; + :is(#sidebarContainer, #viewerContainer, #loadingBar) { + transition-duration: 0s; } .doorHanger, -.doorHangerRight{ - border-radius:2px; - box-shadow:0 1px 5px var(--doorhanger-border-color), 0 0 0 1px var(--doorhanger-border-color); - border:var(--doorhanger-border-color-whcm); - background-color:var(--doorhanger-bg-color); - inset-block-start:calc(100% + var(--doorhanger-height) - 2px); +.doorHangerRight { + border-radius: 2px; + box-shadow: + 0 1px 5px var(--doorhanger-border-color), + 0 0 0 1px var(--doorhanger-border-color); + border: var(--doorhanger-border-color-whcm); + background-color: var(--doorhanger-bg-color); + inset-block-start: calc(100% + var(--doorhanger-height) - 2px); } -:is(.doorHanger,.doorHangerRight)::after,:is(.doorHanger,.doorHangerRight)::before{ - bottom:100%; - border-style:solid; - border-color:transparent; - content:""; - height:0; - width:0; - position:absolute; - pointer-events:none; - opacity:var(--doorhanger-triangle-opacity-whcm); - } - -:is(.doorHanger,.doorHangerRight)::before{ - border-width:calc(var(--doorhanger-height) + 2px); - border-bottom-color:var(--doorhanger-border-color); - } - -:is(.doorHanger,.doorHangerRight)::after{ - border-width:var(--doorhanger-height); - } - -.doorHangerRight{ - inset-inline-end:calc(50% - var(--doorhanger-height) - 1px); +:is(.doorHanger, .doorHangerRight)::after, +:is(.doorHanger, .doorHangerRight)::before { + bottom: 100%; + border-style: solid; + border-color: transparent; + content: ''; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + opacity: var(--doorhanger-triangle-opacity-whcm); } -.doorHangerRight::before{ - inset-inline-end:-1px; - } - -.doorHangerRight::after{ - border-bottom-color:var(--doorhanger-bg-color); - inset-inline-end:1px; - } - -.doorHanger{ - inset-inline-start:calc(50% - var(--doorhanger-height) - 1px); +:is(.doorHanger, .doorHangerRight)::before { + border-width: calc(var(--doorhanger-height) + 2px); + border-bottom-color: var(--doorhanger-border-color); } -.doorHanger::before{ - inset-inline-start:-1px; - } - -.doorHanger::after{ - border-bottom-color:var(--toolbar-bg-color); - inset-inline-start:1px; - } - -.dialogButton{ - border:none; - background:none; - width:28px; - height:28px; - outline:none; +:is(.doorHanger, .doorHangerRight)::after { + border-width: var(--doorhanger-height); } -.dialogButton:is(:hover, :focus-visible){ - background-color:var(--dialog-button-hover-bg-color); +.doorHangerRight { + inset-inline-end: calc(50% - var(--doorhanger-height) - 1px); } -.dialogButton:is(:hover, :focus-visible) > span{ - color:var(--dialog-button-hover-color); +.doorHangerRight::before { + inset-inline-end: -1px; } -.splitToolbarButtonSeparator{ - float:var(--inline-start); - width:0; - height:62%; - border-left:1px solid var(--separator-color); - border-right:none; +.doorHangerRight::after { + border-bottom-color: var(--doorhanger-bg-color); + inset-inline-end: 1px; } -.dialogButton{ - min-width:16px; - margin:2px 1px; - padding:2px 6px 0; - border:none; - border-radius:2px; - color:var(--main-color); - font-size:12px; - line-height:14px; - -webkit-user-select:none; - -moz-user-select:none; - user-select:none; - cursor:default; - box-sizing:border-box; +.doorHanger { + inset-inline-start: calc(50% - var(--doorhanger-height) - 1px); } -.treeItemToggler::before{ - position:absolute; - display:inline-block; - width:16px; - height:16px; - - content:""; - background-color:var(--toolbar-icon-bg-color); - -webkit-mask-size:cover; - mask-size:cover; +.doorHanger::before { + inset-inline-start: -1px; } -#sidebarToggleButton::before{ - -webkit-mask-image:var(--toolbarButton-sidebarToggle-icon); - mask-image:var(--toolbarButton-sidebarToggle-icon); - transform:scaleX(var(--dir-factor)); +.doorHanger::after { + border-bottom-color: var(--toolbar-bg-color); + inset-inline-start: 1px; } -#secondaryToolbarToggleButton::before{ - -webkit-mask-image:var(--toolbarButton-secondaryToolbarToggle-icon); - mask-image:var(--toolbarButton-secondaryToolbarToggle-icon); - transform:scaleX(var(--dir-factor)); +.dialogButton { + border: none; + background: none; + width: 28px; + height: 28px; + outline: none; } -#previous::before{ - -webkit-mask-image:var(--toolbarButton-pageUp-icon); - mask-image:var(--toolbarButton-pageUp-icon); +.dialogButton:is(:hover, :focus-visible) { + background-color: var(--dialog-button-hover-bg-color); } -#next::before{ - -webkit-mask-image:var(--toolbarButton-pageDown-icon); - mask-image:var(--toolbarButton-pageDown-icon); +.dialogButton:is(:hover, :focus-visible) > span { + color: var(--dialog-button-hover-color); } -#zoomOutButton::before{ - -webkit-mask-image:var(--toolbarButton-zoomOut-icon); - mask-image:var(--toolbarButton-zoomOut-icon); +.splitToolbarButtonSeparator { + float: var(--inline-start); + width: 0; + height: 62%; + border-left: 1px solid var(--separator-color); + border-right: none; } -#zoomInButton::before{ - -webkit-mask-image:var(--toolbarButton-zoomIn-icon); - mask-image:var(--toolbarButton-zoomIn-icon); +.dialogButton { + min-width: 16px; + margin: 2px 1px; + padding: 2px 6px 0; + border: none; + border-radius: 2px; + color: var(--main-color); + font-size: 12px; + line-height: 14px; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + cursor: default; + box-sizing: border-box; } -#presentationMode::before{ - -webkit-mask-image:var(--toolbarButton-presentationMode-icon); - mask-image:var(--toolbarButton-presentationMode-icon); +.treeItemToggler::before { + position: absolute; + display: inline-block; + width: 16px; + height: 16px; + + content: ''; + background-color: var(--toolbar-icon-bg-color); + -webkit-mask-size: cover; + mask-size: cover; } -#editorFreeTextButton::before{ - -webkit-mask-image:var(--toolbarButton-editorFreeText-icon); - mask-image:var(--toolbarButton-editorFreeText-icon); +#sidebarToggleButton::before { + -webkit-mask-image: var(--toolbarButton-sidebarToggle-icon); + mask-image: var(--toolbarButton-sidebarToggle-icon); + transform: scaleX(var(--dir-factor)); } -#editorHighlightButton::before{ - -webkit-mask-image:var(--toolbarButton-editorHighlight-icon); - mask-image:var(--toolbarButton-editorHighlight-icon); +#secondaryToolbarToggleButton::before { + -webkit-mask-image: var(--toolbarButton-secondaryToolbarToggle-icon); + mask-image: var(--toolbarButton-secondaryToolbarToggle-icon); + transform: scaleX(var(--dir-factor)); } -#editorInkButton::before{ - -webkit-mask-image:var(--toolbarButton-editorInk-icon); - mask-image:var(--toolbarButton-editorInk-icon); +#previous::before { + -webkit-mask-image: var(--toolbarButton-pageUp-icon); + mask-image: var(--toolbarButton-pageUp-icon); } -#editorStampButton::before{ - -webkit-mask-image:var(--toolbarButton-editorStamp-icon); - mask-image:var(--toolbarButton-editorStamp-icon); +#next::before { + -webkit-mask-image: var(--toolbarButton-pageDown-icon); + mask-image: var(--toolbarButton-pageDown-icon); } -#printButton::before{ - -webkit-mask-image:var(--toolbarButton-print-icon); - mask-image:var(--toolbarButton-print-icon); +#zoomOutButton::before { + -webkit-mask-image: var(--toolbarButton-zoomOut-icon); + mask-image: var(--toolbarButton-zoomOut-icon); } -#secondaryOpenFile::before{ - -webkit-mask-image:var(--toolbarButton-openFile-icon); - mask-image:var(--toolbarButton-openFile-icon); +#zoomInButton::before { + -webkit-mask-image: var(--toolbarButton-zoomIn-icon); + mask-image: var(--toolbarButton-zoomIn-icon); } -#downloadButton::before{ - -webkit-mask-image:var(--toolbarButton-download-icon); - mask-image:var(--toolbarButton-download-icon); +#presentationMode::before { + -webkit-mask-image: var(--toolbarButton-presentationMode-icon); + mask-image: var(--toolbarButton-presentationMode-icon); } -#viewBookmark::before{ - -webkit-mask-image:var(--toolbarButton-bookmark-icon); - mask-image:var(--toolbarButton-bookmark-icon); +#editorFreeTextButton::before { + -webkit-mask-image: var(--toolbarButton-editorFreeText-icon); + mask-image: var(--toolbarButton-editorFreeText-icon); } -#currentOutlineItem::before{ - -webkit-mask-image:var(--toolbarButton-currentOutlineItem-icon); - mask-image:var(--toolbarButton-currentOutlineItem-icon); - transform:scaleX(var(--dir-factor)); +#editorHighlightButton::before { + -webkit-mask-image: var(--toolbarButton-editorHighlight-icon); + mask-image: var(--toolbarButton-editorHighlight-icon); } -#viewFindButton::before{ - -webkit-mask-image:var(--toolbarButton-search-icon); - mask-image:var(--toolbarButton-search-icon); +#editorInkButton::before { + -webkit-mask-image: var(--toolbarButton-editorInk-icon); + mask-image: var(--toolbarButton-editorInk-icon); } -.pdfSidebarNotification::after{ - position:absolute; - display:inline-block; - top:2px; - inset-inline-end:2px; - content:""; - background-color:rgb(112 219 85); - height:9px; - width:9px; - border-radius:50%; +#editorStampButton::before { + -webkit-mask-image: var(--toolbarButton-editorStamp-icon); + mask-image: var(--toolbarButton-editorStamp-icon); } -.verticalToolbarSeparator{ - display:block; - margin-inline:2px; - width:0; - height:80%; - border-left:1px solid var(--separator-color); - border-right:none; - box-sizing:border-box; +#printButton::before { + -webkit-mask-image: var(--toolbarButton-print-icon); + mask-image: var(--toolbarButton-print-icon); } -.horizontalToolbarSeparator{ - display:block; - margin:6px 0; - border-top:1px solid var(--doorhanger-separator-color); - border-bottom:none; - height:0; - width:100%; +#secondaryOpenFile::before { + -webkit-mask-image: var(--toolbarButton-openFile-icon); + mask-image: var(--toolbarButton-openFile-icon); } -.toggleButton{ - display:inline; +#downloadButton::before { + -webkit-mask-image: var(--toolbarButton-download-icon); + mask-image: var(--toolbarButton-download-icon); } -.toggleButton:has( > input:checked){ - color:var(--toggled-btn-color); - background-color:var(--toggled-btn-bg-color); - } - -.toggleButton:is(:hover,:has( > input:focus-visible)){ - color:var(--toggled-btn-color); - background-color:var(--button-hover-color); - } - -.toggleButton > input{ - position:absolute; - top:50%; - left:50%; - opacity:0; - width:0; - height:0; - } - -.toolbarField{ - padding:4px 7px; - margin:3px 0; - border-radius:2px; - background-color:var(--field-bg-color); - background-clip:padding-box; - border:1px solid var(--field-border-color); - box-shadow:none; - color:var(--field-color); - font-size:12px; - line-height:16px; - outline:none; +#viewBookmark::before { + -webkit-mask-image: var(--toolbarButton-bookmark-icon); + mask-image: var(--toolbarButton-bookmark-icon); } -.toolbarField:focus{ - border-color:#0a84ff; - } - -#pageNumber{ - -moz-appearance:textfield; - text-align:end; - width:40px; - background-size:0 0; - transition-property:none; +#currentOutlineItem::before { + -webkit-mask-image: var(--toolbarButton-currentOutlineItem-icon); + mask-image: var(--toolbarButton-currentOutlineItem-icon); + transform: scaleX(var(--dir-factor)); } -#pageNumber::-webkit-inner-spin-button{ - -webkit-appearance:none; - } - -.loadingInput:has( > .loading:is(#pageNumber))::after{ - display:inline; - visibility:visible; - - transition-property:visibility; - transition-delay:var(--loading-icon-delay); - } - -.loadingInput{ - position:relative; +#viewFindButton::before { + -webkit-mask-image: var(--toolbarButton-search-icon); + mask-image: var(--toolbarButton-search-icon); } -.loadingInput::after{ - position:absolute; - visibility:hidden; - display:none; - width:var(--icon-size); - height:var(--icon-size); +.pdfSidebarNotification::after { + position: absolute; + display: inline-block; + top: 2px; + inset-inline-end: 2px; + content: ''; + background-color: rgb(112 219 85); + height: 9px; + width: 9px; + border-radius: 50%; +} - content:""; - background-color:var(--toolbar-icon-bg-color); - -webkit-mask-size:cover; - mask-size:cover; - -webkit-mask-image:var(--loading-icon); - mask-image:var(--loading-icon); - } +.verticalToolbarSeparator { + display: block; + margin-inline: 2px; + width: 0; + height: 80%; + border-left: 1px solid var(--separator-color); + border-right: none; + box-sizing: border-box; +} -.loadingInput.start::after{ - inset-inline-start:4px; - } +.horizontalToolbarSeparator { + display: block; + margin: 6px 0; + border-top: 1px solid var(--doorhanger-separator-color); + border-bottom: none; + height: 0; + width: 100%; +} -.loadingInput.end::after{ - inset-inline-end:4px; - } +.toggleButton { + display: inline; +} + +.toggleButton:has(> input:checked) { + color: var(--toggled-btn-color); + background-color: var(--toggled-btn-bg-color); +} + +.toggleButton:is(:hover, :has(> input:focus-visible)) { + color: var(--toggled-btn-color); + background-color: var(--button-hover-color); +} + +.toggleButton > input { + position: absolute; + top: 50%; + left: 50%; + opacity: 0; + width: 0; + height: 0; +} + +.toolbarField { + padding: 4px 7px; + margin: 3px 0; + border-radius: 2px; + background-color: var(--field-bg-color); + background-clip: padding-box; + border: 1px solid var(--field-border-color); + box-shadow: none; + color: var(--field-color); + font-size: 12px; + line-height: 16px; + outline: none; +} + +.toolbarField:focus { + border-color: #0a84ff; +} + +#pageNumber { + -moz-appearance: textfield; + text-align: end; + width: 40px; + background-size: 0 0; + transition-property: none; +} + +#pageNumber::-webkit-inner-spin-button { + -webkit-appearance: none; +} + +.loadingInput:has(> .loading:is(#pageNumber))::after { + display: inline; + visibility: visible; + + transition-property: visibility; + transition-delay: var(--loading-icon-delay); +} + +.loadingInput { + position: relative; +} + +.loadingInput::after { + position: absolute; + visibility: hidden; + display: none; + width: var(--icon-size); + height: var(--icon-size); + + content: ''; + background-color: var(--toolbar-icon-bg-color); + -webkit-mask-size: cover; + mask-size: cover; + -webkit-mask-image: var(--loading-icon); + mask-image: var(--loading-icon); +} + +.loadingInput.start::after { + inset-inline-start: 4px; +} + +.loadingInput.end::after { + inset-inline-end: 4px; +} #thumbnailView, #outlineView, #attachmentsView, -#layersView{ - position:absolute; - width:calc(100% - 8px); - inset-block:0; - padding:4px 4px 0; - overflow:auto; - -webkit-user-select:none; - -moz-user-select:none; - user-select:none; +#layersView { + position: absolute; + width: calc(100% - 8px); + inset-block: 0; + padding: 4px 4px 0; + overflow: auto; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; } -#thumbnailView{ - width:calc(100% - 60px); - padding:10px 30px 0; +#thumbnailView { + width: calc(100% - 60px); + padding: 10px 30px 0; } -#thumbnailView > a:is(:active, :focus){ - outline:0; +#thumbnailView > a:is(:active, :focus) { + outline: 0; } -.thumbnail{ - --thumbnail-width:0; - --thumbnail-height:0; +.thumbnail { + --thumbnail-width: 0; + --thumbnail-height: 0; - float:var(--inline-start); - width:var(--thumbnail-width); - height:var(--thumbnail-height); - margin:0 10px 5px; - padding:1px; - border:7px solid transparent; - border-radius:2px; + float: var(--inline-start); + width: var(--thumbnail-width); + height: var(--thumbnail-height); + margin: 0 10px 5px; + padding: 1px; + border: 7px solid transparent; + border-radius: 2px; } -#thumbnailView > a:last-of-type > .thumbnail{ - margin-bottom:10px; +#thumbnailView > a:last-of-type > .thumbnail { + margin-bottom: 10px; } a:focus > .thumbnail, -.thumbnail:hover{ - border-color:var(--thumbnail-hover-color); +.thumbnail:hover { + border-color: var(--thumbnail-hover-color); } -.thumbnail.selected{ - border-color:var(--thumbnail-selected-color) !important; +.thumbnail.selected { + border-color: var(--thumbnail-selected-color) !important; } -.thumbnailImage{ - width:var(--thumbnail-width); - height:var(--thumbnail-height); - opacity:0.9; +.thumbnailImage { + width: var(--thumbnail-width); + height: var(--thumbnail-height); + opacity: 0.9; } a:focus > .thumbnail > .thumbnailImage, -.thumbnail:hover > .thumbnailImage{ - opacity:0.95; +.thumbnail:hover > .thumbnailImage { + opacity: 0.95; } -.thumbnail.selected > .thumbnailImage{ - opacity:1 !important; +.thumbnail.selected > .thumbnailImage { + opacity: 1 !important; } -.thumbnail:not([data-loaded]) > .thumbnailImage{ - width:calc(var(--thumbnail-width) - 2px); - height:calc(var(--thumbnail-height) - 2px); - border:1px dashed rgb(132 132 132); +.thumbnail:not([data-loaded]) > .thumbnailImage { + width: calc(var(--thumbnail-width) - 2px); + height: calc(var(--thumbnail-height) - 2px); + border: 1px dashed rgb(132 132 132); } .treeWithDeepNesting > .treeItem, -.treeItem > .treeItems{ - margin-inline-start:20px; +.treeItem > .treeItems { + margin-inline-start: 20px; } -.treeItem > a{ - text-decoration:none; - display:inline-block; - min-width:calc(100% - 4px); - height:auto; - margin-bottom:1px; - padding:2px 0 5px; - padding-inline-start:4px; - border-radius:2px; - color:var(--treeitem-color); - font-size:13px; - line-height:15px; - -webkit-user-select:none; - -moz-user-select:none; - user-select:none; - white-space:normal; - cursor:pointer; +.treeItem > a { + text-decoration: none; + display: inline-block; + min-width: calc(100% - 4px); + height: auto; + margin-bottom: 1px; + padding: 2px 0 5px; + padding-inline-start: 4px; + border-radius: 2px; + color: var(--treeitem-color); + font-size: 13px; + line-height: 15px; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + white-space: normal; + cursor: pointer; } -#layersView .treeItem > a *{ - cursor:pointer; +#layersView .treeItem > a * { + cursor: pointer; } -#layersView .treeItem > a > label{ - padding-inline-start:4px; +#layersView .treeItem > a > label { + padding-inline-start: 4px; } -#layersView .treeItem > a > label > input{ - float:var(--inline-start); - margin-top:1px; +#layersView .treeItem > a > label > input { + float: var(--inline-start); + margin-top: 1px; } -.treeItemToggler{ - position:relative; - float:var(--inline-start); - height:0; - width:0; - color:rgb(255 255 255 / 0.5); +.treeItemToggler { + position: relative; + float: var(--inline-start); + height: 0; + width: 0; + color: rgb(255 255 255 / 0.5); } -.treeItemToggler::before{ - inset-inline-end:4px; - -webkit-mask-image:var(--treeitem-expanded-icon); - mask-image:var(--treeitem-expanded-icon); +.treeItemToggler::before { + inset-inline-end: 4px; + -webkit-mask-image: var(--treeitem-expanded-icon); + mask-image: var(--treeitem-expanded-icon); } -.treeItemToggler.treeItemsHidden::before{ - -webkit-mask-image:var(--treeitem-collapsed-icon); - mask-image:var(--treeitem-collapsed-icon); - transform:scaleX(var(--dir-factor)); +.treeItemToggler.treeItemsHidden::before { + -webkit-mask-image: var(--treeitem-collapsed-icon); + mask-image: var(--treeitem-collapsed-icon); + transform: scaleX(var(--dir-factor)); } -.treeItemToggler.treeItemsHidden ~ .treeItems{ - display:none; +.treeItemToggler.treeItemsHidden ~ .treeItems { + display: none; } -.treeItem.selected > a{ - background-color:var(--treeitem-selected-bg-color); - color:var(--treeitem-selected-color); +.treeItem.selected > a { + background-color: var(--treeitem-selected-bg-color); + color: var(--treeitem-selected-color); } .treeItemToggler:hover, .treeItemToggler:hover + a, .treeItemToggler:hover ~ .treeItems, -.treeItem > a:hover{ - background-color:var(--treeitem-bg-color); - background-clip:padding-box; - border-radius:2px; - color:var(--treeitem-hover-color); +.treeItem > a:hover { + background-color: var(--treeitem-bg-color); + background-clip: padding-box; + border-radius: 2px; + color: var(--treeitem-hover-color); } -#outlineOptionsContainer{ - display:none; +#outlineOptionsContainer { + display: none; } -#sidebarContainer:has(#outlineView:not(.hidden)) #outlineOptionsContainer{ - display:inline flex; - } - -.dialogButton{ - width:auto; - margin:3px 4px 2px !important; - padding:2px 11px; - color:var(--main-color); - background-color:var(--dialog-button-bg-color); - border:var(--dialog-button-border) !important; +#sidebarContainer:has(#outlineView:not(.hidden)) #outlineOptionsContainer { + display: inline flex; } -dialog{ - margin:auto; - padding:15px; - border-spacing:4px; - color:var(--main-color); - font:message-box; - font-size:12px; - line-height:14px; - background-color:var(--doorhanger-bg-color); - border:1px solid rgb(0 0 0 / 0.5); - border-radius:4px; - box-shadow:0 1px 4px rgb(0 0 0 / 0.3); +.dialogButton { + width: auto; + margin: 3px 4px 2px !important; + padding: 2px 11px; + color: var(--main-color); + background-color: var(--dialog-button-bg-color); + border: var(--dialog-button-border) !important; } -dialog::backdrop{ - background-color:rgb(0 0 0 / 0.2); +dialog { + margin: auto; + padding: 15px; + border-spacing: 4px; + color: var(--main-color); + font: message-box; + font-size: 12px; + line-height: 14px; + background-color: var(--doorhanger-bg-color); + border: 1px solid rgb(0 0 0 / 0.5); + border-radius: 4px; + box-shadow: 0 1px 4px rgb(0 0 0 / 0.3); } -dialog > .row{ - display:table-row; +dialog::backdrop { + background-color: rgb(0 0 0 / 0.2); } -dialog > .row > *{ - display:table-cell; +dialog > .row { + display: table-row; } -dialog .toolbarField{ - margin:5px 0; +dialog > .row > * { + display: table-cell; } -dialog .separator{ - display:block; - margin:4px 0; - height:0; - width:100%; - border-top:1px solid var(--separator-color); - border-bottom:none; +dialog .toolbarField { + margin: 5px 0; } -dialog .buttonRow{ - text-align:center; - vertical-align:middle; +dialog .separator { + display: block; + margin: 4px 0; + height: 0; + width: 100%; + border-top: 1px solid var(--separator-color); + border-bottom: none; } -dialog :link{ - color:rgb(255 255 255); +dialog .buttonRow { + text-align: center; + vertical-align: middle; } -#passwordDialog{ - text-align:center; +dialog :link { + color: rgb(255 255 255); } -#passwordDialog .toolbarField{ - width:200px; +#passwordDialog { + text-align: center; } -#documentPropertiesDialog{ - text-align:left; +#passwordDialog .toolbarField { + width: 200px; } -#documentPropertiesDialog .row > *{ - min-width:100px; - text-align:start; +#documentPropertiesDialog { + text-align: left; } -#documentPropertiesDialog .row > span{ - width:125px; - word-wrap:break-word; +#documentPropertiesDialog .row > * { + min-width: 100px; + text-align: start; } -#documentPropertiesDialog .row > p{ - max-width:225px; - word-wrap:break-word; +#documentPropertiesDialog .row > span { + width: 125px; + word-wrap: break-word; } -#documentPropertiesDialog .buttonRow{ - margin-top:10px; +#documentPropertiesDialog .row > p { + max-width: 225px; + word-wrap: break-word; } -.grab-to-pan-grab{ - cursor:grab !important; +#documentPropertiesDialog .buttonRow { + margin-top: 10px; +} + +.grab-to-pan-grab { + cursor: grab !important; } .grab-to-pan-grab - *:not(input):not(textarea):not(button):not(select):not(:link){ - cursor:inherit !important; + *:not(input):not(textarea):not(button):not(select):not(:link) { + cursor: inherit !important; } .grab-to-pan-grab:active, -.grab-to-pan-grabbing{ - cursor:grabbing !important; +.grab-to-pan-grabbing { + cursor: grabbing !important; } -.grab-to-pan-grabbing{ - position:fixed; - background:rgb(0 0 0 / 0); - display:block; - inset:0; - overflow:hidden; - z-index:50000; +.grab-to-pan-grabbing { + position: fixed; + background: rgb(0 0 0 / 0); + display: block; + inset: 0; + overflow: hidden; + z-index: 50000; } -.toolbarButton{ - height:100%; - aspect-ratio:1; - display:flex; - align-items:center; - justify-content:center; - background:none; - border:none; - color:var(--main-color); - outline:none; - border-radius:2px; - box-sizing:border-box; - font:message-box; - flex:none; - position:relative; - padding:0; +.toolbarButton { + height: 100%; + aspect-ratio: 1; + display: flex; + align-items: center; + justify-content: center; + background: none; + border: none; + color: var(--main-color); + outline: none; + border-radius: 2px; + box-sizing: border-box; + font: message-box; + flex: none; + position: relative; + padding: 0; } -.toolbarButton > span{ - display:inline-block; - width:0; - height:0; - overflow:hidden; - } - -.toolbarButton::before{ - opacity:var(--toolbar-icon-opacity); - display:inline-block; - width:var(--icon-size); - height:var(--icon-size); - content:""; - background-color:var(--toolbar-icon-bg-color); - -webkit-mask-size:cover; - mask-size:cover; - -webkit-mask-position:center; - mask-position:center; - } - -.toolbarButton.toggled{ - background-color:var(--toggled-btn-bg-color); - color:var(--toggled-btn-color); - } - -.toolbarButton.toggled::before{ - background-color:var(--toggled-btn-color); - } - -.toolbarButton.toggled:hover{ - outline:var(--toggled-hover-btn-outline) !important; - } - -.toolbarButton.toggled:hover:active{ - background-color:var(--toggled-hover-active-btn-color); - } - -.toolbarButton:is(:hover,:focus-visible){ - background-color:var(--button-hover-color); - } - -.toolbarButton:is(:hover,:focus-visible)::before{ - background-color:var(--toolbar-icon-hover-bg-color); - } - -.toolbarButton:is([disabled="disabled"],[disabled]){ - opacity:0.5; - pointer-events:none; - } - -.toolbarButton.labeled{ - width:100%; - min-height:var(--menuitem-height); - justify-content:flex-start; - gap:8px; - padding-inline-start:12px; - aspect-ratio:unset; - text-align:start; - white-space:normal; - cursor:default; - } - -.toolbarButton.labeled:is(a){ - text-decoration:none; - } - -.toolbarButton.labeled[href="#"]:is(a){ - opacity:0.5; - pointer-events:none; - } - -.toolbarButton.labeled::before{ - opacity:var(--doorhanger-icon-opacity); - } - -.toolbarButton.labeled:is(:hover,:focus-visible){ - background-color:var(--doorhanger-hover-bg-color); - color:var(--doorhanger-hover-color); - } - -.toolbarButton.labeled > span{ - display:inline-block; - width:-moz-max-content; - width:max-content; - height:auto; - } - -.toolbarButtonWithContainer{ - height:100%; - aspect-ratio:1; - display:inline-block; - position:relative; - flex:none; +.toolbarButton > span { + display: inline-block; + width: 0; + height: 0; + overflow: hidden; } -.toolbarButtonWithContainer > .toolbarButton{ - width:100%; - height:100%; - } - -.toolbarButtonWithContainer .menu{ - padding-block:5px; - } - -.toolbarButtonWithContainer .menuContainer{ - width:100%; - height:auto; - max-height:calc( - var(--viewer-container-height) - var(--toolbar-height) - - var(--doorhanger-height) - ); - display:flex; - flex-direction:column; - box-sizing:border-box; - overflow-y:auto; - } - -.toolbarButtonWithContainer .editorParamsToolbar{ - height:auto; - width:220px; - position:absolute; - z-index:30000; - cursor:default; - } - -:is(.toolbarButtonWithContainer .editorParamsToolbar) #editorStampAddImage::before{ - -webkit-mask-image:var(--editorParams-stampAddImage-icon); - mask-image:var(--editorParams-stampAddImage-icon); - } - -:is(.toolbarButtonWithContainer .editorParamsToolbar) .editorParamsLabel{ - flex:none; - font:menu; - font-size:13px; - font-style:normal; - font-weight:400; - line-height:150%; - color:var(--main-color); - width:-moz-fit-content; - width:fit-content; - inset-inline-start:0; - } - -:is(.toolbarButtonWithContainer .editorParamsToolbar) .editorParamsToolbarContainer{ - width:100%; - height:auto; - display:flex; - flex-direction:column; - box-sizing:border-box; - padding-inline:10px; - padding-block:10px; - } - -:is(:is(.toolbarButtonWithContainer .editorParamsToolbar) .editorParamsToolbarContainer) > .editorParamsSetter{ - min-height:26px; - display:flex; - align-items:center; - justify-content:space-between; - } - -:is(:is(.toolbarButtonWithContainer .editorParamsToolbar) .editorParamsToolbarContainer) .editorParamsColor{ - width:32px; - height:32px; - flex:none; - padding:0; - } - -:is(:is(.toolbarButtonWithContainer .editorParamsToolbar) .editorParamsToolbarContainer) .editorParamsSlider{ - background-color:transparent; - width:90px; - flex:0 1 0; - font:message-box; - } - -:is(:is(:is(.toolbarButtonWithContainer .editorParamsToolbar) .editorParamsToolbarContainer) .editorParamsSlider)::-moz-range-progress{ - background-color:black; - } - -:is(:is(:is(.toolbarButtonWithContainer .editorParamsToolbar) .editorParamsToolbarContainer) .editorParamsSlider)::-webkit-slider-runnable-track,:is(:is(:is(.toolbarButtonWithContainer .editorParamsToolbar) .editorParamsToolbarContainer) .editorParamsSlider)::-moz-range-track{ - background-color:black; - } - -:is(:is(:is(.toolbarButtonWithContainer .editorParamsToolbar) .editorParamsToolbarContainer) .editorParamsSlider)::-webkit-slider-thumb,:is(:is(:is(.toolbarButtonWithContainer .editorParamsToolbar) .editorParamsToolbarContainer) .editorParamsSlider)::-moz-range-thumb{ - background-color:white; - } - -#secondaryToolbar{ - height:auto; - width:220px; - position:absolute; - z-index:30000; - cursor:default; - min-height:26px; - max-height:calc(var(--viewer-container-height) - 40px); +.toolbarButton::before { + opacity: var(--toolbar-icon-opacity); + display: inline-block; + width: var(--icon-size); + height: var(--icon-size); + content: ''; + background-color: var(--toolbar-icon-bg-color); + -webkit-mask-size: cover; + mask-size: cover; + -webkit-mask-position: center; + mask-position: center; } -:is(#secondaryToolbar #secondaryToolbarButtonContainer) #secondaryOpenFile::before{ - -webkit-mask-image:var(--toolbarButton-openFile-icon); - mask-image:var(--toolbarButton-openFile-icon); - } - -:is(#secondaryToolbar #secondaryToolbarButtonContainer) #secondaryPrint::before{ - -webkit-mask-image:var(--toolbarButton-print-icon); - mask-image:var(--toolbarButton-print-icon); - } - -:is(#secondaryToolbar #secondaryToolbarButtonContainer) #secondaryDownload::before{ - -webkit-mask-image:var(--toolbarButton-download-icon); - mask-image:var(--toolbarButton-download-icon); - } - -:is(#secondaryToolbar #secondaryToolbarButtonContainer) #presentationMode::before{ - -webkit-mask-image:var(--toolbarButton-presentationMode-icon); - mask-image:var(--toolbarButton-presentationMode-icon); - } - -:is(#secondaryToolbar #secondaryToolbarButtonContainer) #viewBookmark::before{ - -webkit-mask-image:var(--toolbarButton-bookmark-icon); - mask-image:var(--toolbarButton-bookmark-icon); - } - -:is(#secondaryToolbar #secondaryToolbarButtonContainer) #firstPage::before{ - -webkit-mask-image:var(--secondaryToolbarButton-firstPage-icon); - mask-image:var(--secondaryToolbarButton-firstPage-icon); - } - -:is(#secondaryToolbar #secondaryToolbarButtonContainer) #lastPage::before{ - -webkit-mask-image:var(--secondaryToolbarButton-lastPage-icon); - mask-image:var(--secondaryToolbarButton-lastPage-icon); - } - -:is(#secondaryToolbar #secondaryToolbarButtonContainer) #pageRotateCcw::before{ - -webkit-mask-image:var(--secondaryToolbarButton-rotateCcw-icon); - mask-image:var(--secondaryToolbarButton-rotateCcw-icon); - } - -:is(#secondaryToolbar #secondaryToolbarButtonContainer) #pageRotateCw::before{ - -webkit-mask-image:var(--secondaryToolbarButton-rotateCw-icon); - mask-image:var(--secondaryToolbarButton-rotateCw-icon); - } - -:is(#secondaryToolbar #secondaryToolbarButtonContainer) #cursorSelectTool::before{ - -webkit-mask-image:var(--secondaryToolbarButton-selectTool-icon); - mask-image:var(--secondaryToolbarButton-selectTool-icon); - } - -:is(#secondaryToolbar #secondaryToolbarButtonContainer) #cursorHandTool::before{ - -webkit-mask-image:var(--secondaryToolbarButton-handTool-icon); - mask-image:var(--secondaryToolbarButton-handTool-icon); - } - -:is(#secondaryToolbar #secondaryToolbarButtonContainer) #scrollPage::before{ - -webkit-mask-image:var(--secondaryToolbarButton-scrollPage-icon); - mask-image:var(--secondaryToolbarButton-scrollPage-icon); - } - -:is(#secondaryToolbar #secondaryToolbarButtonContainer) #scrollVertical::before{ - -webkit-mask-image:var(--secondaryToolbarButton-scrollVertical-icon); - mask-image:var(--secondaryToolbarButton-scrollVertical-icon); - } - -:is(#secondaryToolbar #secondaryToolbarButtonContainer) #scrollHorizontal::before{ - -webkit-mask-image:var(--secondaryToolbarButton-scrollHorizontal-icon); - mask-image:var(--secondaryToolbarButton-scrollHorizontal-icon); - } - -:is(#secondaryToolbar #secondaryToolbarButtonContainer) #scrollWrapped::before{ - -webkit-mask-image:var(--secondaryToolbarButton-scrollWrapped-icon); - mask-image:var(--secondaryToolbarButton-scrollWrapped-icon); - } - -:is(#secondaryToolbar #secondaryToolbarButtonContainer) #spreadNone::before{ - -webkit-mask-image:var(--secondaryToolbarButton-spreadNone-icon); - mask-image:var(--secondaryToolbarButton-spreadNone-icon); - } - -:is(#secondaryToolbar #secondaryToolbarButtonContainer) #spreadOdd::before{ - -webkit-mask-image:var(--secondaryToolbarButton-spreadOdd-icon); - mask-image:var(--secondaryToolbarButton-spreadOdd-icon); - } - -:is(#secondaryToolbar #secondaryToolbarButtonContainer) #spreadEven::before{ - -webkit-mask-image:var(--secondaryToolbarButton-spreadEven-icon); - mask-image:var(--secondaryToolbarButton-spreadEven-icon); - } - -:is(#secondaryToolbar #secondaryToolbarButtonContainer) #imageAltTextSettings::before{ - -webkit-mask-image:var(--secondaryToolbarButton-imageAltTextSettings-icon); - mask-image:var(--secondaryToolbarButton-imageAltTextSettings-icon); - } - -:is(#secondaryToolbar #secondaryToolbarButtonContainer) #documentProperties::before{ - -webkit-mask-image:var(--secondaryToolbarButton-documentProperties-icon); - mask-image:var(--secondaryToolbarButton-documentProperties-icon); - } - -#findbar{ - --input-horizontal-padding:4px; - --findbar-padding:2px; - - width:-moz-max-content; - - width:max-content; - max-width:90vw; - min-height:var(--toolbar-height); - height:auto; - position:absolute; - z-index:30000; - cursor:default; - padding:0; - min-width:300px; - background-color:var(--toolbar-bg-color); - box-sizing:border-box; - flex-wrap:wrap; - justify-content:flex-start; +.toolbarButton.toggled { + background-color: var(--toggled-btn-bg-color); + color: var(--toggled-btn-color); } -#findbar > *{ - height:var(--toolbar-height); - padding:var(--findbar-padding); - } - -#findbar #findInputContainer{ - margin-inline-start:2px; - } - -:is(#findbar #findInputContainer) #findPreviousButton::before{ - -webkit-mask-image:var(--findbarButton-previous-icon); - mask-image:var(--findbarButton-previous-icon); - } - -:is(#findbar #findInputContainer) #findNextButton::before{ - -webkit-mask-image:var(--findbarButton-next-icon); - mask-image:var(--findbarButton-next-icon); - } - -:is(#findbar #findInputContainer) #findInput{ - width:200px; - padding:5px var(--input-horizontal-padding); - } - -:is(:is(#findbar #findInputContainer) #findInput)::-moz-placeholder{ - font-style:normal; - } - -:is(:is(#findbar #findInputContainer) #findInput)::placeholder{ - font-style:normal; - } - -.loadingInput:has( > [data-status="pending"]:is(:is(#findbar #findInputContainer) #findInput))::after{ - display:inline; - visibility:visible; - inset-inline-end:calc(var(--input-horizontal-padding) + 1px); - } - -[data-status="notFound"]:is(:is(#findbar #findInputContainer) #findInput){ - background-color:rgb(255 102 102); - } - -#findbar #findbarMessageContainer{ - display:none; - gap:4px; - } - -:is(#findbar #findbarMessageContainer):has( > :is(#findResultsCount,#findMsg):not(:empty)){ - display:inline flex; - } - -:is(#findbar #findbarMessageContainer) #findResultsCount{ - background-color:rgb(217 217 217); - color:rgb(82 82 82); - padding-block:4px; - } - -:is(:is(#findbar #findbarMessageContainer) #findResultsCount):empty{ - display:none; - } - -[data-status="notFound"]:is(:is(#findbar #findbarMessageContainer) #findMsg){ - font-weight:bold; - } - -:is(:is(#findbar #findbarMessageContainer) #findMsg):empty{ - display:none; - } - -#findbar.wrapContainers{ - flex-direction:column; - align-items:flex-start; - height:-moz-max-content; - height:max-content; - } - -#findbar.wrapContainers .toolbarLabel{ - margin:0 4px; - } - -#findbar.wrapContainers #findbarMessageContainer{ - flex-wrap:wrap; - flex-flow:column nowrap; - align-items:flex-start; - height:-moz-max-content; - height:max-content; - } - -:is(#findbar.wrapContainers #findbarMessageContainer) #findResultsCount{ - height:calc(var(--toolbar-height) - 2 * var(--findbar-padding)); - } - -:is(#findbar.wrapContainers #findbarMessageContainer) #findMsg{ - min-height:var(--toolbar-height); - } - -@page{ - margin:0; +.toolbarButton.toggled::before { + background-color: var(--toggled-btn-color); } -#printContainer{ - display:none; +.toolbarButton.toggled:hover { + outline: var(--toggled-hover-btn-outline) !important; } -@media print{ - body{ - background:rgb(0 0 0 / 0) none; +.toolbarButton.toggled:hover:active { + background-color: var(--toggled-hover-active-btn-color); +} + +.toolbarButton:is(:hover, :focus-visible) { + background-color: var(--button-hover-color); +} + +.toolbarButton:is(:hover, :focus-visible)::before { + background-color: var(--toolbar-icon-hover-bg-color); +} + +.toolbarButton:is([disabled='disabled'], [disabled]) { + opacity: 0.5; + pointer-events: none; +} + +.toolbarButton.labeled { + width: 100%; + min-height: var(--menuitem-height); + justify-content: flex-start; + gap: 8px; + padding-inline-start: 12px; + aspect-ratio: unset; + text-align: start; + white-space: normal; + cursor: default; +} + +.toolbarButton.labeled:is(a) { + text-decoration: none; +} + +.toolbarButton.labeled[href='#']:is(a) { + opacity: 0.5; + pointer-events: none; +} + +.toolbarButton.labeled::before { + opacity: var(--doorhanger-icon-opacity); +} + +.toolbarButton.labeled:is(:hover, :focus-visible) { + background-color: var(--doorhanger-hover-bg-color); + color: var(--doorhanger-hover-color); +} + +.toolbarButton.labeled > span { + display: inline-block; + width: -moz-max-content; + width: max-content; + height: auto; +} + +.toolbarButtonWithContainer { + height: 100%; + aspect-ratio: 1; + display: inline-block; + position: relative; + flex: none; +} + +.toolbarButtonWithContainer > .toolbarButton { + width: 100%; + height: 100%; +} + +.toolbarButtonWithContainer .menu { + padding-block: 5px; +} + +.toolbarButtonWithContainer .menuContainer { + width: 100%; + height: auto; + max-height: calc( + var(--viewer-container-height) - var(--toolbar-height) - + var(--doorhanger-height) + ); + display: flex; + flex-direction: column; + box-sizing: border-box; + overflow-y: auto; +} + +.toolbarButtonWithContainer .editorParamsToolbar { + height: auto; + width: 220px; + position: absolute; + z-index: 30000; + cursor: default; +} + +:is(.toolbarButtonWithContainer .editorParamsToolbar) + #editorStampAddImage::before { + -webkit-mask-image: var(--editorParams-stampAddImage-icon); + mask-image: var(--editorParams-stampAddImage-icon); +} + +:is(.toolbarButtonWithContainer .editorParamsToolbar) .editorParamsLabel { + flex: none; + font: menu; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: 150%; + color: var(--main-color); + width: -moz-fit-content; + width: fit-content; + inset-inline-start: 0; +} + +:is(.toolbarButtonWithContainer .editorParamsToolbar) + .editorParamsToolbarContainer { + width: 100%; + height: auto; + display: flex; + flex-direction: column; + box-sizing: border-box; + padding-inline: 10px; + padding-block: 10px; +} + +:is( + :is(.toolbarButtonWithContainer .editorParamsToolbar) + .editorParamsToolbarContainer + ) + > .editorParamsSetter { + min-height: 26px; + display: flex; + align-items: center; + justify-content: space-between; +} + +:is( + :is(.toolbarButtonWithContainer .editorParamsToolbar) + .editorParamsToolbarContainer + ) + .editorParamsColor { + width: 32px; + height: 32px; + flex: none; + padding: 0; +} + +:is( + :is(.toolbarButtonWithContainer .editorParamsToolbar) + .editorParamsToolbarContainer + ) + .editorParamsSlider { + background-color: transparent; + width: 90px; + flex: 0 1 0; + font: message-box; +} + +:is( + :is( + :is(.toolbarButtonWithContainer .editorParamsToolbar) + .editorParamsToolbarContainer + ) + .editorParamsSlider + )::-moz-range-progress { + background-color: black; +} + +:is( + :is( + :is(.toolbarButtonWithContainer .editorParamsToolbar) + .editorParamsToolbarContainer + ) + .editorParamsSlider + )::-webkit-slider-runnable-track, +:is( + :is( + :is(.toolbarButtonWithContainer .editorParamsToolbar) + .editorParamsToolbarContainer + ) + .editorParamsSlider + )::-moz-range-track { + background-color: black; +} + +:is( + :is( + :is(.toolbarButtonWithContainer .editorParamsToolbar) + .editorParamsToolbarContainer + ) + .editorParamsSlider + )::-webkit-slider-thumb, +:is( + :is( + :is(.toolbarButtonWithContainer .editorParamsToolbar) + .editorParamsToolbarContainer + ) + .editorParamsSlider + )::-moz-range-thumb { + background-color: white; +} + +#secondaryToolbar { + height: auto; + width: 220px; + position: absolute; + z-index: 30000; + cursor: default; + min-height: 26px; + max-height: calc(var(--viewer-container-height) - 40px); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) + #secondaryOpenFile::before { + -webkit-mask-image: var(--toolbarButton-openFile-icon); + mask-image: var(--toolbarButton-openFile-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) + #secondaryPrint::before { + -webkit-mask-image: var(--toolbarButton-print-icon); + mask-image: var(--toolbarButton-print-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) + #secondaryDownload::before { + -webkit-mask-image: var(--toolbarButton-download-icon); + mask-image: var(--toolbarButton-download-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) + #presentationMode::before { + -webkit-mask-image: var(--toolbarButton-presentationMode-icon); + mask-image: var(--toolbarButton-presentationMode-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #viewBookmark::before { + -webkit-mask-image: var(--toolbarButton-bookmark-icon); + mask-image: var(--toolbarButton-bookmark-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #firstPage::before { + -webkit-mask-image: var(--secondaryToolbarButton-firstPage-icon); + mask-image: var(--secondaryToolbarButton-firstPage-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #lastPage::before { + -webkit-mask-image: var(--secondaryToolbarButton-lastPage-icon); + mask-image: var(--secondaryToolbarButton-lastPage-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #pageRotateCcw::before { + -webkit-mask-image: var(--secondaryToolbarButton-rotateCcw-icon); + mask-image: var(--secondaryToolbarButton-rotateCcw-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #pageRotateCw::before { + -webkit-mask-image: var(--secondaryToolbarButton-rotateCw-icon); + mask-image: var(--secondaryToolbarButton-rotateCw-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) + #cursorSelectTool::before { + -webkit-mask-image: var(--secondaryToolbarButton-selectTool-icon); + mask-image: var(--secondaryToolbarButton-selectTool-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) + #cursorHandTool::before { + -webkit-mask-image: var(--secondaryToolbarButton-handTool-icon); + mask-image: var(--secondaryToolbarButton-handTool-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #scrollPage::before { + -webkit-mask-image: var(--secondaryToolbarButton-scrollPage-icon); + mask-image: var(--secondaryToolbarButton-scrollPage-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) + #scrollVertical::before { + -webkit-mask-image: var(--secondaryToolbarButton-scrollVertical-icon); + mask-image: var(--secondaryToolbarButton-scrollVertical-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) + #scrollHorizontal::before { + -webkit-mask-image: var(--secondaryToolbarButton-scrollHorizontal-icon); + mask-image: var(--secondaryToolbarButton-scrollHorizontal-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #scrollWrapped::before { + -webkit-mask-image: var(--secondaryToolbarButton-scrollWrapped-icon); + mask-image: var(--secondaryToolbarButton-scrollWrapped-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #spreadNone::before { + -webkit-mask-image: var(--secondaryToolbarButton-spreadNone-icon); + mask-image: var(--secondaryToolbarButton-spreadNone-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #spreadOdd::before { + -webkit-mask-image: var(--secondaryToolbarButton-spreadOdd-icon); + mask-image: var(--secondaryToolbarButton-spreadOdd-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #spreadEven::before { + -webkit-mask-image: var(--secondaryToolbarButton-spreadEven-icon); + mask-image: var(--secondaryToolbarButton-spreadEven-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) + #imageAltTextSettings::before { + -webkit-mask-image: var(--secondaryToolbarButton-imageAltTextSettings-icon); + mask-image: var(--secondaryToolbarButton-imageAltTextSettings-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) + #documentProperties::before { + -webkit-mask-image: var(--secondaryToolbarButton-documentProperties-icon); + mask-image: var(--secondaryToolbarButton-documentProperties-icon); +} + +#findbar { + --input-horizontal-padding: 4px; + --findbar-padding: 2px; + + width: -moz-max-content; + + width: max-content; + max-width: 90vw; + min-height: var(--toolbar-height); + height: auto; + position: absolute; + z-index: 30000; + cursor: default; + padding: 0; + min-width: 300px; + background-color: var(--toolbar-bg-color); + box-sizing: border-box; + flex-wrap: wrap; + justify-content: flex-start; +} + +#findbar > * { + height: var(--toolbar-height); + padding: var(--findbar-padding); +} + +#findbar #findInputContainer { + margin-inline-start: 2px; +} + +:is(#findbar #findInputContainer) #findPreviousButton::before { + -webkit-mask-image: var(--findbarButton-previous-icon); + mask-image: var(--findbarButton-previous-icon); +} + +:is(#findbar #findInputContainer) #findNextButton::before { + -webkit-mask-image: var(--findbarButton-next-icon); + mask-image: var(--findbarButton-next-icon); +} + +:is(#findbar #findInputContainer) #findInput { + width: 200px; + padding: 5px var(--input-horizontal-padding); +} + +:is(:is(#findbar #findInputContainer) #findInput)::-moz-placeholder { + font-style: normal; +} + +:is(:is(#findbar #findInputContainer) #findInput)::placeholder { + font-style: normal; +} + +.loadingInput:has( + > [data-status='pending']:is(:is(#findbar #findInputContainer) #findInput) + )::after { + display: inline; + visibility: visible; + inset-inline-end: calc(var(--input-horizontal-padding) + 1px); +} + +[data-status='notFound']:is(:is(#findbar #findInputContainer) #findInput) { + background-color: rgb(255 102 102); +} + +#findbar #findbarMessageContainer { + display: none; + gap: 4px; +} + +:is(#findbar #findbarMessageContainer):has( + > :is(#findResultsCount, #findMsg):not(:empty) + ) { + display: inline flex; +} + +:is(#findbar #findbarMessageContainer) #findResultsCount { + background-color: rgb(217 217 217); + color: rgb(82 82 82); + padding-block: 4px; +} + +:is(:is(#findbar #findbarMessageContainer) #findResultsCount):empty { + display: none; +} + +[data-status='notFound']:is(:is(#findbar #findbarMessageContainer) #findMsg) { + font-weight: bold; +} + +:is(:is(#findbar #findbarMessageContainer) #findMsg):empty { + display: none; +} + +#findbar.wrapContainers { + flex-direction: column; + align-items: flex-start; + height: -moz-max-content; + height: max-content; +} + +#findbar.wrapContainers .toolbarLabel { + margin: 0 4px; +} + +#findbar.wrapContainers #findbarMessageContainer { + flex-wrap: wrap; + flex-flow: column nowrap; + align-items: flex-start; + height: -moz-max-content; + height: max-content; +} + +:is(#findbar.wrapContainers #findbarMessageContainer) #findResultsCount { + height: calc(var(--toolbar-height) - 2 * var(--findbar-padding)); +} + +:is(#findbar.wrapContainers #findbarMessageContainer) #findMsg { + min-height: var(--toolbar-height); +} + +@page { + margin: 0; +} + +#printContainer { + display: none; +} + +@media print { + body { + background: rgb(0 0 0 / 0) none; } - body[data-pdfjsprinting] #outerContainer{ - display:none; + body[data-pdfjsprinting] #outerContainer { + display: none; } - body[data-pdfjsprinting] #printContainer{ - display:block; + body[data-pdfjsprinting] #printContainer { + display: block; } - #printContainer{ - height:100%; + #printContainer { + height: 100%; } - #printContainer > .printedPage{ - page-break-after:always; - page-break-inside:avoid; - height:100%; - width:100%; + #printContainer > .printedPage { + page-break-after: always; + page-break-inside: avoid; + height: 100%; + width: 100%; - display:flex; - flex-direction:column; - justify-content:center; - align-items:center; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; } - #printContainer > .xfaPrintedPage .xfaPage{ - position:absolute; + #printContainer > .xfaPrintedPage .xfaPage { + position: absolute; } - #printContainer > .xfaPrintedPage{ - page-break-after:always; - page-break-inside:avoid; - width:100%; - height:100%; - position:relative; + #printContainer > .xfaPrintedPage { + page-break-after: always; + page-break-inside: avoid; + width: 100%; + height: 100%; + position: relative; } - #printContainer > .printedPage :is(canvas, img){ - max-width:100%; - max-height:100%; + #printContainer > .printedPage :is(canvas, img) { + max-width: 100%; + max-height: 100%; - direction:ltr; - display:block; + direction: ltr; + display: block; } } -.visibleMediumView{ - display:none !important; +.visibleMediumView { + display: none !important; } -.toolbarLabel{ - width:-moz-max-content; - width:max-content; - min-width:16px; - height:100%; - padding-inline:4px; - margin:2px; - border-radius:2px; - color:var(--main-color); - font-size:12px; - line-height:14px; - text-align:left; - -webkit-user-select:none; - -moz-user-select:none; - user-select:none; - cursor:default; - box-sizing:border-box; +.toolbarLabel { + width: -moz-max-content; + width: max-content; + min-width: 16px; + height: 100%; + padding-inline: 4px; + margin: 2px; + border-radius: 2px; + color: var(--main-color); + font-size: 12px; + line-height: 14px; + text-align: left; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + cursor: default; + box-sizing: border-box; - display:inline flex; - flex-direction:column; - align-items:center; - justify-content:center; + display: inline flex; + flex-direction: column; + align-items: center; + justify-content: center; } -.toolbarLabel > label{ - width:100%; - } - -.toolbarHorizontalGroup{ - height:100%; - display:inline flex; - flex-direction:row; - align-items:center; - justify-content:space-between; - gap:1px; - box-sizing:border-box; +.toolbarLabel > label { + width: 100%; } -.dropdownToolbarButton{ - display:inline flex; - flex-direction:row; - align-items:center; - justify-content:center; - position:relative; - - width:-moz-fit-content; - - width:fit-content; - min-width:140px; - padding:0; - background-color:var(--dropdown-btn-bg-color); - border:var(--dropdown-btn-border); - border-radius:2px; - color:var(--main-color); - font-size:12px; - line-height:14px; - -webkit-user-select:none; - -moz-user-select:none; - user-select:none; - cursor:default; - box-sizing:border-box; - outline:none; +.toolbarHorizontalGroup { + height: 100%; + display: inline flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 1px; + box-sizing: border-box; } -.dropdownToolbarButton:hover{ - background-color:var(--button-hover-color); - } +.dropdownToolbarButton { + display: inline flex; + flex-direction: row; + align-items: center; + justify-content: center; + position: relative; -.dropdownToolbarButton > select{ - -webkit-appearance:none; - -moz-appearance:none; - appearance:none; - width:inherit; - min-width:inherit; - height:28px; - font:message-box; - font-size:12px; - color:var(--main-color); - margin:0; - padding-block:1px 2px; - padding-inline:6px 38px; - border:none; - outline:none; - background-color:var(--dropdown-btn-bg-color); - } + width: -moz-fit-content; -:is(.dropdownToolbarButton > select) > option{ - background:var(--doorhanger-bg-color); - color:var(--main-color); - } - -:is(.dropdownToolbarButton > select):is(:hover,:focus-visible){ - background-color:var(--button-hover-color); - color:var(--toggled-btn-color); - } - -.dropdownToolbarButton::after{ - position:absolute; - display:inline; - width:var(--icon-size); - height:var(--icon-size); - - content:""; - background-color:var(--toolbar-icon-bg-color); - -webkit-mask-size:cover; - mask-size:cover; - - inset-inline-end:4px; - pointer-events:none; - -webkit-mask-image:var(--toolbarButton-menuArrow-icon); - mask-image:var(--toolbarButton-menuArrow-icon); - } - -.dropdownToolbarButton:is(:hover,:focus-visible,:active)::after{ - background-color:var(--toolbar-icon-hover-bg-color); - } - -#toolbarContainer{ - --menuitem-height:calc(var(--toolbar-height) - 6px); - - width:100%; - height:var(--toolbar-height); - padding:var(--toolbar-vertical-padding) var(--toolbar-horizontal-padding); - position:relative; - box-sizing:border-box; - font:message-box; - background-color:var(--toolbar-bg-color); - box-shadow:var(--toolbar-box-shadow); - border-bottom:var(--toolbar-border-bottom); + width: fit-content; + min-width: 140px; + padding: 0; + background-color: var(--dropdown-btn-bg-color); + border: var(--dropdown-btn-border); + border-radius: 2px; + color: var(--main-color); + font-size: 12px; + line-height: 14px; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + cursor: default; + box-sizing: border-box; + outline: none; } -#toolbarContainer #toolbarViewer{ - width:100%; - height:100%; - justify-content:space-between; +.dropdownToolbarButton:hover { + background-color: var(--button-hover-color); +} + +.dropdownToolbarButton > select { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + width: inherit; + min-width: inherit; + height: 28px; + font: message-box; + font-size: 12px; + color: var(--main-color); + margin: 0; + padding-block: 1px 2px; + padding-inline: 6px 38px; + border: none; + outline: none; + background-color: var(--dropdown-btn-bg-color); +} + +:is(.dropdownToolbarButton > select) > option { + background: var(--doorhanger-bg-color); + color: var(--main-color); +} + +:is(.dropdownToolbarButton > select):is(:hover, :focus-visible) { + background-color: var(--button-hover-color); + color: var(--toggled-btn-color); +} + +.dropdownToolbarButton::after { + position: absolute; + display: inline; + width: var(--icon-size); + height: var(--icon-size); + + content: ''; + background-color: var(--toolbar-icon-bg-color); + -webkit-mask-size: cover; + mask-size: cover; + + inset-inline-end: 4px; + pointer-events: none; + -webkit-mask-image: var(--toolbarButton-menuArrow-icon); + mask-image: var(--toolbarButton-menuArrow-icon); +} + +.dropdownToolbarButton:is(:hover, :focus-visible, :active)::after { + background-color: var(--toolbar-icon-hover-bg-color); +} + +#toolbarContainer { + --menuitem-height: calc(var(--toolbar-height) - 6px); + + width: 100%; + height: var(--toolbar-height); + padding: var(--toolbar-vertical-padding) var(--toolbar-horizontal-padding); + position: relative; + box-sizing: border-box; + font: message-box; + background-color: var(--toolbar-bg-color); + box-shadow: var(--toolbar-box-shadow); + border-bottom: var(--toolbar-border-bottom); +} + +#toolbarContainer #toolbarViewer { + width: 100%; + height: 100%; + justify-content: space-between; +} + +:is(#toolbarContainer #toolbarViewer) > * { + flex: none; +} + +:is(#toolbarContainer #toolbarViewer) input { + font: message-box; +} + +:is(#toolbarContainer #toolbarViewer) .toolbarButtonSpacer { + width: 30px; + display: block; + height: 1px; +} + +:is(#toolbarContainer #toolbarViewer) + #toolbarViewerLeft + #numPages.toolbarLabel { + padding-inline-start: 3px; + flex: none; +} + +#toolbarContainer #loadingBar { + --progressBar-percent: 0%; + --progressBar-end-offset: 0; + + position: absolute; + top: var(--toolbar-height); + inset-inline: 0 var(--progressBar-end-offset); + height: 4px; + background-color: var(--progressBar-bg-color); + border-bottom: 1px solid var(--toolbar-border-color); + transition-property: inset-inline-start; + transition-duration: var(--sidebar-transition-duration); + transition-timing-function: var(--sidebar-transition-timing-function); +} + +:is(#toolbarContainer #loadingBar) .progress { + position: absolute; + top: 0; + inset-inline-start: 0; + width: 100%; + transform: scaleX(var(--progressBar-percent)); + transform-origin: calc(50% - 50% * var(--dir-factor)) 0; + height: 100%; + background-color: var(--progressBar-color); + overflow: hidden; + transition: transform 200ms; +} + +.indeterminate:is(#toolbarContainer #loadingBar) .progress { + transform: none; + background-color: var(--progressBar-bg-color); + transition: none; +} + +:is(.indeterminate:is(#toolbarContainer #loadingBar) .progress) .glimmer { + position: absolute; + top: 0; + inset-inline-start: 0; + height: 100%; + width: calc(100% + 150px); + background: repeating-linear-gradient( + 135deg, + var(--progressBar-blend-color) 0, + var(--progressBar-bg-color) 5px, + var(--progressBar-bg-color) 45px, + var(--progressBar-color) 55px, + var(--progressBar-color) 95px, + var(--progressBar-blend-color) 100px + ); + animation: progressIndeterminate 1s linear infinite; +} + +#secondaryToolbar #firstPage::before { + -webkit-mask-image: var(--secondaryToolbarButton-firstPage-icon); + mask-image: var(--secondaryToolbarButton-firstPage-icon); +} + +#secondaryToolbar #lastPage::before { + -webkit-mask-image: var(--secondaryToolbarButton-lastPage-icon); + mask-image: var(--secondaryToolbarButton-lastPage-icon); +} + +#secondaryToolbar #pageRotateCcw::before { + -webkit-mask-image: var(--secondaryToolbarButton-rotateCcw-icon); + mask-image: var(--secondaryToolbarButton-rotateCcw-icon); +} + +#secondaryToolbar #pageRotateCw::before { + -webkit-mask-image: var(--secondaryToolbarButton-rotateCw-icon); + mask-image: var(--secondaryToolbarButton-rotateCw-icon); +} + +#secondaryToolbar #cursorSelectTool::before { + -webkit-mask-image: var(--secondaryToolbarButton-selectTool-icon); + mask-image: var(--secondaryToolbarButton-selectTool-icon); +} + +#secondaryToolbar #cursorHandTool::before { + -webkit-mask-image: var(--secondaryToolbarButton-handTool-icon); + mask-image: var(--secondaryToolbarButton-handTool-icon); +} + +#secondaryToolbar #scrollPage::before { + -webkit-mask-image: var(--secondaryToolbarButton-scrollPage-icon); + mask-image: var(--secondaryToolbarButton-scrollPage-icon); +} + +#secondaryToolbar #scrollVertical::before { + -webkit-mask-image: var(--secondaryToolbarButton-scrollVertical-icon); + mask-image: var(--secondaryToolbarButton-scrollVertical-icon); +} + +#secondaryToolbar #scrollHorizontal::before { + -webkit-mask-image: var(--secondaryToolbarButton-scrollHorizontal-icon); + mask-image: var(--secondaryToolbarButton-scrollHorizontal-icon); +} + +#secondaryToolbar #scrollWrapped::before { + -webkit-mask-image: var(--secondaryToolbarButton-scrollWrapped-icon); + mask-image: var(--secondaryToolbarButton-scrollWrapped-icon); +} + +#secondaryToolbar #spreadNone::before { + -webkit-mask-image: var(--secondaryToolbarButton-spreadNone-icon); + mask-image: var(--secondaryToolbarButton-spreadNone-icon); +} + +#secondaryToolbar #spreadOdd::before { + -webkit-mask-image: var(--secondaryToolbarButton-spreadOdd-icon); + mask-image: var(--secondaryToolbarButton-spreadOdd-icon); +} + +#secondaryToolbar #spreadEven::before { + -webkit-mask-image: var(--secondaryToolbarButton-spreadEven-icon); + mask-image: var(--secondaryToolbarButton-spreadEven-icon); +} + +#secondaryToolbar #documentProperties::before { + -webkit-mask-image: var(--secondaryToolbarButton-documentProperties-icon); + mask-image: var(--secondaryToolbarButton-documentProperties-icon); +} + +@media all and (max-width: 840px) { + #sidebarContainer { + background-color: var(--sidebar-narrow-bg-color); } - -:is(#toolbarContainer #toolbarViewer) > *{ - flex:none; - } - -:is(#toolbarContainer #toolbarViewer) input{ - font:message-box; - } - -:is(#toolbarContainer #toolbarViewer) .toolbarButtonSpacer{ - width:30px; - display:block; - height:1px; - } - -:is(#toolbarContainer #toolbarViewer) #toolbarViewerLeft #numPages.toolbarLabel{ - padding-inline-start:3px; - flex:none; - } - -#toolbarContainer #loadingBar{ - --progressBar-percent:0%; - --progressBar-end-offset:0; - - position:absolute; - top:var(--toolbar-height); - inset-inline:0 var(--progressBar-end-offset); - height:4px; - background-color:var(--progressBar-bg-color); - border-bottom:1px solid var(--toolbar-border-color); - transition-property:inset-inline-start; - transition-duration:var(--sidebar-transition-duration); - transition-timing-function:var(--sidebar-transition-timing-function); - } - -:is(#toolbarContainer #loadingBar) .progress{ - position:absolute; - top:0; - inset-inline-start:0; - width:100%; - transform:scaleX(var(--progressBar-percent)); - transform-origin:calc(50% - 50% * var(--dir-factor)) 0; - height:100%; - background-color:var(--progressBar-color); - overflow:hidden; - transition:transform 200ms; - } - -.indeterminate:is(#toolbarContainer #loadingBar) .progress{ - transform:none; - background-color:var(--progressBar-bg-color); - transition:none; - } - -:is(.indeterminate:is(#toolbarContainer #loadingBar) .progress) .glimmer{ - position:absolute; - top:0; - inset-inline-start:0; - height:100%; - width:calc(100% + 150px); - background:repeating-linear-gradient( - 135deg, - var(--progressBar-blend-color) 0, - var(--progressBar-bg-color) 5px, - var(--progressBar-bg-color) 45px, - var(--progressBar-color) 55px, - var(--progressBar-color) 95px, - var(--progressBar-blend-color) 100px - ); - animation:progressIndeterminate 1s linear infinite; - } - -#secondaryToolbar #firstPage::before{ - -webkit-mask-image:var(--secondaryToolbarButton-firstPage-icon); - mask-image:var(--secondaryToolbarButton-firstPage-icon); - } - -#secondaryToolbar #lastPage::before{ - -webkit-mask-image:var(--secondaryToolbarButton-lastPage-icon); - mask-image:var(--secondaryToolbarButton-lastPage-icon); - } - -#secondaryToolbar #pageRotateCcw::before{ - -webkit-mask-image:var(--secondaryToolbarButton-rotateCcw-icon); - mask-image:var(--secondaryToolbarButton-rotateCcw-icon); - } - -#secondaryToolbar #pageRotateCw::before{ - -webkit-mask-image:var(--secondaryToolbarButton-rotateCw-icon); - mask-image:var(--secondaryToolbarButton-rotateCw-icon); - } - -#secondaryToolbar #cursorSelectTool::before{ - -webkit-mask-image:var(--secondaryToolbarButton-selectTool-icon); - mask-image:var(--secondaryToolbarButton-selectTool-icon); - } - -#secondaryToolbar #cursorHandTool::before{ - -webkit-mask-image:var(--secondaryToolbarButton-handTool-icon); - mask-image:var(--secondaryToolbarButton-handTool-icon); - } - -#secondaryToolbar #scrollPage::before{ - -webkit-mask-image:var(--secondaryToolbarButton-scrollPage-icon); - mask-image:var(--secondaryToolbarButton-scrollPage-icon); - } - -#secondaryToolbar #scrollVertical::before{ - -webkit-mask-image:var(--secondaryToolbarButton-scrollVertical-icon); - mask-image:var(--secondaryToolbarButton-scrollVertical-icon); - } - -#secondaryToolbar #scrollHorizontal::before{ - -webkit-mask-image:var(--secondaryToolbarButton-scrollHorizontal-icon); - mask-image:var(--secondaryToolbarButton-scrollHorizontal-icon); - } - -#secondaryToolbar #scrollWrapped::before{ - -webkit-mask-image:var(--secondaryToolbarButton-scrollWrapped-icon); - mask-image:var(--secondaryToolbarButton-scrollWrapped-icon); - } - -#secondaryToolbar #spreadNone::before{ - -webkit-mask-image:var(--secondaryToolbarButton-spreadNone-icon); - mask-image:var(--secondaryToolbarButton-spreadNone-icon); - } - -#secondaryToolbar #spreadOdd::before{ - -webkit-mask-image:var(--secondaryToolbarButton-spreadOdd-icon); - mask-image:var(--secondaryToolbarButton-spreadOdd-icon); - } - -#secondaryToolbar #spreadEven::before{ - -webkit-mask-image:var(--secondaryToolbarButton-spreadEven-icon); - mask-image:var(--secondaryToolbarButton-spreadEven-icon); - } - -#secondaryToolbar #documentProperties::before{ - -webkit-mask-image:var(--secondaryToolbarButton-documentProperties-icon); - mask-image:var(--secondaryToolbarButton-documentProperties-icon); - } - -@media all and (max-width: 840px){ - #sidebarContainer{ - background-color:var(--sidebar-narrow-bg-color); - } - #outerContainer.sidebarOpen #viewerContainer{ - inset-inline-start:0 !important; + #outerContainer.sidebarOpen #viewerContainer { + inset-inline-start: 0 !important; } } -@media all and (max-width: 750px){ - #outerContainer .hiddenMediumView{ - display:none !important; +@media all and (max-width: 750px) { + #outerContainer .hiddenMediumView { + display: none !important; } - #outerContainer .visibleMediumView:not(.hidden, [hidden]){ - display:inline-block !important; + #outerContainer .visibleMediumView:not(.hidden, [hidden]) { + display: inline-block !important; } } -@media all and (max-width: 690px){ +@media all and (max-width: 690px) { .hiddenSmallView, - .hiddenSmallView *{ - display:none !important; + .hiddenSmallView * { + display: none !important; } - #toolbarContainer #toolbarViewer .toolbarButtonSpacer{ - width:0; + #toolbarContainer #toolbarViewer .toolbarButtonSpacer { + width: 0; } } -@media all and (max-width: 560px){ - #scaleSelectContainer{ - display:none; +@media all and (max-width: 560px) { + #scaleSelectContainer { + display: none; } } diff --git a/src/App-styles.ts b/src/App-styles.ts deleted file mode 100644 index 4fcf3c0..0000000 --- a/src/App-styles.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { - AppBar, - Button, - Toolbar, - Typography, - Box, - TextField, - InputLabel, -} from "@mui/material"; -import { styled } from "@mui/system"; - -export const AppContainer = styled(Box)(({ theme }) => ({ - display: "flex", - alignItems: "center", - flexDirection: 'column', - width: "100vw", - background: "rgba(39, 40, 44, 1)", - height: "100vh", - radius: "15px", - overflow: 'hidden' -})); -export const AuthenticatedContainer = styled(Box)(({ theme }) => ({ - display: "flex", - width: "100%", - height: "100%", - justifyContent: "space-between" -})); -export const AuthenticatedContainerInnerLeft = styled(Box)(({ theme }) => ({ - display: "flex", - alignItems: "center", - flexDirection: 'column', - height: "100%", - width: "100%" -})); -export const AuthenticatedContainerInnerRight = styled(Box)(({ theme }) => ({ - display: "flex", - alignItems: "center", - flexDirection: 'column', - width: "60px", - height: "100%", - background: "rgba(0, 0, 0, 0.1)" - -})); -export const AuthenticatedContainerInnerTop = styled(Box)(({ theme }) => ({ - display: "flex", - alignItems: "center", - justifyContent: "flex-start", - width: "100%px", - height: "60px", - background: "rgba(0, 0, 0, 0.1)", - padding: '20px' -})); - -export const TextP = styled(Typography)(({ theme }) => ({ - fontSize: "13px", - fontWeight: 600, - fontFamily: "Inter", - color: "white" -})); - -export const TextItalic = styled("span")(({ theme }) => ({ - fontSize: "13px", - fontWeight: 600, - fontFamily: "Inter", - color: "white", - fontStyle: "italic" -})); - -export const TextSpan = styled("span")(({ theme }) => ({ - fontSize: "13px", - fontFamily: "Inter", - fontWeight: 800, - color: "white" -})); - -export const AddressBox = styled(Box)` -display: flex; -border: 1px solid var(--50-white, rgba(255, 255, 255, 0.5)); -justify-content: space-between; -align-items: center; -width: auto; -height: 25px; -padding: 5px 15px 5px 15px; -gap: 5px; -border-radius: 100px; -font-family: Inter; -font-size: 12px; -font-weight: 600; -line-height: 14.52px; -text-align: left; -color: var(--50-white, rgba(255, 255, 255, 0.5)); -cursor: pointer; -transition: all 0.2s; -&:hover { - background-color: rgba(41, 41, 43, 1); - color: white; - svg path { - fill: white; // Fill color changes to white on hover - } - } - -` - -export const CustomButton = styled(Box)` - -/* Authenticate */ - -box-sizing: 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)); -border-radius: 5px; - - display: inline-flex; - - justify-content: center; - align-items: center; - - width: fit-content; - transition: all 0.2s; - color: black; - min-width: 160px; - cursor: pointer; - font-weight: 600; - font-family: Inter; - color: white; - text-align: center; - &:hover { - background-color: rgba(41, 41, 43, 1); - color: white; - svg path { - fill: white; // Fill color changes to white on hover - } - } -`; -interface CustomButtonProps { - bgColor?: string; - color?: string; -} -export const CustomButtonAccept = styled(Box)( - ({ bgColor, color }) => ({ - boxSizing: "border-box", - padding: "15px 20px", - gap: "10px", - border: "0.5px solid rgba(255, 255, 255, 0.5)", - filter: "drop-shadow(1px 4px 10.5px rgba(0,0,0,0.3))", - borderRadius: 5, - display: "inline-flex", - justifyContent: "center", - alignItems: "center", - width: "fit-content", - transition: "all 0.2s", - minWidth: 160, - cursor: "pointer", - fontWeight: 600, - fontFamily: "Inter", - textAlign: "center", - opacity: 0.7, - // Use the passed-in props or fallback defaults - backgroundColor: bgColor || "transparent", - color: color || "white", - - "&:hover": { - opacity: 1, - backgroundColor: bgColor - ? bgColor - : "rgba(41, 41, 43, 1)", // fallback hover bg - color: color || "white", - svg: { - path: { - fill: color || "white", - }, - }, - }, - }) -); - - -export const CustomInput = styled(TextField)({ - width: "183px", // Adjust the width as needed - borderRadius: "5px", - // backgroundColor: "rgba(30, 30, 32, 1)", - outline: "none", - input: { - fontSize: 10, - fontFamily: "Inter", - fontWeight: 400, - color: "white", - "&::placeholder": { - fontSize: 16, - color: "rgba(255, 255, 255, 0.2)", - }, - outline: "none", - padding: "10px", - }, - "& .MuiOutlinedInput-root": { - "& fieldset": { - border: '0.5px solid rgba(255, 255, 255, 0.5)', - }, - "&:hover fieldset": { - border: '0.5px solid rgba(255, 255, 255, 0.5)', - }, - "&.Mui-focused fieldset": { - border: '0.5px solid rgba(255, 255, 255, 0.5)', - }, - }, - "& .MuiInput-underline:before": { - borderBottom: "none", - }, - "& .MuiInput-underline:hover:not(.Mui-disabled):before": { - borderBottom: "none", - }, - "& .MuiInput-underline:after": { - borderBottom: "none", - }, -}); - -export const CustomLabel = styled(InputLabel)` - font-weight: 400; - font-family: Inter; - font-size: 10px; - line-height: 12px; - color: rgba(255, 255, 255, 0.5); - -` \ No newline at end of file diff --git a/src/App.css b/src/App.css deleted file mode 100644 index e69de29..0000000 diff --git a/src/App.tsx b/src/App.tsx index d779131..8a36a7d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,9 +5,8 @@ import { useMemo, useRef, useState, -} from "react"; -import "./App.css"; -import { useDropzone } from "react-dropzone"; +} from 'react'; +import { useDropzone } from 'react-dropzone'; import { Box, Button, @@ -20,46 +19,35 @@ import { DialogContentText, DialogTitle, FormControlLabel, - Input, - InputLabel, - Popover, Tooltip, Typography, -} from "@mui/material"; + useTheme, +} from '@mui/material'; import { JsonView, allExpanded, darkStyles } from 'react-json-view-lite'; import 'react-json-view-lite/dist/index.css'; -import { decryptStoredWallet } from "./utils/decryptWallet"; -import { CountdownCircleTimer } from "react-countdown-circle-timer"; -import Logo1 from "./assets/svgs/Logo1.svg"; -import Logo1Dark from "./assets/svgs/Logo1Dark.svg"; -import RefreshIcon from "@mui/icons-material/Refresh"; -import Logo2 from "./assets/svgs/Logo2.svg"; -import Copy from "./assets/svgs/Copy.svg"; -import ltcLogo from "./assets/ltc.png"; +import { decryptStoredWallet } from './utils/decryptWallet'; +import { CountdownCircleTimer } from 'react-countdown-circle-timer'; +import Logo1Dark from './assets/svgs/Logo1Dark.svg'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import DownloadIcon from '@mui/icons-material/Download'; +import ltcLogo from './assets/ltc.png'; import PersonSearchIcon from '@mui/icons-material/PersonSearch'; -import qortLogo from "./assets/qort.png"; -import { CopyToClipboard } from "react-copy-to-clipboard"; -import Download from "./assets/svgs/Download.svg"; -import Logout from "./assets/svgs/Logout.svg"; -import Return from "./assets/svgs/Return.svg"; +import qortLogo from './assets/qort.png'; +import { Return } from './assets/Icons/Return.tsx'; import WarningIcon from '@mui/icons-material/Warning'; -import Success from "./assets/svgs/Success.svg"; -import Info from "./assets/svgs/Info.svg"; -import CloseIcon from "@mui/icons-material/Close"; import './utils/seedPhrase/RandomSentenceGenerator'; import EngineeringIcon from '@mui/icons-material/Engineering'; import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet'; import PriorityHighIcon from '@mui/icons-material/PriorityHigh'; import { createAccount, - generateRandomSentence, saveFileToDisk, saveSeedPhraseToDisk, -} from "./utils/generateWallet/generateWallet"; -import { kdf } from "./deps/kdf"; -import { generateSaveWalletData } from "./utils/generateWallet/storeWallet"; -import { crypto, walletVersion } from "./constants/decryptWallet"; -import PhraseWallet from "./utils/generateWallet/phrase-wallet"; +} from './utils/generateWallet/generateWallet'; +import { kdf } from './deps/kdf'; +import { generateSaveWalletData } from './utils/generateWallet/storeWallet'; +import { crypto, walletVersion } from './constants/decryptWallet'; +import PhraseWallet from './utils/generateWallet/phrase-wallet'; import { AddressBox, AppContainer, @@ -68,121 +56,114 @@ import { AuthenticatedContainerInnerRight, CustomButton, CustomButtonAccept, - CustomInput, CustomLabel, TextItalic, TextP, TextSpan, -} from "./App-styles"; -import { Spacer } from "./common/Spacer"; -import { Loader } from "./components/Loader"; -import { PasswordField, ErrorText } from "./components"; -import { ChatGroup } from "./components/Chat/ChatGroup"; -import { Group, requestQueueMemberNames } from "./components/Group/Group"; -import { TaskManager } from "./components/TaskManager/TaskManger"; -import { useModal } from "./common/useModal"; -import { LoadingButton } from "@mui/lab"; -import { Label } from "./components/Group/AddGroup"; -import { CustomizedSnackbars } from "./components/Snackbar/Snackbar"; -import SettingsIcon from "@mui/icons-material/Settings"; +} from './styles/App-styles.ts'; +import { Spacer } from './common/Spacer'; +import { Loader } from './components/Loader'; +import { PasswordField, ErrorText } from './components'; +import { Group, requestQueueMemberNames } from './components/Group/Group'; +import { TaskManager } from './components/TaskManager/TaskManager.tsx'; +import { useModal } from './common/useModal'; +import { CustomizedSnackbars } from './components/Snackbar/Snackbar'; +import SettingsIcon from '@mui/icons-material/Settings'; +import LogoutIcon from '@mui/icons-material/Logout'; import HelpIcon from '@mui/icons-material/Help'; - import { cleanUrl, - getFee, getProtocol, getWallets, groupApi, - groupApiLocal, groupApiSocket, - groupApiSocketLocal, storeWallets, -} from "./background"; +} from './background'; import { executeEvent, subscribeToEvent, unsubscribeFromEvent, -} from "./utils/events"; +} from './utils/events'; import { requestQueueCommentCount, requestQueuePublishedAccouncements, -} from "./components/Chat/GroupAnnouncements"; -import { requestQueueGroupJoinRequests } from "./components/Group/GroupJoinRequests"; -import { DrawerComponent } from "./components/Drawer/Drawer"; -import { AddressQRCode } from "./components/AddressQRCode"; -import { Settings } from "./components/Group/Settings"; -import { MainAvatar } from "./components/MainAvatar"; -import { useRetrieveDataLocalStorage } from "./useRetrieveDataLocalStorage"; -import { useQortalGetSaveSettings } from "./useQortalGetSaveSettings"; -import { useRecoilState, useResetRecoilState, useSetRecoilState } from "recoil"; +} from './components/Chat/GroupAnnouncements'; +import { requestQueueGroupJoinRequests } from './components/Group/GroupJoinRequests'; +import { DrawerComponent } from './components/Drawer/Drawer'; +import { AddressQRCode } from './components/AddressQRCode'; +import { Settings } from './components/Group/Settings'; +import { MainAvatar } from './components/MainAvatar'; +import { useRetrieveDataLocalStorage } from './useRetrieveDataLocalStorage'; +import { useQortalGetSaveSettings } from './useQortalGetSaveSettings'; import { canSaveSettingToQdnAtom, enabledDevModeAtom, - fullScreenAtom, + groupAnnouncementsAtom, + groupChatTimestampsAtom, + groupsOwnerNamesAtom, groupsPropertiesAtom, hasSettingsChangedAtom, isDisabledEditorEnterAtom, + isRunningPublicNodeAtom, isUsingImportExportSettingsAtom, lastPaymentSeenTimestampAtom, mailsAtom, + memberGroupsAtom, + mutedGroupsAtom, oldPinnedAppsAtom, qMailLastEnteredTimestampAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom, -} from "./atoms/global"; -import { useAppFullScreen } from "./useAppFullscreen"; -import { NotAuthenticated } from "./ExtStates/NotAuthenticated"; -import { - openIndexedDB, - showSaveFilePicker, -} from "./components/Apps/useQortalMessageListener"; -import { fileToBase64 } from "./utils/fileReading"; -import { handleGetFileFromIndexedDB } from "./utils/indexedDB"; -import { CoreSyncStatus } from "./components/CoreSyncStatus"; -import { Wallets } from "./Wallets"; -import { RandomSentenceGenerator } from "./utils/seedPhrase/RandomSentenceGenerator"; -import { useFetchResources } from "./common/useFetchResources"; -import { Tutorials } from "./components/Tutorials/Tutorials"; -import { useHandleTutorials } from "./components/Tutorials/useHandleTutorials"; -import BoundedNumericTextField from "./common/BoundedNumericTextField"; -import { useHandleUserInfo } from "./components/Group/useHandleUserInfo"; -import { Minting } from "./components/Minting/Minting"; -import { isRunningGateway } from "./qortalRequests"; -import { QMailStatus } from "./components/QMailStatus"; -import { GlobalActions } from "./components/GlobalActions/GlobalActions"; -import { useBlockedAddresses } from "./components/Group/useBlockUsers"; -import { WalletIcon } from "./assets/Icons/WalletIcon"; -import { DrawerUserLookup } from "./components/Drawer/DrawerUserLookup"; -import { UserLookup } from "./components/UserLookup.tsx/UserLookup"; -import { RegisterName } from "./components/RegisterName"; -import { BuyQortInformation } from "./components/BuyQortInformation"; -import { QortPayment } from "./components/QortPayment"; -import { GeneralNotifications } from "./components/GeneralNotifications"; -import { PdfViewer } from "./common/PdfViewer"; + timestampEnterDataAtom, + txListAtom, +} from './atoms/global'; +import { NotAuthenticated } from './ExtStates/NotAuthenticated'; +import { handleGetFileFromIndexedDB } from './utils/indexedDB'; +import { Wallets } from './Wallets'; +import { useFetchResources } from './common/useFetchResources'; +import { Tutorials } from './components/Tutorials/Tutorials'; +import { useHandleTutorials } from './components/Tutorials/useHandleTutorials'; +import { useHandleUserInfo } from './components/Group/useHandleUserInfo'; +import { Minting } from './components/Minting/Minting'; +import { isRunningGateway } from './qortalRequests'; +import { QMailStatus } from './components/QMailStatus'; +import { GlobalActions } from './components/GlobalActions/GlobalActions'; +import { useBlockedAddresses } from './components/Group/useBlockUsers'; +import { WalletIcon } from './assets/Icons/WalletIcon'; +import { UserLookup } from './components/UserLookup.tsx/UserLookup'; +import { RegisterName } from './components/RegisterName'; +import { BuyQortInformation } from './components/BuyQortInformation'; +import { QortPayment } from './components/QortPayment'; +import { GeneralNotifications } from './components/GeneralNotifications'; +import { PdfViewer } from './common/PdfViewer'; +import ThemeSelector from './components/Theme/ThemeSelector.tsx'; +import { useTranslation } from 'react-i18next'; +import LanguageSelector from './components/Language/LanguageSelector.tsx'; +import { DownloadWallet } from './components/Auth/DownloadWallet.tsx'; +import { CopyIcon } from './assets/Icons/CopyIcon.tsx'; +import { SuccessIcon } from './assets/Icons/SuccessIcon.tsx'; +import { useAtom, useSetAtom } from 'jotai'; +import { useResetAtom } from 'jotai/utils'; type extStates = - | "not-authenticated" - | "authenticated" - | "send-qort" - | "web-app-request-connection" - | "web-app-request-payment" - | "web-app-request-authentication" - | "download-wallet" - | "create-wallet" - | "transfer-success-regular" - | "transfer-success-request" - | "wallet-dropped" - | "web-app-request-buy-order" - | "buy-order-submitted" - | "wallets" - | "group"; + | 'not-authenticated' + | 'authenticated' + | 'send-qort' + | 'web-app-request-connection' + | 'web-app-request-payment' + | 'web-app-request-authentication' + | 'download-wallet' + | 'create-wallet' + | 'transfer-success-regular' + | 'transfer-success-request' + | 'wallet-dropped' + | 'web-app-request-buy-order' + | 'buy-order-submitted' + | 'wallets' + | 'group'; interface MyContextInterface { - txList: any[]; - memberGroups: any[]; - setTxList: (val) => void; - setMemberGroups: (val) => void; isShow: boolean; onCancel: () => void; onOk: () => void; @@ -191,41 +172,15 @@ interface MyContextInterface { } const defaultValues: MyContextInterface = { - txList: [], - memberGroups: [], - setTxList: () => {}, - setMemberGroups: () => {}, isShow: false, onCancel: () => {}, onOk: () => {}, show: () => {}, message: { - publishFee: "", - message: "", + publishFee: '', + message: '', }, }; -export let isMobile = false; - -const isMobileDevice = () => { - const userAgent = navigator.userAgent || navigator.vendor || window.opera; - - if (/android/i.test(userAgent)) { - return true; // Android device - } - - if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) { - return true; // iOS device - } - - return false; -}; - -if (isMobileDevice()) { - isMobile = true; - console.log("Running on a mobile device"); -} else { - console.log("Running on a desktop"); -} export const allQueues = { requestQueueCommentCount: requestQueueCommentCount, @@ -238,7 +193,7 @@ const controlAllQueues = (action) => { Object.keys(allQueues).forEach((key) => { const val = allQueues[key]; try { - if (typeof val[action] === "function") { + if (typeof val[action] === 'function') { val[action](); } } catch (error) { @@ -247,7 +202,6 @@ const controlAllQueues = (action) => { }); }; - export const clearAllQueues = () => { Object.keys(allQueues).forEach((key) => { const val = allQueues[key]; @@ -260,31 +214,31 @@ export const clearAllQueues = () => { }; export const pauseAllQueues = () => { - controlAllQueues("pause"); - window.sendMessage("pauseAllQueues", {}).catch((error) => { + controlAllQueues('pause'); + window.sendMessage('pauseAllQueues', {}).catch((error) => { console.error( - "Failed to pause all queues:", - error.message || "An error occurred" - ); - }); -}; -export const resumeAllQueues = () => { - controlAllQueues("resume"); - window.sendMessage("resumeAllQueues", {}).catch((error) => { - console.error( - "Failed to resume all queues:", - error.message || "An error occurred" + 'Failed to pause all queues:', + error.message || 'An error occurred' ); }); }; +export const resumeAllQueues = () => { + controlAllQueues('resume'); + window.sendMessage('resumeAllQueues', {}).catch((error) => { + console.error( + 'Failed to resume all queues:', + error.message || 'An error occurred' + ); + }); +}; const defaultValuesGlobal = { openTutorialModal: null, - setOpenTutorialModal: ()=> {} -} + setOpenTutorialModal: () => {}, +}; + export const MyContext = createContext(defaultValues); -export const GlobalContext = createContext(defaultValuesGlobal); export let globalApiKey: string | null = null; @@ -299,14 +253,7 @@ export const getBaseApiReact = (customApi?: string) => { return groupApi; } }; -// export const getArbitraryEndpointReact = () => { -// if (globalApiKey) { -// return `/arbitrary/resources/search`; -// } else { -// return `/arbitrary/resources/searchsimple`; -// } -// }; export const getArbitraryEndpointReact = () => { if (globalApiKey) { return `/arbitrary/resources/searchsimple`; @@ -314,6 +261,7 @@ export const getArbitraryEndpointReact = () => { return `/arbitrary/resources/searchsimple`; } }; + export const getBaseApiReactSocket = (customApi?: string) => { if (customApi) { return customApi; @@ -321,17 +269,17 @@ export const getBaseApiReactSocket = (customApi?: string) => { if (globalApiKey?.url) { return `${ - getProtocol(globalApiKey?.url) === "http" ? "ws://" : "wss://" + getProtocol(globalApiKey?.url) === 'http' ? 'ws://' : 'wss://' }${cleanUrl(globalApiKey?.url)}`; } else { return groupApiSocket; } }; + export const isMainWindow = true; function App() { - const [extState, setExtstate] = useState("not-authenticated"); - const [desktopViewMode, setDesktopViewMode] = useState("home"); - + const [extState, setExtstate] = useState('not-authenticated'); + const [desktopViewMode, setDesktopViewMode] = useState('home'); const [backupjson, setBackupjson] = useState(null); const [rawWallet, setRawWallet] = useState(null); const [ltcBalanceLoading, setLtcBalanceLoading] = useState(false); @@ -339,46 +287,54 @@ function App() { const [decryptedWallet, setdecryptedWallet] = useState(null); const [requestConnection, setRequestConnection] = useState(null); const [requestBuyOrder, setRequestBuyOrder] = useState(null); - const [authenticatedMode, setAuthenticatedMode] = useState("qort"); + const [authenticatedMode, setAuthenticatedMode] = useState('qort'); const [requestAuthentication, setRequestAuthentication] = useState(null); const [userInfo, setUserInfo] = useState(null); const [balance, setBalance] = useState(null); const [ltcBalance, setLtcBalance] = useState(null); - const [paymentTo, setPaymentTo] = useState(""); + const [paymentTo, setPaymentTo] = useState(''); const [paymentAmount, setPaymentAmount] = useState(0); - const [paymentPassword, setPaymentPassword] = useState(""); - const [sendPaymentError, setSendPaymentError] = useState(""); - const [sendPaymentSuccess, setSendPaymentSuccess] = useState(""); + const [paymentPassword, setPaymentPassword] = useState(''); + const [sendPaymentError, setSendPaymentError] = useState(''); + const [sendPaymentSuccess, setSendPaymentSuccess] = useState(''); const [countdown, setCountdown] = useState(null); const [walletToBeDownloaded, setWalletToBeDownloaded] = useState(null); const [walletToBeDownloadedPassword, setWalletToBeDownloadedPassword] = - useState(""); + useState(''); const [isMain, setIsMain] = useState(true); const isMainRef = useRef(false); - const [authenticatePassword, setAuthenticatePassword] = useState(""); + const [authenticatePassword, setAuthenticatePassword] = useState(''); const [sendqortState, setSendqortState] = useState(null); const [isLoading, setIsLoading] = useState(false); const [isLoadingSendCoin, setIsLoadingSendCoin] = useState(false); + const { t } = useTranslation(['auth', 'core']); + const theme = useTheme(); + const [ walletToBeDownloadedPasswordConfirm, setWalletToBeDownloadedPasswordConfirm, - ] = useState(""); + ] = useState(''); const [walletToBeDownloadedError, setWalletToBeDownloadedError] = - useState(""); + useState(''); const [walletToBeDecryptedError, setWalletToBeDecryptedError] = - useState(""); - const [txList, setTxList] = useState([]); - const [memberGroups, setMemberGroups] = useState([]); + useState(''); const [isFocused, setIsFocused] = useState(true); - const [hasSettingsChanged, setHasSettingsChanged] = useRecoilState( + const [hasSettingsChanged, setHasSettingsChanged] = useAtom( hasSettingsChangedAtom ); - const balanceSetIntervalRef = useRef(null) - const {downloadResource} = useFetchResources() - const holdRefExtState = useRef("not-authenticated"); + + const balanceSetIntervalRef = useRef(null); + const downloadResource = useFetchResources(); + const holdRefExtState = useRef('not-authenticated'); const isFocusedRef = useRef(true); - const {showTutorial, openTutorialModal, shownTutorialsInitiated, setOpenTutorialModal, hasSeenGettingStarted} = useHandleTutorials() + const { + showTutorial, + openTutorialModal, + shownTutorialsInitiated, + setOpenTutorialModal, + hasSeenGettingStarted, + } = useHandleTutorials(); const { isShow, onCancel, onOk, show, message } = useModal(); const { isShow: isShowUnsavedChanges, @@ -394,7 +350,7 @@ function App() { show: showInfo, message: messageInfo, } = useModal(); - + const { onCancel: onCancelQortalRequest, onOk: onOkQortalRequest, @@ -409,200 +365,232 @@ function App() { isShow: isShowQortalRequestExtension, message: messageQortalRequestExtension, } = useModal(); - - const [isRunningPublicNode, setIsRunningPublicNode] = useState(false) + const setIsRunningPublicNode = useSetAtom(isRunningPublicNodeAtom); const [infoSnack, setInfoSnack] = useState(null); const [openSnack, setOpenSnack] = useState(false); const [hasLocalNode, setHasLocalNode] = useState(false); const [isOpenDrawerProfile, setIsOpenDrawerProfile] = useState(false); - const [isOpenDrawerLookup, setIsOpenDrawerLookup] = useState(false) - const [apiKey, setApiKey] = useState(""); + const [isOpenDrawerLookup, setIsOpenDrawerLookup] = useState(false); + const [apiKey, setApiKey] = useState(''); const [isOpenSendQort, setIsOpenSendQort] = useState(false); const [isOpenSendQortSuccess, setIsOpenSendQortSuccess] = useState(false); - const [rootHeight, setRootHeight] = useState("100%"); - const {isUserBlocked, + const { + isUserBlocked, addToBlockList, - removeBlockFromList, getAllBlockedUsers} = useBlockedAddresses() - const [currentNode, setCurrentNode] = useState({ - url: "http://127.0.0.1:12391", - }); - const [useLocalNode, setUseLocalNode] = useState(false); - - const [confirmRequestRead, setConfirmRequestRead] = useState(false); + removeBlockFromList, + getAllBlockedUsers, + } = useBlockedAddresses(); + const [currentNode, setCurrentNode] = useState({ + url: 'http://127.0.0.1:12391', + }); + const [useLocalNode, setUseLocalNode] = useState(false); + + const [confirmRequestRead, setConfirmRequestRead] = useState(false); const [isSettingsOpen, setIsSettingsOpen] = useState(false); - const [showSeed, setShowSeed] = useState(false) - const [creationStep, setCreationStep] = useState(1) - const {getIndividualUserInfo} = useHandleUserInfo() + const [showSeed, setShowSeed] = useState(false); + const [creationStep, setCreationStep] = useState(1); + const getIndividualUserInfo = useHandleUserInfo(); const qortalRequestCheckbox1Ref = useRef(null); useRetrieveDataLocalStorage(userInfo?.address); - useQortalGetSaveSettings(userInfo?.name, extState === "authenticated"); - const [fullScreen, setFullScreen] = useRecoilState(fullScreenAtom); - const [isEnabledDevMode, setIsEnabledDevMode] = - useRecoilState(enabledDevModeAtom); - const setIsDisabledEditorEnter = useSetRecoilState(isDisabledEditorEnterAtom) - const [isOpenMinting, setIsOpenMinting] = useState(false) - const { toggleFullScreen } = useAppFullScreen(setFullScreen); - const generatorRef = useRef(null) - const exportSeedphrase = ()=> { - const seedPhrase = generatorRef.current.parsedString - saveSeedPhraseToDisk(seedPhrase) - } + useQortalGetSaveSettings(userInfo?.name, extState === 'authenticated'); + const setIsEnabledDevMode = useSetAtom(enabledDevModeAtom); + + const setIsDisabledEditorEnter = useSetAtom(isDisabledEditorEnterAtom); + + const [isOpenMinting, setIsOpenMinting] = useState(false); + const generatorRef = useRef(null); + + const exportSeedphrase = () => { + const seedPhrase = generatorRef.current.parsedString; + saveSeedPhraseToDisk(seedPhrase); + }; + const passwordRef = useRef(null); + useEffect(() => { - if (extState === "wallet-dropped" && passwordRef.current) { + if (extState === 'wallet-dropped' && passwordRef.current) { passwordRef.current.focus(); } }, [extState]); + useEffect(() => { - const isDevModeFromStorage = localStorage.getItem("isEnabledDevMode"); + const isDevModeFromStorage = localStorage.getItem('isEnabledDevMode'); if (isDevModeFromStorage) { setIsEnabledDevMode(JSON.parse(isDevModeFromStorage)); } }, []); - useEffect(()=> { - isRunningGateway().then((res)=> { - setIsRunningPublicNode(res) - }).catch((error)=> { - console.error(error) + useEffect(() => { + isRunningGateway() + .then((res) => { + setIsRunningPublicNode(res); }) - }, [extState]) - - useEffect(()=> { - if(!shownTutorialsInitiated) return - if(extState === 'not-authenticated'){ - showTutorial('create-account') - } else if(extState === "create-wallet" && walletToBeDownloaded){ - showTutorial('important-information') - } else if(extState === "authenticated"){ - showTutorial('getting-started') - } - }, [extState, walletToBeDownloaded, shownTutorialsInitiated]) + .catch((error) => { + console.error(error); + }); + }, [extState]); useEffect(() => { - // Attach a global event listener for double-click - const handleDoubleClick = () => { - toggleFullScreen(); - }; + if (!shownTutorialsInitiated) return; + if (extState === 'not-authenticated') { + showTutorial('create-account'); + } else if (extState === 'create-wallet' && walletToBeDownloaded) { + showTutorial('important-information'); + } else if (extState === 'authenticated') { + showTutorial('getting-started'); + } + }, [extState, walletToBeDownloaded, shownTutorialsInitiated]); - // Add the event listener to the root HTML document - document.documentElement.addEventListener("dblclick", handleDoubleClick); - - // Clean up the event listener on unmount - return () => { - document.documentElement.removeEventListener( - "dblclick", - handleDoubleClick - ); - }; - }, [toggleFullScreen]); //resets for recoil - const resetAtomSortablePinnedAppsAtom = useResetRecoilState( - sortablePinnedAppsAtom + const resetAtomSortablePinnedAppsAtom = useResetAtom(sortablePinnedAppsAtom); + const resetAtomIsUsingImportExportSettingsAtom = useResetAtom( + isUsingImportExportSettingsAtom ); - const resetAtomIsUsingImportExportSettingsAtom = useResetRecoilState(isUsingImportExportSettingsAtom) - const resetAtomCanSaveSettingToQdnAtom = useResetRecoilState( + const resetAtomCanSaveSettingToQdnAtom = useResetAtom( canSaveSettingToQdnAtom ); - const resetAtomSettingsQDNLastUpdatedAtom = useResetRecoilState( + const resetAtomSettingsQDNLastUpdatedAtom = useResetAtom( settingsQDNLastUpdatedAtom ); - const resetAtomSettingsLocalLastUpdatedAtom = useResetRecoilState( + const resetAtomSettingsLocalLastUpdatedAtom = useResetAtom( settingsLocalLastUpdatedAtom ); - const resetAtomOldPinnedAppsAtom = useResetRecoilState(oldPinnedAppsAtom); - const resetAtomQMailLastEnteredTimestampAtom = useResetRecoilState(qMailLastEnteredTimestampAtom) - const resetAtomMailsAtom = useResetRecoilState(mailsAtom) - const resetGroupPropertiesAtom = useResetRecoilState(groupsPropertiesAtom) - const resetLastPaymentSeenTimestampAtom = useResetRecoilState(lastPaymentSeenTimestampAtom) + const resetAtomOldPinnedAppsAtom = useResetAtom(oldPinnedAppsAtom); + const resetAtomQMailLastEnteredTimestampAtom = useResetAtom( + qMailLastEnteredTimestampAtom + ); + const resetAtomMailsAtom = useResetAtom(mailsAtom); + const resetGroupPropertiesAtom = useResetAtom(groupsPropertiesAtom); + const resetLastPaymentSeenTimestampAtom = useResetAtom( + lastPaymentSeenTimestampAtom + ); + const resetGroupsOwnerNamesAtom = useResetAtom(groupsOwnerNamesAtom); + const resetGroupAnnouncementsAtom = useResetAtom(groupAnnouncementsAtom); + const resetMutedGroupsAtom = useResetAtom(mutedGroupsAtom); + const resetGroupChatTimestampsAtom = useResetAtom(groupChatTimestampsAtom); + const resetTimestampEnterAtom = useResetAtom(timestampEnterDataAtom); + const resettxListAtomAtom = useResetAtom(txListAtom); + const resetmemberGroupsAtomAtom = useResetAtom(memberGroupsAtom); + const resetAllRecoil = () => { resetAtomSortablePinnedAppsAtom(); resetAtomCanSaveSettingToQdnAtom(); resetAtomSettingsQDNLastUpdatedAtom(); resetAtomSettingsLocalLastUpdatedAtom(); resetAtomOldPinnedAppsAtom(); - resetAtomIsUsingImportExportSettingsAtom() - resetAtomQMailLastEnteredTimestampAtom() - resetAtomMailsAtom() - resetGroupPropertiesAtom() - resetLastPaymentSeenTimestampAtom() - + resetAtomIsUsingImportExportSettingsAtom(); + resetAtomQMailLastEnteredTimestampAtom(); + resetAtomMailsAtom(); + resetGroupPropertiesAtom(); + resetLastPaymentSeenTimestampAtom(); + resetGroupsOwnerNamesAtom(); + resetGroupAnnouncementsAtom(); + resetMutedGroupsAtom(); + resetGroupChatTimestampsAtom(); + resetTimestampEnterAtom(); + resettxListAtomAtom(); + resetmemberGroupsAtomAtom(); }; - useEffect(() => { - if (!isMobile) return; - // Function to set the height of the app to the viewport height - const resetHeight = () => { - const height = window.visualViewport - ? window.visualViewport.height - : window.innerHeight; - // Set the height to the root element (usually #root) - document.getElementById("root").style.height = height + "px"; - setRootHeight(height + "px"); - }; - // Set the initial height - resetHeight(); + const contextValue = useMemo( + () => ({ + isShow, + onCancel, + onOk, + show, + userInfo, + message, + showInfo, + openSnackGlobal: openSnack, + setOpenSnackGlobal: setOpenSnack, + infoSnackCustom: infoSnack, + setInfoSnackCustom: setInfoSnack, + downloadResource, + getIndividualUserInfo, + isUserBlocked, + addToBlockList, + removeBlockFromList, + getAllBlockedUsers, + showTutorial, + openTutorialModal, + setOpenTutorialModal, + hasSeenGettingStarted, + }), + [ + isShow, + onCancel, + onOk, + show, + userInfo, + message, + showInfo, + openSnack, + setOpenSnack, + infoSnack, + setInfoSnack, + downloadResource, + getIndividualUserInfo, + isUserBlocked, + addToBlockList, + removeBlockFromList, + getAllBlockedUsers, + showTutorial, + openTutorialModal, + setOpenTutorialModal, + hasSeenGettingStarted, + ] + ); - // Add event listeners for resize and visualViewport changes - window.addEventListener("resize", resetHeight); - window.visualViewport?.addEventListener("resize", resetHeight); - - // Clean up the event listeners when the component unmounts - return () => { - window.removeEventListener("resize", resetHeight); - window.visualViewport?.removeEventListener("resize", resetHeight); - }; - }, []); const handleSetGlobalApikey = (key) => { globalApiKey = key; }; + useEffect(() => { try { setIsLoading(true); window - .sendMessage("getApiKey") - .then((response) => { - if (response) { - handleSetGlobalApikey(response); - setApiKey(response); - } - }) - .catch((error) => { - console.error( - "Failed to get API key:", - error?.message || "An error occurred" - ); - }).finally(()=> { - window - .sendMessage("getWalletInfo") + .sendMessage('getApiKey') .then((response) => { - if (response && response?.walletInfo) { - setRawWallet(response?.walletInfo); - if ( - holdRefExtState.current === "web-app-request-payment" || - holdRefExtState.current === "web-app-request-connection" || - holdRefExtState.current === "web-app-request-buy-order" - ) - return; - if (response?.hasKeyPair) { - setExtstate("authenticated"); - } else { - setExtstate("wallet-dropped"); - } + if (response) { + handleSetGlobalApikey(response); + setApiKey(response); } }) .catch((error) => { - console.error("Failed to get wallet info:", error); + console.error( + 'Failed to get API key:', + error?.message || 'An error occurred' + ); + }) + .finally(() => { + window + .sendMessage('getWalletInfo') + .then((response) => { + if (response && response?.walletInfo) { + setRawWallet(response?.walletInfo); + if ( + holdRefExtState.current === 'web-app-request-payment' || + holdRefExtState.current === 'web-app-request-connection' || + holdRefExtState.current === 'web-app-request-buy-order' + ) + return; + if (response?.hasKeyPair) { + setExtstate('authenticated'); + } else { + setExtstate('wallet-dropped'); + } + } + }) + .catch((error) => { + console.error('Failed to get wallet info:', error); + }); }); - }) } catch (error) { - + console.log(error); } finally { setIsLoading(false); - } - }, []); useEffect(() => { if (extState) { @@ -610,51 +598,31 @@ function App() { } }, [extState]); - useEffect(()=> { + useEffect(() => { try { - const val = localStorage.getItem('settings-disable-editor-enter'); - if(val){ - const parsedVal = JSON.parse(val) - if(parsedVal === false || parsedVal === true){ - setIsDisabledEditorEnter(parsedVal) + const val = localStorage.getItem('settings-disable-editor-enter'); + if (val) { + const parsedVal = JSON.parse(val); + if (parsedVal === false || parsedVal === true) { + setIsDisabledEditorEnter(parsedVal); } } } catch (error) { - + console.log(error); } - }, []) + }, []); useEffect(() => { isFocusedRef.current = isFocused; }, [isFocused]); - // const checkIfUserHasLocalNode = useCallback(async () => { - // try { - // const url = `http://127.0.0.1:12391/admin/status`; - // const response = await fetch(url, { - // method: "GET", - // headers: { - // "Content-Type": "application/json", - // }, - // }); - // const data = await response.json(); - // if (data?.isSynchronizing === false && data?.syncPercent === 100) { - // setHasLocalNode(true); - // } - // } catch (error) {} - // }, []); - - // useEffect(() => { - // checkIfUserHasLocalNode(); - // }, [extState]); - const address = useMemo(() => { - if (!rawWallet?.address0) return ""; + if (!rawWallet?.address0) return ''; return rawWallet.address0; }, [rawWallet]); const { getRootProps, getInputProps } = useDropzone({ accept: { - "application/json": [".json"], // Only accept JSON files + 'application/json': ['.json'], // Only accept JSON files }, maxFiles: 1, onDrop: async (acceptedFiles) => { @@ -662,8 +630,8 @@ function App() { const fileContents = await new Promise((resolve, reject) => { const reader = new FileReader(); - reader.onabort = () => reject("File reading was aborted"); - reader.onerror = () => reject("File reading has failed"); + reader.onabort = () => reject('File reading was aborted'); + reader.onerror = () => reject('File reading has failed'); reader.onload = () => { // Resolve the promise with the reader result when reading completes resolve(reader.result); @@ -677,26 +645,28 @@ function App() { let pf: any; try { - if (typeof fileContents !== "string") return; + if (typeof fileContents !== 'string') return; pf = JSON.parse(fileContents); - } catch (e) {} + } catch (e) { + console.log(error); + } try { const requiredFields = [ - "address0", - "salt", - "iv", - "version", - "encryptedSeed", - "mac", - "kdfThreads", + 'address0', + 'salt', + 'iv', + 'version', + 'encryptedSeed', + 'mac', + 'kdfThreads', ]; for (const field of requiredFields) { - if (!(field in pf)) throw new Error(field + " not found in JSON"); + if (!(field in pf)) throw new Error(field + ' not found in JSON'); } setRawWallet(pf); // setExtstate("authenticated"); - setExtstate("wallet-dropped"); + setExtstate('wallet-dropped'); setdecryptedWallet(null); } catch (e) { console.log(e); @@ -725,57 +695,57 @@ function App() { }; }; - - const balanceSetInterval = ()=> { + const balanceSetInterval = () => { try { - if(balanceSetIntervalRef?.current){ + if (balanceSetIntervalRef?.current) { clearInterval(balanceSetIntervalRef?.current); } let isCalling = false; - balanceSetIntervalRef.current = setInterval(async () => { + balanceSetIntervalRef.current = setInterval(async () => { if (isCalling) return; isCalling = true; window - .sendMessage("balance") - .then((response) => { - if (!response?.error && !isNaN(+response)) { - setBalance(response); - } - isCalling = false; - }) - .catch((error) => { - console.error("Failed to get balance:", error); - isCalling = false; - }); + .sendMessage('balance') + .then((response) => { + if (!response?.error && !isNaN(+response)) { + setBalance(response); + } + isCalling = false; + }) + .catch((error) => { + console.error('Failed to get balance:', error); + isCalling = false; + }); }, 40000); } catch (error) { - console.error(error) + console.error(error); } - } + }; const getBalanceFunc = () => { setQortBalanceLoading(true); window - .sendMessage("balance") + .sendMessage('balance') .then((response) => { if (!response?.error && !isNaN(+response)) { setBalance(response); } - + setQortBalanceLoading(false); }) .catch((error) => { - console.error("Failed to get balance:", error); + console.error('Failed to get balance:', error); setQortBalanceLoading(false); - }).finally(()=> { - balanceSetInterval() + }) + .finally(() => { + balanceSetInterval(); }); }; const getLtcBalanceFunc = () => { setLtcBalanceLoading(true); window - .sendMessage("ltcBalance") + .sendMessage('ltcBalance') .then((response) => { if (!response?.error && !isNaN(+response)) { setLtcBalance(response); @@ -783,11 +753,10 @@ function App() { setLtcBalanceLoading(false); }) .catch((error) => { - console.error("Failed to get LTC balance:", error); + console.error('Failed to get LTC balance:', error); setLtcBalanceLoading(false); }); }; - const clearAllStates = () => { setRequestConnection(null); @@ -795,19 +764,19 @@ function App() { }; const qortalRequestPermissonFromExtension = async (message, event) => { - if (message.action === "QORTAL_REQUEST_PERMISSION") { + if (message.action === 'QORTAL_REQUEST_PERMISSION') { try { if (message?.payload?.checkbox1) { qortalRequestCheckbox1Ref.current = message?.payload?.checkbox1?.value || false; } - setConfirmRequestRead(false) + setConfirmRequestRead(false); await showQortalRequestExtension(message?.payload); if (qortalRequestCheckbox1Ref.current) { event.source.postMessage( { - action: "QORTAL_REQUEST_PERMISSION_RESPONSE", + action: 'QORTAL_REQUEST_PERMISSION_RESPONSE', requestId: message?.requestId, result: { accepted: true, @@ -820,7 +789,7 @@ function App() { } event.source.postMessage( { - action: "QORTAL_REQUEST_PERMISSION_RESPONSE", + action: 'QORTAL_REQUEST_PERMISSION_RESPONSE', requestId: message?.requestId, result: { accepted: true, @@ -831,7 +800,7 @@ function App() { } catch (error) { event.source.postMessage( { - action: "QORTAL_REQUEST_PERMISSION_RESPONSE", + action: 'QORTAL_REQUEST_PERMISSION_RESPONSE', requestId: message?.requestId, result: { accepted: false, @@ -851,43 +820,43 @@ function App() { } const message = event.data; - if (message?.action === "CHECK_FOCUS") { + if (message?.action === 'CHECK_FOCUS') { event.source.postMessage( - { action: "CHECK_FOCUS_RESPONSE", isFocused: isFocusedRef.current }, + { action: 'CHECK_FOCUS_RESPONSE', isFocused: isFocusedRef.current }, event.origin ); - } else if (message.action === "NOTIFICATION_OPEN_DIRECT") { - executeEvent("openDirectMessage", { + } else if (message.action === 'NOTIFICATION_OPEN_DIRECT') { + executeEvent('openDirectMessage', { from: message.payload.from, }); - } else if (message.action === "NOTIFICATION_OPEN_GROUP") { - executeEvent("openGroupMessage", { + } else if (message.action === 'NOTIFICATION_OPEN_GROUP') { + executeEvent('openGroupMessage', { from: message.payload.from, }); - } else if (message.action === "NOTIFICATION_OPEN_ANNOUNCEMENT_GROUP") { - executeEvent("openGroupAnnouncement", { + } else if (message.action === 'NOTIFICATION_OPEN_ANNOUNCEMENT_GROUP') { + executeEvent('openGroupAnnouncement', { from: message.payload.from, }); - } else if (message.action === "NOTIFICATION_OPEN_THREAD_NEW_POST") { - executeEvent("openThreadNewPost", { + } else if (message.action === 'NOTIFICATION_OPEN_THREAD_NEW_POST') { + executeEvent('openThreadNewPost', { data: message.payload.data, }); } else if ( - message.action === "QORTAL_REQUEST_PERMISSION" && + message.action === 'QORTAL_REQUEST_PERMISSION' && message?.isFromExtension ) { qortalRequestPermissonFromExtension(message, event); - } else if (message?.action === "getFileFromIndexedDB") { + } else if (message?.action === 'getFileFromIndexedDB') { handleGetFileFromIndexedDB(event); } }; // Attach the event listener - window.addEventListener("message", messageHandler); + window.addEventListener('message', messageHandler); // Clean up the event listener on component unmount return () => { - window.removeEventListener("message", messageHandler); + window.removeEventListener('message', messageHandler); }; }, []); @@ -907,7 +876,6 @@ function App() { // REMOVED FOR MOBILE APP }; - const getUserInfo = useCallback(async (useTimer?: boolean) => { try { if (useTimer) { @@ -918,18 +886,20 @@ function App() { }); } window - .sendMessage("userInfo") + .sendMessage('userInfo') .then((response) => { if (response && !response.error) { setUserInfo(response); } }) .catch((error) => { - console.error("Failed to get user info:", error); + console.error('Failed to get user info:', error); }); getBalanceFunc(); - } catch (error) {} + } catch (error) { + console.log(error); + } }, []); useEffect(() => { @@ -939,13 +909,13 @@ function App() { useEffect(() => { return () => { - console.log("exit"); + console.log('exit'); }; }, []); useEffect(() => { if ( - authenticatedMode === "ltc" && + authenticatedMode === 'ltc' && !ltcBalanceLoading && ltcBalance === null ) { @@ -953,27 +923,6 @@ function App() { } }, [authenticatedMode]); - const confirmPasswordToDownload = async () => { - try { - setWalletToBeDownloadedError(""); - if (!walletToBeDownloadedPassword) { - setSendPaymentError("Please enter your password"); - return; - } - setIsLoading(true); - await new Promise((res) => { - setTimeout(() => { - res(); - }, 250); - }); - const res = await saveWalletFunc(walletToBeDownloadedPassword); - } catch (error: any) { - setWalletToBeDownloadedError(error?.message); - } finally { - setIsLoading(false); - } - }; - const saveFileToDiskFunc = async () => { try { await saveFileToDisk( @@ -982,44 +931,44 @@ function App() { ); } catch (error: any) { setWalletToBeDownloadedError(error?.message); - } finally { } }; - const saveWalletToLocalStorage = async (newWallet)=> { + const saveWalletToLocalStorage = async (newWallet) => { try { - getWallets().then((res)=> { - - if(res && Array.isArray(res)){ - const wallets = [...res, newWallet] - storeWallets(wallets) - } else { - storeWallets([newWallet]) - } - setIsLoading(false) - }).catch((error)=> { - console.error(error) - setIsLoading(false) - }) + getWallets() + .then((res) => { + if (res && Array.isArray(res)) { + const wallets = [...res, newWallet]; + storeWallets(wallets); + } else { + storeWallets([newWallet]); + } + setIsLoading(false); + }) + .catch((error) => { + console.error(error); + setIsLoading(false); + }); } catch (error) { - console.error(error) + console.error(error); } - } + }; const createAccountFunc = async () => { try { if (!walletToBeDownloadedPassword) { - setWalletToBeDownloadedError("Please enter a password"); + setWalletToBeDownloadedError('Please enter a password'); return; } if (!walletToBeDownloadedPasswordConfirm) { - setWalletToBeDownloadedError("Please confirm your password"); + setWalletToBeDownloadedError('Please confirm your password'); return; } if ( walletToBeDownloadedPasswordConfirm !== walletToBeDownloadedPassword ) { - setWalletToBeDownloadedError("Password fields do not match!"); + setWalletToBeDownloadedError('Password fields do not match!'); return; } setIsLoading(true); @@ -1035,21 +984,21 @@ function App() { () => {} ); window - .sendMessage("decryptWallet", { + .sendMessage('decryptWallet', { password: walletToBeDownloadedPassword, wallet, }) .then((response) => { if (response && !response.error) { setRawWallet(wallet); - saveWalletToLocalStorage(wallet) + saveWalletToLocalStorage(wallet); setWalletToBeDownloaded({ wallet, qortAddress: wallet.address0, }); window - .sendMessage("userInfo") + .sendMessage('userInfo') .then((response2) => { setIsLoading(false); if (response2 && !response2.error) { @@ -1058,7 +1007,7 @@ function App() { }) .catch((error) => { setIsLoading(false); - console.error("Failed to get user info:", error); + console.error('Failed to get user info:', error); }); getBalanceFunc(); @@ -1069,7 +1018,7 @@ function App() { }) .catch((error) => { setIsLoading(false); - console.error("Failed to decrypt wallet:", error); + console.error('Failed to decrypt wallet:', error); }); } catch (error: any) { setWalletToBeDownloadedError(error?.message); @@ -1077,55 +1026,56 @@ function App() { } }; - const logoutFunc = async () => { + const logoutFunc = useCallback(async () => { try { if (hasSettingsChanged) { await showUnsavedChanges({ message: - "Your settings have changed. If you logout you will lose your changes. Click on the save button in the header to keep your changed settings.", - }); - } else if(extState === 'authenticated') { + 'Your settings have changed. If you logout you will lose your changes. Click on the save button in the header to keep your changed settings.', + }); // TODO translate + } else if (extState === 'authenticated') { await showUnsavedChanges({ - message: - "Are you sure you would like to logout?", + message: 'Are you sure you would like to logout?', }); } window - .sendMessage("logout", {}) + .sendMessage('logout', {}) .then((response) => { if (response) { - executeEvent("logout-event", {}); + executeEvent('logout-event', {}); resetAllStates(); } }) .catch((error) => { console.error( - "Failed to log out:", - error.message || "An error occurred" + 'Failed to log out:', + error.message || 'An error occurred' ); }); - } catch (error) {} - }; + } catch (error) { + console.log(error); + } + }, [hasSettingsChanged, extState]); const returnToMain = () => { - setPaymentTo(""); + setPaymentTo(''); setPaymentAmount(0); - setPaymentPassword(""); - setSendPaymentError(""); - setSendPaymentSuccess(""); + setPaymentPassword(''); + setSendPaymentError(''); + setSendPaymentSuccess(''); setCountdown(null); setWalletToBeDownloaded(null); - setWalletToBeDownloadedPassword(""); - setShowSeed(false) - setCreationStep(1) - setExtstate("authenticated"); + setWalletToBeDownloadedPassword(''); + setShowSeed(false); + setCreationStep(1); + setExtstate('authenticated'); setIsOpenSendQort(false); setIsOpenSendQortSuccess(false); }; const resetAllStates = () => { - setExtstate("not-authenticated"); - setAuthenticatedMode("qort"); + setExtstate('not-authenticated'); + setAuthenticatedMode('qort'); setBackupjson(null); setRawWallet(null); setdecryptedWallet(null); @@ -1135,25 +1085,23 @@ function App() { setUserInfo(null); setBalance(null); setLtcBalance(null); - setPaymentTo(""); + setPaymentTo(''); setPaymentAmount(0); - setPaymentPassword(""); - setSendPaymentError(""); - setSendPaymentSuccess(""); + setPaymentPassword(''); + setSendPaymentError(''); + setSendPaymentSuccess(''); setCountdown(null); setWalletToBeDownloaded(null); - setWalletToBeDownloadedPassword(""); - setShowSeed(false) - setCreationStep(1) + setWalletToBeDownloadedPassword(''); + setShowSeed(false); + setCreationStep(1); - setWalletToBeDownloadedPasswordConfirm(""); - setWalletToBeDownloadedError(""); + setWalletToBeDownloadedPasswordConfirm(''); + setWalletToBeDownloadedError(''); setSendqortState(null); setHasLocalNode(false); - setTxList([]); - setMemberGroups([]); resetAllRecoil(); - if(balanceSetIntervalRef?.current){ + if (balanceSetIntervalRef?.current) { clearInterval(balanceSetIntervalRef?.current); } }; @@ -1166,7 +1114,7 @@ function App() { const authenticateWallet = async () => { try { setIsLoading(true); - setWalletToBeDecryptedError(""); + setWalletToBeDecryptedError(''); await new Promise((res) => { setTimeout(() => { res(); @@ -1174,7 +1122,7 @@ function App() { }); window .sendMessage( - "decryptWallet", + 'decryptWallet', { password: authenticatePassword, wallet: rawWallet, @@ -1183,12 +1131,12 @@ function App() { ) .then((response) => { if (response && !response.error) { - setAuthenticatePassword(""); - setExtstate("authenticated"); - setWalletToBeDecryptedError(""); + setAuthenticatePassword(''); + setExtstate('authenticated'); + setWalletToBeDecryptedError(''); window - .sendMessage("userInfo") + .sendMessage('userInfo') .then((response) => { setIsLoading(false); if (response && !response.error) { @@ -1197,20 +1145,20 @@ function App() { }) .catch((error) => { setIsLoading(false); - console.error("Failed to get user info:", error); + console.error('Failed to get user info:', error); }); getBalanceFunc(); window - .sendMessage("getWalletInfo") + .sendMessage('getWalletInfo') .then((response) => { if (response && response.walletInfo) { setRawWallet(response.walletInfo); } }) .catch((error) => { - console.error("Failed to get wallet info:", error); + console.error('Failed to get wallet info:', error); }); } else if (response?.error) { setIsLoading(false); @@ -1219,10 +1167,10 @@ function App() { }) .catch((error) => { setIsLoading(false); - console.error("Failed to decrypt wallet:", error); + console.error('Failed to decrypt wallet:', error); }); } catch (error) { - setWalletToBeDecryptedError("Unable to authenticate. Wrong password"); + setWalletToBeDecryptedError('Unable to authenticate. Wrong password'); } }; @@ -1231,14 +1179,6 @@ function App() { // Handler for when the window gains focus const handleFocus = () => { setIsFocused(true); - if (isMobile) { - window.sendMessage("clearAllNotifications", {}).catch((error) => { - console.error( - "Failed to clear notifications:", - error.message || "An error occurred" - ); - }); - } }; // Handler for when the window loses focus @@ -1247,33 +1187,25 @@ function App() { }; // Attach the event listeners - window.addEventListener("focus", handleFocus); - window.addEventListener("blur", handleBlur); + window.addEventListener('focus', handleFocus); + window.addEventListener('blur', handleBlur); // Optionally, listen for visibility changes const handleVisibilityChange = () => { - if (document.visibilityState === "visible") { + if (document.visibilityState === 'visible') { setIsFocused(true); - if (isMobile) { - window.sendMessage("clearAllNotifications", {}).catch((error) => { - console.error( - "Failed to clear notifications:", - error.message || "An error occurred" - ); - }); - } } else { setIsFocused(false); } }; - document.addEventListener("visibilitychange", handleVisibilityChange); + document.addEventListener('visibilitychange', handleVisibilityChange); // Cleanup the event listeners on component unmount return () => { - window.removeEventListener("focus", handleFocus); - window.removeEventListener("blur", handleBlur); - document.removeEventListener("visibilitychange", handleVisibilityChange); + window.removeEventListener('focus', handleFocus); + window.removeEventListener('blur', handleBlur); + document.removeEventListener('visibilitychange', handleVisibilityChange); }; }, []); @@ -1283,15 +1215,15 @@ function App() { setOpenSnack(true); setInfoSnack({ type, - message + message, }); }; useEffect(() => { - subscribeToEvent("openGlobalSnackBar", openGlobalSnackBarFunc); + subscribeToEvent('openGlobalSnackBar', openGlobalSnackBarFunc); return () => { - unsubscribeFromEvent("openGlobalSnackBar", openGlobalSnackBarFunc); + unsubscribeFromEvent('openGlobalSnackBar', openGlobalSnackBarFunc); }; }, []); @@ -1303,342 +1235,370 @@ function App() { }; useEffect(() => { - subscribeToEvent("openPaymentInternal", openPaymentInternal); + subscribeToEvent('openPaymentInternal', openPaymentInternal); return () => { - unsubscribeFromEvent("openPaymentInternal", openPaymentInternal); + unsubscribeFromEvent('openPaymentInternal', openPaymentInternal); }; }, []); - - - const renderProfileLeft = ()=> { - - return - - - {authenticatedMode === "qort" && ( - LITECOIN WALLET} - placement="left" - arrow - sx={{ fontSize: "24" }} - slotProps={{ - tooltip: { - sx: { - color: "#ffffff", - backgroundColor: "#444444", - }, + const renderProfileLeft = () => { + return ( + + + + {authenticatedMode === 'qort' && ( + + LITECOIN WALLET + + } + placement="left" + arrow + sx={{ fontSize: '24' }} + slotProps={{ + tooltip: { + sx: { + color: theme.palette.text.primary, + backgroundColor: theme.palette.background.default, }, - arrow: { - sx: { - color: "#444444", - }, + }, + arrow: { + sx: { + color: theme.palette.text.primary, }, - }} - > - { - - setAuthenticatedMode("ltc"); - }} - src={ltcLogo} - style={{ - cursor: "pointer", - width: "20px", - height: "auto", - }} - /> - - )} - {authenticatedMode === "ltc" && ( - QORTAL WALLET} - placement="left" - arrow - sx={{ fontSize: "24" }} - slotProps={{ - tooltip: { - sx: { - color: "#ffffff", - backgroundColor: "#444444", - }, - }, - arrow: { - sx: { - color: "#444444", - }, - }, - }} - > - { - setAuthenticatedMode("qort"); - }} - src={qortLogo} - style={{ - cursor: "pointer", - width: "20px", - height: "auto", - }} - /> - - )} - - - - {authenticatedMode === "ltc" ? ( - <> - - - - - {rawWallet?.ltcAddress?.slice(0, 6)}... - {rawWallet?.ltcAddress?.slice(-4)} - - - - {ltcBalanceLoading && ( - - )} - {!isNaN(+ltcBalance) && !ltcBalanceLoading && ( - - - {ltcBalance} LTC - - { + setAuthenticatedMode('ltc'); + }} + src={ltcLogo} + style={{ + cursor: 'pointer', + width: '20px', + height: 'auto', + }} + /> + + )} + {authenticatedMode === 'ltc' && ( + + QORTAL WALLET + + } + placement="left" + arrow + sx={{ fontSize: '24' }} + slotProps={{ + tooltip: { + sx: { + color: theme.palette.text.primary, + backgroundColor: theme.palette.background.default, + }, + }, + arrow: { + sx: { + color: theme.palette.text.primary, + }, + }, }} + > + { + setAuthenticatedMode('qort'); + }} + src={qortLogo} + style={{ + cursor: 'pointer', + width: '20px', + height: 'auto', + }} + /> + + )} + + + + + {authenticatedMode === 'ltc' ? ( + <> + + + + + { + if (rawWallet?.ltcAddress) { + navigator.clipboard + .writeText(rawWallet.ltcAddress) + .catch((err) => { + console.error('Failed to copy LTC address:', err); + }); + } + }} + > + + {rawWallet?.ltcAddress?.slice(0, 6)}... + {rawWallet?.ltcAddress?.slice(-4)}{' '} + + + + + + + {ltcBalanceLoading && ( + + )} + {!isNaN(+ltcBalance) && !ltcBalanceLoading && ( + + + {ltcBalance} LTC + + + + + )} + + + ) : ( + <> + - + + + + + {userInfo?.name} + + + + + { + if (rawWallet?.address0) { + navigator.clipboard + .writeText(rawWallet.address0) + .catch((err) => { + console.error('Failed to copy address:', err); + }); + } + }} + > + + {rawWallet?.address0?.slice(0, 6)}... + {rawWallet?.address0?.slice(-4)}{' '} + + + + + + {qortBalanceLoading && ( + + )} + {!qortBalanceLoading && balance >= 0 && ( + + + {balance?.toFixed(2)} QORT + + + + )} + + + {userInfo && !userInfo?.name && ( + { + executeEvent('openRegisterName', {}); + }} + > + REGISTER NAME + + )} + + { + setIsOpenSendQort(true); + // setExtstate("send-qort"); + setIsOpenDrawerProfile(false); + }} + > + Transfer QORT + + + )} - - - ) : ( - <> - - { + executeEvent('addTab', { + data: { service: 'APP', name: 'q-trade' }, + }); + executeEvent('open-apps-mode', {}); }} > - {userInfo?.name} + Get QORT at Q-Trade - - - - {rawWallet?.address0?.slice(0, 6)}... - {rawWallet?.address0?.slice(-4)} - - - - {qortBalanceLoading && ( - - )} - {!qortBalanceLoading && balance >= 0 && ( - - - {balance?.toFixed(2)} QORT - - - - )} - - - {userInfo && !userInfo?.name && ( - { - executeEvent('openRegisterName', {}) - }} - > - REGISTER NAME - - )} - - { - setIsOpenSendQort(true); - // setExtstate("send-qort"); - setIsOpenDrawerProfile(false); - }} - > - Transfer QORT - - - - )} - { - executeEvent("addTab", { - data: { service: "APP", name: "q-trade" }, - }); - executeEvent("open-apps-mode", {}); - }} - > - Get QORT at Q-Trade - - - } + + ); + }; const renderProfile = () => { return ( - {isMobile && ( - - { - setIsOpenDrawerProfile(false); - }} - sx={{ - cursor: "pointer", - color: "white", - }} - /> - - )} - {desktopViewMode !== "apps" && - desktopViewMode !== "dev" && - desktopViewMode !== "chat" && ( - <> - {renderProfileLeft()} - - )} + {desktopViewMode !== 'apps' && + desktopViewMode !== 'dev' && + desktopViewMode !== 'chat' && <>{renderProfileLeft()}} - - {!isMobile && ( - <> - - LOG OUT} - placement="left" - arrow - sx={{ fontSize: "24" }} - slotProps={{ - tooltip: { - sx: { - color: "#ffffff", - backgroundColor: "#444444", - }, - }, - arrow: { - sx: { - color: "#444444", - }, - }, - }} - > - { - logoutFunc(); - setIsOpenDrawerProfile(false); - }} + + { + logoutFunc(); + setIsOpenDrawerProfile(false); + }} + > + - - - )} + > + {t('core:action.logout')} + + } + placement="left" + arrow + sx={{ fontSize: '24' }} + slotProps={{ + tooltip: { + sx: { + color: theme.palette.text.primary, + backgroundColor: theme.palette.background.paper, + }, + }, + arrow: { + sx: { + color: theme.palette.text.primary, + }, + }, + }} + > + + + + SETTINGS} + title={ + + {t('core:settings')} + + } placement="left" arrow - sx={{ fontSize: "24" }} + sx={{ fontSize: '24' }} slotProps={{ tooltip: { sx: { - color: "#ffffff", - backgroundColor: "#444444", + color: theme.palette.text.primary, + backgroundColor: theme.palette.background.paper, }, }, arrow: { sx: { - color: "#444444", + color: theme.palette.text.primary, }, }, }} > + + { setIsOpenDrawerLookup(true); }} > USER LOOKUP} + title={ + + {t('core:user_lookup')} + + } placement="left" arrow - sx={{ fontSize: "24" }} + sx={{ fontSize: '24' }} slotProps={{ tooltip: { sx: { - color: "#ffffff", - backgroundColor: "#444444", + color: theme.palette.text.primary, + backgroundColor: theme.palette.background.paper, }, }, arrow: { sx: { - color: "#444444", + color: theme.palette.text.primary, }, }, }} > + + { - executeEvent('openWalletsApp', {}) + executeEvent('openWalletsApp', {}); }} > WALLETS} + title={ + + {t('core:wallet.wallet_other')} + + } placement="left" arrow - sx={{ fontSize: "24" }} + sx={{ fontSize: '24' }} slotProps={{ tooltip: { sx: { - color: "#ffffff", - backgroundColor: "#444444", + color: theme.palette.text.primary, + backgroundColor: theme.palette.background.paper, }, }, arrow: { sx: { - color: "#444444", + color: theme.palette.text.primary, }, }, }} > - - + {desktopViewMode !== 'home' && ( <> - - YOUR ACCOUNT} - placement="left" - arrow - sx={{ fontSize: "24" }} - slotProps={{ - tooltip: { - sx: { - color: "#ffffff", - backgroundColor: "#444444", - }, - }, - arrow: { - sx: { - color: "#444444", - }, - }, - }} - > - { - setIsOpenDrawerProfile(true); - }}> - - - - + + {t('auth:account.your')} + + } + placement="left" + arrow + sx={{ fontSize: '24' }} + slotProps={{ + tooltip: { + sx: { + color: theme.palette.text.primary, + backgroundColor: theme.palette.background.paper, + }, + }, + arrow: { + sx: { + color: theme.palette.text.primary, + }, + }, + }} + > + { + setIsOpenDrawerProfile(true); + }} + > + + + )} - - - - + + - + + {extState === 'authenticated' && ( )} - + - {extState === "authenticated" && isMainWindow && ( - - - - - + {extState === 'authenticated' && isMainWindow && ( + <> + + + )} - - { - try { - const res = await isRunningGateway() - if(res) throw new Error('Cannot view minting details on the gateway') - setIsOpenMinting(true) - } catch (error) { - setOpenSnack(true) - setInfoSnack({ - type: 'error', - message: error?.message - }) - } - }}> + + + { + try { + const res = await isRunningGateway(); + if (res) + throw new Error( + 'Cannot view minting details on the gateway' + ); + setIsOpenMinting(true); + } catch (error) { + setOpenSnack(true); + setInfoSnack({ + type: 'error', + message: error?.message, + }); + } + }} + > MINTING STATUS} + title={ + + {t('core:minting_status')} + + } placement="left" arrow - sx={{ fontSize: "24" }} + sx={{ fontSize: '24' }} slotProps={{ tooltip: { sx: { - color: "#ffffff", - backgroundColor: "#444444", + color: theme.palette.text.primary, + backgroundColor: theme.palette.background.paper, }, }, arrow: { sx: { - color: "#444444", + color: theme.palette.text.primary, }, }, }} > - + - + - {(desktopViewMode === "apps" || desktopViewMode === "home") && ( - { - if(desktopViewMode === "apps"){ - showTutorial('qapps', true) - } else { - showTutorial('getting-started', true) - } - }} > - TUTORIAL} - placement="left" - arrow - sx={{ fontSize: "24" }} - slotProps={{ - tooltip: { - sx: { - color: "#ffffff", - backgroundColor: "#444444", - }, + + {(desktopViewMode === 'apps' || desktopViewMode === 'home') && ( + { + if (desktopViewMode === 'apps') { + showTutorial('qapps', true); + } else { + showTutorial('getting-started', true); + } + }} + > + + {t('core:tutorial')} + + } + placement="left" + arrow + sx={{ fontSize: '24' }} + slotProps={{ + tooltip: { + sx: { + color: theme.palette.text.primary, + backgroundColor: theme.palette.background.paper, }, - arrow: { - sx: { - color: "#444444", - }, + }, + arrow: { + sx: { + color: theme.palette.text.primary, }, + }, + }} + > + - - - - )} - - - BACKUP WALLET} - placement="left" - arrow - sx={{ fontSize: "24" }} - slotProps={{ - tooltip: { - sx: { - color: "#ffffff", - backgroundColor: "#444444", - }, - }, - arrow: { - sx: { - color: "#444444", - }, - }, - }} - > - + + + )} + + + + { - setExtstate("download-wallet"); + setExtstate('download-wallet'); setIsOpenDrawerProfile(false); }} - src={Download} - style={{ - cursor: "pointer", - width: '20px' - }} - /> - + > + + {t('core:action.backup_wallet')} + + } + placement="left" + arrow + sx={{ fontSize: '24' }} + slotProps={{ + tooltip: { + sx: { + color: theme.palette.text.primary, + backgroundColor: theme.palette.background.paper, + }, + }, + arrow: { + sx: { + color: theme.palette.text.primary, + }, + }, + }} + > + + + - + ); @@ -1937,1491 +1964,239 @@ function App() { return ( - - - {extState === "not-authenticated" && ( - - )} - {/* {extState !== "not-authenticated" && ( + + + {extState === 'not-authenticated' && ( + + )} + {/* {extState !== "not-authenticated" && ( )} */} - {extState === "authenticated" && isMainWindow && ( - + {extState === 'authenticated' && isMainWindow && ( - {!isMobile && renderProfile()} + {renderProfile()} - - - - )} - {isOpenSendQort && isMainWindow && ( - - + )} + {isOpenSendQort && isMainWindow && ( - - - - { - setIsOpenSendQort(false); - setIsOpenSendQortSuccess(true); - }} - defaultPaymentTo={paymentTo} - /> - - )} - - {isShowQortalRequest && !isMainWindow && ( - <> - - - - {messageQortalRequest?.text1} - - - {messageQortalRequest?.text2 && ( - <> - - - - {messageQortalRequest?.text2} - - - - - )} - {messageQortalRequest?.text3 && ( - <> - - - {messageQortalRequest?.text3} - - - - - )} - - {messageQortalRequest?.text4 && ( + - - {messageQortalRequest?.text4} - - - )} - - {messageQortalRequest?.html && ( -
- )} - - - - {messageQortalRequest?.highlightedText} - - - {messageQortalRequest?.fee && ( - <> - - - - {"Fee: "} - {messageQortalRequest?.fee} - {" QORT"} - - - - )} - {messageQortalRequest?.checkbox1 && ( - - { - qortalRequestCheckbox1Ref.current = e.target.checked; - }} - edge="start" - tabIndex={-1} - disableRipple - defaultChecked={messageQortalRequest?.checkbox1?.value} - sx={{ - "&.Mui-checked": { - color: "white", // Customize the color when checked - }, - "& .MuiSvgIcon-root": { - color: "white", - }, - }} - /> - - - {messageQortalRequest?.checkbox1?.label} - - - )} - - - - onOkQortalRequest("accepted")} - > - accept - - onCancelQortalRequest()} - > - decline - - - {sendPaymentError} - - )} - {extState === "web-app-request-buy-order" && !isMainWindow && ( - <> - - - - The Application

{" "} - {requestBuyOrder?.hostname}

- - is requesting {requestBuyOrder?.crosschainAtInfo?.length}{" "} - {`buy order${ - requestBuyOrder?.crosschainAtInfo.length === 1 ? "" : "s" - }`} - -
- - - {requestBuyOrder?.crosschainAtInfo?.reduce((latest, cur) => { - return latest + +cur?.qortAmount; - }, 0)}{" "} - QORT - - - - FOR - - - - {roundUpToDecimals( - requestBuyOrder?.crosschainAtInfo?.reduce((latest, cur) => { - return latest + +cur?.expectedForeignAmount; - }, 0) - )} - {` ${requestBuyOrder?.crosschainAtInfo?.[0]?.foreignBlockchain}`} - - {/* - - - Confirm Wallet Password - - - setPaymentPassword(e.target.value)} - /> */} - - - confirmBuyOrder(false)} - > - accept - - confirmBuyOrder(true)} - > - decline - - - {sendPaymentError} - - )} - - {extState === "web-app-request-payment" && !isMainWindow && ( - <> - - - - The Application

{" "} - {sendqortState?.hostname}

- is requesting a payment -
- - - {sendqortState?.description} - - - - {sendqortState?.amount} QORT - - {/* - - - Confirm Wallet Password - - - setPaymentPassword(e.target.value)} - /> */} - - - confirmPayment(false)} - > - accept - - confirmPayment(true)} - > - decline - - - {sendPaymentError} - - )} - {extState === "web-app-request-connection" && !isMainWindow && ( - <> - -
- -
- - - The Application

{" "} - {requestConnection?.hostname}

- is requestion a connection -
- - - - responseToConnectionRequest( - true, - requestConnection?.hostname, - requestConnection.interactionId - ) - } - > - accept - - - responseToConnectionRequest( - false, - requestConnection?.hostname, - requestConnection.interactionId - ) - } - > - decline - - - - )} - {extState === "web-app-request-authentication" && !isMainWindow && ( - <> - -
- -
- - - The Application

{" "} - {requestConnection?.hostname}

- requests authentication -
- - - - - - Authenticate - - - { - setExtstate("create-wallet"); - }} - > - Create account - - - )} - {extState === "wallets" && ( - <> - - - { - setRawWallet(null); - setExtstate("not-authenticated"); - logoutFunc(); - }} - src={Return} - /> - - - - - )} - {rawWallet && extState === "wallet-dropped" && ( - <> - - - { - setRawWallet(null); - setExtstate("wallets"); - logoutFunc(); - }} - src={Return} - /> - - -
- -
- - - {rawWallet?.name ? rawWallet?.name : rawWallet?.address0} - - - Authenticate - - - - - <> - - Wallet Password - - - setAuthenticatePassword(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") { - authenticateWallet(); - } - }} - ref={passwordRef} - /> - {useLocalNode ? ( - <> - - - {"Using node: "} {currentNode?.url} - - - ) : ( - <> - - - {"Using public node"} - - - )} - - - - Authenticate - - {walletToBeDecryptedError} - - - )} - {extState === "download-wallet" && ( - <> - - - - - -
- -
- - - - Download Account - - - - {!walletToBeDownloaded && ( - <> - - Confirm Wallet Password - - - - setWalletToBeDownloadedPassword(e.target.value) - - } - /> - - - Confirm password - - {walletToBeDownloadedError} - - )} - - {walletToBeDownloaded && ( - <> - { - await saveFileToDiskFunc(); - await showInfo({ - message: `Keep your account file secure.`, - }); - }} - > - Download account - - - )} - - )} - {extState === "create-wallet" && ( - <> - {!walletToBeDownloaded && ( - <> - - - { - if(creationStep === 2){ - setCreationStep(1) - return - } - setExtstate("not-authenticated"); - setShowSeed(false) - setCreationStep(1) - setWalletToBeDownloadedPasswordConfirm('') - setWalletToBeDownloadedPassword('') - }} - src={Return} - /> - - -
- -
- - - Set up your Qortal account - - - - - - A ‘ { - setShowSeed(true) - }} style={{ - fontSize: '14px', - color: 'steelblue', - cursor: 'pointer' - }}>SEEDPHRASE ’ has been randomly generated in the background. - - - - - If you wish to VIEW THE SEEDPHRASE, click the word 'SEEDPHRASE' in this text. Seedphrases are used to generate the private key for your Qortal account. For security by default, seedphrases are NOT displayed unless specifically chosen. - - - Create your Qortal account by clicking NEXT below. - - - - { - setCreationStep(2) - }}> - Next - - -
- -
- - - - Your seedphrase - - - {generatorRef.current?.parsedString} - - - - Export Seedphrase - - - - - - - - - - -
- - - - Wallet Password - - - - setWalletToBeDownloadedPassword(e.target.value) - } + onClick={returnToMain} /> - - - Confirm Wallet Password - - - - setWalletToBeDownloadedPasswordConfirm(e.target.value) - } - /> - - There is no minimum length requirement - + - - Create Account - - - {walletToBeDownloadedError} - - )} + - {walletToBeDownloaded && ( - <> - - - - - Congrats, you’re all set up! - - - - - Save your account in a place where you will remember it! - - - { - await saveFileToDiskFunc(); - returnToMain(); - await showInfo({ - message: `Keep your wallet file secure.`, - }); - }} - > - Backup Account - - - )} - - )} - {isOpenSendQortSuccess && ( - - - - - - The transfer was succesful! - - - { - returnToMain(); - }} - > - Continue - - - )} - {extState === "transfer-success-request" && ( - <> - - - - - The transfer was succesful! - - - { - window.close(); - }} - > - Continue - - - )} - {extState === "buy-order-submitted" && ( - <> - - - - - Your buy order was submitted - - - { - window.close(); - }} - > - Close - - - )} - {countdown && ( - - {/* */} - { - window.close(); - }} - size={75} - strokeWidth={8} - > - {({ remainingTime }) => {remainingTime}} - - - )} - {isLoading && } - {isShow && ( - - {message.paymentFee ? "Payment" : "Publish"} - - - {message.message} - - {message?.paymentFee && ( - - payment fee: {message.paymentFee} - - )} - {message?.publishFee && ( - - publish fee: {message.publishFee} - - )} - - - - - - - - )} - {isShowInfo && ( - - {"Important Info"} - - - {messageInfo.message} - - - - - - - )} - {isShowUnsavedChanges && ( - - {"LOGOUT"} - - - {messageUnsavedChanges.message} - - - - - - - - )} - {isShowQortalRequestExtension && isMainWindow && ( - - { - onCancelQortalRequestExtension(); - }} - size={50} - strokeWidth={5} - > - {({ remainingTime }) => {remainingTime}} - - + { + setIsOpenSendQort(false); + setIsOpenSendQortSuccess(true); + }} + defaultPaymentTo={paymentTo} + /> + + )} + {isShowQortalRequest && !isMainWindow && ( + <> + - {messageQortalRequestExtension?.text1} + {messageQortalRequest?.text1} - {messageQortalRequestExtension?.text2 && ( + + {messageQortalRequest?.text2 && ( <> - {messageQortalRequestExtension?.text2} + {messageQortalRequest?.text2} )} - {messageQortalRequestExtension?.text3 && ( + {messageQortalRequest?.text3 && ( <> - {messageQortalRequestExtension?.text3} + {messageQortalRequest?.text3} + )} - {messageQortalRequestExtension?.text4 && ( + {messageQortalRequest?.text4 && ( - {messageQortalRequestExtension?.text4} + {messageQortalRequest?.text4} )} - {messageQortalRequestExtension?.html && ( + {messageQortalRequest?.html && (
)} - {messageQortalRequestExtension?.highlightedText} + {messageQortalRequest?.highlightedText} - {messageQortalRequestExtension?.json && ( - <> - - - - - - - )} - - {messageQortalRequestExtension?.fee && ( + {messageQortalRequest?.fee && ( <> - {"Fee: "} - {messageQortalRequestExtension?.fee} - {" QORT"} + {'Fee: '} + {messageQortalRequest?.fee} + {' QORT'} )} - {messageQortalRequestExtension?.appFee && ( - <> - - {"App Fee: "} - {messageQortalRequestExtension?.appFee} - {" QORT"} - - - - )} - {messageQortalRequestExtension?.foreignFee && ( - <> - - - - {"Foreign Fee: "} - {messageQortalRequestExtension?.foreignFee} - - - - )} - {messageQortalRequestExtension?.checkbox1 && ( + {messageQortalRequest?.checkbox1 && ( - {messageQortalRequestExtension?.checkbox1?.label} + {messageQortalRequest?.checkbox1?.label} )} -{messageQortalRequestExtension?.confirmCheckbox && ( - setConfirmRequestRead(e.target.checked)} - checked={confirmRequestRead} - edge="start" - tabIndex={-1} - disableRipple - sx={{ - "&.Mui-checked": { - color: "white", - }, - "& .MuiSvgIcon-root": { - color: "white", - }, - }} - /> - } - label={ - - - I have read this request - - - - } - /> - )} - - { - if(messageQortalRequestExtension?.confirmCheckbox && !confirmRequestRead) return - onOkQortalRequestExtension("accepted") + minWidth: '102px', }} + onClick={() => onOkQortalRequest('accepted')} > accept - - + onCancelQortalRequestExtension()} + onClick={() => onCancelQortalRequest()} > decline - + {sendPaymentError} + + )} + {extState === 'web-app-request-buy-order' && !isMainWindow && ( + <> + + + + The Application

{' '} + {requestBuyOrder?.hostname}

+ + is requesting {requestBuyOrder?.crosschainAtInfo?.length}{' '} + {`buy order${ + requestBuyOrder?.crosschainAtInfo.length === 1 ? '' : 's' + }`} + +
+ + + {requestBuyOrder?.crosschainAtInfo?.reduce((latest, cur) => { + return latest + +cur?.qortAmount; + }, 0)}{' '} + QORT + + + + FOR + + + + {roundUpToDecimals( + requestBuyOrder?.crosschainAtInfo?.reduce((latest, cur) => { + return latest + +cur?.expectedForeignAmount; + }, 0) + )} + {` ${requestBuyOrder?.crosschainAtInfo?.[0]?.foreignBlockchain}`} + + + + + confirmBuyOrder(false)} + > + accept + + confirmBuyOrder(true)} + > + decline + + + {sendPaymentError} + + )} + {extState === 'web-app-request-payment' && !isMainWindow && ( + <> + + + + The Application

{' '} + {sendqortState?.hostname}

+ is requesting a payment +
+ + + {sendqortState?.description} + + + + {sendqortState?.amount} QORT + + + + + confirmPayment(false)} + > + accept + + confirmPayment(true)} + > + decline + + + {sendPaymentError} + + )} + {extState === 'web-app-request-connection' && !isMainWindow && ( + <> + +
+ +
+ + + The Application

{' '} + {requestConnection?.hostname}

+ is requestion a connection +
+ + + + responseToConnectionRequest( + true, + requestConnection?.hostname, + requestConnection.interactionId + ) + } + > + accept + + + responseToConnectionRequest( + false, + requestConnection?.hostname, + requestConnection.interactionId + ) + } + > + decline + + + + )} + {extState === 'web-app-request-authentication' && !isMainWindow && ( + <> + +
+ +
+ + + + The Application

{' '} + {requestConnection?.hostname}

+ requests authentication +
+ + + + + + + + + + Authenticate + + + + + { + setExtstate('create-wallet'); + }} + > + {t('auth:create_account', { postProcess: 'capitalize' })} + + + )} + {extState === 'wallets' && ( + <> + + + + { + setRawWallet(null); + setExtstate('not-authenticated'); + logoutFunc(); + }} + /> + + + + + )} + {rawWallet && extState === 'wallet-dropped' && ( + <> + + + { + setRawWallet(null); + setExtstate('wallets'); + logoutFunc(); + }} + /> + + +
+ +
+ + + + {rawWallet?.name ? rawWallet?.name : rawWallet?.address0} + + + + + + {t('auth:authenticate', { postProcess: 'capitalize' })} + + + + + + <> + + {t('auth:wallet.password', { postProcess: 'capitalize' })} + + + + + setAuthenticatePassword(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + authenticateWallet(); + } + }} + ref={passwordRef} + /> + {useLocalNode ? ( + <> + + + {t('auth:node.using', { postProcess: 'capitalize' })}:{' '} + {currentNode?.url} + + + ) : ( + <> + + + {t('auth:node.using_public', { postProcess: 'capitalize' })} + + + )} + + + + + {t('auth:authenticate', { postProcess: 'capitalize' })} + + + {walletToBeDecryptedError} + + + )} + {extState === 'download-wallet' && ( + + )} + {extState === 'create-wallet' && ( + <> + {!walletToBeDownloaded && ( + <> + + + + { + if (creationStep === 2) { + setCreationStep(1); + return; + } + setExtstate('not-authenticated'); + setShowSeed(false); + setCreationStep(1); + setWalletToBeDownloadedPasswordConfirm(''); + setWalletToBeDownloadedPassword(''); + }} + /> + + + + +
+ +
+ + + + + Set up your Qortal account + + + + + + + + A ‘{' '} + { + setShowSeed(true); + }} + style={{ + fontSize: '14px', + color: 'steelblue', + cursor: 'pointer', + }} + > + SEEDPHRASE + {' '} + ’ has been randomly generated in the background. + + + If you wish to VIEW THE SEEDPHRASE, click the word + 'SEEDPHRASE' in this text. Seedphrases are used to + generate the private key for your Qortal account. For + security by default, seedphrases are NOT displayed unless + specifically chosen. + + + Create your Qortal account by clicking{' '} + + NEXT + {' '} + below. + + + { + setCreationStep(2); + }} + > + Next + + +
+ +
+ + + + + Your seedphrase + + + + {generatorRef.current?.parsedString} + + + + Export Seedphrase + + + + + + + +
+ + + + + + Wallet Password + + + + + + setWalletToBeDownloadedPassword(e.target.value) + } + /> + + + + + Confirm Wallet Password + + + + + + setWalletToBeDownloadedPasswordConfirm(e.target.value) + } + /> + + + + There is no minimum length requirement + + + + + + {t('auth:create_account', { postProcess: 'capitalize' })} + + + {walletToBeDownloadedError} + + )} + + {walletToBeDownloaded && ( + <> + + + + + Congrats, you’re all set up! + + + + + + Save your account in a place where you will remember it! + + + + { + await saveFileToDiskFunc(); + returnToMain(); + await showInfo({ + message: `Keep your wallet file secure.`, + }); + }} + > + {t('core:action.backup_account', { + postProcess: 'capitalize', + })} + + + )} + + )} + {isOpenSendQortSuccess && ( + + + + + + {t('core:message.success.transfer', { + postProcess: 'capitalize', + })} + + + { + returnToMain(); + }} + > + {t('core:action.continue', { postProcess: 'capitalize' })} + -
+ )} + {extState === 'transfer-success-request' && ( + <> + + + + + {t('core:message.success.transfer', { + postProcess: 'capitalize', + })} + + + { + window.close(); + }} + > + {t('core:action.continue', { postProcess: 'capitalize' })} + + + )} + {extState === 'buy-order-submitted' && ( + <> + + + + + {t('core:message.success.order_submitted', { + postProcess: 'capitalize', + })} + + + { + window.close(); + }} + > + {t('core:action.close', { postProcess: 'capitalize' })} + + + )} + + {countdown && ( + + {/* */} + { + window.close(); + }} + size={75} + strokeWidth={8} + > + {({ remainingTime }) => {remainingTime}} + + + )} + {isLoading && } + {isShow && ( + + + {message.paymentFee ? 'Payment' : 'Publish'} + + + + {message.message} + + {message?.paymentFee && ( + + {t('core:fee.payment', { + postProcess: 'capitalize', + })} + : {message.paymentFee} + + )} + {message?.publishFee && ( + + {t('core:fee.publish', { + postProcess: 'capitalize', + })} + : {message.publishFee} + + )} + + + + + + + )} + {isShowInfo && ( + + + {'Important Info'} + + + + {messageInfo.message} + + + + + + + )} + {isShowUnsavedChanges && ( + + {'LOGOUT'} + + + {messageUnsavedChanges.message} + + + + + + + + )} + {isShowQortalRequestExtension && isMainWindow && ( + + { + onCancelQortalRequestExtension(); + }} + size={50} + strokeWidth={5} + > + {({ remainingTime }) => {remainingTime}} + + + + + {messageQortalRequestExtension?.text1} + + + {messageQortalRequestExtension?.text2 && ( + <> + + + + {messageQortalRequestExtension?.text2} + + + + + )} + {messageQortalRequestExtension?.text3 && ( + <> + + + {messageQortalRequestExtension?.text3} + + + + + )} + + {messageQortalRequestExtension?.text4 && ( + + + {messageQortalRequestExtension?.text4} + + + )} + + {messageQortalRequestExtension?.html && ( +
+ )} + + + + {messageQortalRequestExtension?.highlightedText} + + + {messageQortalRequestExtension?.json && ( + <> + + + + + + )} + + {messageQortalRequestExtension?.fee && ( + <> + + + + {'Fee: '} + {messageQortalRequestExtension?.fee} + {' QORT'} + + + + )} + {messageQortalRequestExtension?.appFee && ( + <> + + {'App Fee: '} + {messageQortalRequestExtension?.appFee} + {' QORT'} + + + + )} + {messageQortalRequestExtension?.foreignFee && ( + <> + + + + {'Foreign Fee: '} + {messageQortalRequestExtension?.foreignFee} + + + + )} + {messageQortalRequestExtension?.checkbox1 && ( + + { + qortalRequestCheckbox1Ref.current = e.target.checked; + }} + edge="start" + tabIndex={-1} + disableRipple + defaultChecked={ + messageQortalRequestExtension?.checkbox1?.value + } + sx={{ + '&.Mui-checked': { + color: theme.palette.text.secondary, // Customize the color when checked + }, + '& .MuiSvgIcon-root': { + color: theme.palette.text.secondary, + }, + }} + /> + + + {messageQortalRequestExtension?.checkbox1?.label} + + + )} + + {messageQortalRequestExtension?.confirmCheckbox && ( + setConfirmRequestRead(e.target.checked)} + checked={confirmRequestRead} + edge="start" + tabIndex={-1} + disableRipple + sx={{ + '&.Mui-checked': { + color: theme.palette.text.secondary, + }, + '& .MuiSvgIcon-root': { + color: theme.palette.text.secondary, + }, + }} + /> + } + label={ + + + {t('core:message.success.request_read', { + postProcess: 'capitalize', + })} + + + + } + /> + )} + + + + { + if ( + messageQortalRequestExtension?.confirmCheckbox && + !confirmRequestRead + ) + return; + onOkQortalRequestExtension('accepted'); + }} + > + {t('core:action.accept', { + postProcess: 'capitalize', + })} + + onCancelQortalRequestExtension()} + > + {t('core:action.decline', { + postProcess: 'capitalize', + })} + + + {sendPaymentError} + +
+ )} + {isSettingsOpen && ( + + )} + + + {renderProfileLeft()} + + + + + + {extState === 'create-wallet' && walletToBeDownloaded && ( + { + showTutorial('important-information', true); + }} + sx={{ + position: 'fixed', + bottom: '25px', + right: '25px', + }} + > + + )} - {isSettingsOpen && ( - + {isOpenMinting && ( + )} - - - {renderProfileLeft()} - - - - - - {extState === "create-wallet" && walletToBeDownloaded && ( - { - showTutorial('important-information', true) - }} sx={{ - position: 'fixed', - bottom: '25px', - right: '25px' - }}> - - - )} - {isOpenMinting && ( - - )} + + + ); } diff --git a/src/ExtStates/NotAuthenticated.tsx b/src/ExtStates/NotAuthenticated.tsx index 978ff49..789f0fe 100644 --- a/src/ExtStates/NotAuthenticated.tsx +++ b/src/ExtStates/NotAuthenticated.tsx @@ -1,11 +1,16 @@ -import React, { useCallback, useContext, useEffect, useRef, useState } from "react"; -import { Spacer } from "../common/Spacer"; -import { CustomButton, TextItalic, TextP, TextSpan } from "../App-styles"; +import React, { + useCallback, + useContext, + useEffect, + useRef, + useState, +} from 'react'; +import { Spacer } from '../common/Spacer'; +import { CustomButton, TextP, TextSpan } from '../styles/App-styles'; import { Box, Button, ButtonBase, - Checkbox, Dialog, DialogActions, DialogContent, @@ -16,51 +21,50 @@ import { Switch, TextField, 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"; + useTheme, +} from '@mui/material'; +import Logo1Dark from '../assets/svgs/Logo1Dark.svg'; import HelpIcon from '@mui/icons-material/Help'; -import { CustomizedSnackbars } from "../components/Snackbar/Snackbar"; -import { set } from "lodash"; -import { cleanUrl, gateways, isUsingLocal } from "../background"; -import { GlobalContext } from "../App"; +import { CustomizedSnackbars } from '../components/Snackbar/Snackbar'; +import { cleanUrl, gateways } from '../background'; import Tooltip, { TooltipProps, tooltipClasses } from '@mui/material/Tooltip'; +import ThemeSelector from '../components/Theme/ThemeSelector'; +import { useTranslation } from 'react-i18next'; +import LanguageSelector from '../components/Language/LanguageSelector'; +import { MyContext } from '../App'; const manifestData = { - version: "0.5.3", + version: '0.5.4', }; - export const HtmlTooltip = styled(({ className, ...props }: TooltipProps) => ( ))(({ theme }) => ({ [`& .${tooltipClasses.tooltip}`]: { - backgroundColor: '#232428', - color: 'white', + backgroundColor: theme.palette.background.paper, + color: theme.palette.text.primary, maxWidth: 320, padding: '20px', fontSize: theme.typography.pxToRem(12), }, })); + function removeTrailingSlash(url) { return url.replace(/\/+$/, ''); } - export const NotAuthenticated = ({ getRootProps, getInputProps, setExtstate, - apiKey, setApiKey, globalApiKey, handleSetGlobalApikey, currentNode, setCurrentNode, - useLocalNode, - setUseLocalNode + useLocalNode, + setUseLocalNode, }) => { const [isValidApiKey, setIsValidApiKey] = useState(null); const [hasLocalNode, setHasLocalNode] = useState(null); @@ -68,27 +72,29 @@ export const NotAuthenticated = ({ 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 [mode, setMode] = React.useState('list'); const [customNodes, setCustomNodes] = React.useState(null); // 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("https://"); - const [customApikey, setCustomApiKey] = React.useState(""); - const [showSelectApiKey, setShowSelectApiKey] = useState(false) - const [enteredApiKey, setEnteredApiKey] = useState('') + const [url, setUrl] = React.useState('https://'); + const [customApikey, setCustomApiKey] = React.useState(''); + const [showSelectApiKey, setShowSelectApiKey] = useState(false); + const [enteredApiKey, setEnteredApiKey] = useState(''); const [customNodeToSaveIndex, setCustomNodeToSaveIndex] = React.useState(null); - const { showTutorial, hasSeenGettingStarted } = useContext(GlobalContext); + const { showTutorial, hasSeenGettingStarted } = useContext(MyContext); + const theme = useTheme(); + const { t } = useTranslation(['auth', 'core']); const importedApiKeyRef = useRef(null); const currentNodeRef = useRef(null); const hasLocalNodeRef = useRef(null); - const isLocal = cleanUrl(currentNode?.url) === "127.0.0.1:12391"; + const isLocal = cleanUrl(currentNode?.url) === '127.0.0.1:12391'; const handleFileChangeApiKey = (event) => { - setShowSelectApiKey(false) + setShowSelectApiKey(false); const file = event.target.files[0]; // Get the selected file if (file) { const reader = new FileReader(); @@ -96,34 +102,32 @@ 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){ + 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 - }) + url: 'http://127.0.0.1:12391', + apikey: text, + }); } else { copyPrev[findLocalIndex] = { - url: "http://127.0.0.1:12391", - apikey: text - } + url: 'http://127.0.0.1:12391', + apikey: text, + }; } - window - .sendMessage("setCustomNodes", copyPrev) - .catch((error) => { + window.sendMessage('setCustomNodes', copyPrev).catch((error) => { console.error( - "Failed to set custom nodes:", - error.message || "An error occurred" + 'Failed to set custom nodes:', + error.message || 'An error occurred' ); }); - return copyPrev - }) - + return copyPrev; + }); } - }; reader.readAsText(file); // Read the file as text } @@ -133,22 +137,20 @@ export const NotAuthenticated = ({ try { const url = `http://127.0.0.1:12391/admin/status`; const response = await fetch(url, { - method: "GET", + method: 'GET', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, }); const data = await response.json(); if (data?.height) { setHasLocalNode(true); - return true + return true; } - return false - + return false; } catch (error) { - return false - - } + return false; + } }, []); useEffect(() => { @@ -157,25 +159,27 @@ export const NotAuthenticated = ({ useEffect(() => { window - .sendMessage("getCustomNodesFromStorage") + .sendMessage('getCustomNodesFromStorage') .then((response) => { - - setCustomNodes(response || []); - if(window?.electronAPI?.setAllowedDomains){ - window.electronAPI.setAllowedDomains(response?.map((node)=> node.url)) + setCustomNodes(response || []); + 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); } - 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( - "Failed to get custom nodes from storage:", - error.message || "An error occurred" + 'Failed to get custom nodes from storage:', + error.message || 'An error occurred' ); }); }, []); @@ -183,6 +187,7 @@ export const NotAuthenticated = ({ useEffect(() => { importedApiKeyRef.current = importedApiKey; }, [importedApiKey]); + useEffect(() => { currentNodeRef.current = currentNode; }, [currentNode]); @@ -191,56 +196,58 @@ export const NotAuthenticated = ({ hasLocalNodeRef.current = hasLocalNode; }, [hasLocalNode]); - - const validateApiKey = useCallback(async (key, fromStartUp) => { try { - if(key === "isGateway") return - const isLocalKey = cleanUrl(key?.url) === "127.0.0.1:12391"; - if (fromStartUp && key?.url && key?.apikey && !isLocalKey && !gateways.some(gateway => key?.url?.includes(gateway))) { + if (key === 'isGateway') return; + const isLocalKey = cleanUrl(key?.url) === '127.0.0.1:12391'; + if ( + fromStartUp && + key?.url && + key?.apikey && + !isLocalKey && + !gateways.some((gateway) => key?.url?.includes(gateway)) + ) { setCurrentNode({ url: key?.url, apikey: key?.apikey, }); - let isValid = false + 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 + 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 (data2 === 'true') { + isValid = true; } } - + if (isValid) { setIsValidApiKey(true); setUseLocalNode(true); - return + return; } - } if (!currentNodeRef.current) return; - const stillHasLocal = await checkIfUserHasLocalNode() + const stillHasLocal = await checkIfUserHasLocalNode(); if (isLocalKey && !stillHasLocal && !fromStartUp) { - throw new Error("Please turn on your local node"); + 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"; + cleanUrl(currentNodeRef.current?.url) === '127.0.0.1:12391'; if (isLocalKey && !isCurrentNodeLocal) { setIsValidApiKey(false); setUseLocalNode(false); @@ -248,90 +255,89 @@ export const NotAuthenticated = ({ } let payload = {}; - if (currentNodeRef.current?.url === "http://127.0.0.1:12391") { + if (currentNodeRef.current?.url === 'http://127.0.0.1:12391') { payload = { apikey: importedApiKeyRef.current || key?.apikey, url: currentNodeRef.current?.url, }; - if(!payload?.apikey){ + if (!payload?.apikey) { try { - const generateUrl = "http://127.0.0.1:12391/admin/apikey/generate"; + const generateUrl = 'http://127.0.0.1:12391/admin/apikey/generate'; const generateRes = await fetch(generateUrl, { - method: "POST", - }) + method: 'POST', + }); let res; - try { - res = await generateRes.clone().json(); - } catch (e) { - res = await generateRes.text(); - } + try { + res = await generateRes.clone().json(); + } catch (e) { + res = await generateRes.text(); + } if (res != null && !res.error && res.length >= 8) { payload = { apikey: res, url: currentNodeRef.current?.url, }; - + setImportedApiKey(res); // Store the file content in the state - - 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: res - }) - } else { - copyPrev[findLocalIndex] = { - url: "http://127.0.0.1:12391", - apikey: res - } - } - window - .sendMessage("setCustomNodes", copyPrev) - .catch((error) => { - console.error( - "Failed to set custom nodes:", - error.message || "An error occurred" + + 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: res, + }); + } else { + copyPrev[findLocalIndex] = { + url: 'http://127.0.0.1:12391', + apikey: res, + }; + } + window + .sendMessage('setCustomNodes', copyPrev) + .catch((error) => { + console.error( + 'Failed to set custom nodes:', + error.message || 'An error occurred' + ); + }); + return copyPrev; }); - return copyPrev - }) - - } } catch (error) { - console.error(error) + console.error(error); } } } else if (currentNodeRef.current) { payload = currentNodeRef.current; } - let isValid = false - + 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 && data === 'true'){ - isValid = 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 (data2 === 'true') { + isValid = true; } } - if (isValid) { window - .sendMessage("setApiKey", payload) + .sendMessage('setApiKey', payload) .then((response) => { if (response) { handleSetGlobalApikey(payload); @@ -344,31 +350,32 @@ export const NotAuthenticated = ({ }) .catch((error) => { console.error( - "Failed to set API key:", - error.message || "An error occurred" + 'Failed to set API key:', + error.message || t('core:error', { postProcess: 'capitalize' }) ); }); } else { setIsValidApiKey(false); setUseLocalNode(false); - if(!fromStartUp){ + if (!fromStartUp) { setInfoSnack({ - type: "error", - message: "Select a valid apikey", + type: 'error', + message: t('auth:apikey.select_valid', { + postProcess: 'capitalize', + }), }); setOpenSnack(true); } - } } catch (error) { setIsValidApiKey(false); setUseLocalNode(false); if (fromStartUp) { setCurrentNode({ - url: "http://127.0.0.1:12391", + url: 'http://127.0.0.1:12391', }); window - .sendMessage("setApiKey", "isGateway") + .sendMessage('setApiKey', 'isGateway') .then((response) => { if (response) { setApiKey(null); @@ -377,20 +384,27 @@ export const NotAuthenticated = ({ }) .catch((error) => { console.error( - "Failed to set API key:", - error.message || "An error occurred" + 'Failed to set API key:', + error.message || + t('core:error', { + postProcess: 'capitalize', + }) ); }); - return + return; } - if(!fromStartUp){ - setInfoSnack({ - type: "error", - message: error?.message || "Select a valid apikey", - }); - setOpenSnack(true); - } - console.error("Error validating API key:", error); + if (!fromStartUp) { + setInfoSnack({ + type: 'error', + message: + error?.message || + t('auth:apikey.select_valid', { + postProcess: 'capitalize', + }), + }); + setOpenSnack(true); + } + console.error('Error validating API key:', error); } }, []); @@ -401,8 +415,9 @@ export const NotAuthenticated = ({ }, [apiKey]); const addCustomNode = () => { - setMode("add-node"); + setMode('add-node'); }; + const saveCustomNodes = (myNodes, isFullListOfNodes) => { let nodes = [...(myNodes || [])]; if (!isFullListOfNodes && customNodeToSaveIndex !== null) { @@ -418,26 +433,28 @@ export const NotAuthenticated = ({ } setCustomNodes(nodes); - + setCustomNodeToSaveIndex(null); if (!nodes) return; window - .sendMessage("setCustomNodes", nodes) + .sendMessage('setCustomNodes', nodes) .then((response) => { if (response) { - setMode("list"); - setUrl("https://"); - setCustomApiKey(""); - if(window?.electronAPI?.setAllowedDomains){ - window.electronAPI.setAllowedDomains(nodes?.map((node) => node.url)) - } + setMode('list'); + setUrl('https://'); + setCustomApiKey(''); + if (window?.electronAPI?.setAllowedDomains) { + window.electronAPI.setAllowedDomains( + nodes?.map((node) => node.url) + ); + } // add alert if needed } }) .catch((error) => { console.error( - "Failed to set custom nodes:", - error.message || "An error occurred" + 'Failed to set custom nodes:', + error.message || 'An error occurred' ); }); }; @@ -448,163 +465,184 @@ export const NotAuthenticated = ({
+ + - WELCOME TO - QORTAL + {t('auth:welcome', { postProcess: 'capitalize' })} + + {' '} + QORTAL + - + - - Your wallet is like your digital ID on Qortal, and is how you will login to the Qortal User Interface. It holds your public address and the Qortal name you will eventually choose. Every transaction you make is linked to your ID, and this is where you manage all your QORT and other tradeable cryptocurrencies on Qortal. - - } - > - setExtstate('wallets')}> - {/* */} - Accounts - + + + {t('auth:tips.digital_id', { postProcess: 'capitalize' })} + + + } + > + setExtstate('wallets')}> + {t('auth:account.account_many', { postProcess: 'capitalize' })} + - {/* - - */} - New users start here! - - Creating an account means creating a new wallet and digital ID to start using Qortal. Once you have made your account, you can start doing things like obtaining some QORT, buying a name and avatar, publishing videos and blogs, and much more. - - } - > - { - setExtstate("create-wallet"); - }} - sx={{ - backgroundColor: hasSeenGettingStarted === false && 'var(--green)', - color: hasSeenGettingStarted === false && 'black', - "&:hover": { - backgroundColor: hasSeenGettingStarted === false && 'var(--green)', - color: hasSeenGettingStarted === false && 'black' - } - }} + disableHoverListener={hasSeenGettingStarted === true} + placement="right" + title={ + + + {t('auth:tips.new_users', { postProcess: 'capitalize' })} + + + + {t('auth:tips.new_account', { postProcess: 'capitalize' })} + + + } > - Create account - + { + setExtstate('create-wallet'); + }} + sx={{ + backgroundColor: + hasSeenGettingStarted === false && theme.palette.other.positive, + color: hasSeenGettingStarted === false && 'black', + '&:hover': { + backgroundColor: + hasSeenGettingStarted === false && + theme.palette.other.positive, + color: hasSeenGettingStarted === false && 'black', + }, + }} + > + {t('auth:create_account', { postProcess: 'capitalize' })} + - + - {"Using node: "} {currentNode?.url} + {t('auth:node.using', { postProcess: 'capitalize' })}:{' '} + {currentNode?.url} + <> <> - For advanced users + + {t('auth:advanced_users', { postProcess: 'capitalize' })} + { if (event.target.checked) { validateApiKey(currentNode); } else { setCurrentNode({ - url: "http://127.0.0.1:12391", + url: 'http://127.0.0.1:12391', }); setUseLocalNode(false); window - .sendMessage("setApiKey", null) + .sendMessage('setApiKey', null) .then((response) => { if (response) { setApiKey(null); @@ -613,30 +651,43 @@ export const NotAuthenticated = ({ }) .catch((error) => { console.error( - "Failed to set API key:", - error.message || "An error occurred" + 'Failed to set API key:', + error.message || 'An error occurred' ); }); } }} disabled={false} - defaultChecked /> } - label={`Use ${isLocal ? "Local" : "Custom"} Node`} + label={ + isLocal + ? t('auth:node.use_local', { postProcess: 'capitalize' }) + : t('auth:node.use_custom', { postProcess: 'capitalize' }) + } /> - {currentNode?.url === "http://127.0.0.1:12391" && ( + {currentNode?.url === 'http://127.0.0.1:12391' && ( <> - {`api key : ${importedApiKey}`} + > + {t('auth:apikey.key', { postProcess: 'capitalize' })}:{' '} + {importedApiKey} + )} - Build version: {manifestData?.version} + {t('auth:build_version', { postProcess: 'capitalize' })}: + {manifestData?.version} + - {"Custom nodes"} + + {' '} + {t('auth:node.custom_many', { postProcess: 'capitalize' })}: + - {mode === "list" && ( + {mode === 'list' && ( http://127.0.0.1:12391 @@ -751,25 +807,25 @@ export const NotAuthenticated = ({ return ( {node?.url} + + @@ -834,13 +895,13 @@ export const NotAuthenticated = ({ })} )} - {mode === "add-node" && ( + {mode === 'add-node' && ( + - {mode === "list" && ( + {mode === 'list' && ( + + )} + + {mode === 'list' && ( <> )} - {mode === "list" && ( - - )} - {mode === "add-node" && ( + {mode === 'add-node' && ( <> )} @@ -907,105 +970,121 @@ export const NotAuthenticated = ({ )} - {showSelectApiKey && ( + {showSelectApiKey && ( - {"Enter apikey"} + + {t('auth:apikey.enter', { postProcess: 'capitalize' })} + - setEnteredApiKey(e.target.value)}/> - + type="file" + accept=".txt" + hidden + onChange={handleFileChangeApiKey} // File input handler + /> + - - - - - - + + + + )} - { - showTutorial('create-account', true) - }} sx={{ - position: 'fixed', - bottom: '25px', - right: '25px' - }}> - - + { + showTutorial('create-account', true); + }} + sx={{ + position: 'fixed', + bottom: '25px', + right: '25px', + }} + > + + + + + ); }; diff --git a/src/Wallets.tsx b/src/Wallets.tsx index 4211fd0..281e07e 100644 --- a/src/Wallets.tsx +++ b/src/Wallets.tsx @@ -1,48 +1,59 @@ -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"; -import ListItemText from "@mui/material/ListItemText"; -import ListItemAvatar from "@mui/material/ListItemAvatar"; -import Avatar from "@mui/material/Avatar"; -import Typography from "@mui/material/Typography"; -import { Box, Button, ButtonBase, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Input } from "@mui/material"; -import { CustomButton } from "./App-styles"; -import { useDropzone } from "react-dropzone"; -import EditIcon from "@mui/icons-material/Edit"; -import { Label } from "./components/Group/AddGroup"; -import { Spacer } from "./common/Spacer"; -import { getWallets, storeWallets, walletVersion } from "./background"; -import { useModal } from "./common/useModal"; -import PhraseWallet from "./utils/generateWallet/phrase-wallet"; -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"; +import React, { useContext, useEffect, useState } from 'react'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import Divider from '@mui/material/Divider'; +import ListItemText from '@mui/material/ListItemText'; +import ListItemAvatar from '@mui/material/ListItemAvatar'; +import Avatar from '@mui/material/Avatar'; +import Typography from '@mui/material/Typography'; +import { + Box, + Button, + ButtonBase, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + Input, + useTheme, +} from '@mui/material'; +import { CustomButton } from './styles/App-styles'; +import { useDropzone } from 'react-dropzone'; +import EditIcon from '@mui/icons-material/Edit'; +import { Label } from './components/Group/AddGroup'; +import { Spacer } from './common/Spacer'; +import { getWallets, storeWallets, walletVersion } from './background'; +import { useModal } from './common/useModal'; +import PhraseWallet from './utils/generateWallet/phrase-wallet'; +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 { MyContext } from './App'; -const parsefilenameQortal = (filename)=> { - return filename.startsWith("qortal_backup_") ? filename.slice(14) : filename; - } +const parsefilenameQortal = (filename) => { + return filename.startsWith('qortal_backup_') ? filename.slice(14) : filename; +}; export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => { const [wallets, setWallets] = useState([]); const [isLoading, setIsLoading] = useState(true); - const [seedValue, setSeedValue] = useState(""); - const [seedName, setSeedName] = useState(""); - const [seedError, setSeedError] = useState(""); - const { hasSeenGettingStarted } = useContext(GlobalContext); + const [seedValue, setSeedValue] = useState(''); + const [seedName, setSeedName] = useState(''); + const [seedError, setSeedError] = useState(''); + const { hasSeenGettingStarted } = useContext(MyContext); - const [password, setPassword] = useState(""); + const [password, setPassword] = useState(''); const [isOpenSeedModal, setIsOpenSeedModal] = useState(false); const [isLoadingEncryptSeed, setIsLoadingEncryptSeed] = useState(false); - - const { isShow, onCancel, onOk, show, } = useModal(); + const theme = useTheme(); + const { isShow, onCancel, onOk, show } = useModal(); const { getRootProps, getInputProps } = useDropzone({ accept: { - "application/json": [".json"], // Only accept JSON files + 'application/json': ['.json'], // Only accept JSON files }, onDrop: async (acceptedFiles) => { const files: any = acceptedFiles; @@ -53,8 +64,8 @@ export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => { const fileContents = await new Promise((resolve, reject) => { const reader = new FileReader(); - reader.onabort = () => reject("File reading was aborted"); - reader.onerror = () => reject("File reading has failed"); + reader.onabort = () => reject('File reading was aborted'); + reader.onerror = () => reject('File reading has failed'); reader.onload = () => { // Resolve the promise with the reader result when reading completes resolve(reader.result); @@ -63,9 +74,9 @@ export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => { // Read the file as text reader.readAsText(file); }); - if (typeof fileContents !== "string") continue; - const parsedData = JSON.parse(fileContents) - importedWallets.push({...parsedData, filename: file?.name}); + if (typeof fileContents !== 'string') continue; + const parsedData = JSON.parse(fileContents); + importedWallets.push({ ...parsedData, filename: file?.name }); } catch (error) { console.error(error); } @@ -108,83 +119,85 @@ export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => { }); }; - const handleSetSeedValue = async ()=> { + const handleSetSeedValue = async () => { try { - setIsOpenSeedModal(true) - const {seedValue, seedName, password} = await show({ - message: "", - publishFee: "", + setIsOpenSeedModal(true); + const { seedValue, seedName, password } = await show({ + message: '', + publishFee: '', }); - setIsLoadingEncryptSeed(true) - const res = await decryptStoredWalletFromSeedPhrase(seedValue) + setIsLoadingEncryptSeed(true); + const res = await decryptStoredWalletFromSeedPhrase(seedValue); const wallet2 = new PhraseWallet(res, walletVersion); const wallet = await wallet2.generateSaveWalletData( password, crypto.kdfThreads, () => {} ); - if(wallet?.address0){ - setWallets([...wallets, { - ...wallet, - name: seedName - }]); - setIsOpenSeedModal(false) - setSeedValue('') - setSeedName('') - setPassword('') - setSeedError('') + if (wallet?.address0) { + setWallets([ + ...wallets, + { + ...wallet, + name: seedName, + }, + ]); + setIsOpenSeedModal(false); + setSeedValue(''); + setSeedName(''); + setPassword(''); + setSeedError(''); } else { - setSeedError('Could not create account.') + setSeedError('Could not create account.'); } - } catch (error) { - setSeedError(error?.message || 'Could not create account.') + setSeedError(error?.message || 'Could not create account.'); } finally { - setIsLoadingEncryptSeed(false) + setIsLoadingEncryptSeed(false); } - } + }; const selectedWalletFunc = (wallet) => { setRawWallet(wallet); - setExtState("wallet-dropped"); + setExtState('wallet-dropped'); }; - useEffect(()=> { - setIsLoading(true) - getWallets().then((res)=> { - - if(res && Array.isArray(res)){ - setWallets(res) + useEffect(() => { + setIsLoading(true); + getWallets() + .then((res) => { + if (res && Array.isArray(res)) { + setWallets(res); } - setIsLoading(false) - }).catch((error)=> { - console.error(error) - setIsLoading(false) - }) - }, []) + setIsLoading(false); + }) + .catch((error) => { + console.error(error); + setIsLoading(false); + }); + }, []); - useEffect(()=> { - if(!isLoading && wallets && Array.isArray(wallets)){ - storeWallets(wallets) + useEffect(() => { + if (!isLoading && wallets && Array.isArray(wallets)) { + storeWallets(wallets); } - }, [wallets, isLoading]) + }, [wallets, isLoading]); - if(isLoading) return null + if (isLoading) return null; return (
- {(wallets?.length === 0 || - !wallets) ? ( - <> - No accounts saved - - - ): ( - <> - Your saved accounts - - - )} + {wallets?.length === 0 || !wallets ? ( + <> + No accounts saved + + + ) : ( + <> + Your saved accounts + + + )} {rawWallet && ( @@ -196,175 +209,201 @@ export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => { )} {wallets?.length > 0 && ( - - {wallets?.map((wallet, idx) => { - return ( - <> - - - - ); - })} - + + {wallets?.map((wallet, idx) => { + return ( + <> + + + + ); + })} + )} - + - - 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. - - } - > - - - Add seed-phrase - - - 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. - - } - > - - - Add account - + disableHoverListener={hasSeenGettingStarted === true} + title={ + + + 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. + + + } + > + + Add seed-phrase + + + + + + 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. + + + } + > + + + Add account + - { - if (e.key === 'Enter' && seedValue && seedName && password) { - onOk({seedValue, seedName, password}); - } - }} - > - - Type or paste in your seed-phrase - - - + { + if (e.key === 'Enter' && seedValue && seedName && password) { + onOk({ seedValue, seedName, password }); + } + }} + > + + Type or paste in your seed-phrase + + + + - setSeedName(e.target.value)} - /> - - - setSeedName(e.target.value)} + /> + + + + + setSeedValue(e.target.value)} autoComplete="off" sx={{ - width: '100%' + width: '100%', }} /> - - - + + + setPassword(e.target.value)} autoComplete="off" sx={{ - width: '100%' + width: '100%', }} /> - - - - - - - + + + + { - if(!seedValue || !seedName || !password) return - onOk({seedValue, seedName, password}); - }} - autoFocus - > - Add - - {seedError} - - - + disabled={!seedValue || !seedName || !password} + variant="contained" + onClick={() => { + if (!seedValue || !seedName || !password) return; + onOk({ seedValue, seedName, password }); + }} + autoFocus + > + Add + + + {seedError} + + +
- ); }; const WalletItem = ({ wallet, updateWalletItem, idx, setSelectedWallet }) => { - const [name, setName] = useState(""); - const [note, setNote] = useState(""); + const [name, setName] = useState(''); + const [note, setNote] = useState(''); const [isEdit, setIsEdit] = useState(false); + const theme = useTheme(); useEffect(() => { if (wallet?.name) { @@ -382,71 +421,78 @@ const WalletItem = ({ wallet, updateWalletItem, idx, setSelectedWallet }) => { }} sx={{ width: '100%', - padding: '10px' + padding: '10px', }} - > {wallet?.address0} {wallet?.note} - Login + + Login + } /> + { - e.stopPropagation(); - setIsEdit(true); - }} - edge="end" - aria-label="edit" - > - - + sx={{ + alignSelf: 'flex-start', + }} + onClick={(e) => { + e.stopPropagation(); + setIsEdit(true); + }} + edge="end" + aria-label="edit" + > + + {isEdit && ( @@ -455,10 +501,12 @@ const WalletItem = ({ wallet, updateWalletItem, idx, setSelectedWallet }) => { value={name} onChange={(e) => setName(e.target.value)} sx={{ - width: "100%", + width: '100%', }} /> + + { maxLength: 100, }} sx={{ - width: "100%", + width: '100%', }} /> + + - + {logo?.name} + + - - - - ) -} + Trade QORT + + + + + + + Benefits of having QORT + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/components/Chat/AdminSpace.tsx b/src/components/Chat/AdminSpace.tsx index 6eaf541..49109aa 100644 --- a/src/components/Chat/AdminSpace.tsx +++ b/src/components/Chat/AdminSpace.tsx @@ -1,21 +1,7 @@ -import React, { - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -import { GroupMail } from "../Group/Forum/GroupMail"; -import { MyContext, isMobile } from "../../App"; -import { getRootHeight } from "../../utils/mobile/mobileUtils"; -import { Box, Typography } from "@mui/material"; -import { AdminSpaceInner } from "./AdminSpaceInner"; - - - - - +import { useContext, useEffect, useState } from 'react'; +import { MyContext } from '../../App'; +import { Box, Typography } from '@mui/material'; +import { AdminSpaceInner } from './AdminSpaceInner'; export const AdminSpace = ({ selectedGroup, @@ -26,11 +12,12 @@ export const AdminSpace = ({ isAdmin, myAddress, hide, - defaultThread, + defaultThread, setDefaultThread, - setIsForceShowCreationKeyPopup + setIsForceShowCreationKeyPopup, + balance, + isOwner, }) => { - const { rootHeight } = useContext(MyContext); const [isMoved, setIsMoved] = useState(false); useEffect(() => { if (hide) { @@ -42,26 +29,40 @@ export const AdminSpace = ({ return (
- {!isAdmin && Sorry, this space is only for Admins.} - {isAdmin && } - -
+ style={{ + display: 'flex', + flexDirection: 'column', + height: 'calc(100vh - 70px)', + left: hide && '-1000px', + opacity: hide ? 0 : 1, + position: hide ? 'fixed' : 'relative', + visibility: hide && 'hidden', + width: '100%', + overflow: 'auto', + }} + > + {!isAdmin && ( + + Sorry, this space is only for Admins. + + )} + {isAdmin && ( + + )} +
); }; diff --git a/src/components/Chat/AdminSpaceInner.tsx b/src/components/Chat/AdminSpaceInner.tsx index f469e9e..9a1cffd 100644 --- a/src/components/Chat/AdminSpaceInner.tsx +++ b/src/components/Chat/AdminSpaceInner.tsx @@ -1,39 +1,43 @@ -import React, { useCallback, useContext, useEffect, useState } from "react"; +import { useCallback, useContext, useEffect, useState } from 'react'; import { MyContext, getArbitraryEndpointReact, getBaseApiReact, -} from "../../App"; -import { Box, Button, Typography } from "@mui/material"; +} 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"; +} 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 { GroupAvatar } from '../GroupAvatar'; export const getPublishesFromAdminsAdminSpace = async ( admins: string[], groupId ) => { - const queryString = admins.map((name) => `name=${name}`).join("&"); + 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"); + throw new Error('network error'); } const adminData = await response.json(); const filterId = adminData.filter( (data: any) => data.identifier === `admins-symmetric-qchat-group-${groupId}` ); + if (filterId?.length === 0) { return false; } + const sortedData = filterId.sort((a: any, b: any) => { // Get the most recent date for both a and b const dateA = a.updated ? new Date(a.updated) : new Date(a.created); @@ -50,6 +54,9 @@ export const AdminSpaceInner = ({ selectedGroup, adminsWithNames, setIsForceShowCreationKeyPopup, + balance, + userInfo, + isOwner, }) => { const [adminGroupSecretKey, setAdminGroupSecretKey] = useState(null); const [isFetchingAdminGroupSecretKey, setIsFetchingAdminGroupSecretKey] = @@ -63,7 +70,7 @@ export const AdminSpaceInner = ({ const [groupSecretKeyPublishDetails, setGroupSecretKeyPublishDetails] = useState(null); const [isLoadingPublishKey, setIsLoadingPublishKey] = useState(false); - const { show, setTxList, setInfoSnackCustom, setOpenSnackGlobal } = + const { show, setInfoSnackCustom, setOpenSnackGlobal } = useContext(MyContext); const getAdminGroupSecretKey = useCallback(async () => { @@ -87,10 +94,11 @@ export const AdminSpaceInner = ({ const dataint8Array = base64ToUint8Array(decryptedKey.data); const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); if (!validateSecretKey(decryptedKeyToObject)) - throw new Error("SecretKey is not valid"); + throw new Error('SecretKey is not valid'); setAdminGroupSecretKey(decryptedKeyToObject); setAdminGroupSecretKeyPublishDetails(getLatestPublish); } catch (error) { + console.log(error); } finally { setIsFetchingAdminGroupSecretKey(false); } @@ -106,6 +114,7 @@ export const AdminSpaceInner = ({ if (getLatestPublish === false) setGroupSecretKeyPublishDetails(false); setGroupSecretKeyPublishDetails(getLatestPublish); } catch (error) { + console.log(error); } finally { setIsFetchingGroupSecretKey(false); } @@ -113,15 +122,17 @@ export const AdminSpaceInner = ({ const createCommonSecretForAdmins = async () => { try { - const fee = await getFee("ARBITRARY"); + const fee = await getFee('ARBITRARY'); + await show({ - message: "Would you like to perform an ARBITRARY transaction?", - publishFee: fee.fee + " QORT", + message: 'Would you like to perform an ARBITRARY transaction?', + publishFee: fee.fee + ' QORT', }); + setIsLoadingPublishKey(true); window - .sendMessage("encryptAndPublishSymmetricKeyGroupChatForAdmins", { + .sendMessage('encryptAndPublishSymmetricKeyGroupChatForAdmins', { groupId: selectedGroup, previousData: adminGroupSecretKey, admins: adminsWithNames, @@ -129,27 +140,29 @@ export const AdminSpaceInner = ({ .then((response) => { if (!response?.error) { setInfoSnackCustom({ - type: "success", + 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.", + '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", + 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", + type: 'error', + message: error?.message || 'unable to re-encrypt secret key', }); setOpenSnackGlobal(true); }); - } catch (error) {} + } catch (error) { + console.log(error); + } }; useEffect(() => { @@ -159,27 +172,32 @@ export const AdminSpaceInner = ({ return ( - Reminder: After publishing the key, it will take a couple of minutes for it to appear. Please just wait. + + Reminder: After publishing the key, it will take a couple of minutes for + it to appear. Please just wait. + {isFetchingGroupSecretKey && ( @@ -191,33 +209,47 @@ export const AdminSpaceInner = ({ )} {groupSecretKeyPublishDetails && ( - Last encryption date:{" "} + Last encryption date:{' '} {formatTimestampForum( groupSecretKeyPublishDetails?.updated || groupSecretKeyPublishDetails?.created - )}{" "} + )}{' '} {` by ${groupSecretKeyPublishDetails?.name}`} )} - + - This key is to encrypt GROUP related content. This is the only one used in this UI as of now. All group members will be able to see content encrypted with this key. + + + This key is to encrypt GROUP related content. This is the only one + used in this UI as of now. All group members will be able to see + content encrypted with this key. + + + {isFetchingAdminGroupSecretKey && ( @@ -228,21 +260,58 @@ export const AdminSpaceInner = ({ )} {adminGroupSecretKeyPublishDetails && ( - Last encryption date:{" "} + Last encryption date:{' '} {formatTimestampForum( adminGroupSecretKeyPublishDetails?.updated || adminGroupSecretKeyPublishDetails?.created )} )} - + - This key is to encrypt ADMIN related content. Only admins would see content encrypted with it. + + + This key is to encrypt ADMIN related content. Only admins would see + content encrypted with it. + + + {isOwner && ( + + Group Avatar + + + + )} ); }; diff --git a/src/components/Chat/AnnouncementDiscussion.tsx b/src/components/Chat/AnnouncementDiscussion.tsx index 5603de0..e4c2538 100644 --- a/src/components/Chat/AnnouncementDiscussion.tsx +++ b/src/components/Chat/AnnouncementDiscussion.tsx @@ -1,18 +1,31 @@ -import React, { useMemo, useRef, useState } from "react"; -import TipTap from "./TipTap"; -import { AuthenticatedContainerInnerTop, CustomButton } from "../../App-styles"; -import { Box, CircularProgress } from "@mui/material"; -import { objectToBase64 } from "../../qdn/encryption/group-encryption"; -import ShortUniqueId from "short-unique-id"; -import { LoadingSnackbar } from "../Snackbar/LoadingSnackbar"; -import { getBaseApi, getFee } from "../../background"; -import { decryptPublishes, getTempPublish, handleUnencryptedPublishes, saveTempPublish } from "./GroupAnnouncements"; -import { AnnouncementList } from "./AnnouncementList"; -import { Spacer } from "../../common/Spacer"; +import React, { useMemo, useRef, useState } from 'react'; +import TipTap from './TipTap'; +import { + AuthenticatedContainerInnerTop, + CustomButton, +} from '../../styles/App-styles'; +import { Box, CircularProgress, useTheme } from '@mui/material'; +import { objectToBase64 } from '../../qdn/encryption/group-encryption'; +import ShortUniqueId from 'short-unique-id'; +import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar'; +import { getBaseApi, getFee } from '../../background'; +import { + decryptPublishes, + getTempPublish, + handleUnencryptedPublishes, + saveTempPublish, +} from './GroupAnnouncements'; +import { AnnouncementList } from './AnnouncementList'; +import { Spacer } from '../../common/Spacer'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; -import { getArbitraryEndpointReact, getBaseApiReact, isMobile, pauseAllQueues, resumeAllQueues } from "../../App"; +import { + getArbitraryEndpointReact, + getBaseApiReact, + pauseAllQueues, + resumeAllQueues, +} from '../../App'; -const tempKey = 'accouncement-comment' +const tempKey = 'accouncement-comment'; const uid = new ShortUniqueId({ length: 8 }); export const AnnouncementDiscussion = ({ @@ -23,43 +36,40 @@ export const AnnouncementDiscussion = ({ setSelectedAnnouncement, show, myName, - isPrivate + isPrivate, }) => { + const theme = useTheme(); const [isSending, setIsSending] = useState(false); const [isLoading, setIsLoading] = useState(false); const [isFocusedParent, setIsFocusedParent] = useState(false); - const [comments, setComments] = useState([]) - const [tempPublishedList, setTempPublishedList] = useState([]) - const firstMountRef = useRef(false) - const [data, setData] = useState({}) + const [comments, setComments] = useState([]); + const [tempPublishedList, setTempPublishedList] = useState([]); + const firstMountRef = useRef(false); + const [data, setData] = useState({}); const editorRef = useRef(null); const setEditorRef = (editorInstance) => { editorRef.current = editorInstance; }; - + const clearEditorContent = () => { if (editorRef.current) { editorRef.current.chain().focus().clearContent().run(); - if(isMobile){ - setTimeout(() => { - editorRef.current?.chain().blur().run(); - setIsFocusedParent(false) - }, 200); - } } }; const getData = async ({ identifier, name }, isPrivate) => { try { - const res = await fetch( `${getBaseApiReact()}/arbitrary/DOCUMENT/${name}/${identifier}?encoding=base64` ); - if(!res?.ok) return + if (!res?.ok) return; const data = await res.text(); - const response = isPrivate === false ? handleUnencryptedPublishes([data]) : await decryptPublishes([{ data }], secretKey); - + const response = + isPrivate === false + ? handleUnencryptedPublishes([data]) + : await decryptPublishes([{ data }], secretKey); + const messageData = response[0]; setData((prev) => { return { @@ -67,19 +77,21 @@ export const AnnouncementDiscussion = ({ [`${identifier}-${name}`]: messageData, }; }); - - } catch (error) {} + } catch (error) { + console.log(error); + } }; const publishAnc = async ({ encryptedData, identifier }: any) => { try { if (!selectedAnnouncement) return; - + return new Promise((res, rej) => { - window.sendMessage("publishGroupEncryptedResource", { - encryptedData, - identifier, - }) + window + .sendMessage('publishGroupEncryptedResource', { + encryptedData, + identifier, + }) .then((response) => { if (!response?.error) { res(response); @@ -88,63 +100,64 @@ export const AnnouncementDiscussion = ({ rej(response.error); }) .catch((error) => { - rej(error.message || "An error occurred"); + rej(error.message || 'An error occurred'); }); - }); - } catch (error) {} + } catch (error) { + console.log(error); + } }; - const setTempData = async ()=> { + const setTempData = async () => { try { - const getTempAnnouncements = await getTempPublish() - if(getTempAnnouncements[tempKey]){ - let tempData = [] - Object.keys(getTempAnnouncements[tempKey] || {}).map((key)=> { - const value = getTempAnnouncements[tempKey][key] - if(value.data?.announcementId === selectedAnnouncement.identifier){ - tempData.push(value.data) + const getTempAnnouncements = await getTempPublish(); + if (getTempAnnouncements[tempKey]) { + let tempData = []; + Object.keys(getTempAnnouncements[tempKey] || {}).map((key) => { + const value = getTempAnnouncements[tempKey][key]; + if (value.data?.announcementId === selectedAnnouncement.identifier) { + tempData.push(value.data); + } + }); + setTempPublishedList(tempData); } - }) - setTempPublishedList(tempData) - } } catch (error) { - + console.log(error); } - - } + }; const publishComment = async () => { try { - pauseAllQueues() - const fee = await getFee('ARBITRARY') + pauseAllQueues(); + const fee = await getFee('ARBITRARY'); await show({ - message: "Would you like to perform a ARBITRARY transaction?" , - publishFee: fee.fee + ' QORT' - }) + message: 'Would you like to perform a ARBITRARY transaction?', + publishFee: fee.fee + ' QORT', + }); if (isSending) return; if (editorRef.current) { const htmlContent = editorRef.current.getHTML(); - - if (!htmlContent?.trim() || htmlContent?.trim() === "

") return; + + if (!htmlContent?.trim() || htmlContent?.trim() === '

') return; setIsSending(true); const message = { version: 1, extra: {}, message: htmlContent, }; - const secretKeyObject = isPrivate === false ? null : await getSecretKey(false, true); - const message64: any = await objectToBase64(message); - - const encryptSingle = isPrivate === false ? message64 : await encryptChatMessage( - message64, - secretKeyObject - ); + const secretKeyObject = + isPrivate === false ? null : await getSecretKey(false, true); + const message64: any = await objectToBase64(message); + + const encryptSingle = + isPrivate === false + ? message64 + : await encryptChatMessage(message64, secretKeyObject); const randomUid = uid.rnd(); const identifier = `cm-${selectedAnnouncement.identifier}-${randomUid}`; const res = await publishAnc({ encryptedData: encryptSingle, - identifier + identifier, }); const dataToSaveToStorage = { @@ -153,18 +166,18 @@ export const AnnouncementDiscussion = ({ service: 'DOCUMENT', tempData: message, created: Date.now(), - announcementId: selectedAnnouncement.identifier - } - await saveTempPublish({data: dataToSaveToStorage, key: tempKey}) - setTempData() - + announcementId: selectedAnnouncement.identifier, + }; + await saveTempPublish({ data: dataToSaveToStorage, key: tempKey }); + setTempData(); + clearEditorContent(); } // send chat message } catch (error) { console.error(error); } finally { - resumeAllQueues() + resumeAllQueues(); setIsSending(false); } }; @@ -172,7 +185,6 @@ export const AnnouncementDiscussion = ({ const getComments = React.useCallback( async (selectedAnnouncement, isPrivate) => { try { - setIsLoading(true); const offset = 0; @@ -181,19 +193,20 @@ export const AnnouncementDiscussion = ({ const identifier = `cm-${selectedAnnouncement.identifier}`; const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${offset}&reverse=true&prefix=true`; const response = await fetch(url, { - method: "GET", + method: 'GET', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, }); const responseData = await response.json(); - setTempData() + setTempData(); setComments(responseData); setIsLoading(false); for (const data of responseData) { getData({ name: data.name, identifier: data.identifier }, isPrivate); } } catch (error) { + console.log(error); } finally { setIsLoading(false); @@ -203,119 +216,122 @@ export const AnnouncementDiscussion = ({ [secretKey] ); - const loadMore = async()=> { + const loadMore = async () => { try { setIsLoading(true); - const offset = comments.length + const offset = comments.length; const identifier = `cm-${selectedAnnouncement.identifier}`; - const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${offset}&reverse=true&prefix=true`; - const response = await fetch(url, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); - const responseData = await response.json(); + const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${offset}&reverse=true&prefix=true`; + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + const responseData = await response.json(); - setComments((prev)=> [...prev, ...responseData]); - setIsLoading(false); - for (const data of responseData) { - getData({ name: data.name, identifier: data.identifier }, isPrivate); - } + setComments((prev) => [...prev, ...responseData]); + setIsLoading(false); + for (const data of responseData) { + getData({ name: data.name, identifier: data.identifier }, isPrivate); + } } catch (error) { - + console.log(error); } - - } + }; const combinedListTempAndReal = useMemo(() => { // Combine the two lists const combined = [...tempPublishedList, ...comments]; - + // Remove duplicates based on the "identifier" const uniqueItems = new Map(); - combined.forEach(item => { - uniqueItems.set(item.identifier, item); // This will overwrite duplicates, keeping the last occurrence + combined.forEach((item) => { + uniqueItems.set(item.identifier, item); // This will overwrite duplicates, keeping the last occurrence }); - + // Convert the map back to an array and sort by "created" timestamp in descending order - const sortedList = Array.from(uniqueItems.values()).sort((a, b) => b.created - a.created); - + const sortedList = Array.from(uniqueItems.values()).sort( + (a, b) => b.created - a.created + ); + return sortedList; }, [tempPublishedList, comments]); React.useEffect(() => { - if(!secretKey && isPrivate) return + if (!secretKey && isPrivate) return; if (selectedAnnouncement && !firstMountRef.current && isPrivate !== null) { getComments(selectedAnnouncement, isPrivate); - firstMountRef.current = true + firstMountRef.current = true; } }, [selectedAnnouncement, secretKey, isPrivate]); return (
-
+
+ + setSelectedAnnouncement(null)} + sx={{ + cursor: 'pointer', + }} + /> + - - setSelectedAnnouncement(null)} sx={{ - cursor: 'pointer' - }} /> - -
{}} + setSelectedAnnouncement={() => {}} disableComment showLoadMore={comments.length > 0 && comments.length % 20 === 0} loadMore={loadMore} myName={myName} - />
- - {isFocusedParent && ( - { - if(isSending) return - setIsFocusedParent(false) - clearEditorContent() - // Unfocus the editor - }} - style={{ - marginTop: 'auto', - alignSelf: 'center', - cursor: isSending ? 'default' : 'pointer', - flexShrink: 0, - padding: isMobile && '5px', - fontSize: isMobile && '14px', - background: 'red', - }} - > - - {` Close`} - - - )} - { - if (isSending) return; - publishComment(); - }} - style={{ - marginTop: "auto", - alignSelf: "center", - cursor: isSending ? "default" : "pointer", - background: isSending && "rgba(0, 0, 0, 0.8)", + - {isSending && ( - { + if (isSending) return; + setIsFocusedParent(false); + clearEditorContent(); + // Unfocus the editor }} - /> + style={{ + alignSelf: 'center', + background: 'red', + cursor: isSending ? 'default' : 'pointer', + flexShrink: 0, + fontSize: '14px', + marginTop: 'auto', + padding: '5px', + }} + > + {` Close`} + )} - {` Publish Comment`} - - - + { + if (isSending) return; + publishComment(); + }} + style={{ + alignSelf: 'center', + background: theme.palette.background.default, + cursor: isSending ? 'default' : 'pointer', + flexShrink: 0, + fontSize: '14px', + marginTop: 'auto', + padding: '5px', + }} + > + {isSending && ( + + )} + {` Publish Comment`} + +
- +
diff --git a/src/components/Chat/AnnouncementItem.tsx b/src/components/Chat/AnnouncementItem.tsx index 758a451..e6ad671 100644 --- a/src/components/Chat/AnnouncementItem.tsx +++ b/src/components/Chat/AnnouncementItem.tsx @@ -1,173 +1,205 @@ -import { Message } from "@chatscope/chat-ui-kit-react"; -import React, { useEffect, useState } from "react"; -import { useInView } from "react-intersection-observer"; -import { MessageDisplay } from "./MessageDisplay"; -import { Avatar, Box, Typography } from "@mui/material"; -import { formatTimestamp } from "../../utils/time"; +import React, { useEffect, useState } from 'react'; +import { MessageDisplay } from './MessageDisplay'; +import { Avatar, Box, Typography, useTheme } from '@mui/material'; +import { formatTimestamp } from '../../utils/time'; import ChatBubbleIcon from '@mui/icons-material/ChatBubble'; import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; -import { getBaseApi } from "../../background"; -import { requestQueueCommentCount } from "./GroupAnnouncements"; -import { CustomLoader } from "../../common/CustomLoader"; -import { getArbitraryEndpointReact, getBaseApiReact } from "../../App"; -import { WrapperUserAction } from "../WrapperUserAction"; -export const AnnouncementItem = ({ message, messageData, setSelectedAnnouncement, disableComment, myName }) => { +import { getBaseApi } from '../../background'; +import { requestQueueCommentCount } from './GroupAnnouncements'; +import { CustomLoader } from '../../common/CustomLoader'; +import { getArbitraryEndpointReact, getBaseApiReact } from '../../App'; +import { WrapperUserAction } from '../WrapperUserAction'; - const [commentLength, setCommentLength] = useState(0) - const getNumberOfComments = React.useCallback( - async () => { - try { - const offset = 0; +export const AnnouncementItem = ({ + message, + messageData, + setSelectedAnnouncement, + disableComment, + myName, +}) => { + const theme = useTheme(); + const [commentLength, setCommentLength] = useState(0); + const getNumberOfComments = React.useCallback(async () => { + try { + const offset = 0; - // dispatch(setIsLoadingGlobal(true)) - const identifier = `cm-${message.identifier}`; - const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=0&includemetadata=false&offset=${offset}&reverse=true&prefix=true`; - - const response = await requestQueueCommentCount.enqueue(() => { - return fetch(url, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); - }) - const responseData = await response.json(); + // dispatch(setIsLoadingGlobal(true)) + const identifier = `cm-${message.identifier}`; + const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=0&includemetadata=false&offset=${offset}&reverse=true&prefix=true`; + + const response = await requestQueueCommentCount.enqueue(() => { + return fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + }); + const responseData = await response.json(); + + setCommentLength(responseData?.length); + } catch (error) { + console.log(error); + } + }, []); + + useEffect(() => { + if (disableComment) return; + getNumberOfComments(); + }, []); - setCommentLength(responseData?.length); - - } catch (error) { - } finally { - // dispatch(setIsLoadingGlobal(false)) - } - }, - [] - ); - useEffect(()=> { - if(disableComment) return - getNumberOfComments() - }, []) return (
- - - - {message?.name?.charAt(0)} - - - - + + {message?.name?.charAt(0)} + + + - {message?.name} - - - {!messageData?.decryptedData && ( - - - - )} - {messageData?.decryptedData?.message && ( - <> - {messageData?.type === "notification" ? ( - - ) : ( - - )} - - )} + + + {message?.name} + + + {!messageData?.decryptedData && ( + + + + )} + {messageData?.decryptedData?.message && ( + <> + {messageData?.type === 'notification' ? ( + + ) : ( + + )} + + )} - - - {formatTimestamp(message.created)} + + + {formatTimestamp(message.created)} + + - - {!disableComment && ( - setSelectedAnnouncement(message)}> - - - - {commentLength ? ( - {`${commentLength > 1 ? `${commentLength} comments` : `${commentLength} comment`}`} - ) : ( - Leave comment - )} - + {!disableComment && ( + setSelectedAnnouncement(message)} + > + + + {commentLength ? ( + {`${commentLength > 1 ? `${commentLength} comments` : `${commentLength} comment`}`} + ) : ( + + Leave comment + + )} + + - - - )} - + )}
); }; diff --git a/src/components/Chat/AnnouncementList.tsx b/src/components/Chat/AnnouncementList.tsx index b55ebb5..9c32cc1 100644 --- a/src/components/Chat/AnnouncementList.tsx +++ b/src/components/Chat/AnnouncementList.tsx @@ -1,13 +1,8 @@ -import React, { useCallback, useState, useEffect, useRef } from "react"; -import { - List, - AutoSizer, - CellMeasurerCache, - CellMeasurer, -} from "react-virtualized"; -import { AnnouncementItem } from "./AnnouncementItem"; -import { Box } from "@mui/material"; -import { CustomButton } from "../../App-styles"; +import { useState, useEffect, useRef } from 'react'; +import { CellMeasurerCache } from 'react-virtualized'; +import { AnnouncementItem } from './AnnouncementItem'; +import { Box } from '@mui/material'; +import { CustomButton } from '../../styles/App-styles'; const cache = new CellMeasurerCache({ fixedWidth: true, @@ -21,9 +16,8 @@ export const AnnouncementList = ({ disableComment, showLoadMore, loadMore, - myName + myName, }) => { - const listRef = useRef(); const [messages, setMessages] = useState(initialMessages); @@ -35,39 +29,44 @@ export const AnnouncementList = ({ setMessages(initialMessages); }, [initialMessages]); - return (
{messages.map((message) => { - const messageData = message?.tempData ? { - decryptedData: message?.tempData - } : announcementData[`${message.identifier}-${message.name}`]; + const messageData = message?.tempData + ? { + decryptedData: message?.tempData, + } + : announcementData[`${message.identifier}-${message.name}`]; return ( - -
- -
- +
+ +
); })} {/* @@ -83,16 +82,20 @@ export const AnnouncementList = ({ /> )} */} - - {showLoadMore && ( - Load older announcements - )} - + + {showLoadMore && ( + + Load older announcements + + )} +
); }; diff --git a/src/components/Chat/ChatContainer.tsx b/src/components/Chat/ChatContainer.tsx deleted file mode 100644 index 399e5a4..0000000 --- a/src/components/Chat/ChatContainer.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React, { useState } from "react"; -import InfiniteScroll from "react-infinite-scroller"; -import { - MainContainer, - ChatContainer, - MessageList, - Message, - MessageInput, - Avatar -} from "@chatscope/chat-ui-kit-react"; -import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css"; - -export const ChatContainerComp = ({messages}) => { - // const [messages, setMessages] = useState([ - // { id: 1, text: "Hello! How are you?", sender: "Joe"}, - // { id: 2, text: "I'm good, thank you!", sender: "Me" } - // ]); - - // const loadMoreMessages = () => { - // // Simulate loading more messages (you could fetch these from an API) - // const moreMessages = [ - // { id: 3, text: "What about you?", sender: "Joe", direction: "incoming" }, - // { id: 4, text: "I'm great, thanks!", sender: "Me", direction: "outgoing" } - // ]; - // setMessages((prevMessages) => [...moreMessages, ...prevMessages]); - // }; - - return ( -
- - - - {messages.map((msg) => ( - - {msg.direction === "incoming" && } - - ))} - - - - - -
- ); -}; - - diff --git a/src/components/Chat/ChatDirect.tsx b/src/components/Chat/ChatDirect.tsx index d23f026..1b6b256 100644 --- a/src/components/Chat/ChatDirect.tsx +++ b/src/components/Chat/ChatDirect.tsx @@ -1,52 +1,76 @@ -import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react' +import React, { + useCallback, + useEffect, + useMemo, + useReducer, + useRef, + useState, +} from 'react'; -import { objectToBase64 } from '../../qdn/encryption/group-encryption' -import { ChatList } from './ChatList' -import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css"; -import Tiptap from './TipTap' -import { CustomButton } from '../../App-styles' +import { ChatList } from './ChatList'; +import Tiptap from './TipTap'; +import { CustomButton } from '../../styles/App-styles'; import CircularProgress from '@mui/material/CircularProgress'; -import { Box, ButtonBase, Input, Typography } from '@mui/material'; +import { Box, ButtonBase, Input, Typography, useTheme } from '@mui/material'; import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar'; import { getNameInfo } from '../Group/Group'; import { Spacer } from '../../common/Spacer'; import { CustomizedSnackbars } from '../Snackbar/Snackbar'; -import { getBaseApiReact, getBaseApiReactSocket, isMobile, pauseAllQueues, resumeAllQueues } from '../../App'; +import { + getBaseApiReact, + getBaseApiReactSocket, + pauseAllQueues, + resumeAllQueues, +} from '../../App'; import { getPublicKey } from '../../background'; import { useMessageQueue } from '../../MessageQueueContext'; -import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from '../../utils/events'; +import { + executeEvent, + subscribeToEvent, + unsubscribeFromEvent, +} from '../../utils/events'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; -import ShortUniqueId from "short-unique-id"; +import ShortUniqueId from 'short-unique-id'; import { ReturnIcon } from '../../assets/Icons/ReturnIcon'; import { ExitIcon } from '../../assets/Icons/ExitIcon'; -import { MessageItem, ReplyPreview } from './MessageItem'; - +import { ReplyPreview } from './MessageItem'; const uid = new ShortUniqueId({ length: 5 }); +export const ChatDirect = ({ + myAddress, + isNewChat, + selectedDirect, + setSelectedDirect, + setNewChat, + getTimestampEnterChat, + myName, + balance, + close, + setMobileViewModeKeepOpen, +}) => { + const theme = useTheme(); + const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue(); + const [isFocusedParent, setIsFocusedParent] = useState(false); + const [onEditMessage, setOnEditMessage] = useState(null); -export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDirect, setNewChat, getTimestampEnterChat, myName, balance, close, setMobileViewModeKeepOpen}) => { - const { queueChats, addToQueue, processWithNewMessages} = useMessageQueue(); - const [isFocusedParent, setIsFocusedParent] = useState(false); - const [onEditMessage, setOnEditMessage] = useState(null) - - const [messages, setMessages] = useState([]) - const [isSending, setIsSending] = useState(false) - const [directToValue, setDirectToValue] = useState('') - const hasInitialized = useRef(false) - const [isLoading, setIsLoading] = useState(false) + const [messages, setMessages] = useState([]); + const [isSending, setIsSending] = useState(false); + const [directToValue, setDirectToValue] = useState(''); + const hasInitialized = useRef(false); + const [isLoading, setIsLoading] = useState(false); const [openSnack, setOpenSnack] = React.useState(false); const [infoSnack, setInfoSnack] = React.useState(null); - const [publicKeyOfRecipient, setPublicKeyOfRecipient] = React.useState("") - const hasInitializedWebsocket = useRef(false) - const [chatReferences, setChatReferences] = useState({}) + const [publicKeyOfRecipient, setPublicKeyOfRecipient] = React.useState(''); + const hasInitializedWebsocket = useRef(false); + const [chatReferences, setChatReferences] = useState({}); const editorRef = useRef(null); const socketRef = useRef(null); const timeoutIdRef = useRef(null); - const [messageSize, setMessageSize] = useState(0) + const [messageSize, setMessageSize] = useState(0); const groupSocketTimeoutRef = useRef(null); - const [replyMessage, setReplyMessage] = useState(null) + const [replyMessage, setReplyMessage] = useState(null); const setEditorRef = (editorInstance) => { editorRef.current = editorInstance; }; @@ -55,42 +79,49 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi const triggerRerender = () => { forceUpdate(); // Trigger re-render by updating the state }; - const publicKeyOfRecipientRef = useRef(null) - const getPublicKeyFunc = async (address)=> { + const publicKeyOfRecipientRef = useRef(null); + const getPublicKeyFunc = async (address) => { try { - const publicKey = await getPublicKey(address) - if(publicKeyOfRecipientRef.current !== selectedDirect?.address) return - setPublicKeyOfRecipient(publicKey) + const publicKey = await getPublicKey(address); + if (publicKeyOfRecipientRef.current !== selectedDirect?.address) return; + setPublicKeyOfRecipient(publicKey); } catch (error) { - + console.log(error); } - } + }; - const tempMessages = useMemo(()=> { - if(!selectedDirect?.address) return [] - if(queueChats[selectedDirect?.address]){ - return queueChats[selectedDirect?.address]?.filter((item)=> !item?.chatReference) + const tempMessages = useMemo(() => { + if (!selectedDirect?.address) return []; + if (queueChats[selectedDirect?.address]) { + return queueChats[selectedDirect?.address]?.filter( + (item) => !item?.chatReference + ); } - return [] - }, [selectedDirect?.address, queueChats]) + return []; + }, [selectedDirect?.address, queueChats]); - const tempChatReferences = useMemo(()=> { - if(!selectedDirect?.address) return [] - if(queueChats[selectedDirect?.address]){ - return queueChats[selectedDirect?.address]?.filter((item)=> !!item?.chatReference) + const tempChatReferences = useMemo(() => { + if (!selectedDirect?.address) return []; + if (queueChats[selectedDirect?.address]) { + return queueChats[selectedDirect?.address]?.filter( + (item) => !!item?.chatReference + ); } - return [] - }, [selectedDirect?.address, queueChats]) + return []; + }, [selectedDirect?.address, queueChats]); - useEffect(()=> { - if(selectedDirect?.address){ - publicKeyOfRecipientRef.current = selectedDirect?.address - getPublicKeyFunc(publicKeyOfRecipientRef.current) + useEffect(() => { + if (selectedDirect?.address) { + publicKeyOfRecipientRef.current = selectedDirect?.address; + getPublicKeyFunc(publicKeyOfRecipientRef.current); } - }, [selectedDirect?.address]) - + }, [selectedDirect?.address]); - const middletierFunc = async (data: any, selectedDirectAddress: string, myAddress: string) => { + const middletierFunc = async ( + data: any, + selectedDirectAddress: string, + myAddress: string + ) => { try { if (hasInitialized.current) { decryptMessages(data, true); @@ -99,9 +130,9 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi hasInitialized.current = true; const url = `${getBaseApiReact()}/chat/messages?involving=${selectedDirectAddress}&involving=${myAddress}&encoding=BASE64&limit=0&reverse=false`; const response = await fetch(url, { - method: "GET", + method: 'GET', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, }); const responseData = await response.json(); @@ -109,609 +140,622 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi } catch (error) { console.error(error); } - } + }; - const decryptMessages = (encryptedMessages: any[], isInitiated: boolean)=> { - try { - return new Promise((res, rej)=> { - window.sendMessage("decryptDirect", { + const decryptMessages = (encryptedMessages: any[], isInitiated: boolean) => { + try { + return new Promise((res, rej) => { + window + .sendMessage('decryptDirect', { data: encryptedMessages, involvingAddress: selectedDirect?.address, }) - .then((decryptResponse) => { - if (!decryptResponse?.error) { - const response = processWithNewMessages(decryptResponse, selectedDirect?.address); - res(response); - - if (isInitiated) { - const formatted = response.filter((rawItem) => !rawItem?.chatReference).map((item) => ({ + .then((decryptResponse) => { + if (!decryptResponse?.error) { + const response = processWithNewMessages( + decryptResponse, + selectedDirect?.address + ); + res(response); + + if (isInitiated) { + const formatted = response + .filter((rawItem) => !rawItem?.chatReference) + .map((item) => ({ ...item, id: item.signature, text: item.message, unread: item?.sender === myAddress ? false : true, })); - setMessages((prev) => [...prev, ...formatted]); - setChatReferences((prev) => { - const organizedChatReferences = { ...prev }; - response.filter((rawItem) => !!rawItem?.chatReference && rawItem?.type === 'edit').forEach((item) => { - try { - organizedChatReferences[item.chatReference] = { - ...(organizedChatReferences[item.chatReference] || {}), - edit: item - }; - } catch(error){ + setMessages((prev) => [...prev, ...formatted]); + setChatReferences((prev) => { + const organizedChatReferences = { ...prev }; - } - }) - return organizedChatReferences - }) - } else { - hasInitialized.current = true; - const formatted = response.filter((rawItem) => !rawItem?.chatReference) + response + .filter( + (rawItem) => + !!rawItem?.chatReference && rawItem?.type === 'edit' + ) + .forEach((item) => { + try { + organizedChatReferences[item.chatReference] = { + ...(organizedChatReferences[item.chatReference] || + {}), + edit: item, + }; + } catch (error) { + console.log(error); + } + }); + return organizedChatReferences; + }); + } else { + hasInitialized.current = true; + const formatted = response + .filter((rawItem) => !rawItem?.chatReference) .map((item) => ({ ...item, id: item.signature, text: item.message, unread: false, })); - setMessages(formatted); + setMessages(formatted); - setChatReferences((prev) => { - const organizedChatReferences = { ...prev }; + setChatReferences((prev) => { + const organizedChatReferences = { ...prev }; - response.filter((rawItem) => !!rawItem?.chatReference && rawItem?.type === 'edit').forEach((item) => { - try { - organizedChatReferences[item.chatReference] = { - ...(organizedChatReferences[item.chatReference] || {}), - edit: item - }; - } catch(error){ - - } - }) - return organizedChatReferences - }) - } - return; + response + .filter( + (rawItem) => + !!rawItem?.chatReference && rawItem?.type === 'edit' + ) + .forEach((item) => { + try { + organizedChatReferences[item.chatReference] = { + ...(organizedChatReferences[item.chatReference] || + {}), + edit: item, + }; + } catch (error) { + console.log(error); + } + }); + return organizedChatReferences; + }); } - rej(response.error); - }) - .catch((error) => { - rej(error.message || "An error occurred"); - }); - - }) - } catch (error) { - - } + return; + } + rej(response.error); + }) + .catch((error) => { + rej(error.message || 'An error occurred'); + }); + }); + } catch (error) { + console.log(error); } + }; - const forceCloseWebSocket = () => { + const forceCloseWebSocket = () => { + if (socketRef.current) { + clearTimeout(timeoutIdRef.current); + clearTimeout(groupSocketTimeoutRef.current); + socketRef.current.close(1000, 'forced'); + socketRef.current = null; + } + }; + + const pingWebSocket = () => { + try { + if (socketRef.current?.readyState === WebSocket.OPEN) { + socketRef.current.send('ping'); + timeoutIdRef.current = setTimeout(() => { + if (socketRef.current) { + socketRef.current.close(); + clearTimeout(groupSocketTimeoutRef.current); + } + }, 5000); // Close if no pong in 5 seconds + } + } catch (error) { + console.error('Error during ping:', error); + } + }; + + const initWebsocketMessageGroup = () => { + forceCloseWebSocket(); // Close any existing connection + + if (!selectedDirect?.address || !myAddress) return; + + const socketLink = `${getBaseApiReactSocket()}/websockets/chat/messages?involving=${selectedDirect?.address}&involving=${myAddress}&encoding=BASE64&limit=100`; + socketRef.current = new WebSocket(socketLink); + + socketRef.current.onopen = () => { + setTimeout(pingWebSocket, 50); // Initial ping + }; + + socketRef.current.onmessage = (e) => { + try { + if (e.data === 'pong') { + clearTimeout(timeoutIdRef.current); + groupSocketTimeoutRef.current = setTimeout(pingWebSocket, 45000); // Ping every 45 seconds + } else { + middletierFunc( + JSON.parse(e.data), + selectedDirect?.address, + myAddress + ); + + setIsLoading(false); + } + } catch (error) { + console.error('Error handling WebSocket message:', error); + } + }; + + socketRef.current.onclose = (event) => { + clearTimeout(groupSocketTimeoutRef.current); + clearTimeout(timeoutIdRef.current); + console.warn(`WebSocket closed: ${event.reason || 'unknown reason'}`); + if (event.reason !== 'forced' && event.code !== 1000) { + setTimeout(() => initWebsocketMessageGroup(), 10000); // Retry after 10 seconds + } + }; + + socketRef.current.onerror = (error) => { + console.error('WebSocket error:', error); + clearTimeout(groupSocketTimeoutRef.current); + clearTimeout(timeoutIdRef.current); if (socketRef.current) { - clearTimeout(timeoutIdRef.current); - clearTimeout(groupSocketTimeoutRef.current); - socketRef.current.close(1000, 'forced'); - socketRef.current = null; + socketRef.current.close(); } }; - - const pingWebSocket = () => { - try { - if (socketRef.current?.readyState === WebSocket.OPEN) { - socketRef.current.send('ping'); - timeoutIdRef.current = setTimeout(() => { - if (socketRef.current) { - socketRef.current.close(); - clearTimeout(groupSocketTimeoutRef.current); - } - }, 5000); // Close if no pong in 5 seconds - } - } catch (error) { - console.error('Error during ping:', error); - } + }; + + const setDirectChatValueFunc = async (e) => { + setDirectToValue(e.detail.directToValue); + }; + useEffect(() => { + subscribeToEvent('setDirectToValueNewChat', setDirectChatValueFunc); + + return () => { + unsubscribeFromEvent('setDirectToValueNewChat', setDirectChatValueFunc); }; - + }, []); - const initWebsocketMessageGroup = () => { - forceCloseWebSocket(); // Close any existing connection - - if (!selectedDirect?.address || !myAddress) return; - - const socketLink = `${getBaseApiReactSocket()}/websockets/chat/messages?involving=${selectedDirect?.address}&involving=${myAddress}&encoding=BASE64&limit=100`; - socketRef.current = new WebSocket(socketLink); - - socketRef.current.onopen = () => { - setTimeout(pingWebSocket, 50); // Initial ping - }; - - socketRef.current.onmessage = (e) => { - try { - if (e.data === 'pong') { - clearTimeout(timeoutIdRef.current); - groupSocketTimeoutRef.current = setTimeout(pingWebSocket, 45000); // Ping every 45 seconds - } else { - middletierFunc(JSON.parse(e.data), selectedDirect?.address, myAddress) + useEffect(() => { + if (hasInitializedWebsocket.current || isNewChat) return; + setIsLoading(true); + initWebsocketMessageGroup(); + hasInitializedWebsocket.current = true; - setIsLoading(false); - } - } catch (error) { - console.error('Error handling WebSocket message:', error); - } - }; - - socketRef.current.onclose = (event) => { - clearTimeout(groupSocketTimeoutRef.current); - clearTimeout(timeoutIdRef.current); - console.warn(`WebSocket closed: ${event.reason || 'unknown reason'}`); - if (event.reason !== 'forced' && event.code !== 1000) { - setTimeout(() => initWebsocketMessageGroup(), 10000); // Retry after 10 seconds - } - }; - - socketRef.current.onerror = (error) => { - console.error('WebSocket error:', error); - clearTimeout(groupSocketTimeoutRef.current); - clearTimeout(timeoutIdRef.current); - if (socketRef.current) { - socketRef.current.close(); - } - }; + return () => { + forceCloseWebSocket(); // Clean up WebSocket on component unmount }; + }, [selectedDirect?.address, myAddress, isNewChat]); - const setDirectChatValueFunc = async (e)=> { - setDirectToValue(e.detail.directToValue) - } - useEffect(() => { - subscribeToEvent("setDirectToValueNewChat", setDirectChatValueFunc); - - return () => { - unsubscribeFromEvent("setDirectToValueNewChat", setDirectChatValueFunc); - }; - }, []); - - useEffect(() => { - if (hasInitializedWebsocket.current || isNewChat) return; - setIsLoading(true); - initWebsocketMessageGroup(); - hasInitializedWebsocket.current = true; - - return () => { - forceCloseWebSocket(); // Clean up WebSocket on component unmount - }; - }, [selectedDirect?.address, myAddress, isNewChat]); + const sendChatDirect = async ( + { chatReference = undefined, messageText, otherData }: any, + address, + publicKeyOfRecipient, + isNewChatVar + ) => { + try { + const directTo = isNewChatVar ? directToValue : address; + if (!directTo) return; + return new Promise((res, rej) => { + window + .sendMessage( + 'sendChatDirect', + { + directTo, + chatReference, + messageText, + otherData, + publicKeyOfRecipient, + address: directTo, + }, + 120000 + ) + .then(async (response) => { + if (!response?.error) { + if (isNewChatVar) { + let getRecipientName = null; + try { + getRecipientName = await getNameInfo(response.recipient); + } catch (error) { + console.error('Error fetching recipient name:', error); + } + setSelectedDirect({ + address: response.recipient, + name: getRecipientName, + timestamp: Date.now(), + sender: myAddress, + senderName: myName, + }); + setNewChat(null); + window + .sendMessage('addTimestampEnterChat', { + timestamp: Date.now(), + groupId: response.recipient, + }) + .catch((error) => { + console.error( + 'Failed to add timestamp:', + error.message || 'An error occurred' + ); + }); - -const sendChatDirect = async ({ chatReference = undefined, messageText, otherData}: any, address, publicKeyOfRecipient, isNewChatVar)=> { - try { - const directTo = isNewChatVar ? directToValue : address - - if(!directTo) return - return new Promise((res, rej)=> { - window.sendMessage("sendChatDirect", { - directTo, - chatReference, - messageText, - otherData, - publicKeyOfRecipient, - address: directTo, - }, 120000) - .then(async (response) => { - if (!response?.error) { - if (isNewChatVar) { - let getRecipientName = null; - try { - getRecipientName = await getNameInfo(response.recipient); - } catch (error) { - console.error("Error fetching recipient name:", error); + setTimeout(() => { + getTimestampEnterChat(); + }, 400); } - setSelectedDirect({ - address: response.recipient, - name: getRecipientName, - timestamp: Date.now(), - sender: myAddress, - senderName: myName, - }); - setNewChat(null); - - window.sendMessage("addTimestampEnterChat", { - timestamp: Date.now(), - groupId: response.recipient, - }).catch((error) => { - console.error("Failed to add timestamp:", error.message || "An error occurred"); - }); - - setTimeout(() => { - getTimestampEnterChat(); - }, 400); + res(response); + return; } - res(response); - return; - } - rej(response.error); - }) - .catch((error) => { - rej(error.message || "An error occurred"); - }); - - }) - } catch (error) { - throw new Error(error) - } finally { - } -} -const clearEditorContent = () => { - if (editorRef.current) { - setMessageSize(0) - editorRef.current.chain().focus().clearContent().run(); - if(isMobile){ - setTimeout(() => { - editorRef.current?.chain().blur().run(); - setIsFocusedParent(false) - executeEvent("sent-new-message-group", {}) - setTimeout(() => { - triggerRerender(); - }, 300); - }, 200); + rej(response.error); + }) + .catch((error) => { + rej(error.message || 'An error occurred'); + }); + }); + } catch (error) { + throw new Error(error); } - } -}; -useEffect(() => { - if (!editorRef?.current) return; - const handleUpdate = () => { - const htmlContent = editorRef?.current.getHTML(); - const stringified = JSON.stringify(htmlContent); - const size = new Blob([stringified]).size; - setMessageSize(size + 200); }; - - // Add a listener for the editorRef?.current's content updates - editorRef?.current.on('update', handleUpdate); - - // Cleanup the listener on unmount - return () => { - editorRef?.current.off('update', handleUpdate); + const clearEditorContent = () => { + if (editorRef.current) { + setMessageSize(0); + editorRef.current.chain().focus().clearContent().run(); + } }; -}, [editorRef?.current]); + useEffect(() => { + if (!editorRef?.current) return; + const handleUpdate = () => { + const htmlContent = editorRef?.current.getHTML(); + const stringified = JSON.stringify(htmlContent); + const size = new Blob([stringified]).size; + setMessageSize(size + 200); + }; - const sendMessage = async ()=> { - try { - if(messageSize > 4000) return + // Add a listener for the editorRef?.current's content updates + editorRef?.current.on('update', handleUpdate); - - if(+balance < 4) throw new Error('You need at least 4 QORT to send a message') - if(isSending) return - if (editorRef.current) { - const htmlContent = editorRef.current.getHTML(); - - if(!htmlContent?.trim() || htmlContent?.trim() === '

') return - setIsSending(true) - pauseAllQueues() - const message = JSON.stringify(htmlContent) - - - if(isNewChat){ - await sendChatDirect({ messageText: htmlContent}, null, null, true) - return + // Cleanup the listener on unmount + return () => { + editorRef?.current.off('update', handleUpdate); + }; + }, [editorRef?.current]); + + 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; + if (editorRef.current) { + const htmlContent = editorRef.current.getHTML(); + + if (!htmlContent?.trim() || htmlContent?.trim() === '

') return; + setIsSending(true); + pauseAllQueues(); + const message = JSON.stringify(htmlContent); + + if (isNewChat) { + await sendChatDirect({ messageText: htmlContent }, null, null, true); + return; } - let repliedTo = replyMessage?.signature + let repliedTo = replyMessage?.signature; - if (replyMessage?.chatReference) { - repliedTo = replyMessage?.chatReference - } - let chatReference = onEditMessage?.signature + if (replyMessage?.chatReference) { + repliedTo = replyMessage?.chatReference; + } + let chatReference = onEditMessage?.signature; const otherData = { ...(onEditMessage?.decryptedData || {}), specialId: uid.rnd(), repliedTo: onEditMessage ? onEditMessage?.repliedTo : repliedTo, - type: chatReference ? 'edit' : '' - } + type: chatReference ? 'edit' : '', + }; const sendMessageFunc = async () => { - return await sendChatDirect({ chatReference, messageText: htmlContent, otherData}, selectedDirect?.address, publicKeyOfRecipient, false) + return await sendChatDirect( + { chatReference, messageText: htmlContent, otherData }, + selectedDirect?.address, + publicKeyOfRecipient, + false + ); }; - - // Add the function to the queue const messageObj = { message: { timestamp: Date.now(), - senderName: myName, - sender: myAddress, - ...(otherData || {}), - text: htmlContent, + senderName: myName, + sender: myAddress, + ...(otherData || {}), + text: htmlContent, }, - chatReference - } - addToQueue(sendMessageFunc, messageObj, 'chat-direct', - selectedDirect?.address ); + chatReference, + }; + addToQueue( + sendMessageFunc, + messageObj, + 'chat-direct', + selectedDirect?.address + ); setTimeout(() => { - executeEvent("sent-new-message-group", {}) + executeEvent('sent-new-message-group', {}); }, 150); - clearEditorContent() - setReplyMessage(null) - setOnEditMessage(null) - - } - // send chat message - } catch (error) { - const errorMsg = error?.message || error - setInfoSnack({ - type: "error", - message: errorMsg === 'invalid signature' ? 'You need at least 4 QORT to send a message' : errorMsg, - }); - setOpenSnack(true); - console.error(error) - } finally { - setIsSending(false) - resumeAllQueues() + clearEditorContent(); + setReplyMessage(null); + setOnEditMessage(null); } + // send chat message + } catch (error) { + const errorMsg = error?.message || error; + setInfoSnack({ + type: 'error', + message: + errorMsg === 'invalid signature' + ? 'You need at least 4 QORT to send a message' + : errorMsg, + }); + setOpenSnack(true); + console.error(error); + } finally { + setIsSending(false); + resumeAllQueues(); } + }; - const onReply = useCallback((message)=> { - if(onEditMessage){ - clearEditorContent() + const onReply = useCallback( + (message) => { + if (onEditMessage) { + clearEditorContent(); } - setReplyMessage(message) - setOnEditMessage(null) - editorRef?.current?.chain().focus() - }, [onEditMessage]) - - - const onEdit = useCallback((message)=> { - setOnEditMessage(message) - setReplyMessage(null) - editorRef.current.chain().focus().setContent(message?.text).run(); - - }, []) - + setReplyMessage(message); + setOnEditMessage(null); + editorRef?.current?.chain().focus(); + }, + [onEditMessage] + ); + + const onEdit = useCallback((message) => { + setOnEditMessage(message); + setReplyMessage(null); + editorRef.current.chain().focus().setContent(message?.text).run(); + }, []); + return ( -
- {!isMobile && ( - + - - Close Direct Chat - - )} - {isMobile && ( - - - - { - close() - }} - > - - - - - {isNewChat ? '' : selectedDirect?.name || (selectedDirect?.address?.slice(0,10) + '...')} - - - { - setSelectedDirect(null) - setMobileViewModeKeepOpen('') - setNewChat(false) - }} - > - - - - - - )} + }} + > + + + Close Direct Chat + + + {isNewChat && ( <> - - setDirectToValue(e.target.value)} /> - + + setDirectToValue(e.target.value)} + /> )} - - - -
-
+ +
+
- {replyMessage && ( - - - - { - setReplyMessage(null) - setOnEditMessage(null) - - }} - > - - - - )} - {onEditMessage && ( - - - - { - setReplyMessage(null) - setOnEditMessage(null) - - clearEditorContent() - - - }} - > - - - - )} - - - {messageSize > 750 && ( - - 4000 ? 'var(--danger)' : 'unset' - }}>{`Your message size is of ${messageSize} bytes out of a maximum of 4000`} - - - )} -
- - - - { - - if(isSending) return - sendMessage() - }} - style={{ - marginTop: 'auto', - alignSelf: 'center', - cursor: isSending ? 'default' : 'pointer', - background: isSending && 'rgba(0, 0, 0, 0.8)', - flexShrink: 0, - padding: '5px', - width: '100px', - minWidth: 'auto' + }} + > + {replyMessage && ( + - {isSending && ( - + + { + setReplyMessage(null); + setOnEditMessage(null); + }} + > + + + + )} + {onEditMessage && ( + + + + { + setReplyMessage(null); + setOnEditMessage(null); + clearEditorContent(); + }} + > + + + + )} + + + {messageSize > 750 && ( + + 4000 ? theme.palette.other.danger : 'unset', + }} + >{`Your message size is of ${messageSize} bytes out of a maximum of 4000`} + + )} +
+ + + { + if (isSending) return; + sendMessage(); + }} + style={{ + alignSelf: 'center', + background: isSending + ? theme.palette.background.default + : theme.palette.background.paper, + cursor: isSending ? 'default' : 'pointer', + flexShrink: 0, + marginTop: 'auto', + minWidth: 'auto', + padding: '5px', + width: '100px', + }} + > + {isSending && ( + - )} - {` Send`} - - - + )} + {` Send`} + +
- - + + + +
- ) -} + ); +}; diff --git a/src/components/Chat/ChatGroup.tsx b/src/components/Chat/ChatGroup.tsx index e054bd9..0dd626e 100644 --- a/src/components/Chat/ChatGroup.tsx +++ b/src/components/Chat/ChatGroup.tsx @@ -1,100 +1,135 @@ -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' -import { base64ToUint8Array, decodeBase64ForUIChatMessages, objectToBase64 } from '../../qdn/encryption/group-encryption' -import { ChatContainerComp } from './ChatContainer' -import { ChatList } from './ChatList' -import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css"; -import Tiptap from './TipTap' -import { CustomButton } from '../../App-styles' +import React, { + useCallback, + useContext, + useEffect, + useMemo, + useReducer, + useRef, + useState, +} from 'react'; +import { + decodeBase64ForUIChatMessages, + objectToBase64, +} from '../../qdn/encryption/group-encryption'; +import { ChatList } from './ChatList'; +import Tiptap from './TipTap'; +import { CustomButton } from '../../styles/App-styles'; import CircularProgress from '@mui/material/CircularProgress'; -import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar' -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, subscribeToEvent, unsubscribeFromEvent } from '../../utils/events' -import { Box, ButtonBase, Divider, Typography } from '@mui/material' -import ShortUniqueId from "short-unique-id"; -import { ReplyPreview } from './MessageItem' -import { ExitIcon } from '../../assets/Icons/ExitIcon' -import { RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS } from '../../constants/resourceTypes' -import { isExtMsg } from '../../background' -import AppViewerContainer from '../Apps/AppViewerContainer' -import CloseIcon from "@mui/icons-material/Close"; -import { throttle } from 'lodash' +import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar'; +import { + getBaseApiReact, + getBaseApiReactSocket, + 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, + subscribeToEvent, + unsubscribeFromEvent, +} from '../../utils/events'; +import { Box, ButtonBase, Divider, Typography, useTheme } from '@mui/material'; +import ShortUniqueId from 'short-unique-id'; +import { ReplyPreview } from './MessageItem'; +import { ExitIcon } from '../../assets/Icons/ExitIcon'; +import { RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS } from '../../constants/resourceTypes'; +import { isExtMsg } from '../../background'; +import AppViewerContainer from '../Apps/AppViewerContainer'; +import CloseIcon from '@mui/icons-material/Close'; +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, isPrivate}) => { - const {isUserBlocked} = useContext(MyContext) - const [messages, setMessages] = useState([]) - const [chatReferences, setChatReferences] = useState({}) - const [isSending, setIsSending] = useState(false) - const [isLoading, setIsLoading] = useState(false) +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); + const [isLoading, setIsLoading] = useState(false); const [isMoved, setIsMoved] = useState(false); const [openSnack, setOpenSnack] = React.useState(false); const [infoSnack, setInfoSnack] = React.useState(null); - const hasInitialized = useRef(false) + const hasInitialized = useRef(false); const [isFocusedParent, setIsFocusedParent] = useState(false); - const [replyMessage, setReplyMessage] = useState(null) - const [onEditMessage, setOnEditMessage] = useState(null) - const [isOpenQManager, setIsOpenQManager] = useState(null) + const [replyMessage, setReplyMessage] = useState(null); + const [onEditMessage, setOnEditMessage] = useState(null); + const [isOpenQManager, setIsOpenQManager] = useState(null); -const [messageSize, setMessageSize] = useState(0) - const hasInitializedWebsocket = useRef(false) + const [messageSize, setMessageSize] = useState(0); + const hasInitializedWebsocket = useRef(false); const socketRef = useRef(null); // WebSocket reference const timeoutIdRef = useRef(null); // Timeout ID reference const groupSocketTimeoutRef = useRef(null); // Group Socket Timeout reference const editorRef = useRef(null); const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue(); const [, forceUpdate] = useReducer((x) => x + 1, 0); - const lastReadTimestamp = useRef(null) + const lastReadTimestamp = useRef(null); const handleUpdateRef = useRef(null); - const getTimestampEnterChat = async (selectedGroup) => { try { return new Promise((res, rej) => { - window.sendMessage("getTimestampEnterChat") - .then((response) => { - if (!response?.error) { - if(response && selectedGroup){ - lastReadTimestamp.current = response[selectedGroup] || undefined - window.sendMessage("addTimestampEnterChat", { - timestamp: Date.now(), - groupId: selectedGroup - }).catch((error) => { - console.error("Failed to add timestamp:", error.message || "An error occurred"); + window + .sendMessage('getTimestampEnterChat') + .then((response) => { + if (!response?.error) { + if (response && selectedGroup) { + lastReadTimestamp.current = + response[selectedGroup] || undefined; + window + .sendMessage('addTimestampEnterChat', { + timestamp: Date.now(), + groupId: selectedGroup, + }) + .catch((error) => { + console.error( + 'Failed to add timestamp:', + error.message || 'An error occurred' + ); + }); + + setTimeout(() => { + getTimestampEnterChatParent(); + }, 600); + } + + res(response); + return; + } + rej(response.error); + }) + .catch((error) => { + rej(error.message || 'An error occurred'); }); - - - setTimeout(() => { - getTimestampEnterChatParent(); - }, 600); - } - - res(response); - return; - } - rej(response.error); - }) - .catch((error) => { - rej(error.message || "An error occurred"); - }); - }); - } catch (error) {} + } catch (error) { + console.log(error); + } }; - useEffect(()=> { - if(!selectedGroup) return - getTimestampEnterChat(selectedGroup) - }, [selectedGroup]) - - + useEffect(() => { + if (!selectedGroup) return; + getTimestampEnterChat(selectedGroup); + }, [selectedGroup]); const members = useMemo(() => { const uniqueMembers = new Set(); @@ -115,70 +150,79 @@ const [messageSize, setMessageSize] = useState(0) editorRef.current = editorInstance; }; - const tempMessages = useMemo(()=> { - if(!selectedGroup) return [] - if(queueChats[selectedGroup]){ - return queueChats[selectedGroup]?.filter((item)=> !item?.chatReference) + const tempMessages = useMemo(() => { + if (!selectedGroup) return []; + if (queueChats[selectedGroup]) { + return queueChats[selectedGroup]?.filter((item) => !item?.chatReference); } - return [] - }, [selectedGroup, queueChats]) - const tempChatReferences = useMemo(()=> { - if(!selectedGroup) return [] - if(queueChats[selectedGroup]){ - return queueChats[selectedGroup]?.filter((item)=> !!item?.chatReference) + return []; + }, [selectedGroup, queueChats]); + const tempChatReferences = useMemo(() => { + if (!selectedGroup) return []; + if (queueChats[selectedGroup]) { + return queueChats[selectedGroup]?.filter((item) => !!item?.chatReference); } - return [] - }, [selectedGroup, queueChats]) + return []; + }, [selectedGroup, queueChats]); - const secretKeyRef = useRef(null) + const secretKeyRef = useRef(null); - useEffect(()=> { - if(secretKey){ - secretKeyRef.current = secretKey + useEffect(() => { + if (secretKey) { + secretKeyRef.current = secretKey; } - }, [secretKey]) + }, [secretKey]); - // const getEncryptedSecretKey = useCallback(()=> { - // const response = getResource() - // const decryptResponse = decryptResource() - // return - // }, []) + // const getEncryptedSecretKey = useCallback(()=> { + // const response = getResource() + // const decryptResponse = decryptResource() + // return + // }, []) - - const checkForFirstSecretKeyNotification = (messages)=> { - messages?.forEach((message)=> { + const checkForFirstSecretKeyNotification = (messages) => { + messages?.forEach((message) => { try { - const decodeMsg = atob(message.data); - if(decodeMsg === PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY){ - handleSecretKeyCreationInProgress() - return + const decodeMsg = atob(message.data); + if (decodeMsg === PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY) { + handleSecretKeyCreationInProgress(); + return; } } catch (error) { - + console.log(error); } - }) - } + }); + }; - 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 updateChatMessagesWithBlocksFunc = (e) => { + if (e.detail) { + setMessages((prev) => + prev?.filter((item) => { + return !isUserBlocked(item?.sender, item?.senderName); + }) + ); + } + }; - const middletierFunc = async (data: any, groupId: string) => { + useEffect(() => { + subscribeToEvent( + 'updateChatMessagesWithBlocks', + updateChatMessagesWithBlocksFunc + ); + + return () => { + unsubscribeFromEvent( + 'updateChatMessagesWithBlocks', + updateChatMessagesWithBlocksFunc + ); + }; + }, []); + + const middletierFunc = async (data: any, groupId: string) => { try { if (hasInitialized.current) { - const dataRemovedBlock = data?.filter((item)=> !isUserBlocked(item?.sender, item?.senderName)) + const dataRemovedBlock = data?.filter( + (item) => !isUserBlocked(item?.sender, item?.senderName) + ); decryptMessages(dataRemovedBlock, true); return; @@ -186,351 +230,466 @@ const [messageSize, setMessageSize] = useState(0) hasInitialized.current = true; const url = `${getBaseApiReact()}/chat/messages?txGroupId=${groupId}&encoding=BASE64&limit=0&reverse=false`; const response = await fetch(url, { - method: "GET", + method: 'GET', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, }); const responseData = await response.json(); - const dataRemovedBlock = responseData?.filter((item)=> { - return !isUserBlocked(item?.sender, item?.senderName) - }) + const dataRemovedBlock = responseData?.filter((item) => { + return !isUserBlocked(item?.sender, item?.senderName); + }); decryptMessages(dataRemovedBlock, false); } catch (error) { console.error(error); } - } - - const decryptMessages = (encryptedMessages: any[], isInitiated: boolean )=> { - try { - if(!secretKeyRef.current){ - checkForFirstSecretKeyNotification(encryptedMessages) - } - return new Promise((res, rej)=> { - window.sendMessage("decryptSingle", { + }; + + const decryptMessages = (encryptedMessages: any[], isInitiated: boolean) => { + try { + if (!secretKeyRef.current) { + checkForFirstSecretKeyNotification(encryptedMessages); + } + return new Promise((res, rej) => { + window + .sendMessage('decryptSingle', { data: encryptedMessages, secretKeyObject: secretKey, }) - .then((response) => { - if (!response?.error) { - const filterUIMessages = encryptedMessages.filter((item) => !isExtMsg(item.data)); - const decodedUIMessages = decodeBase64ForUIChatMessages(filterUIMessages); - - const combineUIAndExtensionMsgsBefore = [...decodedUIMessages, ...response]; - const combineUIAndExtensionMsgs = processWithNewMessages( - combineUIAndExtensionMsgsBefore.map((item) => ({ - ...item, - ...(item?.decryptedData || {}), - })), - selectedGroup - ); - res(combineUIAndExtensionMsgs); - - if (isInitiated) { + .then((response) => { + if (!response?.error) { + const filterUIMessages = encryptedMessages.filter( + (item) => !isExtMsg(item.data) + ); + const decodedUIMessages = + decodeBase64ForUIChatMessages(filterUIMessages); - const formatted = combineUIAndExtensionMsgs - .filter((rawItem) => !rawItem?.chatReference) - .map((item) => { - const additionalFields = item?.data === 'NDAwMQ==' ? { - text: "

First group key created.

" - } : {} - return { - ...item, - id: item.signature, - text: item?.decryptedData?.message || "", - repliedTo: item?.repliedTo || item?.decryptedData?.repliedTo, - unread: item?.sender === myAddress ? false : !!item?.chatReference ? false : true, - isNotEncrypted: !!item?.messageText, - ...additionalFields - } - }); - setMessages((prev) => [...prev, ...formatted]); - - setChatReferences((prev) => { - const organizedChatReferences = { ...prev }; - combineUIAndExtensionMsgs - .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"){ - organizedChatReferences[item.chatReference] = { - ...(organizedChatReferences[item.chatReference] || {}), - edit: item.decryptedData, - }; - } else if(item?.type === "edit" || item?.isEdited){ - organizedChatReferences[item.chatReference] = { - ...(organizedChatReferences[item.chatReference] || {}), - edit: item, - }; - } else { - const content = item?.content || item.decryptedData?.content; - const sender = item.sender; - const newTimestamp = item.timestamp; - 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); - return; - } - - organizedChatReferences[item.chatReference] = { - ...(organizedChatReferences[item.chatReference] || {}), - reactions: organizedChatReferences[item.chatReference]?.reactions || {}, - }; - - organizedChatReferences[item.chatReference].reactions[content] = - organizedChatReferences[item.chatReference].reactions[content] || []; - - let latestTimestampForSender = null; - - organizedChatReferences[item.chatReference].reactions[content] = - organizedChatReferences[item.chatReference].reactions[content].filter((reaction) => { - if (reaction.sender === sender) { - latestTimestampForSender = Math.max(latestTimestampForSender || 0, reaction.timestamp); - } - return reaction.sender !== sender; - }); - - if (latestTimestampForSender && newTimestamp < latestTimestampForSender) { - return; - } - - if (contentState !== false) { - organizedChatReferences[item.chatReference].reactions[content].push(item); - } - - if (organizedChatReferences[item.chatReference].reactions[content].length === 0) { - delete organizedChatReferences[item.chatReference].reactions[content]; - } + const combineUIAndExtensionMsgsBefore = [ + ...decodedUIMessages, + ...response, + ]; + const combineUIAndExtensionMsgs = processWithNewMessages( + combineUIAndExtensionMsgsBefore.map((item) => ({ + ...item, + ...(item?.decryptedData || {}), + })), + selectedGroup + ); + res(combineUIAndExtensionMsgs); + + if (isInitiated) { + const formatted = combineUIAndExtensionMsgs + .filter((rawItem) => !rawItem?.chatReference) + .map((item) => { + const additionalFields = + item?.data === 'NDAwMQ==' + ? { + text: '

First group key created.

', } - - } catch (error) { - console.error("Error processing reaction/edit item:", error, item); - } - }); - - return organizedChatReferences; + : {}; + return { + ...item, + id: item.signature, + text: item?.decryptedData?.message || '', + repliedTo: + item?.repliedTo || item?.decryptedData?.repliedTo, + unread: + item?.sender === myAddress + ? false + : !!item?.chatReference + ? false + : true, + isNotEncrypted: !!item?.messageText, + ...additionalFields, + }; }); - } else { - let firstUnreadFound = false; - const formatted = combineUIAndExtensionMsgs - .filter((rawItem) => !rawItem?.chatReference) - .map((item) => { - const additionalFields = item?.data === 'NDAwMQ==' ? { - text: "

First group key created.

" - } : {} - const divide = lastReadTimestamp.current && !firstUnreadFound && item.timestamp > lastReadTimestamp.current && myAddress !== item?.sender; - - if(divide){ - firstUnreadFound = true - } - return { - ...item, - id: item.signature, - text: item?.decryptedData?.message || "", - repliedTo: item?.repliedTo || item?.decryptedData?.repliedTo, - isNotEncrypted: !!item?.messageText, - unread: false, - divide, - ...additionalFields - } - }); - setMessages(formatted); - - setChatReferences((prev) => { - const organizedChatReferences = { ...prev }; - - combineUIAndExtensionMsgs - .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"){ - organizedChatReferences[item.chatReference] = { - ...(organizedChatReferences[item.chatReference] || {}), - edit: item.decryptedData, - }; - } else if(item?.type === "edit" || item?.isEdited){ - organizedChatReferences[item.chatReference] = { - ...(organizedChatReferences[item.chatReference] || {}), - edit: item, - }; - } else { - const content = item?.content || item.decryptedData?.content; + setMessages((prev) => [...prev, ...formatted]); + + setChatReferences((prev) => { + const organizedChatReferences = { ...prev }; + combineUIAndExtensionMsgs + .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') { + organizedChatReferences[item.chatReference] = { + ...(organizedChatReferences[item.chatReference] || + {}), + edit: item.decryptedData, + }; + } else if (item?.type === 'edit' || item?.isEdited) { + organizedChatReferences[item.chatReference] = { + ...(organizedChatReferences[item.chatReference] || + {}), + edit: item, + }; + } else { + const content = + item?.content || item.decryptedData?.content; const sender = item.sender; const newTimestamp = item.timestamp; - 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); + 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 + ); return; } - + organizedChatReferences[item.chatReference] = { - ...(organizedChatReferences[item.chatReference] || {}), - reactions: organizedChatReferences[item.chatReference]?.reactions || {}, + ...(organizedChatReferences[item.chatReference] || + {}), + reactions: + organizedChatReferences[item.chatReference] + ?.reactions || {}, }; - - organizedChatReferences[item.chatReference].reactions[content] = - organizedChatReferences[item.chatReference].reactions[content] || []; - + + organizedChatReferences[item.chatReference].reactions[ + content + ] = + organizedChatReferences[item.chatReference] + .reactions[content] || []; + let latestTimestampForSender = null; - - organizedChatReferences[item.chatReference].reactions[content] = - organizedChatReferences[item.chatReference].reactions[content].filter((reaction) => { - if (reaction.sender === sender) { - latestTimestampForSender = Math.max(latestTimestampForSender || 0, reaction.timestamp); - } - return reaction.sender !== sender; - }); - - if (latestTimestampForSender && newTimestamp < latestTimestampForSender) { + + organizedChatReferences[item.chatReference].reactions[ + content + ] = organizedChatReferences[ + item.chatReference + ].reactions[content].filter((reaction) => { + if (reaction.sender === sender) { + latestTimestampForSender = Math.max( + latestTimestampForSender || 0, + reaction.timestamp + ); + } + return reaction.sender !== sender; + }); + + if ( + latestTimestampForSender && + newTimestamp < latestTimestampForSender + ) { return; } - + if (contentState !== false) { - organizedChatReferences[item.chatReference].reactions[content].push(item); + organizedChatReferences[ + item.chatReference + ].reactions[content].push(item); } - - if (organizedChatReferences[item.chatReference].reactions[content].length === 0) { - delete organizedChatReferences[item.chatReference].reactions[content]; + + if ( + organizedChatReferences[item.chatReference] + .reactions[content].length === 0 + ) { + delete organizedChatReferences[item.chatReference] + .reactions[content]; } } - } catch (error) { - console.error("Error processing reaction item:", error, item); - } - }); - - return organizedChatReferences; + } catch (error) { + console.error( + 'Error processing reaction/edit item:', + error, + item + ); + } + }); + + return organizedChatReferences; + }); + } else { + let firstUnreadFound = false; + const formatted = combineUIAndExtensionMsgs + .filter((rawItem) => !rawItem?.chatReference) + .map((item) => { + const additionalFields = + item?.data === 'NDAwMQ==' + ? { + text: '

First group key created.

', + } + : {}; + const divide = + lastReadTimestamp.current && + !firstUnreadFound && + item.timestamp > lastReadTimestamp.current && + myAddress !== item?.sender; + + if (divide) { + firstUnreadFound = true; + } + return { + ...item, + id: item.signature, + text: item?.decryptedData?.message || '', + repliedTo: + item?.repliedTo || item?.decryptedData?.repliedTo, + isNotEncrypted: !!item?.messageText, + unread: false, + divide, + ...additionalFields, + }; }); - } + setMessages(formatted); + + setChatReferences((prev) => { + const organizedChatReferences = { ...prev }; + + combineUIAndExtensionMsgs + .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') { + organizedChatReferences[item.chatReference] = { + ...(organizedChatReferences[item.chatReference] || + {}), + edit: item.decryptedData, + }; + } else if (item?.type === 'edit' || item?.isEdited) { + organizedChatReferences[item.chatReference] = { + ...(organizedChatReferences[item.chatReference] || + {}), + edit: item, + }; + } else { + const content = + item?.content || item.decryptedData?.content; + const sender = item.sender; + const newTimestamp = item.timestamp; + 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 + ); + return; + } + + organizedChatReferences[item.chatReference] = { + ...(organizedChatReferences[item.chatReference] || + {}), + reactions: + organizedChatReferences[item.chatReference] + ?.reactions || {}, + }; + + organizedChatReferences[item.chatReference].reactions[ + content + ] = + organizedChatReferences[item.chatReference] + .reactions[content] || []; + + let latestTimestampForSender = null; + + organizedChatReferences[item.chatReference].reactions[ + content + ] = organizedChatReferences[ + item.chatReference + ].reactions[content].filter((reaction) => { + if (reaction.sender === sender) { + latestTimestampForSender = Math.max( + latestTimestampForSender || 0, + reaction.timestamp + ); + } + return reaction.sender !== sender; + }); + + if ( + latestTimestampForSender && + newTimestamp < latestTimestampForSender + ) { + return; + } + + if (contentState !== false) { + organizedChatReferences[ + item.chatReference + ].reactions[content].push(item); + } + + if ( + organizedChatReferences[item.chatReference] + .reactions[content].length === 0 + ) { + delete organizedChatReferences[item.chatReference] + .reactions[content]; + } + } + } catch (error) { + console.error( + 'Error processing reaction item:', + error, + item + ); + } + }); + + return organizedChatReferences; + }); } - rej(response.error); - }) - .catch((error) => { - rej(error.message || "An error occurred"); - }); - - }) - } catch (error) { - - } + } + rej(response.error); + }) + .catch((error) => { + rej(error.message || 'An error occurred'); + }); + }); + } catch (error) { + console.log(error); } + }; - + const forceCloseWebSocket = () => { + if (socketRef.current) { + clearTimeout(timeoutIdRef.current); + clearTimeout(groupSocketTimeoutRef.current); + socketRef.current.close(1000, 'forced'); + socketRef.current = null; + } + }; - const forceCloseWebSocket = () => { - if (socketRef.current) { - - clearTimeout(timeoutIdRef.current); - clearTimeout(groupSocketTimeoutRef.current); - socketRef.current.close(1000, 'forced'); - socketRef.current = null; + const pingGroupSocket = () => { + try { + if (socketRef.current?.readyState === WebSocket.OPEN) { + socketRef.current.send('ping'); + timeoutIdRef.current = setTimeout(() => { + if (socketRef.current) { + socketRef.current.close(); + clearTimeout(groupSocketTimeoutRef.current); + } + }, 5000); // Close if no pong in 5 seconds + } + } catch (error) { + console.error('Error during ping:', error); + } + }; + const initWebsocketMessageGroup = () => { + let socketLink = `${getBaseApiReactSocket()}/websockets/chat/messages?txGroupId=${selectedGroup}&encoding=BASE64&limit=100`; + socketRef.current = new WebSocket(socketLink); + + socketRef.current.onopen = () => { + setTimeout(pingGroupSocket, 50); + }; + socketRef.current.onmessage = (e) => { + try { + if (e.data === 'pong') { + clearTimeout(timeoutIdRef.current); + groupSocketTimeoutRef.current = setTimeout(pingGroupSocket, 45000); // Ping every 45 seconds + } else { + middletierFunc(JSON.parse(e.data), selectedGroup); + setIsLoading(false); + } + } catch (error) { + console.log(error); } }; - - const pingGroupSocket = () => { - try { - if (socketRef.current?.readyState === WebSocket.OPEN) { - socketRef.current.send('ping'); - timeoutIdRef.current = setTimeout(() => { - if (socketRef.current) { - socketRef.current.close(); - clearTimeout(groupSocketTimeoutRef.current); - } - }, 5000); // Close if no pong in 5 seconds - } - } catch (error) { - console.error('Error during ping:', error); + socketRef.current.onclose = () => { + clearTimeout(groupSocketTimeoutRef.current); + clearTimeout(timeoutIdRef.current); + console.warn(`WebSocket closed: ${event.reason || 'unknown reason'}`); + if (event.reason !== 'forced' && event.code !== 1000) { + setTimeout(() => initWebsocketMessageGroup(), 1000); // Retry after 10 seconds + } + }; + socketRef.current.onerror = (e) => { + clearTimeout(groupSocketTimeoutRef.current); + clearTimeout(timeoutIdRef.current); + if (socketRef.current) { + socketRef.current.close(); + } + }; + }; + + useEffect(() => { + if (hasInitializedWebsocket.current) return; + if (triedToFetchSecretKey && !secretKey) { + forceCloseWebSocket(); + setMessages([]); + setIsLoading(true); + initWebsocketMessageGroup(); } - } - const initWebsocketMessageGroup = () => { - + }, [triedToFetchSecretKey, secretKey, isPrivate]); - let socketLink = `${getBaseApiReactSocket()}/websockets/chat/messages?txGroupId=${selectedGroup}&encoding=BASE64&limit=100` - socketRef.current = new WebSocket(socketLink) + useEffect(() => { + if (isPrivate === null) return; + if (isPrivate === false || !secretKey || hasInitializedWebsocket.current) + return; + forceCloseWebSocket(); + setMessages([]); + setIsLoading(true); + pauseAllQueues(); + setTimeout(() => { + resumeAllQueues(); + }, 6000); + initWebsocketMessageGroup(); + hasInitializedWebsocket.current = true; + }, [secretKey, isPrivate]); - - socketRef.current.onopen = () => { - setTimeout(pingGroupSocket, 50) - } - socketRef.current.onmessage = (e) => { - try { - if (e.data === 'pong') { - clearTimeout(timeoutIdRef.current); - groupSocketTimeoutRef.current = setTimeout(pingGroupSocket, 45000); // Ping every 45 seconds - } else { - middletierFunc(JSON.parse(e.data), selectedGroup) - setIsLoading(false) - } - } catch (error) { - - } - - - } - socketRef.current.onclose = () => { - clearTimeout(groupSocketTimeoutRef.current); - clearTimeout(timeoutIdRef.current); - console.warn(`WebSocket closed: ${event.reason || 'unknown reason'}`); - if (event.reason !== 'forced' && event.code !== 1000) { - setTimeout(() => initWebsocketMessageGroup(), 1000); // Retry after 10 seconds - } - } - socketRef.current.onerror = (e) => { - clearTimeout(groupSocketTimeoutRef.current); - clearTimeout(timeoutIdRef.current); - if (socketRef.current) { - socketRef.current.close(); - } - } - } + useEffect(() => { + const notifications = messages.filter( + (message) => message?.decryptedData?.type === 'notification' + ); + if (notifications.length === 0) return; + const latestNotification = notifications.reduce((latest, current) => { + return current.timestamp > latest.timestamp ? current : latest; + }, notifications[0]); + handleNewEncryptionNotification(latestNotification); + }, [messages]); - useEffect(()=> { - if(hasInitializedWebsocket.current) return - if(triedToFetchSecretKey && !secretKey){ - forceCloseWebSocket() - setMessages([]) - setIsLoading(true) - initWebsocketMessageGroup() - } - }, [triedToFetchSecretKey, secretKey, isPrivate]) - - useEffect(()=> { - if(isPrivate === null) return - if(isPrivate === false || !secretKey || hasInitializedWebsocket.current) return - forceCloseWebSocket() - setMessages([]) - setIsLoading(true) - pauseAllQueues() - setTimeout(() => { - resumeAllQueues() - }, 6000); - initWebsocketMessageGroup() - hasInitializedWebsocket.current = true - }, [secretKey, isPrivate]) - - - useEffect(()=> { - const notifications = messages.filter((message)=> message?.decryptedData - ?.type === 'notification') - if(notifications.length === 0) return - const latestNotification = notifications.reduce((latest, current) => { - return current.timestamp > latest.timestamp ? current : latest; - }, notifications[0]); - handleNewEncryptionNotification(latestNotification) - - }, [messages]) - - - const encryptChatMessage = async (data: string, secretKeyObject: any, reactiontypeNumber?: number)=> { + const encryptChatMessage = async ( + data: string, + secretKeyObject: any, + reactiontypeNumber?: number + ) => { try { - return new Promise((res, rej)=> { - window.sendMessage("encryptSingle", { - data, - secretKeyObject, - typeNumber: reactiontypeNumber, - }) + return new Promise((res, rej) => { + window + .sendMessage('encryptSingle', { + data, + secretKeyObject, + typeNumber: reactiontypeNumber, + }) .then((response) => { if (!response?.error) { res(response); @@ -539,172 +698,181 @@ const [messageSize, setMessageSize] = useState(0) rej(response.error); }) .catch((error) => { - rej(error.message || "An error occurred"); + rej(error.message || 'An error occurred'); }); - - }) + }); } catch (error) { - + console.log(error); } -} + }; -const sendChatGroup = async ({groupId, typeMessage = undefined, chatReference = undefined, messageText}: any)=> { - try { - return new Promise((res, rej)=> { - window.sendMessage("sendChatGroup", { - groupId, - typeMessage, - chatReference, - messageText, - }, 120000) - .then((response) => { - if (!response?.error) { - res(response); - return; - } - rej(response.error); - }) - .catch((error) => { - rej(error.message || "An error occurred"); - }); - - }) - } catch (error) { - throw new Error(error) - } -} -const clearEditorContent = () => { - if (editorRef.current) { - setMessageSize(0) - editorRef.current.chain().focus().clearContent().run(); - if(isMobile){ - setTimeout(() => { - editorRef.current?.chain().blur().run(); - setIsFocusedParent(false) - executeEvent("sent-new-message-group", {}) - setTimeout(() => { - triggerRerender(); - }, 300); - }, 200); + const sendChatGroup = async ({ + groupId, + typeMessage = undefined, + chatReference = undefined, + messageText, + }: any) => { + try { + return new Promise((res, rej) => { + window + .sendMessage( + 'sendChatGroup', + { + groupId, + typeMessage, + chatReference, + messageText, + }, + 120000 + ) + .then((response) => { + if (!response?.error) { + res(response); + return; + } + rej(response.error); + }) + .catch((error) => { + rej(error.message || 'An error occurred'); + }); + }); + } catch (error) { + throw new Error(error); } - } -}; + }; + const clearEditorContent = () => { + if (editorRef.current) { + setMessageSize(0); + editorRef.current.chain().focus().clearContent().run(); + } + }; + 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(); + if (editorRef.current) { + const htmlContent = editorRef.current.getHTML(); - 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() - if (editorRef.current) { - const htmlContent = editorRef.current.getHTML(); - - if(!htmlContent?.trim() || htmlContent?.trim() === '

') return - + if (!htmlContent?.trim() || htmlContent?.trim() === '

') return; - setIsSending(true) - const message = isPrivate === false ? editorRef.current.getJSON() : htmlContent - const secretKeyObject = await getSecretKey(false, true) + setIsSending(true); + const message = + isPrivate === false ? editorRef.current.getJSON() : htmlContent; + const secretKeyObject = await getSecretKey(false, true); - let repliedTo = replyMessage?.signature + let repliedTo = replyMessage?.signature; - if (replyMessage?.chatReference) { - repliedTo = replyMessage?.chatReference - } - let chatReference = onEditMessage?.signature - - const publicData = isPrivate ? {} : { - isEdited : chatReference ? true : false, + if (replyMessage?.chatReference) { + repliedTo = replyMessage?.chatReference; } + let chatReference = onEditMessage?.signature; + + const publicData = isPrivate + ? {} + : { + isEdited: chatReference ? true : false, + }; const otherData = { repliedTo, ...(onEditMessage?.decryptedData || {}), type: chatReference ? 'edit' : '', specialId: uid.rnd(), - ...publicData - } + ...publicData, + }; const objectMessage = { ...(otherData || {}), [isPrivate ? 'message' : 'messageText']: message, - version: 3 - } - const message64: any = await objectToBase64(objectMessage) - - const encryptSingle = isPrivate === false ? JSON.stringify(objectMessage) : await encryptChatMessage(message64, secretKeyObject) - // const res = await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle}) - - const sendMessageFunc = async () => { - return await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle, chatReference}) + version: 3, }; - + const message64: any = await objectToBase64(objectMessage); + + const encryptSingle = + isPrivate === false + ? JSON.stringify(objectMessage) + : await encryptChatMessage(message64, secretKeyObject); + // const res = await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle}) + + const sendMessageFunc = async () => { + return await sendChatGroup({ + groupId: selectedGroup, + messageText: encryptSingle, + chatReference, + }); + }; + // Add the function to the queue const messageObj = { message: { text: htmlContent, timestamp: Date.now(), - senderName: myName, - sender: myAddress, - ...(otherData || {}) + senderName: myName, + sender: myAddress, + ...(otherData || {}), }, - chatReference - } - addToQueue(sendMessageFunc, messageObj, 'chat', - selectedGroup ); + chatReference, + }; + addToQueue(sendMessageFunc, messageObj, 'chat', selectedGroup); setTimeout(() => { - executeEvent("sent-new-message-group", {}) + executeEvent('sent-new-message-group', {}); }, 150); - clearEditorContent() - setReplyMessage(null) - setOnEditMessage(null) - } - // send chat message - } catch (error) { - const errorMsg = error?.message || error - setInfoSnack({ - type: "error", - message: errorMsg, - }); - setOpenSnack(true); - console.error(error) - } finally { - setIsSending(false) - resumeAllQueues() + clearEditorContent(); + setReplyMessage(null); + setOnEditMessage(null); } + // send chat message + } catch (error) { + const errorMsg = error?.message || error; + setInfoSnack({ + type: 'error', + message: errorMsg, + }); + setOpenSnack(true); + console.error(error); + } finally { + setIsSending(false); + resumeAllQueues(); } + }; - useEffect(() => { - if (!editorRef?.current) return; - - handleUpdateRef.current = throttle(async () => { - try { - if(isPrivate){ + useEffect(() => { + if (!editorRef?.current) return; + + handleUpdateRef.current = throttle(async () => { + try { + if (isPrivate) { const htmlContent = editorRef.current.getHTML(); - const message64 = await objectToBase64(JSON.stringify(htmlContent)) - const secretKeyObject = await getSecretKey(false, true) - const encryptSingle = await encryptChatMessage(message64, secretKeyObject) + 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 + const message = JSON.stringify(htmlContent); + const size = new Blob([message]).size; setMessageSize(size + 300); } - - } catch (error) { + } catch (error) { // calc size error - } - }, 1200); - - const currentEditor = editorRef.current; - - currentEditor.on("update", handleUpdateRef.current); - - return () => { - currentEditor.off("update", handleUpdateRef.current); - }; - }, [editorRef, setMessageSize, isPrivate]); + } + }, 1200); + + const currentEditor = editorRef.current; + + currentEditor.on('update', handleUpdateRef.current); + + return () => { + currentEditor.off('update', handleUpdateRef.current); + }; + }, [editorRef, setMessageSize, isPrivate]); useEffect(() => { if (hide) { @@ -713,298 +881,375 @@ const clearEditorContent = () => { setIsMoved(false); // Reset the position immediately when showing } }, [hide]); - - const onReply = useCallback((message)=> { - if(onEditMessage){ - clearEditorContent() - } - setReplyMessage(message) - setOnEditMessage(null) - editorRef?.current?.chain().focus() - }, [onEditMessage]) - - const onEdit = useCallback((message)=> { - setOnEditMessage(message) - setReplyMessage(null) - editorRef.current.chain().focus().setContent(message?.messageText || message?.text).run(); - - }, []) - const handleReaction = useCallback(async (reaction, chatMessage, reactionState = true)=> { - try { - - if(isSending) return - if(+balance < 4) throw new Error('You need at least 4 QORT to send a message') - pauseAllQueues() - - - setIsSending(true) - const message = '' - const secretKeyObject = await getSecretKey(false, true) - - - const otherData = { - specialId: uid.rnd(), - type: 'reaction', - content: reaction, - contentState: reactionState + const onReply = useCallback( + (message) => { + if (onEditMessage) { + clearEditorContent(); } - const objectMessage = { - message, - ...(otherData || {}) + setReplyMessage(message); + setOnEditMessage(null); + editorRef?.current?.chain().focus(); + }, + [onEditMessage] + ); + + const onEdit = useCallback((message) => { + setOnEditMessage(message); + setReplyMessage(null); + editorRef.current + .chain() + .focus() + .setContent(message?.messageText || message?.text) + .run(); + }, []); + const handleReaction = useCallback( + async (reaction, chatMessage, reactionState = true) => { + try { + if (isSending) return; + if (+balance < 4) + throw new Error('You need at least 4 QORT to send a message'); + pauseAllQueues(); + + setIsSending(true); + const message = ''; + const secretKeyObject = await getSecretKey(false, true); + + const otherData = { + specialId: uid.rnd(), + type: 'reaction', + content: reaction, + contentState: reactionState, + }; + const objectMessage = { + message, + ...(otherData || {}), + }; + const message64: any = await objectToBase64(objectMessage); + const reactiontypeNumber = RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS; + const encryptSingle = + isPrivate === false + ? JSON.stringify(objectMessage) + : await encryptChatMessage( + message64, + secretKeyObject, + reactiontypeNumber + ); + // const res = await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle}) + + const sendMessageFunc = async () => { + return await sendChatGroup({ + groupId: selectedGroup, + messageText: encryptSingle, + chatReference: chatMessage.signature, + }); + }; + + // Add the function to the queue + const messageObj = { + message: { + text: message, + timestamp: Date.now(), + senderName: myName, + sender: myAddress, + ...(otherData || {}), + }, + chatReference: chatMessage.signature, + }; + addToQueue(sendMessageFunc, messageObj, 'chat-reaction', selectedGroup); + // setTimeout(() => { + // executeEvent("sent-new-message-group", {}) + // }, 150); + // clearEditorContent() + // setReplyMessage(null) + + // send chat message + } catch (error) { + const errorMsg = error?.message || error; + setInfoSnack({ + type: 'error', + message: errorMsg, + }); + setOpenSnack(true); + console.error(error); + } finally { + setIsSending(false); + resumeAllQueues(); } - const message64: any = await objectToBase64(objectMessage) - const reactiontypeNumber = RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS - const encryptSingle = isPrivate === false ? JSON.stringify(objectMessage) : await encryptChatMessage(message64, secretKeyObject, reactiontypeNumber) - // const res = await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle}) - - const sendMessageFunc = async () => { - return await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle, chatReference: chatMessage.signature}) - }; + }, + [isPrivate] + ); - // Add the function to the queue - const messageObj = { - message: { - text: message, - timestamp: Date.now(), - senderName: myName, - sender: myAddress, - ...(otherData || {}) - }, - chatReference: chatMessage.signature - } - addToQueue(sendMessageFunc, messageObj, 'chat-reaction', - selectedGroup ); - // setTimeout(() => { - // executeEvent("sent-new-message-group", {}) - // }, 150); - // clearEditorContent() - // setReplyMessage(null) + const openQManager = useCallback(() => { + setIsOpenQManager(true); + }, []); - // send chat message - } catch (error) { - const errorMsg = error?.message || error - setInfoSnack({ - type: "error", - message: errorMsg, - }); - setOpenSnack(true); - console.error(error) - } finally { - setIsSending(false) - resumeAllQueues() - } - }, [isPrivate]) + const theme = useTheme(); - const openQManager = useCallback(()=> { - setIsOpenQManager(true) - }, []) - return ( -
- - - - {(!!secretKey || isPrivate === false) && ( -
- -
+ + + {(!!secretKey || isPrivate === false) && ( +
- {replyMessage && ( - - + minHeight: '150px', + overflow: 'hidden', + padding: '20px', + position: isFocusedParent ? 'fixed' : 'relative', + top: isFocusedParent ? '0px' : 'unset', + width: '100%', + zIndex: isFocusedParent ? 5 : 'unset', + border: `1px solid ${theme.palette.border.subtle}`, + borderRadius: '10px', + }} + > +
+ {replyMessage && ( + + - { - setReplyMessage(null) - - setOnEditMessage(null) + { + setReplyMessage(null); - }} - > - - - - )} - {onEditMessage && ( - - + setOnEditMessage(null); + }} + > + + + + )} + {onEditMessage && ( + + - { - setReplyMessage(null) - setOnEditMessage(null) - - clearEditorContent() - - }} - > - - - - )} - - - - {messageSize > 750 && ( - - 4000 ? 'var(--danger)' : 'unset' - }}>{`Your message size is of ${messageSize} bytes out of a maximum of 4000`} + { + setReplyMessage(null); + setOnEditMessage(null); - - )} -
- - + clearEditorContent(); + }} + > + + + + )} - { - - if(isSending) return - sendMessage() + + {messageSize > 750 && ( + + 4000 ? theme.palette.other.danger : 'unset', + }} + >{`Your message size is of ${messageSize} bytes out of a maximum of 4000`} + + )} +
+ + + { + if (isSending) return; + sendMessage(); }} style={{ - marginTop: 'auto', alignSelf: 'center', + background: isSending + ? theme.palette.background.default + : theme.palette.background.paper, cursor: isSending ? 'default' : 'pointer', - background: isSending && 'rgba(0, 0, 0, 0.8)', flexShrink: 0, + marginTop: 'auto', + minWidth: 'auto', padding: '5px', width: '100px', - minWidth: 'auto' - }} > {isSending && ( + size={18} + sx={{ + color: theme.palette.text.primary, + left: '50%', + marginLeft: '-12px', + marginTop: '-12px', + position: 'absolute', + top: '50%', + }} + /> )} {` Send`} - - -
- )} - {isOpenQManager !== null && ( - - - - Q-Manager - { - setIsOpenQManager(false) - }}> - - - - - + +
+ )} + {isOpenQManager !== null && ( + + + + Q-Manager + { + setIsOpenQManager(false); + }} + > + + + + + + + )} - - {/* */} - - + + +
- ) -} + ); +}; diff --git a/src/components/Chat/ChatList.tsx b/src/components/Chat/ChatList.tsx index ef32e7e..56acbc7 100644 --- a/src/components/Chat/ChatList.tsx +++ b/src/components/Chat/ChatList.tsx @@ -1,23 +1,15 @@ -import React, { - useCallback, - useState, - useEffect, - useRef, - useMemo, -} from "react"; -import { useVirtualizer } from "@tanstack/react-virtual"; -import { MessageItem } from "./MessageItem"; -import { subscribeToEvent, unsubscribeFromEvent } from "../../utils/events"; -import { useInView } from "react-intersection-observer"; -import { Box, Typography } from "@mui/material"; -import { ChatOptions } from "./ChatOptions"; -import ErrorBoundary from "../../common/ErrorBoundary"; +import { useCallback, useState, useEffect, useRef, useMemo } from 'react'; +import { useVirtualizer } from '@tanstack/react-virtual'; +import { MessageItem } from './MessageItem'; +import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events'; +import { Box, Button, Typography, useTheme } from '@mui/material'; +import { ChatOptions } from './ChatOptions'; +import ErrorBoundary from '../../common/ErrorBoundary'; export const ChatList = ({ initialMessages, myAddress, tempMessages, - chatId, onReply, onEdit, handleReaction, @@ -29,7 +21,7 @@ export const ChatList = ({ enableMentions, openQManager, hasSecretKey, - isPrivate + isPrivate, }) => { const parentRef = useRef(); const [messages, setMessages] = useState(initialMessages); @@ -42,33 +34,32 @@ export const ChatList = ({ // Initialize the virtualizer const rowVirtualizer = useVirtualizer({ count: messages.length, - getItemKey: (index) => messages[index]?.tempSignature || 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 }); - const isAtBottom = useMemo(()=> { + const isAtBottom = useMemo(() => { if (parentRef.current && rowVirtualizer?.isScrolling !== undefined) { const { scrollTop, scrollHeight, clientHeight } = parentRef.current; - const atBottom = scrollTop + clientHeight >= scrollHeight - 10; // Adjust threshold as needed - return atBottom - } + const atBottom = scrollTop + clientHeight >= scrollHeight - 10; // Adjust threshold as needed + return atBottom; + } - return false - - }, [rowVirtualizer?.isScrolling]) + return false; + }, [rowVirtualizer?.isScrolling]); useEffect(() => { if (!parentRef.current || rowVirtualizer?.isScrolling === undefined) return; - if(isAtBottom){ + if (isAtBottom) { if (scrollingIntervalRef.current) { clearTimeout(scrollingIntervalRef.current); } setShowScrollDownButton(false); return; - } else - if (rowVirtualizer?.isScrolling) { + } else if (rowVirtualizer?.isScrolling) { if (scrollingIntervalRef.current) { clearTimeout(scrollingIntervalRef.current); } @@ -108,7 +99,13 @@ export const ChatList = ({ setTimeout(() => { const hasUnreadMessages = totalMessages.some( - (msg) => msg.unread && !msg?.chatReference && !msg?.isTemp && (!msg?.chatReference && msg?.timestamp > lastSeenUnreadMessageTimestamp.current || 0) + (msg) => + msg.unread && + !msg?.chatReference && + !msg?.isTemp && + ((!msg?.chatReference && + msg?.timestamp > lastSeenUnreadMessageTimestamp.current) || + 0) ); if (parentRef.current) { const { scrollTop, scrollHeight, clientHeight } = parentRef.current; @@ -136,9 +133,9 @@ export const ChatList = ({ const index = initialMsgs ? initialMsgs.length - 1 : messages.length - 1; if (rowVirtualizer) { if (divideIndex) { - rowVirtualizer.scrollToIndex(divideIndex, { align: "start" }); + rowVirtualizer.scrollToIndex(divideIndex, { align: 'start' }); } else { - rowVirtualizer.scrollToIndex(index, { align: "end" }); + rowVirtualizer.scrollToIndex(index, { align: 'end' }); } } handleMessageSeen(); @@ -152,7 +149,7 @@ export const ChatList = ({ })) ); setShowScrollButton(false); - lastSeenUnreadMessageTimestamp.current = Date.now() + lastSeenUnreadMessageTimestamp.current = Date.now(); }, []); const sentNewMessageGroupFunc = useCallback(() => { @@ -166,9 +163,9 @@ export const ChatList = ({ }, [messages]); useEffect(() => { - subscribeToEvent("sent-new-message-group", sentNewMessageGroupFunc); + subscribeToEvent('sent-new-message-group', sentNewMessageGroupFunc); return () => { - unsubscribeFromEvent("sent-new-message-group", sentNewMessageGroupFunc); + unsubscribeFromEvent('sent-new-message-group', sentNewMessageGroupFunc); }; }, [sentNewMessageGroupFunc]); @@ -181,21 +178,24 @@ export const ChatList = ({ const goToMessage = useCallback((idx) => { rowVirtualizer.scrollToIndex(idx); }, []); + + const theme = useTheme(); + return (
- {rowVirtualizer.getVirtualItems().map((virtualRow) => { const index = virtualRow.index; let message = messages[index] || null; // Safeguard against undefined @@ -231,7 +230,7 @@ export const ChatList = ({ let reply = null; let reactions = null; let isUpdating = false; - + try { // Safeguard for message existence if (message) { @@ -239,16 +238,19 @@ export const ChatList = ({ replyIndex = messages.findIndex( (msg) => msg?.signature === message?.repliedTo ); - + if (message?.repliedTo && replyIndex !== -1) { reply = { ...(messages[replyIndex] || {}) }; if (chatReferences?.[reply?.signature]?.edit) { - reply.decryptedData = chatReferences[reply?.signature]?.edit; - reply.text = chatReferences[reply?.signature]?.edit?.message; - reply.editTimestamp = chatReferences[reply?.signature]?.edit?.timestamp + reply.decryptedData = + chatReferences[reply?.signature]?.edit; + reply.text = + chatReferences[reply?.signature]?.edit?.message; + reply.editTimestamp = + chatReferences[reply?.signature]?.edit?.timestamp; } } - + // GroupDirectId logic if (message?.message && message?.groupDirectId) { replyIndex = messages.findIndex( @@ -264,24 +266,34 @@ export const ChatList = ({ status: message?.status, }; } - + // Check for reactions and edits if (chatReferences?.[message.signature]) { - reactions = chatReferences[message.signature]?.reactions || null; - - if (chatReferences[message.signature]?.edit?.message && message?.text) { - message.text = chatReferences[message.signature]?.edit?.message; - message.isEdit = true - message.editTimestamp = chatReferences[message.signature]?.edit?.timestamp + reactions = + chatReferences[message.signature]?.reactions || null; + + if ( + chatReferences[message.signature]?.edit?.message && + message?.text + ) { + message.text = + chatReferences[message.signature]?.edit?.message; + message.isEdit = true; + message.editTimestamp = + chatReferences[message.signature]?.edit?.timestamp; } - if (chatReferences[message.signature]?.edit?.messageText && message?.messageText) { - message.messageText = chatReferences[message.signature]?.edit?.messageText; - message.isEdit = true - message.editTimestamp = chatReferences[message.signature]?.edit?.timestamp + if ( + chatReferences[message.signature]?.edit?.messageText && + message?.messageText + ) { + message.messageText = + chatReferences[message.signature]?.edit?.messageText; + message.isEdit = true; + message.editTimestamp = + chatReferences[message.signature]?.edit?.timestamp; } - } - + // Check if message is updating if ( tempChatReferences?.some( @@ -292,34 +304,37 @@ export const ChatList = ({ } } } catch (error) { - console.error("Error processing message:", error, { index, message }); + console.error('Error processing message:', error, { + index, + message, + }); // Gracefully handle the error by providing fallback data message = null; reply = null; reactions = null; } - // Render fallback if message is null + // Render fallback if message is null if (!message) { - return ( -
- Error loading message. -
- ); - } + return ( +
+ Error loading message. +
+ ); + } return (
- Error loading content: Invalid Data } > - - + +
); })} -
@@ -377,47 +390,49 @@ export const ChatList = ({ )} {showScrollDownButton && !showScrollButton && ( - + )}
{enableMentions && (hasSecretKey || isPrivate === false) && ( { return convert(htmlString, { wordwrap: false, // Disable word wrapping })?.toLowerCase(); }; + const cache = new CellMeasurerCache({ fixedWidth: true, defaultHeight: 50, }); -export const ChatOptions = ({ messages : untransformedMessages, goToMessage, members, myName, selectedGroup, openQManager, isPrivate }) => { - const [mode, setMode] = useState("default"); - const [searchValue, setSearchValue] = useState(""); +export const ChatOptions = ({ + messages: untransformedMessages, + goToMessage, + members, + myName, + selectedGroup, + openQManager, + isPrivate, +}) => { + const [mode, setMode] = useState('default'); + const [searchValue, setSearchValue] = useState(''); const [selectedMember, setSelectedMember] = useState(0); - + const theme = useTheme(); const parentRef = useRef(); const parentRefMentions = useRef(); - const [lastMentionTimestamp, setLastMentionTimestamp] = useState(null) - const [debouncedValue, setDebouncedValue] = useState(""); // Debounced value - const messages = useMemo(()=> { - return untransformedMessages?.map((item)=> { - if(item?.messageText){ - let transformedMessage = item?.messageText + 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 - } + transformedMessage = generateHTML(item?.messageText, [ + StarterKit, + Underline, + Highlight, + Mention, + ]); + return { + ...item, + messageText: transformedMessage, + }; } catch (error) { // error } - } else return item - }) - }, [untransformedMessages]) + } else return item; + }); + }, [untransformedMessages]); + const getTimestampMention = async () => { try { return new Promise((res, rej) => { - window.sendMessage("getTimestampMention") - .then((response) => { - if (!response?.error) { - if(response && selectedGroup && response[selectedGroup]){ - setLastMentionTimestamp(response[selectedGroup]) - - - - - - } - - res(response); - return; - } - rej(response.error); - }) - .catch((error) => { - rej(error.message || "An error occurred"); - }); - + window + .sendMessage('getTimestampMention') + .then((response) => { + if (!response?.error) { + if (response && selectedGroup && response[selectedGroup]) { + setLastMentionTimestamp(response[selectedGroup]); + } + res(response); + return; + } + rej(response.error); + }) + .catch((error) => { + rej(error.message || 'An error occurred'); + }); }); - } catch (error) {} + } catch (error) { + console.log(error); + } }; - useEffect(()=> { - if(mode === 'mentions' && selectedGroup){ - window.sendMessage("addTimestampMention", { - timestamp: Date.now(), - groupId: selectedGroup - }).then((res)=> { - getTimestampMention() - }).catch((error) => { - console.error("Failed to add timestamp:", error.message || "An error occurred"); + useEffect(() => { + if (mode === 'mentions' && selectedGroup) { + window + .sendMessage('addTimestampMention', { + timestamp: Date.now(), + groupId: selectedGroup, + }) + .then((res) => { + getTimestampMention(); + }) + .catch((error) => { + console.error( + 'Failed to add timestamp:', + error.message || 'An error occurred' + ); }); } - }, [mode, selectedGroup]) + }, [mode, selectedGroup]); - useEffect(()=> { - getTimestampMention() - }, []) + useEffect(() => { + getTimestampMention(); + }, []); // Debounce logic useEffect(() => { @@ -151,32 +159,39 @@ export const ChatOptions = ({ messages : untransformedMessages, goToMessage, mem .filter( (message) => message?.senderName === selectedMember && - extractTextFromHTML(isPrivate ? message?.messageText : message?.decryptedData?.message)?.includes( - debouncedValue.toLowerCase() - ) + extractTextFromHTML( + isPrivate ? message?.messageText : message?.decryptedData?.message + )?.includes(debouncedValue.toLowerCase()) ) ?.sort((a, b) => b?.timestamp - a?.timestamp); } return messages .filter((message) => - extractTextFromHTML(isPrivate === false ? message?.messageText : message?.decryptedData?.message)?.includes(debouncedValue.toLowerCase()) + extractTextFromHTML( + isPrivate === false + ? message?.messageText + : message?.decryptedData?.message + )?.includes(debouncedValue.toLowerCase()) ) ?.sort((a, b) => b?.timestamp - a?.timestamp); }, [debouncedValue, messages, selectedMember, isPrivate]); const mentionList = useMemo(() => { - if(!messages || messages.length === 0 || !myName) return [] - if(isPrivate === false){ + if (!messages || messages.length === 0 || !myName) return []; + if (isPrivate === false) { return messages - .filter((message) => - extractTextFromHTML(message?.messageText)?.includes(`@${myName?.toLowerCase()}`) - ) - ?.sort((a, b) => b?.timestamp - a?.timestamp); - + .filter((message) => + extractTextFromHTML(message?.messageText)?.includes( + `@${myName?.toLowerCase()}` + ) + ) + ?.sort((a, b) => b?.timestamp - a?.timestamp); } return messages .filter((message) => - extractTextFromHTML(message?.decryptedData?.message)?.includes(`@${myName?.toLowerCase()}`) + extractTextFromHTML(message?.decryptedData?.message)?.includes( + `@${myName?.toLowerCase()}` + ) ) ?.sort((a, b) => b?.timestamp - a?.timestamp); }, [messages, myName, isPrivate]); @@ -203,60 +218,54 @@ export const ChatOptions = ({ messages : untransformedMessages, goToMessage, mem overscan: 10, // Number of items to render outside the visible area to improve smoothness }); - - - if (mode === "mentions") { + if (mode === 'mentions') { return ( { - setMode("default"); + setMode('default'); }} sx={{ - cursor: "pointer", - color: "white", + cursor: 'pointer', + color: theme.palette.text.primary, }} /> - - - {mentionList?.length === 0 && ( No results @@ -264,72 +273,77 @@ export const ChatOptions = ({ messages : untransformedMessages, goToMessage, mem )}
- {rowVirtualizerMentions.getVirtualItems().map((virtualRow) => { - const index = virtualRow.index; - let message = mentionList[index]; - return ( -
- - -
- ); - })} + {rowVirtualizerMentions + .getVirtualItems() + .map((virtualRow) => { + const index = virtualRow.index; + let message = mentionList[index]; + return ( +
+ +
+ ); + })}
@@ -340,60 +354,63 @@ export const ChatOptions = ({ messages : untransformedMessages, goToMessage, mem ); } - if (mode === "search") { + if (mode === 'search') { return ( { - setMode("default"); + setMode('default'); }} sx={{ - cursor: "pointer", - color: "white", + cursor: 'pointer', + color: theme.palette.text.primary, }} /> - + setSearchValue(e.target.value)} sx={{ ml: 1, flex: 1 }} placeholder="Search chat text" inputProps={{ - "aria-label": "Search for apps", - fontSize: "16px", + 'aria-label': 'Search for apps', + fontSize: '16px', fontWeight: 400, }} /> @@ -402,7 +419,7 @@ export const ChatOptions = ({ messages : untransformedMessages, goToMessage, mem {searchValue && ( { - setSearchValue(""); + setSearchValue(''); }} > @@ -411,52 +428,51 @@ export const ChatOptions = ({ messages : untransformedMessages, goToMessage, mem - - {!!selectedMember && ( - { - setSelectedMember(0); - }} sx={{ - cursor: "pointer", - color: "white", + alignItems: 'center', + display: 'flex', + justifyContent: 'space-between', + padding: '10px', }} - /> - )} - - - + > + + {!!selectedMember && ( + { + setSelectedMember(0); + }} + sx={{ + cursor: 'pointer', + color: theme.palette.text.primary, + }} + /> + )} + + {debouncedValue && searchedList?.length === 0 && ( No results @@ -464,79 +480,81 @@ export const ChatOptions = ({ messages : untransformedMessages, goToMessage, mem )}
{rowVirtualizer.getVirtualItems().map((virtualRow) => { const index = virtualRow.index; let message = searchedList[index]; return ( -
- - Error loading content: Invalid Data - - } - > - + + Error loading content: Invalid Data + + } + > +
- ); })}
@@ -551,46 +569,58 @@ export const ChatOptions = ({ messages : untransformedMessages, goToMessage, mem return ( - { - setMode("search") - }}> + { + setMode('search'); + }} + > SEARCH} + title={ + + SEARCH + + } placement="left" arrow - sx={{ fontSize: "24" }} + sx={{ fontSize: '24' }} slotProps={{ tooltip: { sx: { - color: "#ffffff", - backgroundColor: "#444444", + color: theme.palette.text.primary, + backgroundColor: theme.palette.background.default, }, }, arrow: { sx: { - color: "#444444", + color: theme.palette.text.secondary, }, }, }} @@ -598,157 +628,187 @@ export const ChatOptions = ({ messages : untransformedMessages, goToMessage, mem - { - setMode("default") - setSearchValue('') - setSelectedMember(0) - openQManager() - }}> + { + setMode('default'); + setSearchValue(''); + setSelectedMember(0); + openQManager(); + }} + > Q-MANAGER} + title={ + + Q-MANAGER + + } placement="left" arrow - sx={{ fontSize: "24" }} + sx={{ fontSize: '24' }} slotProps={{ tooltip: { sx: { - color: "#ffffff", - backgroundColor: "#444444", + color: theme.palette.text.primary, + backgroundColor: theme.palette.background.default, }, }, arrow: { sx: { - color: "#444444", + color: theme.palette.text.secondary, }, }, }} > - + - - { - setMode("mentions") - setSearchValue('') - setSelectedMember(0) - }}> - MENTIONED} - placement="left" - arrow - sx={{ fontSize: "24" }} - slotProps={{ - tooltip: { - sx: { - color: "#ffffff", - backgroundColor: "#444444", - }, - }, - arrow: { - sx: { - color: "#444444", - }, - }, + + { + setMode('mentions'); + setSearchValue(''); + setSelectedMember(0); }} > - 0 && (!lastMentionTimestamp || lastMentionTimestamp < mentionList[0]?.timestamp) ? 'var(--unread)' : 'white' - }} /> - - - + + MENTIONED + + } + placement="left" + arrow + sx={{ fontSize: '24' }} + slotProps={{ + tooltip: { + sx: { + color: theme.palette.text.primary, + backgroundColor: theme.palette.background.default, + }, + }, + arrow: { + sx: { + color: theme.palette.text.secondary, + }, + }, + }} + > + 0 && + (!lastMentionTimestamp || + lastMentionTimestamp < mentionList[0]?.timestamp) + ? theme.palette.other.unread + : theme.palette.text.primary, + }} + /> + + - - ); }; - -const ShowMessage = ({message, goToMessage, messages})=> { +const ShowMessage = ({ message, goToMessage, messages }) => { + const theme = useTheme(); return ( - - - - - {message?.senderName?.charAt(0)} - - - {message?.senderName} - - - - - {formatTimestamp(message.timestamp)} - { - const findMsgIndex = messages.findIndex( - (item) => - item?.signature === message?.signature - ); - if (findMsgIndex !== -1) { - goToMessage(findMsgIndex); - } - }} - > - {message?.messageText && ( - - )} - {message?.decryptedData?.message && ( -

" - } - /> - )} - -
-
- ) -} \ No newline at end of file + + + + + {message?.senderName?.charAt(0)} + + + {message?.senderName} + + + + + + + + {formatTimestamp(message.timestamp)} + + { + const findMsgIndex = messages.findIndex( + (item) => item?.signature === message?.signature + ); + if (findMsgIndex !== -1) { + goToMessage(findMsgIndex); + } + }} + > + {message?.messageText && ( + + )} + {message?.decryptedData?.message && ( +

'} + /> + )} +
+
+ ); +}; diff --git a/src/components/Chat/CreateCommonSecret.tsx b/src/components/Chat/CreateCommonSecret.tsx index 4c9f1d4..9cf3b79 100644 --- a/src/components/Chat/CreateCommonSecret.tsx +++ b/src/components/Chat/CreateCommonSecret.tsx @@ -1,35 +1,59 @@ -import { Box, Button, Typography } from '@mui/material' -import React, { useContext } from 'react' +import React, { useContext } from 'react'; +import { Box, Button, Typography, useTheme } from '@mui/material'; import { CustomizedSnackbars } from '../Snackbar/Snackbar'; import { LoadingButton } from '@mui/lab'; -import { MyContext, getArbitraryEndpointReact, getBaseApiReact, pauseAllQueues } from '../../App'; +import { + MyContext, + getArbitraryEndpointReact, + getBaseApiReact, + pauseAllQueues, +} from '../../App'; import { getFee } from '../../background'; -import { decryptResource, getGroupAdmins, validateSecretKey } from '../Group/Group'; +import { + decryptResource, + getGroupAdmins, + validateSecretKey, +} from '../Group/Group'; import { base64ToUint8Array } from '../../qdn/encryption/group-encryption'; import { uint8ArrayToObject } from '../../backgroundFunctions/encryption'; +import { useSetAtom } from 'jotai'; +import { txListAtom } from '../../atoms/global'; -export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, secretKeyDetails, userInfo, noSecretKey, setHideCommonKeyPopup, setIsForceShowCreationKeyPopup, isForceShowCreationKeyPopup}) => { - const { show, setTxList } = useContext(MyContext); +export const CreateCommonSecret = ({ + groupId, + secretKey, + isOwner, + myAddress, + secretKeyDetails, + userInfo, + noSecretKey, + setHideCommonKeyPopup, + setIsForceShowCreationKeyPopup, + isForceShowCreationKeyPopup, +}) => { + const { show } = useContext(MyContext); + const setTxList = useSetAtom(txListAtom); const [openSnack, setOpenSnack] = React.useState(false); const [infoSnack, setInfoSnack] = React.useState(null); - const [isLoading, setIsLoading] = React.useState(false) + const [isLoading, setIsLoading] = React.useState(false); + + const theme = useTheme(); const getPublishesFromAdmins = async (admins: string[]) => { // const validApi = await findUsableApi(); - const queryString = admins.map((name) => `name=${name}`).join("&"); + const queryString = admins.map((name) => `name=${name}`).join('&'); const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT_PRIVATE&identifier=symmetric-qchat-group-${ groupId }&exactmatchnames=true&limit=0&reverse=true&${queryString}&prefix=true`; const response = await fetch(url); - if(!response.ok){ - throw new Error('network error') + if (!response.ok) { + throw new Error('network error'); } const adminData = await response.json(); - + const filterId = adminData.filter( - (data: any) => - data.identifier === `symmetric-qchat-group-${groupId}` + (data: any) => data.identifier === `symmetric-qchat-group-${groupId}` ); if (filterId?.length === 0) { return false; @@ -38,149 +62,182 @@ export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, sec // Get the most recent date for both a and b const dateA = a.updated ? new Date(a.updated) : new Date(a.created); const dateB = b.updated ? new Date(b.updated) : new Date(b.created); - + // Sort by most recent return dateB.getTime() - dateA.getTime(); }); - + return sortedData[0]; }; - const getSecretKey = async (loadingGroupParam?: boolean, secretKeyToPublish?: boolean) => { + + const getSecretKey = async ( + loadingGroupParam?: boolean, + secretKeyToPublish?: boolean + ) => { try { - pauseAllQueues() - - - - const {names} = await getGroupAdmins(groupId); - if(!names.length){ - throw new Error('Network error') + pauseAllQueues(); + + const { names } = await getGroupAdmins(groupId); + if (!names.length) { + throw new Error('Network error'); } const publish = await getPublishesFromAdmins(names); - - + if (publish === false) { - return false; } - + const res = await fetch( `${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${ publish.identifier }?encoding=base64&rebuild=true` ); const data = await res.text(); - + const decryptedKey: any = await decryptResource(data); - + const dataint8Array = base64ToUint8Array(decryptedKey.data); const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); - + if (!validateSecretKey(decryptedKeyToObject)) - throw new Error("SecretKey is not valid"); - + throw new Error('SecretKey is not valid'); + if (decryptedKeyToObject) { - return decryptedKeyToObject; - } else { } - } catch (error) { - - - } finally { + console.log(error); } }; - const createCommonSecret = async ()=> { - try { - const fee = await getFee('ARBITRARY') - await show({ - message: "Would you like to perform an ARBITRARY transaction?" , - publishFee: fee.fee + ' QORT' - }) - setIsLoading(true) + const createCommonSecret = async () => { + try { + const fee = await getFee('ARBITRARY'); + await show({ + message: 'Would you like to perform an ARBITRARY transaction?', + publishFee: fee.fee + ' QORT', + }); + setIsLoading(true); - const secretKey2 = await getSecretKey() - if((!secretKey2 && secretKey2 !== false)) throw new Error('invalid secret key') - if (secretKey2 && !validateSecretKey(secretKey2)) throw new Error('invalid secret key') + const secretKey2 = await getSecretKey(); + if (!secretKey2 && secretKey2 !== false) + throw new Error('invalid secret key'); + if (secretKey2 && !validateSecretKey(secretKey2)) + throw new Error('invalid secret key'); - const secretKeyToSend = !secretKey2 ? null : secretKey2 - - - window.sendMessage("encryptAndPublishSymmetricKeyGroupChat", { - groupId: groupId, - previousData: secretKeyToSend, - }) - .then((response) => { - if (!response?.error) { - setInfoSnack({ - type: "success", - message: "Successfully re-encrypted secret key. It may take a couple of minutes for the changes to propagate. Refresh the group in 5 mins.", - }); - setOpenSnack(true); - setTxList((prev) => [ - { - ...response, - type: 'created-common-secret', - label: `Published secret key for group ${groupId}: awaiting confirmation`, - labelDone: `Published secret key for group ${groupId}: success!`, - done: false, - groupId, - }, - ...prev, - ]); - } - setIsLoading(false); - setTimeout(() => { - setIsForceShowCreationKeyPopup(false) - }, 1000); - }) - .catch((error) => { - console.error("Failed to encrypt and publish symmetric key for group chat:", error.message || "An error occurred"); - setIsLoading(false); + const secretKeyToSend = !secretKey2 ? null : secretKey2; + + window + .sendMessage('encryptAndPublishSymmetricKeyGroupChat', { + groupId: groupId, + previousData: secretKeyToSend, + }) + .then((response) => { + if (!response?.error) { + setInfoSnack({ + type: 'success', + message: + 'Successfully re-encrypted secret key. It may take a couple of minutes for the changes to propagate. Refresh the group in 5 mins.', }); - - } catch (error) { - - } + setOpenSnack(true); + setTxList((prev) => [ + { + ...response, + type: 'created-common-secret', + label: `Published secret key for group ${groupId}: awaiting confirmation`, + labelDone: `Published secret key for group ${groupId}: success!`, + done: false, + groupId, + }, + ...prev, + ]); + } + setIsLoading(false); + setTimeout(() => { + setIsForceShowCreationKeyPopup(false); + }, 1000); + }) + .catch((error) => { + console.error( + 'Failed to encrypt and publish symmetric key for group chat:', + error.message || 'An error occurred' + ); + setIsLoading(false); + }); + } catch (error) { + console.log(error); } - + }; return ( - - Re-encrypt key - {noSecretKey ? ( - - There is no group secret key. Be the first admin to publish one! - - ) : isOwner && secretKeyDetails && userInfo?.name && userInfo.name !== secretKeyDetails?.name ? ( - - The latest group secret key was published by a non-owner. As the owner of the group please re-encrypt the key as a safeguard - - ): isForceShowCreationKeyPopup ? null : ( - - The group member list has changed. Please re-encrypt the secret key. - - )} - - + flexDirection: 'column', + gap: '25px', + maxWidth: '350px', + padding: '25px', + }} + > + + Re-encrypt key + + + {noSecretKey ? ( + + + There is no group secret key. Be the first admin to publish one! + + + ) : isOwner && + secretKeyDetails && + userInfo?.name && + userInfo.name !== secretKeyDetails?.name ? ( + + + The latest group secret key was published by a non-owner. As the + owner of the group please re-encrypt the key as a safeguard + + + ) : isForceShowCreationKeyPopup ? null : ( + + + The group member list has changed. Please re-encrypt the secret key. + + + )} + + + - + + - - ) -} + ); +}; diff --git a/src/components/Chat/GroupAnnouncements.tsx b/src/components/Chat/GroupAnnouncements.tsx index 9ba7668..9ac3ded 100644 --- a/src/components/Chat/GroupAnnouncements.tsx +++ b/src/components/Chat/GroupAnnouncements.tsx @@ -4,54 +4,49 @@ import React, { useMemo, useRef, useState, -} from "react"; -import { CreateCommonSecret } from "./CreateCommonSecret"; -import { reusableGet } from "../../qdn/publish/pubish"; -import { uint8ArrayToObject } from "../../backgroundFunctions/encryption"; +} from 'react'; +import { uint8ArrayToObject } from '../../backgroundFunctions/encryption'; import { base64ToUint8Array, objectToBase64, -} from "../../qdn/encryption/group-encryption"; -import { ChatContainerComp } from "./ChatContainer"; -import { ChatList } from "./ChatList"; -import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css"; -import Tiptap from "./TipTap"; -import { AuthenticatedContainerInnerTop, CustomButton } from "../../App-styles"; -import CircularProgress from "@mui/material/CircularProgress"; -import { getBaseApi, getFee } from "../../background"; -import { LoadingSnackbar } from "../Snackbar/LoadingSnackbar"; -import { Box, Typography } from "@mui/material"; -import { Spacer } from "../../common/Spacer"; -import ShortUniqueId from "short-unique-id"; -import { AnnouncementList } from "./AnnouncementList"; -const uid = new ShortUniqueId({ length: 8 }); -import CampaignIcon from "@mui/icons-material/Campaign"; -import ArrowBackIcon from "@mui/icons-material/ArrowBack"; -import { AnnouncementDiscussion } from "./AnnouncementDiscussion"; +} from '../../qdn/encryption/group-encryption'; +import Tiptap from './TipTap'; +import { CustomButton } from '../../styles/App-styles'; +import CircularProgress from '@mui/material/CircularProgress'; +import { getFee } from '../../background'; +import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar'; +import { Box, Typography, useTheme } from '@mui/material'; +import { Spacer } from '../../common/Spacer'; +import ShortUniqueId from 'short-unique-id'; +import { AnnouncementList } from './AnnouncementList'; +import CampaignIcon from '@mui/icons-material/Campaign'; +import { AnnouncementDiscussion } from './AnnouncementDiscussion'; import { MyContext, getArbitraryEndpointReact, getBaseApiReact, - isMobile, pauseAllQueues, resumeAllQueues, -} from "../../App"; -import { RequestQueueWithPromise } from "../../utils/queue/queue"; -import { CustomizedSnackbars } from "../Snackbar/Snackbar"; -import { addDataPublishesFunc, getDataPublishesFunc } from "../Group/Group"; -import { getRootHeight } from "../../utils/mobile/mobileUtils"; +} from '../../App'; +import { RequestQueueWithPromise } from '../../utils/queue/queue'; +import { CustomizedSnackbars } from '../Snackbar/Snackbar'; +import { addDataPublishesFunc, getDataPublishesFunc } from '../Group/Group'; + +const uid = new ShortUniqueId({ length: 8 }); export const requestQueueCommentCount = new RequestQueueWithPromise(3); + export const requestQueuePublishedAccouncements = new RequestQueueWithPromise( 3 ); export const saveTempPublish = async ({ data, key }: any) => { return new Promise((res, rej) => { - window.sendMessage("saveTempPublish", { - data, - key, - }) + window + .sendMessage('saveTempPublish', { + data, + key, + }) .then((response) => { if (!response?.error) { res(response); @@ -60,37 +55,37 @@ export const saveTempPublish = async ({ data, key }: any) => { rej(response.error); }) .catch((error) => { - rej(error.message || "An error occurred"); + rej(error.message || 'An error occurred'); }); - }); }; export const getTempPublish = async () => { return new Promise((res, rej) => { - window.sendMessage("getTempPublish", {}) - .then((response) => { - if (!response?.error) { - res(response); - return; - } - rej(response.error); - }) - .catch((error) => { - rej(error.message || "An error occurred"); - }); - + window + .sendMessage('getTempPublish', {}) + .then((response) => { + if (!response?.error) { + res(response); + return; + } + rej(response.error); + }) + .catch((error) => { + rej(error.message || 'An error occurred'); + }); }); }; export const decryptPublishes = async (encryptedMessages: any[], secretKey) => { try { return await new Promise((res, rej) => { - window.sendMessage("decryptSingleForPublishes", { - data: encryptedMessages, - secretKeyObject: secretKey, - skipDecodeBase64: true, - }) + window + .sendMessage('decryptSingleForPublishes', { + data: encryptedMessages, + secretKeyObject: secretKey, + skipDecodeBase64: true, + }) .then((response) => { if (!response?.error) { res(response); @@ -99,27 +94,29 @@ export const decryptPublishes = async (encryptedMessages: any[], secretKey) => { rej(response.error); }) .catch((error) => { - rej(error.message || "An error occurred"); + rej(error.message || 'An error occurred'); }); - }); - } catch (error) {} + } catch (error) { + console.log(error); + } }; -export const handleUnencryptedPublishes = (publishes) => { - let publishesData = [] - publishes.forEach((pub)=> { +export const handleUnencryptedPublishes = (publishes) => { + let publishesData = []; + publishes.forEach((pub) => { try { const decryptToUnit8Array = base64ToUint8Array(pub); const decodedData = uint8ArrayToObject(decryptToUnit8Array); - if(decodedData){ - publishesData.push({decryptedData: decodedData}) + if (decodedData) { + publishesData.push({ decryptedData: decodedData }); } } catch (error) { - + console.log(error); } - }) - return publishesData + }); + return publishesData; }; + export const GroupAnnouncements = ({ selectedGroup, secretKey, @@ -130,7 +127,7 @@ export const GroupAnnouncements = ({ isAdmin, hide, myName, - isPrivate + isPrivate, }) => { const [messages, setMessages] = useState([]); const [isSending, setIsSending] = useState(false); @@ -141,7 +138,7 @@ export const GroupAnnouncements = ({ const [selectedAnnouncement, setSelectedAnnouncement] = useState(null); const [isFocusedParent, setIsFocusedParent] = useState(false); - const { show, rootHeight } = React.useContext(MyContext); + const { show } = React.useContext(MyContext); const [openSnack, setOpenSnack] = React.useState(false); const [infoSnack, setInfoSnack] = React.useState(null); const hasInitialized = useRef(false); @@ -159,12 +156,15 @@ export const GroupAnnouncements = ({ useEffect(() => { if (!selectedGroup) return; (async () => { - const res = await getDataPublishesFunc(selectedGroup, "anc"); + const res = await getDataPublishesFunc(selectedGroup, 'anc'); dataPublishes.current = res || {}; })(); }, [selectedGroup]); - const getAnnouncementData = async ({ identifier, name, resource }, isPrivate) => { + const getAnnouncementData = async ( + { identifier, name, resource }, + isPrivate + ) => { try { let data = dataPublishes.current[`${name}-${identifier}`]; if ( @@ -179,14 +179,17 @@ export const GroupAnnouncements = ({ }); if (!res?.ok) return; data = await res.text(); - await addDataPublishesFunc({ ...resource, data }, selectedGroup, "anc"); + await addDataPublishesFunc({ ...resource, data }, selectedGroup, 'anc'); } else { data = data.data; } - const response = isPrivate === false ? handleUnencryptedPublishes([data]) : await decryptPublishes([{ data }], secretKey); + const response = + isPrivate === false + ? handleUnencryptedPublishes([data]) + : await decryptPublishes([{ data }], secretKey); const messageData = response[0]; - if(!messageData) return + if (!messageData) return; setAnnouncementData((prev) => { return { ...prev, @@ -194,12 +197,17 @@ export const GroupAnnouncements = ({ }; }); } catch (error) { - console.error("error", error); + console.error('error', error); } }; useEffect(() => { - if ((!secretKey && isPrivate) || hasInitializedWebsocket.current || isPrivate === null) return; + if ( + (!secretKey && isPrivate) || + hasInitializedWebsocket.current || + isPrivate === null + ) + return; setIsLoading(true); // initWebsocketMessageGroup() hasInitializedWebsocket.current = true; @@ -208,10 +216,11 @@ export const GroupAnnouncements = ({ const encryptChatMessage = async (data: string, secretKeyObject: any) => { try { return new Promise((res, rej) => { - window.sendMessage("encryptSingle", { - data, - secretKeyObject, - }) + window + .sendMessage('encryptSingle', { + data, + secretKeyObject, + }) .then((response) => { if (!response?.error) { res(response); @@ -220,19 +229,21 @@ export const GroupAnnouncements = ({ rej(response.error); }) .catch((error) => { - rej(error.message || "An error occurred"); + rej(error.message || 'An error occurred'); }); - }); - } catch (error) {} + } catch (error) { + console.log(error); + } }; const publishAnc = async ({ encryptedData, identifier }: any) => { return new Promise((res, rej) => { - window.sendMessage("publishGroupEncryptedResource", { - encryptedData, - identifier, - }) + window + .sendMessage('publishGroupEncryptedResource', { + encryptedData, + identifier, + }) .then((response) => { if (!response?.error) { res(response); @@ -241,23 +252,14 @@ export const GroupAnnouncements = ({ rej(response.error); }) .catch((error) => { - rej(error.message || "An error occurred"); + rej(error.message || 'An error occurred'); }); - }); }; + const clearEditorContent = () => { if (editorRef.current) { editorRef.current.chain().focus().clearContent().run(); - if (isMobile) { - setTimeout(() => { - editorRef.current?.chain().blur().run(); - setIsFocusedParent(false); - setTimeout(() => { - triggerRerender(); - }, 300); - }, 200); - } } }; @@ -266,39 +268,46 @@ export const GroupAnnouncements = ({ const getTempAnnouncements = await getTempPublish(); if (getTempAnnouncements?.announcement) { let tempData = []; - Object.keys(getTempAnnouncements?.announcement || {}).filter((annKey)=> annKey?.startsWith(`grp-${selectedGroup}-anc`)).map((key) => { - const value = getTempAnnouncements?.announcement[key]; - tempData.push(value.data); - }); + Object.keys(getTempAnnouncements?.announcement || {}) + .filter((annKey) => annKey?.startsWith(`grp-${selectedGroup}-anc`)) + .map((key) => { + const value = getTempAnnouncements?.announcement[key]; + tempData.push(value.data); + }); setTempPublishedList(tempData); } - } catch (error) {} + } catch (error) { + console.log(error); + } }; const publishAnnouncement = async () => { try { pauseAllQueues(); - const fee = await getFee("ARBITRARY"); + const fee = await getFee('ARBITRARY'); + await show({ - message: "Would you like to perform a ARBITRARY transaction?", - publishFee: fee.fee + " QORT", + message: 'Would you like to perform a ARBITRARY transaction?', + publishFee: fee.fee + ' QORT', }); + if (isSending) return; if (editorRef.current) { const htmlContent = editorRef.current.getHTML(); - if (!htmlContent?.trim() || htmlContent?.trim() === "

") return; + if (!htmlContent?.trim() || htmlContent?.trim() === '

') return; setIsSending(true); const message = { version: 1, extra: {}, message: htmlContent, }; - const secretKeyObject = isPrivate === false ? null : await getSecretKey(false, true); - const message64: any = await objectToBase64(message); - const encryptSingle = isPrivate === false ? message64 : await encryptChatMessage( - message64, - secretKeyObject - ); + const secretKeyObject = + isPrivate === false ? null : await getSecretKey(false, true); + const message64: any = await objectToBase64(message); + const encryptSingle = + isPrivate === false + ? message64 + : await encryptChatMessage(message64, secretKeyObject); const randomUid = uid.rnd(); const identifier = `grp-${selectedGroup}-anc-${randomUid}`; const res = await publishAnc({ @@ -309,13 +318,13 @@ export const GroupAnnouncements = ({ const dataToSaveToStorage = { name: myName, identifier, - service: "DOCUMENT", + service: 'DOCUMENT', tempData: message, created: Date.now(), }; await saveTempPublish({ data: dataToSaveToStorage, - key: "announcement", + key: 'announcement', }); setTempData(selectedGroup); clearEditorContent(); @@ -324,7 +333,7 @@ export const GroupAnnouncements = ({ } catch (error) { if (!error) return; setInfoSnack({ - type: "error", + type: 'error', message: error, }); setOpenSnack(true); @@ -343,9 +352,9 @@ export const GroupAnnouncements = ({ const identifier = `grp-${selectedGroup}-anc-`; const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${offset}&reverse=true&prefix=true`; const response = await fetch(url, { - method: "GET", + method: 'GET', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, }); const responseData = await response.json(); @@ -354,23 +363,30 @@ export const GroupAnnouncements = ({ setAnnouncements(responseData); setIsLoading(false); for (const data of responseData) { - getAnnouncementData({ - name: data.name, - identifier: data.identifier, - resource: data, - }, isPrivate); + getAnnouncementData( + { + name: data.name, + identifier: data.identifier, + resource: data, + }, + isPrivate + ); } } catch (error) { - } finally { - // dispatch(setIsLoadingGlobal(false)) + console.log(error); } }, [secretKey] ); React.useEffect(() => { - if(!secretKey && isPrivate) return - if (selectedGroup && !hasInitialized.current && !hide && isPrivate !== null) { + if (!secretKey && isPrivate) return; + if ( + selectedGroup && + !hasInitialized.current && + !hide && + isPrivate !== null + ) { getAnnouncements(selectedGroup, isPrivate); hasInitialized.current = true; } @@ -384,9 +400,9 @@ export const GroupAnnouncements = ({ const identifier = `grp-${selectedGroup}-anc-`; const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${offset}&reverse=true&prefix=true`; const response = await fetch(url, { - method: "GET", + method: 'GET', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, }); const responseData = await response.json(); @@ -394,21 +410,28 @@ export const GroupAnnouncements = ({ setAnnouncements((prev) => [...prev, ...responseData]); setIsLoading(false); for (const data of responseData) { - getAnnouncementData({ name: data.name, identifier: data.identifier }, isPrivate); + getAnnouncementData( + { name: data.name, identifier: data.identifier }, + isPrivate + ); } - } catch (error) {} + } catch (error) { + console.log(error); + } }; const interval = useRef(null); + const theme = useTheme(); + const checkNewMessages = React.useCallback(async () => { try { const identifier = `grp-${selectedGroup}-anc-`; const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${0}&reverse=true&prefix=true`; const response = await fetch(url, { - method: "GET", + method: 'GET', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, }); const responseData = await response.json(); @@ -416,11 +439,16 @@ export const GroupAnnouncements = ({ if (!latestMessage) { for (const data of responseData) { try { - getAnnouncementData({ - name: data.name, - identifier: data.identifier, - }, isPrivate); - } catch (error) {} + getAnnouncementData( + { + name: data.name, + identifier: data.identifier, + }, + isPrivate + ); + } catch (error) { + console.log(error); + } } setAnnouncements(responseData); return; @@ -434,12 +462,17 @@ export const GroupAnnouncements = ({ for (const data of newArray) { try { - getAnnouncementData({ name: data.name, identifier: data.identifier }, isPrivate); - } catch (error) {} + getAnnouncementData( + { name: data.name, identifier: data.identifier }, + isPrivate + ); + } catch (error) { + console.log(error); + } } setAnnouncements((prev) => [...newArray, ...prev]); } catch (error) { - } finally { + console.log(error); } }, [announcements, secretKey, selectedGroup]); @@ -485,14 +518,13 @@ export const GroupAnnouncements = ({ return (
- {!isMobile && ( - + - - Group Announcements - - )} + /> + Group Announcements + - +
+ {!isLoading && combinedListTempAndReal?.length === 0 && ( No announcements @@ -587,31 +616,28 @@ export const GroupAnnouncements = ({ {isAdmin && (
+ {isFocusedParent && ( @@ -639,49 +666,52 @@ export const GroupAnnouncements = ({ if (isSending) return; setIsFocusedParent(false); clearEditorContent(); - setTimeout(() => { - triggerRerender(); - }, 300); + setTimeout(() => { + triggerRerender(); + }, 300); // Unfocus the editor }} style={{ - marginTop: "auto", - alignSelf: "center", - cursor: isSending ? "default" : "pointer", - background: "var(--danger)", + alignSelf: 'center', + background: theme.palette.other.danger, + cursor: isSending ? 'default' : 'pointer', flexShrink: 0, - padding: isMobile && "5px", - fontSize: isMobile && "14px", + fontSize: '14px', + marginTop: 'auto', + padding: '5px', }} > {` Close`} )} + { if (isSending) return; publishAnnouncement(); }} style={{ - marginTop: "auto", - alignSelf: "center", - cursor: isSending ? "default" : "pointer", - background: isSending && "rgba(0, 0, 0, 0.8)", + alignSelf: 'center', + background: isSending + ? theme.palette.background.default + : theme.palette.background.paper, + cursor: isSending ? 'default' : 'pointer', flexShrink: 0, - padding: isMobile && "5px", - fontSize: isMobile && "14px", + fontSize: '14px', + marginTop: 'auto', + padding: '5px', }} > {isSending && ( )} @@ -701,7 +731,7 @@ export const GroupAnnouncements = ({
diff --git a/src/components/Chat/GroupForum.tsx b/src/components/Chat/GroupForum.tsx index d8dd7e5..551a2db 100644 --- a/src/components/Chat/GroupForum.tsx +++ b/src/components/Chat/GroupForum.tsx @@ -1,19 +1,5 @@ -import React, { - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -import { GroupMail } from "../Group/Forum/GroupMail"; -import { MyContext, isMobile } from "../../App"; -import { getRootHeight } from "../../utils/mobile/mobileUtils"; - - - - - +import { useEffect, useState } from 'react'; +import { GroupMail } from '../Group/Forum/GroupMail'; export const GroupForum = ({ selectedGroup, @@ -23,12 +9,12 @@ export const GroupForum = ({ isAdmin, myAddress, hide, - defaultThread, + defaultThread, setDefaultThread, - isPrivate + isPrivate, }) => { - const { rootHeight } = useContext(MyContext); const [isMoved, setIsMoved] = useState(false); + useEffect(() => { if (hide) { setTimeout(() => setIsMoved(true), 300); // Wait for the fade-out to complete before moving @@ -39,20 +25,27 @@ export const GroupForum = ({ return (
- - -
+ style={{ + display: 'flex', + flexDirection: 'column', + height: 'calc(100vh - 70px)', + left: hide && '-1000px', + opacity: hide ? 0 : 1, + position: hide ? 'fixed' : 'relative', + visibility: hide && 'hidden', + width: '100%', + }} + > + +
); }; diff --git a/src/components/Chat/MentionList.tsx b/src/components/Chat/MentionList.tsx index 85f6890..06cea5c 100644 --- a/src/components/Chat/MentionList.tsx +++ b/src/components/Chat/MentionList.tsx @@ -1,69 +1,68 @@ -import React, { - forwardRef, useEffect, useImperativeHandle, - useState, - } from 'react' - - export default forwardRef((props, ref) => { - const [selectedIndex, setSelectedIndex] = useState(0) - - const selectItem = index => { - const item = props.items[index] - - if (item) { - props.command(item) +import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; + +export default forwardRef((props, ref) => { + const [selectedIndex, setSelectedIndex] = useState(0); + + const selectItem = (index) => { + const item = props.items[index]; + + if (item) { + props.command(item); + } + }; + + const upHandler = () => { + setSelectedIndex( + (selectedIndex + props.items.length - 1) % props.items.length + ); + }; + + const downHandler = () => { + setSelectedIndex((selectedIndex + 1) % props.items.length); + }; + + const enterHandler = () => { + selectItem(selectedIndex); + }; + + useEffect(() => setSelectedIndex(0), [props.items]); + + useImperativeHandle(ref, () => ({ + onKeyDown: ({ event }) => { + if (event.key === 'ArrowUp') { + upHandler(); + return true; } - } - - const upHandler = () => { - setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length) - } - - const downHandler = () => { - setSelectedIndex((selectedIndex + 1) % props.items.length) - } - - const enterHandler = () => { - selectItem(selectedIndex) - } - - useEffect(() => setSelectedIndex(0), [props.items]) - - useImperativeHandle(ref, () => ({ - onKeyDown: ({ event }) => { - if (event.key === 'ArrowUp') { - upHandler() - return true - } - - if (event.key === 'ArrowDown') { - downHandler() - return true - } - - if (event.key === 'Enter') { - enterHandler() - return true - } - - return false - }, - })) - - return ( -
- {props.items.length - ? props.items.map((item, index) => ( - - )) - :
No result
- } -
- ) - }) - \ No newline at end of file + + if (event.key === 'ArrowDown') { + downHandler(); + return true; + } + + if (event.key === 'Enter') { + enterHandler(); + return true; + } + + return false; + }, + })); + + return ( +
+ {props.items.length ? ( + props.items.map((item, index) => ( + + )) + ) : ( +
No result
+ )} +
+ ); +}); diff --git a/src/components/Chat/MessageDisplay.tsx b/src/components/Chat/MessageDisplay.tsx index 52d0d93..2bf3d6f 100644 --- a/src/components/Chat/MessageDisplay.tsx +++ b/src/components/Chat/MessageDisplay.tsx @@ -1,28 +1,29 @@ -import React, { useEffect, useMemo } from 'react'; +import { useMemo } from 'react'; import DOMPurify from 'dompurify'; -import './styles.css'; +import './chat.css'; import { executeEvent } from '../../utils/events'; import { Embed } from '../Embeds/Embed'; +import { Box, useTheme } from '@mui/material'; export const extractComponents = (url) => { - if (!url || !url.startsWith("qortal://")) { + if (!url || !url.startsWith('qortal://')) { return null; } // Skip links starting with "qortal://use-" - if (url.startsWith("qortal://use-")) { + if (url.startsWith('qortal://use-')) { return null; } - url = url.replace(/^(qortal\:\/\/)/, ""); - if (url.includes("/")) { - let parts = url.split("/"); + url = url.replace(/^(qortal\:\/\/)/, ''); + if (url.includes('/')) { + let parts = url.split('/'); const service = parts[0].toUpperCase(); parts.shift(); const name = parts[0]; parts.shift(); let identifier; - const path = parts.join("/"); + const path = parts.join('/'); return { service, name, identifier, path }; } @@ -64,8 +65,7 @@ function processText(input) { } const linkify = (text) => { - if (!text) return ""; // Return an empty string if text is null or undefined - + 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) => { @@ -75,22 +75,68 @@ const linkify = (text) => { return processText(textFormatted); }; - export const MessageDisplay = ({ htmlContent, isReply }) => { + const theme = useTheme(); - - const sanitizedContent = useMemo(()=> { + 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' + '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' + 'href', + 'target', + 'rel', + 'class', + 'src', + 'alt', + 'title', + 'width', + 'height', + 'style', + 'align', + 'valign', + 'colspan', + 'rowspan', + 'border', + 'cellpadding', + 'cellspacing', + 'data-url', ], - }).replace(/]*data-url="qortal:\/\/use-embed\/[^"]*"[^>]*>.*?<\/span>/g, ''); - }, [htmlContent]) + }).replace( + /]*data-url="qortal:\/\/use-embed\/[^"]*"[^>]*>.*?<\/span>/g, + '' + ); + }, [htmlContent]); const handleClick = async (e) => { e.preventDefault(); @@ -98,7 +144,7 @@ export const MessageDisplay = ({ htmlContent, isReply }) => { const target = e.target; if (target.tagName === 'A') { const href = target.getAttribute('href'); - if(window?.electronAPI){ + if (window?.electronAPI) { window.electronAPI.openExternal(href); } else { window.open(href, '_system'); @@ -106,32 +152,32 @@ export const MessageDisplay = ({ htmlContent, isReply }) => { } else if (target.getAttribute('data-url')) { const url = target.getAttribute('data-url'); - let copyUrl = 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 + 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 } - } catch (error) { - //error - } const res = extractComponents(url); if (res) { const { service, name, identifier, path } = res; - executeEvent("addTab", { data: { service, name, identifier, path } }); - executeEvent("open-apps-mode", { }); + executeEvent('addTab', { data: { service, name, identifier, path } }); + executeEvent('open-apps-mode', {}); } } }; @@ -141,19 +187,24 @@ export const MessageDisplay = ({ htmlContent, isReply }) => { let embedData = null; if (embedLink) { - embedData = embedLink[0] + embedData = embedLink[0]; } return ( - <> - {embedLink && ( - - )} -
- + + {embedLink && } +
+ ); }; diff --git a/src/components/Chat/MessageItem.tsx b/src/components/Chat/MessageItem.tsx index 2b1467c..d861dac 100644 --- a/src/components/Chat/MessageItem.tsx +++ b/src/components/Chat/MessageItem.tsx @@ -1,574 +1,660 @@ -import { Message } from "@chatscope/chat-ui-kit-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, Tooltip, Typography } from "@mui/material"; -import { formatTimestamp } from "../../utils/time"; -import { getBaseApi } from "../../background"; -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"; -import { WrapperUserAction } from "../WrapperUserAction"; -import ReplyIcon from "@mui/icons-material/Reply"; -import { Spacer } from "../../common/Spacer"; -import { ReactionPicker } from "../ReactionPicker"; +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, + Tooltip, + Typography, + useTheme, +} from '@mui/material'; +import { formatTimestamp } from '../../utils/time'; +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 { WrapperUserAction } from '../WrapperUserAction'; +import ReplyIcon from '@mui/icons-material/Reply'; +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" +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'; -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 +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, - isTemp, - myAddress, - onReply, - isShowingAsReply, - reply, - replyIndex, - scrollToItem, - handleReaction, - reactions, - isUpdating, - lastSignature, - onEdit, - isPrivate -}) => { - -const {getIndividualUserInfo} = useContext(MyContext) - const [anchorEl, setAnchorEl] = useState(null); - const [selectedReaction, setSelectedReaction] = useState(null); - const [userInfo, setUserInfo] = useState(null) - - -useEffect(()=> { - const getInfo = async ()=> { - if(!message?.sender) return - try { - const res = await getIndividualUserInfo(message?.sender) - if(!res) return null - setUserInfo(res) - } catch (error) { - // - } - } - - getInfo() -}, [message?.sender, getIndividualUserInfo]) - -const htmlText = useMemo(()=> { - - if(message?.messageText){ - return generateHTML(message?.messageText, [ - StarterKit, - Underline, - Highlight, - Mention, - TextStyle - ]) - } - -}, [message?.editTimestamp]) - - - -const htmlReply = useMemo(()=> { - - if(reply?.messageText){ - return generateHTML(reply?.messageText, [ - StarterKit, - Underline, - Highlight, - Mention, - TextStyle - ]) - } - -}, [reply?.editTimestamp]) - -const userAvatarUrl = useMemo(()=> { - return message?.senderName ? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${ - message?.senderName - }/qortal_avatar?async=true` : '' -}, []) - -const onSeenFunc = useCallback(()=> { - onSeen(message.id); -}, [message?.id]) - +}; +const UserBadge = React.memo(({ userInfo }) => { return ( - <> - {message?.divide && ( -
- Unread messages below -
- )} - - - - -
- {isShowingAsReply ? ( - - ) : ( - - - - - {message?.senderName?.charAt(0)} - - - - - - - - - - - )} - - + - - - - {message?.senderName || message?.sender} - - - - - {message?.sender === myAddress && (!message?.isNotEncrypted || isPrivate === false) && ( - { - onEdit(message); - }} - > - - - )} - {!isShowingAsReply && ( - { - onReply(message); - }} - > - - - )} - {!isShowingAsReply && handleReaction && ( - { - - if(reactions && reactions[val] && reactions[val]?.find((item)=> item?.sender === myAddress)){ - handleReaction(val, message, false) - } else { - handleReaction(val, message, true) - } - - }} /> - )} - - - {reply && ( - <> - - { - scrollToItem(replyIndex) - - - }} - > - - - Replied to {reply?.senderName || reply?.senderAddress} - {reply?.messageText && ( - - )} - {reply?.decryptedData?.type === "notification" ? ( - - ) : ( - - )} - - - - )} - {htmlText && ( - - )} - - - {message?.decryptedData?.type === "notification" ? ( - - ) : ( - - )} - - - {reactions && Object.keys(reactions).map((reaction)=> { - const numberOfReactions = reactions[reaction]?.length - // const myReaction = reactions - if(numberOfReactions === 0) return null - return ( - { - event.stopPropagation(); // Prevent event bubbling - setAnchorEl(event.currentTarget); - setSelectedReaction(reaction); - }}> -
{reaction}
{numberOfReactions > 1 && ( - {' '} {numberOfReactions} - )} -
- ) - })} -
- {selectedReaction && ( - { - setAnchorEl(null); - setSelectedReaction(null); - }} - anchorOrigin={{ - vertical: "top", - horizontal: "center", - }} - transformOrigin={{ - vertical: "bottom", - horizontal: "center", - }} - PaperProps={{ - style: { - backgroundColor: "#232428", - color: "white", - }, - }} - > - - - People who reacted with {selectedReaction} - - - {reactions[selectedReaction]?.map((reactionItem) => ( - - - - ))} - - - - - )} - - {message?.isNotEncrypted && isPrivate && ( - - )} - - {isUpdating ? ( - - {message?.status === 'failed-permanent' ? 'Failed to update' : 'Updating...'} - - ) : isTemp ? ( - - {message?.status === 'failed-permanent' ? 'Failed to send' : 'Sending...'} - - ) : ( - <> - {message?.isEdit && ( - - Edited - - )} - - {formatTimestamp(message.timestamp)} - - - )} - -
-
- - -
-
- + src={getBadgeImg(userInfo)} + /> + ); }); +export const MessageItem = React.memo( + ({ + message, + onSeen, + isLast, + isTemp, + myAddress, + onReply, + isShowingAsReply, + reply, + replyIndex, + scrollToItem, + handleReaction, + reactions, + isUpdating, + lastSignature, + onEdit, + isPrivate, + }) => { + const { getIndividualUserInfo } = useContext(MyContext); + const [anchorEl, setAnchorEl] = useState(null); + const [selectedReaction, setSelectedReaction] = useState(null); + const [userInfo, setUserInfo] = useState(null); -export const ReplyPreview = ({message, isEdit})=> { + useEffect(() => { + const getInfo = async () => { + if (!message?.sender) return; + try { + const res = await getIndividualUserInfo(message?.sender); + if (!res) return null; + setUserInfo(res); + } catch (error) { + // + } + }; + + getInfo(); + }, [message?.sender, getIndividualUserInfo]); + + const htmlText = useMemo(() => { + if (message?.messageText) { + return generateHTML(message?.messageText, [ + StarterKit, + Underline, + Highlight, + Mention, + TextStyle, + ]); + } + }, [message?.editTimestamp]); + + const htmlReply = useMemo(() => { + if (reply?.messageText) { + return generateHTML(reply?.messageText, [ + StarterKit, + Underline, + Highlight, + Mention, + TextStyle, + ]); + } + }, [reply?.editTimestamp]); + + const userAvatarUrl = useMemo(() => { + return message?.senderName + ? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${ + message?.senderName + }/qortal_avatar?async=true` + : ''; + }, []); + + const onSeenFunc = useCallback(() => { + onSeen(message.id); + }, [message?.id]); + + const theme = useTheme(); + + return ( + <> + {message?.divide && ( +
+ Unread messages below +
+ )} + + +
+ {isShowingAsReply ? ( + + ) : ( + + + + {message?.senderName?.charAt(0)} + + + + + )} + + + + + + {message?.senderName || message?.sender} + + + + {message?.sender === myAddress && + (!message?.isNotEncrypted || isPrivate === false) && ( + { + onEdit(message); + }} + > + + + )} + {!isShowingAsReply && ( + { + onReply(message); + }} + > + + + )} + {!isShowingAsReply && handleReaction && ( + { + if ( + reactions && + reactions[val] && + reactions[val]?.find( + (item) => item?.sender === myAddress + ) + ) { + handleReaction(val, message, false); + } else { + handleReaction(val, message, true); + } + }} + /> + )} + + + {reply && ( + <> + + { + scrollToItem(replyIndex); + }} + > + + + + Replied to {reply?.senderName || reply?.senderAddress} + + {reply?.messageText && ( + + )} + {reply?.decryptedData?.type === 'notification' ? ( + + ) : ( + + )} + + + + )} + {htmlText && } + + {message?.decryptedData?.type === 'notification' ? ( + + ) : ( + + )} + + + + {reactions && + Object.keys(reactions).map((reaction) => { + const numberOfReactions = reactions[reaction]?.length; + // const myReaction = reactions + if (numberOfReactions === 0) return null; + return ( + { + event.stopPropagation(); // Prevent event bubbling + setAnchorEl(event.currentTarget); + setSelectedReaction(reaction); + }} + > +
+ {reaction} +
{' '} + {numberOfReactions > 1 && ( + + {' '} + {numberOfReactions} + + )} +
+ ); + })} +
+ + {selectedReaction && ( + { + setAnchorEl(null); + setSelectedReaction(null); + }} + anchorOrigin={{ + vertical: 'top', + horizontal: 'center', + }} + transformOrigin={{ + vertical: 'bottom', + horizontal: 'center', + }} + PaperProps={{ + // TODO: deprecated + style: { + backgroundColor: theme.palette.background.default, + color: theme.palette.text.primary, + }, + }} + > + + + People who reacted with {selectedReaction} + + + + {reactions[selectedReaction]?.map((reactionItem) => ( + + + + ))} + + + + + )} + + {message?.isNotEncrypted && isPrivate && ( + + )} + + {isUpdating ? ( + + {message?.status === 'failed-permanent' + ? 'Failed to update' + : 'Updating...'} + + ) : isTemp ? ( + + {message?.status === 'failed-permanent' + ? 'Failed to send' + : 'Sending...'} + + ) : ( + <> + {message?.isEdit && ( + + Edited + + )} + + {formatTimestamp(message.timestamp)} + + + )} + +
+
+
+
+ + ); + } +); + +export const ReplyPreview = ({ message, isEdit = false }) => { + const theme = useTheme(); return ( + + + {isEdit ? ( + - - - {isEdit ? ( - Editing Message - ) : ( - Replied to {message?.senderName || message?.senderAddress} - )} - - {message?.messageText && ( - - )} - {message?.decryptedData?.type === "notification" ? ( - - ) : ( - - )} - - - - ) -} + Editing Message + + ) : ( + + Replied to {message?.senderName || message?.senderAddress} + + )} -const MessageWragger = ({lastMessage, onSeen, isLast, children})=> { + {message?.messageText && ( + + )} - if(lastMessage){ + {message?.decryptedData?.type === 'notification' ? ( + + ) : ( + + )} + + + ); +}; + +const MessageWragger = ({ lastMessage, onSeen, isLast, children }) => { + if (lastMessage) { return ( - {children} - ) + + {children} + + ); } - return children -} + return children; +}; -const WatchComponent = ({onSeen, isLast, children})=> { +const WatchComponent = ({ onSeen, isLast, children }) => { const { ref, inView } = useInView({ threshold: 0.7, // Fully visible triggerOnce: true, // Only trigger once when it becomes visible @@ -578,20 +664,22 @@ const WatchComponent = ({onSeen, isLast, children})=> { useEffect(() => { if (inView && isLast && onSeen) { - - setTimeout(() => { - onSeen(); + setTimeout(() => { + onSeen(); }, 100); - } }, [inView, isLast, onSeen]); - return
- {children} -
- -} \ No newline at end of file + return ( +
+ {children} +
+ ); +}; diff --git a/src/components/Chat/ResizableImage.tsx b/src/components/Chat/ResizableImage.tsx index 8c29292..881c1d7 100644 --- a/src/components/Chat/ResizableImage.tsx +++ b/src/components/Chat/ResizableImage.tsx @@ -1,8 +1,10 @@ -import React, { useRef } from 'react'; +import { useRef } from 'react'; import { NodeViewWrapper } from '@tiptap/react'; +import { useTheme } from '@mui/material'; const ResizableImage = ({ node, updateAttributes, selected }) => { const imgRef = useRef(null); + const theme = useTheme(); const startResizing = (e) => { e.preventDefault(); @@ -40,18 +42,23 @@ const ResizableImage = ({ node, updateAttributes, selected }) => { src={node.attrs.src} alt={node.attrs.alt || ''} title={node.attrs.title || ''} - style={{ width: node.attrs.width || 'auto', display: 'block', margin: '0 auto' }} + style={{ + width: node.attrs.width || 'auto', + display: 'block', + margin: '0 auto', + }} draggable={false} // Prevent image dragging /> +
{ - const { editor } = useCurrentEditor(); - const fileInputRef = useRef(null); - if (!editor) { - return null; - } +const MenuBar = React.memo( + ({ + setEditorRef, + isChat, + isDisabledEditorEnter, + setIsDisabledEditorEnter, + }) => { + const { editor } = useCurrentEditor(); + const fileInputRef = useRef(null); + const theme = useTheme(); - useEffect(() => { - if (editor && setEditorRef) { - setEditorRef(editor); + useEffect(() => { + if (editor && setEditorRef) { + setEditorRef(editor); + } + }, [editor, setEditorRef]); + + if (!editor) { + return null; } - }, [editor, setEditorRef]); - const handleImageUpload = async (file) => { - let compressedFile; - await new Promise((resolve) => { - new Compressor(file, { - quality: 0.6, - maxWidth: 1200, - mimeType: "image/webp", - success(result) { - compressedFile = new File([result], "image.webp", { - type: "image/webp", - }); - resolve(); - }, - error(err) { - console.error("Image compression error:", err); - }, + const handleImageUpload = async (file) => { + let compressedFile; + await new Promise((resolve) => { + new Compressor(file, { + quality: 0.6, + maxWidth: 1200, + mimeType: 'image/webp', + success(result) { + compressedFile = new File([result], 'image.webp', { + type: 'image/webp', + }); + resolve(); + }, + error(err) { + console.error('Image compression error:', err); + }, + }); }); - }); - if (compressedFile) { - const reader = new FileReader(); - reader.onload = () => { - const url = reader.result; - editor - .chain() - .focus() - .setImage({ src: url, style: "width: auto" }) - .run(); - fileInputRef.current.value = ""; - }; - reader.readAsDataURL(compressedFile); - } - }; + if (compressedFile) { + const reader = new FileReader(); + reader.onload = () => { + const url = reader.result; + editor + .chain() + .focus() + .setImage({ src: url, style: 'width: auto' }) + .run(); + fileInputRef.current.value = ''; + }; + reader.readAsDataURL(compressedFile); + } + }; - const triggerImageUpload = () => { - fileInputRef.current.click(); // Trigger the file input click - }; + const triggerImageUpload = () => { + fileInputRef.current.click(); // Trigger the file input click + }; - const handlePaste = (event) => { - const items = event.clipboardData.items; - for (const item of items) { - if (item.type.startsWith("image/")) { - const file = item.getAsFile(); - if (file) { - event.preventDefault(); // Prevent the default paste behavior - handleImageUpload(file); // Call the image upload function + const handlePaste = (event) => { + const items = event.clipboardData.items; + for (const item of items) { + if (item.type.startsWith('image/')) { + const file = item.getAsFile(); + if (file) { + event.preventDefault(); // Prevent the default paste behavior + handleImageUpload(file); // Call the image upload function + } } } - } - }; + }; - useEffect(() => { - if (editor) { - editor.view.dom.addEventListener("paste", handlePaste); - return () => { - editor.view.dom.removeEventListener("paste", handlePaste); - }; - } - }, [editor]); + useEffect(() => { + if (editor) { + editor.view.dom.addEventListener('paste', handlePaste); + return () => { + editor.view.dom.removeEventListener('paste', handlePaste); + }; + } + }, [editor]); - return ( -
-
- editor.chain().focus().toggleBold().run()} - disabled={!editor.can().chain().focus().toggleBold().run()} - sx={{ - color: editor.isActive("bold") ? "white" : "gray", - padding: isMobile ? "5px" : "revert", + return ( +
+
- - - editor.chain().focus().toggleItalic().run()} - disabled={!editor.can().chain().focus().toggleItalic().run()} - sx={{ - color: editor.isActive("italic") ? "white" : "gray", - padding: isMobile ? "5px" : "revert", - }} - > - - - editor.chain().focus().toggleStrike().run()} - disabled={!editor.can().chain().focus().toggleStrike().run()} - sx={{ - color: editor.isActive("strike") ? "white" : "gray", - padding: isMobile ? "5px" : "revert", - }} - > - - - editor.chain().focus().toggleCode().run()} - disabled={!editor.can().chain().focus().toggleCode().run()} - sx={{ - color: editor.isActive("code") ? "white" : "gray", - padding: isMobile ? "5px" : "revert", - }} - > - - - editor.chain().focus().unsetAllMarks().run()} - sx={{ - color: - editor.isActive("bold") || - editor.isActive("italic") || - editor.isActive("strike") || - editor.isActive("code") - ? "white" - : "gray", - padding: isMobile ? "5px" : "revert", - }} - > - - - editor.chain().focus().toggleBulletList().run()} - sx={{ - color: editor.isActive("bulletList") ? "white" : "gray", - padding: isMobile ? "5px" : "revert", - }} - > - - - editor.chain().focus().toggleOrderedList().run()} - sx={{ - color: editor.isActive("orderedList") ? "white" : "gray", - padding: isMobile ? "5px" : "revert", - }} - > - - - editor.chain().focus().toggleCodeBlock().run()} - sx={{ - color: editor.isActive("codeBlock") ? "white" : "gray", - padding: isMobile ? "5px" : "revert", - }} - > - - - editor.chain().focus().toggleBlockquote().run()} - sx={{ - color: editor.isActive("blockquote") ? "white" : "gray", - padding: isMobile ? "5px" : "revert", - }} - > - - - editor.chain().focus().setHorizontalRule().run()} - disabled={!editor.can().chain().focus().setHorizontalRule().run()} - sx={{ color: "gray", padding: isMobile ? "5px" : "revert" }} - > - - - - editor.chain().focus().toggleHeading({ level: 1 }).run() - } - sx={{ - color: editor.isActive("heading", { level: 1 }) ? "white" : "gray", - padding: isMobile ? "5px" : "revert", - }} - > - - - editor.chain().focus().undo().run()} - disabled={!editor.can().chain().focus().undo().run()} - sx={{ color: "gray", padding: isMobile ? "5px" : "revert" }} - > - - - editor.chain().focus().redo().run()} - disabled={!editor.can().chain().focus().redo().run()} - sx={{ color: "gray" }} - > - - - {isChat && ( - { - setIsDisabledEditorEnter(!isDisabledEditorEnter) - }} - > - - editor.chain().focus().toggleBold().run()} + disabled={!editor.can().chain().focus().toggleBold().run()} + sx={{ + color: editor.isActive('bold') + ? theme.palette.text.primary + : theme.palette.text.secondary, + padding: 'revert', + }} + > + + + editor.chain().focus().toggleItalic().run()} + disabled={!editor.can().chain().focus().toggleItalic().run()} + sx={{ + color: editor.isActive('italic') + ? theme.palette.text.primary + : theme.palette.text.secondary, + padding: 'revert', + }} + > + + + editor.chain().focus().toggleStrike().run()} + disabled={!editor.can().chain().focus().toggleStrike().run()} + sx={{ + color: editor.isActive('strike') + ? theme.palette.text.primary + : theme.palette.text.secondary, + padding: 'revert', + }} + > + + + editor.chain().focus().toggleCode().run()} + disabled={!editor.can().chain().focus().toggleCode().run()} + sx={{ + color: editor.isActive('code') + ? theme.palette.text.primary + : theme.palette.text.secondary, + padding: 'revert', + }} + > + + + editor.chain().focus().unsetAllMarks().run()} + sx={{ + color: + editor.isActive('bold') || + editor.isActive('italic') || + editor.isActive('strike') || + editor.isActive('code') + ? theme.palette.text.primary + : theme.palette.text.secondary, + padding: 'revert', + }} + > + + + editor.chain().focus().toggleBulletList().run()} + sx={{ + color: editor.isActive('bulletList') + ? theme.palette.text.primary + : theme.palette.text.secondary, + padding: 'revert', + }} + > + + + editor.chain().focus().toggleOrderedList().run()} + sx={{ + color: editor.isActive('orderedList') + ? theme.palette.text.primary + : theme.palette.text.secondary, + padding: 'revert', + }} + > + + + editor.chain().focus().toggleCodeBlock().run()} + sx={{ + color: editor.isActive('codeBlock') + ? theme.palette.text.primary + : theme.palette.text.secondary, + padding: 'revert', + }} + > + + + editor.chain().focus().toggleBlockquote().run()} + sx={{ + color: editor.isActive('blockquote') + ? theme.palette.text.primary + : theme.palette.text.secondary, + padding: 'revert', + }} + > + + + editor.chain().focus().setHorizontalRule().run()} + disabled={!editor.can().chain().focus().setHorizontalRule().run()} + sx={{ color: 'gray', padding: 'revert' }} + > + + + + editor.chain().focus().toggleHeading({ level: 1 }).run() + } + sx={{ + color: editor.isActive('heading', { level: 1 }) + ? theme.palette.text.primary + : theme.palette.text.secondary, + padding: 'revert', + }} + > + + + editor.chain().focus().undo().run()} + disabled={!editor.can().chain().focus().undo().run()} + sx={{ color: 'gray', padding: 'revert' }} + > + + + editor.chain().focus().redo().run()} + disabled={!editor.can().chain().focus().redo().run()} + sx={{ color: 'gray' }} + > + + + {isChat && ( + { + setIsDisabledEditorEnter(!isDisabledEditorEnter); + }} + > + + disable enter - - )} - {!isChat && ( - <> - - - - handleImageUpload(event.target.files[0])} - accept="image/*" - /> - - )} + + )} + {!isChat && ( + <> + + + + handleImageUpload(event.target.files[0])} + accept="image/*" + /> + + )} +
-
- ); -}; + ); + } +); const extensions = [ Color.configure({ types: [TextStyle.name, ListItem.name] }), @@ -322,7 +347,7 @@ const extensions = [ }, }), Placeholder.configure({ - placeholder: "Start typing here...", + placeholder: 'Start typing here...', }), ImageResize, ]; @@ -340,18 +365,21 @@ export default ({ overrideMobile, customEditorHeight, membersWithNames, - enableMentions + enableMentions, }) => { - const [isDisabledEditorEnter, setIsDisabledEditorEnter] = useRecoilState(isDisabledEditorEnterAtom) + const theme = useTheme(); + const [isDisabledEditorEnter, setIsDisabledEditorEnter] = useAtom( + isDisabledEditorEnterAtom + ); const extensionsFiltered = isChat - ? extensions.filter((item) => item?.name !== "image") + ? extensions.filter((item) => item?.name !== 'image') : extensions; const editorRef = useRef(null); - const setEditorRefFunc = (editorInstance) => { + const setEditorRefFunc = useCallback((editorInstance) => { editorRef.current = editorInstance; setEditorRef(editorInstance); - }; + }, []); // const users = [ // { id: 1, label: 'Alice' }, @@ -359,40 +387,29 @@ export default ({ // { id: 3, label: 'Charlie' }, // ]; - - - const users = useMemo(()=> { - return (membersWithNames || [])?.map((item)=> { + const users = useMemo(() => { + return (membersWithNames || [])?.map((item) => { return { id: item, - label: item - } - }) - }, [membersWithNames]) - - - - + label: item, + }; + }); + }, [membersWithNames]); const usersRef = useRef([]); useEffect(() => { usersRef.current = users; // Keep users up-to-date }, [users]); - const handleFocus = () => { - if (!isMobile) return; - setIsFocusedParent(true); - }; - const handleBlur = () => { const htmlContent = editorRef.current.getHTML(); - if (!htmlContent?.trim() || htmlContent?.trim() === "

") { + if (!htmlContent?.trim() || htmlContent?.trim() === '

') { // Set focus state based on content } }; - const additionalExtensions = useMemo(()=> { - if(!enableMentions) return [] + const additionalExtensions = useMemo(() => { + if (!enableMentions) return []; return [ Mention.configure({ HTMLAttributes: { @@ -409,122 +426,125 @@ export default ({ let popup; // Reference to the Tippy.js instance let component; - return { - onStart: props => { - component = new ReactRenderer(MentionList, { - props, - editor: props.editor, - }) - - if (!props.clientRect) { - return - } - - popup = tippy('body', { - getReferenceClientRect: props.clientRect, - appendTo: () => document.body, - content: component.element, - showOnCreate: true, - interactive: true, - trigger: 'manual', - placement: 'bottom-start', - }) - }, - - onUpdate(props) { - component.updateProps(props) - - if (!props.clientRect) { - return - } - - popup[0].setProps({ - getReferenceClientRect: props.clientRect, - }) - }, - - onKeyDown(props) { - if (props.event.key === 'Escape') { - popup[0].hide() - - return true - } - - return component.ref?.onKeyDown(props) - }, - - onExit() { - popup[0].destroy() - component.destroy() - }, - } + return { + onStart: (props) => { + component = new ReactRenderer(MentionList, { + props, + editor: props.editor, + }); + + if (!props.clientRect) { + return; + } + + popup = tippy('body', { + getReferenceClientRect: props.clientRect, + appendTo: () => document.body, + content: component.element, + showOnCreate: true, + interactive: true, + trigger: 'manual', + placement: 'bottom-start', + }); + }, + + onUpdate(props) { + component.updateProps(props); + + if (!props.clientRect) { + return; + } + + popup[0].setProps({ + getReferenceClientRect: props.clientRect, + }); + }, + + onKeyDown(props) { + if (props.event.key === 'Escape') { + popup[0].hide(); + + return true; + } + + return component.ref?.onKeyDown(props); + }, + + onExit() { + popup[0].destroy(); + component.destroy(); + }, + }; }, }, - }) - ] - }, [enableMentions]) + }), + ]; + }, [enableMentions]); - const handleSetIsDisabledEditorEnter = useCallback((val)=> { - setIsDisabledEditorEnter(val) + const handleSetIsDisabledEditorEnter = useCallback((val) => { + setIsDisabledEditorEnter(val); localStorage.setItem('settings-disable-editor-enter', JSON.stringify(val)); - - }, []) - + }, []); return ( -
- - ) - } - extensions={[...extensionsFiltered, ...additionalExtensions - ]} - content={content} - onCreate={({ editor }) => { - editor.on("focus", handleFocus); // Listen for focus event - editor.on("blur", handleBlur); // Listen for blur event + { - editor.on('focus', handleFocus); // Ensure focus is updated - editor.on('blur', handleBlur); // Ensure blur is updated - }} - editorProps={{ - attributes: { - class: "tiptap-prosemirror", - style: - isMobile ? - `overflow: auto; min-height: ${ - customEditorHeight ? "200px" : "0px" - }; max-height:calc(100svh - ${customEditorHeight || "140px"})`: `overflow: auto; max-height: 250px`, - }, - handleKeyDown(view, event) { - if (!disableEnter && !isDisabledEditorEnter && event.key === "Enter") { - if (event.shiftKey) { - view.dispatch( - view.state.tr.replaceSelectionWith( - view.state.schema.nodes.hardBreak.create() - ) - ); - return true; - } else { - if (typeof onEnter === "function") { - onEnter(); + > + + } + extensions={[...extensionsFiltered, ...additionalExtensions]} + content={content} + onCreate={({ editor }) => { + editor.on('blur', handleBlur); // Listen for blur event + }} + onUpdate={({ editor }) => { + editor.on('blur', handleBlur); // Ensure blur is updated + }} + editorProps={{ + attributes: { + class: 'tiptap-prosemirror', + style: `overflow: auto; max-height: 250px`, + }, + handleKeyDown(view, event) { + if ( + !disableEnter && + !isDisabledEditorEnter && + event.key === 'Enter' + ) { + if (event.shiftKey) { + view.dispatch( + view.state.tr.replaceSelectionWith( + view.state.schema.nodes.hardBreak.create() + ) + ); + return true; + } else { + if (typeof onEnter === 'function') { + onEnter(); + } + return true; } - return true; } - } - return false; - }, - }} - /> -
- + return false; + }, + }} + /> + ); }; diff --git a/src/components/Chat/styles.css b/src/components/Chat/chat.css similarity index 66% rename from src/components/Chat/styles.css rename to src/components/Chat/chat.css index 3c7c570..d563d85 100644 --- a/src/components/Chat/styles.css +++ b/src/components/Chat/chat.css @@ -1,6 +1,6 @@ .tiptap { margin-top: 0; - color: white; /* Set default font color to white */ + color: var(--text-primary); width: 100%; } @@ -26,7 +26,7 @@ line-height: 1.1; margin-top: 2.5rem; text-wrap: pretty; - color: white; /* Ensure heading font color is white */ + color: var(--text-primary); } .tiptap h1, @@ -55,18 +55,18 @@ /* Code and preformatted text styles */ .tiptap code { - background-color: #27282c; /* Set code background color to #27282c */ + background-color: var(--background-default); border-radius: 0.4rem; - color: white; /* Ensure inline code text color is white */ + color: var(--text-primary); font-size: 0.85rem; padding: 0.25em 0.3em; text-wrap: pretty; } .tiptap pre { - background: #27282c; /* Set code block background color to #27282c */ + background: var(--background-default); border-radius: 0.5rem; - color: white; /* Ensure code block text color is white */ + color: var(--text-primary); font-family: 'JetBrainsMono', monospace; margin: 1.5rem 0; padding: 0.75rem 1rem; @@ -86,7 +86,7 @@ border-left: 3px solid var(--gray-3); margin: 1.5rem 0; padding-left: 1rem; - color: white; /* Ensure blockquote text color is white */ + color: var(--text-primary); text-wrap: pretty; } @@ -102,49 +102,49 @@ .tiptap p { font-size: 16px; - color: white; /* Ensure paragraph text color is white */ + color: var(--text-primary); margin: 0px; } - .tiptap p.is-editor-empty:first-child::before { - color: #adb5bd; - content: attr(data-placeholder); - float: left; - height: 0; - pointer-events: none; - } - .tiptap p:empty::before { - content: ''; - display: inline-block; - } +.tiptap p.is-editor-empty:first-child::before { + color: var(--text-primary); + content: attr(data-placeholder); + float: left; + height: 0; + pointer-events: none; +} + +.tiptap p:empty::before { + content: ''; + display: inline-block; +} .tiptap a { - color: cadetblue + color: cadetblue; } .tiptap img { display: block; - max-width: 100%; + max-width: 100%; } .isReply p { font-size: 12px !important; } -.tiptap [data-type="mention"] { +.tiptap [data-type='mention'] { box-decoration-break: clone; - color: lightblue; + color: var(--text-secondary); padding: 0.1rem 0.3rem; } - .unread-divider { + border-bottom: 1px solid var(--text-primary); + border-radius: 2px; + color: var(--text-primary); + display: flex; + justify-content: center; width: 90%; - color: white; - border-bottom: 1px solid white; - display: flex; - justify-content: center; - border-radius: 2px; } .mention-item { @@ -169,11 +169,10 @@ font-size: 16px; width: 100%; border: none; - color: white; - cursor: pointer; + color: var(--text-primary); &:hover, &:hover.is-selected { - background-color: gray; + background-color: var(--background-default); } } -} \ No newline at end of file +} diff --git a/src/components/ContextMenu.tsx b/src/components/ContextMenu.tsx index 491e1f7..3d1ede6 100644 --- a/src/components/ContextMenu.tsx +++ b/src/components/ContextMenu.tsx @@ -1,35 +1,45 @@ import React, { useState, useRef, useMemo, useEffect } from 'react'; -import { ListItemIcon, Menu, MenuItem, Typography, styled } from '@mui/material'; +import { + ListItemIcon, + Menu, + MenuItem, + Typography, + styled, + useTheme, +} from '@mui/material'; import MailOutlineIcon from '@mui/icons-material/MailOutline'; import NotificationsOffIcon from '@mui/icons-material/NotificationsOff'; import { executeEvent } from '../utils/events'; +import { mutedGroupsAtom } from '../atoms/global'; +import { useAtom } from 'jotai'; const CustomStyledMenu = styled(Menu)(({ theme }) => ({ - '& .MuiPaper-root': { - backgroundColor: '#f9f9f9', - borderRadius: '12px', - padding: theme.spacing(1), - boxShadow: '0 5px 15px rgba(0, 0, 0, 0.2)', + '& .MuiPaper-root': { + // backgroundColor: '#f9f9f9', + borderRadius: '12px', + padding: theme.spacing(1), + boxShadow: '0 5px 15px rgba(0, 0, 0, 0.2)', + }, + '& .MuiMenuItem-root': { + fontSize: '14px', // Smaller font size for the menu item text + // color: '#444', + transition: '0.3s background-color', + '&:hover': { + backgroundColor: theme.palette.action.hover, // Explicit hover state }, - '& .MuiMenuItem-root': { - fontSize: '14px', // Smaller font size for the menu item text - color: '#444', - transition: '0.3s background-color', - '&:hover': { - backgroundColor: '#f0f0f0', // Explicit hover state - }, - - }, - })); + }, +})); -export const ContextMenu = ({ children, groupId, getUserSettings, mutedGroups }) => { +export const ContextMenu = ({ children, groupId, getUserSettings }) => { const [menuPosition, setMenuPosition] = useState(null); const longPressTimeout = useRef(null); const preventClick = useRef(false); // Flag to prevent click after long-press or right-click + const theme = useTheme(); + const [mutedGroups] = useAtom(mutedGroupsAtom); - const isMuted = useMemo(()=> { - return mutedGroups.includes(groupId) - }, [mutedGroups, groupId]) + const isMuted = useMemo(() => { + return mutedGroups.includes(groupId); + }, [mutedGroups, groupId]); // Handle right-click (context menu) for desktop const handleContextMenu = (event) => { @@ -67,56 +77,52 @@ export const ContextMenu = ({ children, groupId, getUserSettings, mutedGroups }) } }; - - - const handleSetGroupMute = ()=> { + const handleSetGroupMute = () => { try { - let value = [...mutedGroups] - if(isMuted){ - value = value.filter((group)=> group !== groupId) - } else { - value.push(groupId) - } - window.sendMessage("addUserSettings", { + let value = [...mutedGroups]; + if (isMuted) { + value = value.filter((group) => group !== groupId); + } else { + value.push(groupId); + } + window + .sendMessage('addUserSettings', { keyValue: { key: 'mutedGroups', value, }, }) - .then((response) => { - if (response?.error) { - console.error("Error adding user settings:", response.error); - } else { - console.log("User settings added successfully"); - } - }) - .catch((error) => { - console.error("Failed to add user settings:", error.message || "An error occurred"); - }); - - setTimeout(() => { - getUserSettings() - }, 400); + .then((response) => { + if (response?.error) { + console.error('Error adding user settings:', response.error); + } else { + console.log('User settings added successfully'); + } + }) + .catch((error) => { + console.error( + 'Failed to add user settings:', + error.message || 'An error occurred' + ); + }); - } catch (error) { - - } - } - - + setTimeout(() => { + getUserSettings(); + }, 400); + } catch (error) {} + }; const handleClose = (e) => { e.preventDefault(); - e.stopPropagation(); + e.stopPropagation(); setMenuPosition(null); }; return (
{children} @@ -131,35 +137,48 @@ export const ContextMenu = ({ children, groupId, getUserSettings, mutedGroups }) ? { top: menuPosition.mouseY, left: menuPosition.mouseX } : undefined } - onClick={(e)=> { - e.stopPropagation(); - }} + onClick={(e) => { + e.stopPropagation(); + }} > - { - handleClose(e) - executeEvent("markAsRead", { - groupId - }); - }}> + { + handleClose(e); + executeEvent('markAsRead', { + groupId, + }); + }} + > - + Mark As Read - { - - handleClose(e) - handleSetGroupMute() - - }}> + { + handleClose(e); + handleSetGroupMute(); + }} + > - + - + {isMuted ? 'Unmute ' : 'Mute '}Push Notifications @@ -167,5 +186,3 @@ export const ContextMenu = ({ children, groupId, getUserSettings, mutedGroups })
); }; - - diff --git a/src/components/ContextMenuPinnedApps.tsx b/src/components/ContextMenuPinnedApps.tsx index bb64a4c..b7cea32 100644 --- a/src/components/ContextMenuPinnedApps.tsx +++ b/src/components/ContextMenuPinnedApps.tsx @@ -1,152 +1,188 @@ import React, { useState, useRef } from 'react'; -import { ListItemIcon, Menu, MenuItem, Typography, styled } from '@mui/material'; +import { + ListItemIcon, + Menu, + MenuItem, + Typography, + styled, + useTheme, +} from '@mui/material'; import PushPinIcon from '@mui/icons-material/PushPin'; -import { saveToLocalStorage } from './Apps/AppsNavBar'; -import { useRecoilState } from 'recoil'; +import { saveToLocalStorage } from './Apps/AppsNavBarDesktop'; import { sortablePinnedAppsAtom } from '../atoms/global'; +import { useSetAtom } from 'jotai'; const CustomStyledMenu = styled(Menu)(({ theme }) => ({ - '& .MuiPaper-root': { - backgroundColor: '#f9f9f9', - borderRadius: '12px', - padding: theme.spacing(1), - boxShadow: '0 5px 15px rgba(0, 0, 0, 0.2)', - }, - '& .MuiMenuItem-root': { - fontSize: '14px', - color: '#444', - transition: '0.3s background-color', - '&:hover': { - backgroundColor: '#f0f0f0', - }, + '& .MuiPaper-root': { + borderRadius: '12px', + padding: theme.spacing(1), + boxShadow: '0 5px 15px rgba(0, 0, 0, 0.2)', + }, + '& .MuiMenuItem-root': { + fontSize: '14px', + color: '#444', + transition: '0.3s background-color', + '&:hover': { + backgroundColor: theme.palette.action.hover, }, + }, })); export const ContextMenuPinnedApps = ({ children, app, isMine }) => { - const [menuPosition, setMenuPosition] = useState(null); - const longPressTimeout = useRef(null); - const maxHoldTimeout = useRef(null); - const preventClick = useRef(false); - const startTouchPosition = useRef({ x: 0, y: 0 }); // Track initial touch position - const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(sortablePinnedAppsAtom); + const [menuPosition, setMenuPosition] = useState(null); + const longPressTimeout = useRef(null); + const maxHoldTimeout = useRef(null); + const preventClick = useRef(false); + const startTouchPosition = useRef({ x: 0, y: 0 }); // Track initial touch position - const handleContextMenu = (event) => { - if(isMine) return - event.preventDefault(); - event.stopPropagation(); - preventClick.current = true; - setMenuPosition({ - mouseX: event.clientX, - mouseY: event.clientY, - }); - }; + const setSortablePinnedApps = useSetAtom(sortablePinnedAppsAtom); - const handleTouchStart = (event) => { - if(isMine) return + const theme = useTheme(); - const { clientX, clientY } = event.touches[0]; - startTouchPosition.current = { x: clientX, y: clientY }; + const handleContextMenu = (event) => { + if (isMine) return; + event.preventDefault(); + event.stopPropagation(); + preventClick.current = true; + setMenuPosition({ + mouseX: event.clientX, + mouseY: event.clientY, + }); + }; - longPressTimeout.current = setTimeout(() => { - preventClick.current = true; - - event.stopPropagation(); - setMenuPosition({ - mouseX: clientX, - mouseY: clientY, + const handleTouchStart = (event) => { + if (isMine) return; + + const { clientX, clientY } = event.touches[0]; + startTouchPosition.current = { x: clientX, y: clientY }; + + longPressTimeout.current = setTimeout(() => { + preventClick.current = true; + + event.stopPropagation(); + setMenuPosition({ + mouseX: clientX, + mouseY: clientY, + }); + }, 500); + + // Set a maximum hold duration (e.g., 1.5 seconds) + maxHoldTimeout.current = setTimeout(() => { + clearTimeout(longPressTimeout.current); + }, 1500); + }; + + const handleTouchMove = (event) => { + if (isMine) return; + + const { clientX, clientY } = event.touches[0]; + const { x, y } = startTouchPosition.current; + + // Determine if the touch has moved beyond a small threshold (e.g., 10px) + const movedEnough = + Math.abs(clientX - x) > 10 || Math.abs(clientY - y) > 10; + + if (movedEnough) { + clearTimeout(longPressTimeout.current); + clearTimeout(maxHoldTimeout.current); + } + }; + + const handleTouchEnd = (event) => { + if (isMine) return; + + clearTimeout(longPressTimeout.current); + clearTimeout(maxHoldTimeout.current); + if (preventClick.current) { + event.preventDefault(); + event.stopPropagation(); + preventClick.current = false; + } + }; + + const handleClose = (e) => { + if (isMine) return; + + e.preventDefault(); + e.stopPropagation(); + setMenuPosition(null); + }; + + return ( +
+ {children} + { + e.stopPropagation(); + }} + > + { + handleClose(e); + setSortablePinnedApps((prev) => { + 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; + } }); - }, 500); - - // Set a maximum hold duration (e.g., 1.5 seconds) - maxHoldTimeout.current = setTimeout(() => { - clearTimeout(longPressTimeout.current); - }, 1500); - }; - - const handleTouchMove = (event) => { - if(isMine) return - - const { clientX, clientY } = event.touches[0]; - const { x, y } = startTouchPosition.current; - - // Determine if the touch has moved beyond a small threshold (e.g., 10px) - const movedEnough = Math.abs(clientX - x) > 10 || Math.abs(clientY - y) > 10; - - if (movedEnough) { - clearTimeout(longPressTimeout.current); - clearTimeout(maxHoldTimeout.current); - } - }; - - const handleTouchEnd = (event) => { - if(isMine) return - - clearTimeout(longPressTimeout.current); - clearTimeout(maxHoldTimeout.current); - if (preventClick.current) { - event.preventDefault(); - event.stopPropagation(); - preventClick.current = false; - } - }; - - const handleClose = (e) => { - if(isMine) return - - e.preventDefault(); - e.stopPropagation(); - setMenuPosition(null); - }; - - return ( -
- {children} - { - e.stopPropagation(); - }} - > - { - handleClose(e); - setSortablePinnedApps((prev) => { - 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; - } - }); - }}> - - - - - Unpin app - - - -
- ); + + + + + Unpin app + +
+
+
+ ); }; diff --git a/src/components/CoreSyncStatus.css b/src/components/CoreSyncStatus.css deleted file mode 100644 index 87bf9d7..0000000 --- a/src/components/CoreSyncStatus.css +++ /dev/null @@ -1,59 +0,0 @@ - .lineHeight { - line-height: 33%; - } - - .tooltip { - display: inline-block; - position: relative; - text-align: left; - } - - .tooltip .bottom { - min-width: 225px; - max-width: 250px; - top: 35px; - right: 0px; - /* transform: translate(-50%, 0); */ - padding: 10px 10px; - color: var(--black); - background-color: var(--bg-2); - font-weight: normal; - font-size: 13px; - border-radius: 8px; - position: absolute; - z-index: 99999999; - box-sizing: border-box; - box-shadow: 0 1px 8px rgba(0, 0, 0, 0.5); - border: 1px solid var(--black); - visibility: hidden; - opacity: 0; - transition: opacity 0.2s; - } - - .tooltip:hover .bottom { - visibility: visible; - opacity: 1; - z-index: 100; - } - - .tooltip .bottom i { - position: absolute; - bottom: 100%; - left: 50%; - margin-left: -12px; - width: 24px; - height: 12px; - overflow: hidden; - } - - .tooltip .bottom i::after { - content: ''; - position: absolute; - width: 12px; - height: 12px; - left: 50%; - transform: translate(-50%, 50%) rotate(45deg); - background-color: var(--white); - border: 1px solid var(--black); - box-shadow: 0 1px 8px rgba(0, 0, 0, 0.5); - } \ No newline at end of file diff --git a/src/components/CoreSyncStatus.tsx b/src/components/CoreSyncStatus.tsx index 2334bef..cf39375 100644 --- a/src/components/CoreSyncStatus.tsx +++ b/src/components/CoreSyncStatus.tsx @@ -1,28 +1,34 @@ -import React, { useEffect, useState } from 'react'; -import syncedImg from '../assets/syncStatus/synced.png' -import syncedMintingImg from '../assets/syncStatus/synced_minting.png' -import syncingImg from '../assets/syncStatus/syncing.png' +import { useEffect, useState } from 'react'; +import syncedImg from '../assets/syncStatus/synced.png'; +import syncedMintingImg from '../assets/syncStatus/synced_minting.png'; +import syncingImg from '../assets/syncStatus/syncing.png'; import { getBaseApiReact } from '../App'; -import './CoreSyncStatus.css' -export const CoreSyncStatus = ({imageSize, position}) => { +import '../styles/CoreSyncStatus.css'; +import { useTheme } from '@mui/material'; +import { useTranslation } from 'react-i18next'; + +export const CoreSyncStatus = () => { const [nodeInfos, setNodeInfos] = useState({}); const [coreInfos, setCoreInfos] = useState({}); const [isUsingGateway, setIsUsingGateway] = useState(false); + const { t } = useTranslation(['auth', 'core']); + const theme = useTheme(); + useEffect(() => { const getNodeInfos = async () => { - - try { - setIsUsingGateway(!!getBaseApiReact()?.includes('ext-node.qortal.link')) - const url = `${getBaseApiReact()}/admin/status`; - const response = await fetch(url, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); - const data = await response.json(); + setIsUsingGateway( + !!getBaseApiReact()?.includes('ext-node.qortal.link') + ); + const url = `${getBaseApiReact()}/admin/status`; + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + const data = await response.json(); setNodeInfos(data); } catch (error) { console.error('Request failed', error); @@ -30,14 +36,12 @@ export const CoreSyncStatus = ({imageSize, position}) => { }; const getCoreInfos = async () => { - - try { const url = `${getBaseApiReact()}/admin/info`; const response = await fetch(url, { - method: "GET", + method: 'GET', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, }); const data = await response.json(); @@ -59,55 +63,87 @@ export const CoreSyncStatus = ({imageSize, position}) => { }, []); const renderSyncStatusIcon = () => { - const { isSynchronizing = false, syncPercent = 0, isMintingPossible = false, height = 0, numberOfConnections = 0 } = nodeInfos; - const buildVersion = coreInfos?.buildVersion ? coreInfos?.buildVersion.substring(0, 12) : ''; + const { + isSynchronizing = false, + syncPercent = 0, + isMintingPossible = false, + height = 0, + numberOfConnections = 0, + } = nodeInfos; + const buildVersion = coreInfos?.buildVersion + ? coreInfos?.buildVersion.substring(0, 12) + : ''; let imagePath = syncingImg; - let message = `Synchronizing` + let message = t('core:status.synchronizing', { postProcess: 'capitalize' }); + if (isMintingPossible && !isUsingGateway) { imagePath = syncedMintingImg; - message = `${isSynchronizing ? 'Synchronizing' : 'Synchronized'} ${'(Minting)'}` + message = `${t(`core:message.status.${isSynchronizing ? 'synchronizing' : 'synchronized'}`, { postProcess: 'capitalize' })} ${t('core:message.status.minting')}`; } else if (isSynchronizing === true && syncPercent === 99) { - imagePath = syncingImg + imagePath = syncingImg; } else if (isSynchronizing && !isMintingPossible && syncPercent === 100) { imagePath = syncingImg; - message = `Synchronizing ${isUsingGateway ? '' :'(Not Minting)'}` + message = `${t('core:message.status.synchronizing', { postProcess: 'capitalize' })} ${!isUsingGateway ? t('core:message.status.not_minting') : ''}`; } else if (!isSynchronizing && !isMintingPossible && syncPercent === 100) { - imagePath = syncedImg - message = `Synchronized ${isUsingGateway ? '' :'(Not Minting)'}` + imagePath = syncedImg; + message = `${t('core:message.status.synchronized', { postProcess: 'capitalize' })} ${!isUsingGateway ? t('core:message.status.not_minting') : ''}`; } else if (isSynchronizing && isMintingPossible && syncPercent === 100) { imagePath = syncingImg; - message = `Synchronizing ${isUsingGateway ? '' :'(Minting)'}` + message = `${t('core:message.status.synchronizing', { postProcess: 'capitalize' })} ${!isUsingGateway ? t('core:message.status.minting') : ''}`; } else if (!isSynchronizing && isMintingPossible && syncPercent === 100) { imagePath = syncedMintingImg; - message = `Synchronized ${isUsingGateway ? '' :'(Minting)'}` + message = `${t('core:message.status.synchronized', { postProcess: 'capitalize' })} ${!isUsingGateway ? t('core:message.status.minting') : ''}`; } - - return ( -
- sync status -
-

Core Information

-

Core Version: {buildVersion}

+
+ + sync status + + +
+

{t('core:core.information', { postProcess: 'capitalize' })}

+

+ {t('core:core.version', { postProcess: 'capitalize' })}:{' '} + {buildVersion} +

{message}

-

Block Height: {height || ''}

-

Connected Peers: {numberOfConnections || ''}

-

Using public node: {isUsingGateway?.toString()}

- +

+ {t('core:core.block_height', { postProcess: 'capitalize' })}:{' '} + {height || ''} +

+

+ {t('core:core.peers', { postProcess: 'capitalize' })}:{' '} + + {numberOfConnections || ''} + +

+

+ {t('auth:node.using_public', { postProcess: 'capitalize' })}:{' '} + + {isUsingGateway?.toString()} + +

); }; - return ( -
- {renderSyncStatusIcon()} -
- ); + return
{renderSyncStatusIcon()}
; }; - diff --git a/src/components/Desktop/DesktopFooter.tsx b/src/components/Desktop/DesktopFooter.tsx index 6da8f4e..d85c8a0 100644 --- a/src/components/Desktop/DesktopFooter.tsx +++ b/src/components/Desktop/DesktopFooter.tsx @@ -1,47 +1,49 @@ -import * as React from "react"; -import { - BottomNavigation, - BottomNavigationAction, - ButtonBase, - Typography, -} from "@mui/material"; -import { Home, Groups, Message, ShowChart } from "@mui/icons-material"; -import Box from "@mui/material/Box"; -import BottomLogo from "../../assets/svgs/BottomLogo5.svg"; -import { CustomSvg } from "../../common/CustomSvg"; -import { WalletIcon } from "../../assets/Icons/WalletIcon"; -import { HubsIcon } from "../../assets/Icons/HubsIcon"; -import { TradingIcon } from "../../assets/Icons/TradingIcon"; -import { MessagingIcon } from "../../assets/Icons/MessagingIcon"; -import AppIcon from "../../assets/svgs/AppIcon.svg"; +import { ButtonBase, Typography, useTheme } from '@mui/material'; +import Box from '@mui/material/Box'; +import { HubsIcon } from '../../assets/Icons/HubsIcon'; +import { MessagingIcon } from '../../assets/Icons/MessagingIcon'; +import AppIcon from '../../assets/svgs/AppIcon.svg'; -import { HomeIcon } from "../../assets/Icons/HomeIcon"; -import { Save } from "../Save/Save"; -import { useRecoilState } from "recoil"; -import { enabledDevModeAtom } from "../../atoms/global"; +import { HomeIcon } from '../../assets/Icons/HomeIcon'; +import { Save } from '../Save/Save'; + +import { enabledDevModeAtom } from '../../atoms/global'; +import { useAtom } from 'jotai'; + +export const IconWrapper = ({ + children, + label, + color, + selected, + disableWidth, + customWidth, +}) => { + const theme = useTheme(); -export const IconWrapper = ({ children, label, color, selected, disableWidth, customWidth }) => { return ( {children} {label} @@ -51,24 +53,7 @@ export const IconWrapper = ({ children, label, color, selected, disableWidth, cu }; export const DesktopFooter = ({ - selectedGroup, - groupSection, - isUnread, - goToAnnouncements, - isUnreadChat, - goToChat, - goToThreads, - setOpenManageMembers, - groupChatHasUnread, - groupsAnnHasUnread, - directChatHasUnread, - chatMode, - openDrawerGroups, goToHome, - setIsOpenDrawerProfile, - mobileViewMode, - setMobileViewMode, - setMobileViewModeKeepOpen, hasUnreadGroups, hasUnreadDirects, isHome, @@ -77,32 +62,32 @@ export const DesktopFooter = ({ setDesktopSideView, isApps, setDesktopViewMode, - desktopViewMode, hide, setIsOpenSideViewDirects, - setIsOpenSideViewGroups - + setIsOpenSideViewGroups, }) => { - const [isEnabledDevMode, setIsEnabledDevMode] = useRecoilState(enabledDevModeAtom) + const [isEnabledDevMode, setIsEnabledDevMode] = useAtom(enabledDevModeAtom); - if(hide) return + const theme = useTheme(); + + if (hide) return; return ( - - + + + { - setDesktopViewMode('apps') - setIsOpenSideViewDirects(false) - setIsOpenSideViewGroups(false) + setDesktopViewMode('apps'); + setIsOpenSideViewDirects(false); + setIsOpenSideViewGroups(false); }} > - - + + + { - setDesktopSideView("groups"); + setDesktopSideView('groups'); }} > - + + { - setDesktopSideView("directs"); + setDesktopSideView('directs'); }} > - + - + {isEnabledDevMode && ( { - setDesktopViewMode('dev') - setIsOpenSideViewDirects(false) - setIsOpenSideViewGroups(false) - }} - > - { + setDesktopViewMode('dev'); + setIsOpenSideViewDirects(false); + setIsOpenSideViewGroups(false); + }} > - - - + + + + )} - ); diff --git a/src/components/Desktop/DesktopHeader.tsx b/src/components/Desktop/DesktopHeader.tsx index 4820054..0b6cff8 100644 --- a/src/components/Desktop/DesktopHeader.tsx +++ b/src/components/Desktop/DesktopHeader.tsx @@ -1,47 +1,43 @@ -import * as React from "react"; -import { - BottomNavigation, - BottomNavigationAction, - ButtonBase, - Typography, -} from "@mui/material"; -import { Home, Groups, Message, ShowChart } from "@mui/icons-material"; -import Box from "@mui/material/Box"; -import BottomLogo from "../../assets/svgs/BottomLogo5.svg"; -import { CustomSvg } from "../../common/CustomSvg"; -import { WalletIcon } from "../../assets/Icons/WalletIcon"; -import { HubsIcon } from "../../assets/Icons/HubsIcon"; -import { TradingIcon } from "../../assets/Icons/TradingIcon"; -import { MessagingIcon } from "../../assets/Icons/MessagingIcon"; -import { HomeIcon } from "../../assets/Icons/HomeIcon"; -import { NotificationIcon2 } from "../../assets/Icons/NotificationIcon2"; -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 * as React from 'react'; +import { ButtonBase, Typography, useTheme } from '@mui/material'; +import Box from '@mui/material/Box'; +import { NotificationIcon2 } from '../../assets/Icons/NotificationIcon2'; +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 }) => { +const IconWrapper = ({ + children, + label, + color, + selected, + selectColor, + customHeight, +}) => { return ( {children} { const [value, setValue] = React.useState(0); + const theme = useTheme(); return ( - + {isPrivate && ( - + )} {isPrivate === false && ( - + )} - {selectedGroup?.groupId === '0' ? 'General' :selectedGroup?.groupName} + {selectedGroup?.groupId === '0' + ? 'General' + : selectedGroup?.groupName} - { - goToAnnouncements() + goToAnnouncements(); }} > @@ -158,14 +166,16 @@ export const DesktopHeader = ({ { - goToChat() + goToChat(); }} > @@ -184,15 +194,18 @@ export const DesktopHeader = ({ { - setGroupSection("forum"); - + setGroupSection('forum'); }} > { - setOpenManageMembers(true) - + setOpenManageMembers(true); }} > { - setGroupSection("adminSpace"); - + setGroupSection('adminSpace'); }} > diff --git a/src/components/DesktopSideBar.tsx b/src/components/DesktopSideBar.tsx index 76f2f2e..e2774a7 100644 --- a/src/components/DesktopSideBar.tsx +++ b/src/components/DesktopSideBar.tsx @@ -1,120 +1,150 @@ -import { Box, ButtonBase } from '@mui/material'; -import React from 'react' -import { HomeIcon } from "../assets/Icons/HomeIcon"; -import { MessagingIcon } from "../assets/Icons/MessagingIcon"; -import { Save } from "./Save/Save"; -import { HubsIcon } from "../assets/Icons/HubsIcon"; -import { CoreSyncStatus } from "./CoreSyncStatus"; +import { Box, ButtonBase, useTheme } from '@mui/material'; +import { HomeIcon } from '../assets/Icons/HomeIcon'; +import { Save } from './Save/Save'; import { IconWrapper } from './Desktop/DesktopFooter'; -import AppIcon from "./../assets/svgs/AppIcon.svg"; -import { useRecoilState } from 'recoil'; import { enabledDevModeAtom } from '../atoms/global'; import { AppsIcon } from '../assets/Icons/AppsIcon'; +import ThemeSelector from './Theme/ThemeSelector'; +import { CoreSyncStatus } from './CoreSyncStatus'; +import LanguageSelector from './Language/LanguageSelector'; +import { MessagingIconFilled } from '../assets/Icons/MessagingIconFilled'; +import { useAtom } from 'jotai'; -export const DesktopSideBar = ({goToHome, setDesktopSideView, toggleSideViewDirects, hasUnreadDirects, isDirects, toggleSideViewGroups,hasUnreadGroups, isGroups, isApps, setDesktopViewMode, desktopViewMode, myName }) => { - const [isEnabledDevMode, setIsEnabledDevMode] = useRecoilState(enabledDevModeAtom) +export const DesktopSideBar = ({ + goToHome, + setDesktopSideView, + toggleSideViewDirects, + hasUnreadDirects, + isDirects, + toggleSideViewGroups, + hasUnreadGroups, + isGroups, + isApps, + setDesktopViewMode, + desktopViewMode, + myName, +}) => { + const [isEnabledDevMode, setIsEnabledDevMode] = useAtom(enabledDevModeAtom); + + const theme = useTheme(); return ( - - { - goToHome(); + flexDirection: 'column', + gap: '25px', + height: '100vh', + width: '60px', + backgroundColor: theme.palette.background.surface, + borderRight: `1px solid ${theme.palette.border.subtle}`, + }} + > + + + - }} - > - - { + goToHome(); + }} + > + + - /> - - - { - setDesktopViewMode('apps') - // setIsOpenSideViewDirects(false) - // setIsOpenSideViewGroups(false) - }} - > - - - - - { - setDesktopViewMode('chat') - }} - > + { + setDesktopViewMode('apps'); + }} + > - - - - {/* { - setDesktopSideView("groups"); - toggleSideViewGroups() - }} + color={ + isApps ? theme.palette.text.primary : theme.palette.text.secondary + } + label="Apps" + selected={isApps} + disableWidth > - - - */} - - {/* */} - {isEnabledDevMode && ( - + + + + { + setDesktopViewMode('chat'); + }} + > + + + + + + + + {isEnabledDevMode && ( + { - setDesktopViewMode('dev') + setDesktopViewMode('dev'); }} > - + - )} - - ) -} + )} + + + + + ); +}; diff --git a/src/components/Drawer/Drawer.tsx b/src/components/Drawer/Drawer.tsx index 0296088..65f1250 100644 --- a/src/components/Drawer/Drawer.tsx +++ b/src/components/Drawer/Drawer.tsx @@ -1,32 +1,17 @@ -import * as React from 'react'; import Box from '@mui/material/Box'; import Drawer from '@mui/material/Drawer'; -import Button from '@mui/material/Button'; -import List from '@mui/material/List'; -import Divider from '@mui/material/Divider'; -import ListItem from '@mui/material/ListItem'; -import ListItemButton from '@mui/material/ListItemButton'; -import ListItemIcon from '@mui/material/ListItemIcon'; -import ListItemText from '@mui/material/ListItemText'; -import InboxIcon from '@mui/icons-material/MoveToInbox'; -import MailIcon from '@mui/icons-material/Mail'; -import CloseIcon from '@mui/icons-material/Close'; -import { isMobile } from '../../App'; -export const DrawerComponent = ({open, setOpen, children}) => { - +export const DrawerComponent = ({ open, setOpen, children }) => { const toggleDrawer = (newOpen: boolean) => () => { setOpen(newOpen); }; - return (
- - - {children} - + + {children} +
); -} +}; diff --git a/src/components/Drawer/DrawerUserLookup.tsx b/src/components/Drawer/DrawerUserLookup.tsx index f64e096..9f8427a 100644 --- a/src/components/Drawer/DrawerUserLookup.tsx +++ b/src/components/Drawer/DrawerUserLookup.tsx @@ -1,22 +1,26 @@ -import * as React from 'react'; import Box from '@mui/material/Box'; import Drawer from '@mui/material/Drawer'; -export const DrawerUserLookup = ({open, setOpen, children}) => { - +export const DrawerUserLookup = ({ open, setOpen, children }) => { const toggleDrawer = (newOpen: boolean) => () => { setOpen(newOpen); }; - return (
- - - - {children} - + + + {children} +
); -} +}; diff --git a/src/components/Embeds/AttachmentEmbed.tsx b/src/components/Embeds/AttachmentEmbed.tsx index 3480c31..8086253 100644 --- a/src/components/Embeds/AttachmentEmbed.tsx +++ b/src/components/Embeds/AttachmentEmbed.tsx @@ -1,5 +1,5 @@ -import React, { useContext, useEffect, useMemo, useRef, useState } from "react"; -import { MyContext, getBaseApiReact } from "../../App"; +import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { MyContext, getBaseApiReact } from '../../App'; import { Card, CardContent, @@ -15,285 +15,294 @@ import { Dialog, IconButton, CircularProgress, -} from "@mui/material"; -import { base64ToBlobUrl } from "../../utils/fileReading"; -import { saveFileToDiskGeneric } from "../../utils/generateWallet/generateWallet"; + useTheme, +} from '@mui/material'; +import { base64ToBlobUrl } from '../../utils/fileReading'; +import { saveFileToDiskGeneric } from '../../utils/generateWallet/generateWallet'; import AttachmentIcon from '@mui/icons-material/Attachment'; -import RefreshIcon from "@mui/icons-material/Refresh"; -import OpenInNewIcon from "@mui/icons-material/OpenInNew"; -import { CustomLoader } from "../../common/CustomLoader"; -import { Spacer } from "../../common/Spacer"; -import { FileAttachmentContainer, FileAttachmentFont } from "./Embed-styles"; -import DownloadIcon from "@mui/icons-material/Download"; +import RefreshIcon from '@mui/icons-material/Refresh'; +import OpenInNewIcon from '@mui/icons-material/OpenInNew'; +import { CustomLoader } from '../../common/CustomLoader'; +import { Spacer } from '../../common/Spacer'; +import { FileAttachmentContainer, FileAttachmentFont } from './Embed-styles'; +import DownloadIcon from '@mui/icons-material/Download'; import SaveIcon from '@mui/icons-material/Save'; -import { useSetRecoilState } from "recoil"; -import { blobControllerAtom } from "../../atoms/global"; -import { decodeIfEncoded } from "../../utils/decode"; +import { decodeIfEncoded } from '../../utils/decode'; export const AttachmentCard = ({ - resourceData, - resourceDetails, - owner, - refresh, - openExternal, - external, - isLoadingParent, - errorMsg, - encryptionType, - selectedGroupId - }) => { + resourceData, + resourceDetails, + owner, + refresh, + openExternal, + external, + isLoadingParent, + errorMsg, + encryptionType, + selectedGroupId, +}) => { + const [isOpen, setIsOpen] = useState(true); + const { downloadResource } = useContext(MyContext); + const theme = useTheme(); - const [isOpen, setIsOpen] = useState(true); - const { downloadResource } = useContext(MyContext); - - const saveToDisk = async ()=> { - const { name, service, identifier } = resourceData; - - const url = `${getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}`; - fetch(url) - .then(response => response.blob()) - .then(async blob => { - await saveFileToDiskGeneric(blob, resourceData?.fileName) - }) - .catch(error => { - console.error("Error fetching the video:", error); - }); - } - - const saveToDiskEncrypted = async ()=> { - let blobUrl + const saveToDisk = async () => { + const { name, service, identifier } = resourceData; + + const url = `${getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}`; + fetch(url) + .then((response) => response.blob()) + .then(async (blob) => { + await saveFileToDiskGeneric(blob, resourceData?.fileName); + }) + .catch((error) => { + console.error('Error fetching the video:', error); + }); + }; + + const saveToDiskEncrypted = async () => { + let blobUrl; + try { + const { name, service, identifier, key } = resourceData; + + const url = `${getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}?encoding=base64`; + const res = await fetch(url); + const data = await res.text(); + let decryptedData; try { - const { name, service, identifier,key } = resourceData; - - const url = `${getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}?encoding=base64`; - const res = await fetch(url) - const data = await res.text(); - let decryptedData - try { - if(key && encryptionType === 'private'){ - decryptedData = await window.sendMessage( - "DECRYPT_DATA_WITH_SHARING_KEY", - - { - encryptedData: data, - key: decodeURIComponent(key), - } - - ); - } - if(encryptionType === 'group'){ - decryptedData = await window.sendMessage( - "DECRYPT_QORTAL_GROUP_DATA", - - { - data64: data, - groupId: selectedGroupId, - } - - ); - } - } catch (error) { - throw new Error('Unable to decrypt') + if (key && encryptionType === 'private') { + decryptedData = await window.sendMessage( + 'DECRYPT_DATA_WITH_SHARING_KEY', + + { + encryptedData: data, + key: decodeURIComponent(key), + } + ); + } + if (encryptionType === 'group') { + decryptedData = await window.sendMessage( + 'DECRYPT_QORTAL_GROUP_DATA', + + { + data64: data, + groupId: selectedGroupId, + } + ); } - - if (!decryptedData || decryptedData?.error) throw new Error("Could not decrypt data"); - blobUrl = base64ToBlobUrl(decryptedData, resourceData?.mimeType) - const response = await fetch(blobUrl); - const blob = await response.blob(); - await saveFileToDiskGeneric(blob, resourceData?.fileName) - } catch (error) { - console.error(error) - } finally { - if(blobUrl){ - URL.revokeObjectURL(blobUrl); - } - + throw new Error('Unable to decrypt'); + } + + if (!decryptedData || decryptedData?.error) + throw new Error('Could not decrypt data'); + blobUrl = base64ToBlobUrl(decryptedData, resourceData?.mimeType); + const response = await fetch(blobUrl); + const blob = await response.blob(); + await saveFileToDiskGeneric(blob, resourceData?.fileName); + } catch (error) { + console.error(error); + } finally { + if (blobUrl) { + URL.revokeObjectURL(blobUrl); } } - return ( - + - - + ATTACHMENT embed + + + + - ATTACHMENT embed - - + + {external && ( - - {external && ( - - - - )} - - - - - Created by {decodeIfEncoded(owner)} - - - {encryptionType === 'private' ? "ENCRYPTED" : encryptionType === 'group' ? 'GROUP ENCRYPTED' : "Not encrypted"} - - - - - - - {isLoadingParent && isOpen && ( - - {" "} - {" "} - )} - {errorMsg && ( - + + + + Created by {decodeIfEncoded(owner)} + + + {encryptionType === 'private' + ? 'ENCRYPTED' + : encryptionType === 'group' + ? 'GROUP ENCRYPTED' + : 'Not encrypted'} + + + + + {isLoadingParent && isOpen && ( + + {' '} + {' '} + + )} + {errorMsg && ( + + {' '} + - {" "} + {errorMsg} + {' '} + + )} + + + + + {resourceData?.fileName && ( + <> - {errorMsg} - {" "} - + {resourceData?.fileName} +
+ + )} -
- - - - {resourceData?.fileName && ( + { + if (resourceDetails?.status?.status === 'READY') { + if (encryptionType) { + saveToDiskEncrypted(); + return; + } + saveToDisk(); + return; + } + downloadResource(resourceData); + }} + > + + + {resourceDetails?.status?.status === 'DOWNLOADED' + ? 'BUILDING' + : resourceDetails?.status?.status} + + {!resourceDetails && ( <> - {resourceData?.fileName} - + + Download File )} - { - if(resourceDetails?.status?.status === 'READY'){ - if(encryptionType){ - saveToDiskEncrypted() - return - } - saveToDisk() - return - } - downloadResource(resourceData) - }}> - - - {resourceDetails?.status?.status === 'DOWNLOADED' ? 'BUILDING' : resourceDetails?.status?.status} - {!resourceDetails && ( - <> - - Download File - - - )} - {resourceDetails && resourceDetails?.status?.status !== 'READY' && resourceDetails?.status?.status !== 'FAILED_TO_DOWNLOAD' && ( - <> - - Downloading: {resourceDetails?.status?.percentLoaded || '0'}% - - - )} - {resourceDetails && resourceDetails?.status?.status === 'READY' && ( - <> - - Save to Disk - - - )} - - - - - - - - - ); - }; \ No newline at end of file + {resourceDetails && + resourceDetails?.status?.status !== 'READY' && + resourceDetails?.status?.status !== 'FAILED_TO_DOWNLOAD' && ( + <> + + + Downloading:{' '} + {resourceDetails?.status?.percentLoaded || '0'}% + + + )} + {resourceDetails && + resourceDetails?.status?.status === 'READY' && ( + <> + + Save to Disk + + )} + + + +
+ + ); +}; diff --git a/src/components/Embeds/Embed.tsx b/src/components/Embeds/Embed.tsx index 65b2cef..995284a 100644 --- a/src/components/Embeds/Embed.tsx +++ b/src/components/Embeds/Embed.tsx @@ -1,42 +1,46 @@ -import React, { useEffect, useMemo, useRef, useState } from "react"; -import { getBaseApiReact } from "../../App"; +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { getBaseApiReact } from '../../App'; +import { CustomizedSnackbars } from '../Snackbar/Snackbar'; -import { CustomizedSnackbars } from "../Snackbar/Snackbar"; +import { extractComponents } from '../Chat/MessageDisplay'; +import { executeEvent } from '../../utils/events'; -import { extractComponents } from "../Chat/MessageDisplay"; -import { executeEvent } from "../../utils/events"; - -import { base64ToBlobUrl } from "../../utils/fileReading"; -import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil"; -import { blobControllerAtom, blobKeySelector, resourceKeySelector, selectedGroupIdAtom } from "../../atoms/global"; -import { parseQortalLink } from "./embed-utils"; -import { PollCard } from "./PollEmbed"; -import { ImageCard } from "./ImageEmbed"; -import { AttachmentCard } from "./AttachmentEmbed"; -import { decodeIfEncoded } from "../../utils/decode"; +import { base64ToBlobUrl } from '../../utils/fileReading'; +import { + blobControllerAtom, + blobKeySelector, + resourceKeySelector, + selectedGroupIdAtom, +} from '../../atoms/global'; +import { parseQortalLink } from './embed-utils'; +import { PollCard } from './PollEmbed'; +import { ImageCard } from './ImageEmbed'; +import { AttachmentCard } from './AttachmentEmbed'; +import { decodeIfEncoded } from '../../utils/decode'; +import { useAtom, useAtomValue, useSetAtom } from 'jotai'; const getPoll = async (name) => { const pollName = name; const url = `${getBaseApiReact()}/polls/${pollName}`; const response = await fetch(url, { - method: "GET", + method: 'GET', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, }); const responseData = await response.json(); - if (responseData?.message?.includes("POLL_NO_EXISTS")) { - throw new Error("POLL_NO_EXISTS"); + if (responseData?.message?.includes('POLL_NO_EXISTS')) { + throw new Error('POLL_NO_EXISTS'); } else if (responseData?.pollName) { const urlVotes = `${getBaseApiReact()}/polls/votes/${pollName}`; const responseVotes = await fetch(urlVotes, { - method: "GET", + method: 'GET', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, }); @@ -49,56 +53,66 @@ const getPoll = async (name) => { }; export const Embed = ({ embedLink }) => { - const [errorMsg, setErrorMsg] = useState(""); + const [errorMsg, setErrorMsg] = useState(''); const [isLoading, setIsLoading] = useState(false); const [poll, setPoll] = useState(null); - const [type, setType] = useState(""); + const [type, setType] = useState(''); const hasFetched = useRef(false); const [openSnack, setOpenSnack] = useState(false); const [infoSnack, setInfoSnack] = useState(null); const [external, setExternal] = useState(null); - const [imageUrl, setImageUrl] = useState(""); + const [imageUrl, setImageUrl] = useState(''); const [parsedData, setParsedData] = useState(null); - const setBlobs = useSetRecoilState(blobControllerAtom); - const [selectedGroupId] = useRecoilState(selectedGroupIdAtom) - const resourceData = useMemo(()=> { + const setBlobs = useSetAtom(blobControllerAtom); + const [selectedGroupId] = useAtom(selectedGroupIdAtom); + + const resourceData = useMemo(() => { const parsedDataOnTheFly = parseQortalLink(embedLink); - if(parsedDataOnTheFly?.service && parsedDataOnTheFly?.name && parsedDataOnTheFly?.identifier){ + if ( + parsedDataOnTheFly?.service && + parsedDataOnTheFly?.name && + parsedDataOnTheFly?.identifier + ) { return { - service : parsedDataOnTheFly?.service, + service: parsedDataOnTheFly?.service, name: parsedDataOnTheFly?.name, identifier: parsedDataOnTheFly?.identifier, - fileName: parsedDataOnTheFly?.fileName ? decodeURIComponent(parsedDataOnTheFly?.fileName) : null, - mimeType: parsedDataOnTheFly?.mimeType ? decodeURIComponent(parsedDataOnTheFly?.mimeType) : null, - key: parsedDataOnTheFly?.key ? decodeURIComponent(parsedDataOnTheFly?.key) : null, - } + fileName: parsedDataOnTheFly?.fileName + ? decodeURIComponent(parsedDataOnTheFly?.fileName) + : null, + mimeType: parsedDataOnTheFly?.mimeType + ? decodeURIComponent(parsedDataOnTheFly?.mimeType) + : null, + key: parsedDataOnTheFly?.key + ? decodeURIComponent(parsedDataOnTheFly?.key) + : null, + }; } else { - return null + return null; } - }, [embedLink]) + }, [embedLink]); - const keyIdentifier = useMemo(()=> { - - if(resourceData){ - return `${resourceData.service}-${resourceData.name}-${resourceData.identifier}` + const keyIdentifier = useMemo(() => { + if (resourceData) { + return `${resourceData.service}-${resourceData.name}-${resourceData.identifier}`; } else { - return undefined + return undefined; } - }, [resourceData]) - const blobUrl = useRecoilValue(blobKeySelector(keyIdentifier)); + }, [resourceData]); + + const blobUrl = useAtomValue(blobKeySelector(keyIdentifier)); const handlePoll = async (parsedData) => { try { setIsLoading(true); - setErrorMsg(""); - setType("POLL"); + setErrorMsg(''); + setType('POLL'); if (!parsedData?.name) - throw new Error("Invalid poll embed link. Missing name."); + throw new Error('Invalid poll embed link. Missing name.'); const pollRes = await getPoll(parsedData.name); setPoll(pollRes); - } catch (error) { - setErrorMsg(error?.message || "Invalid embed link"); + setErrorMsg(error?.message || 'Invalid embed link'); } finally { setIsLoading(false); } @@ -106,8 +120,8 @@ export const Embed = ({ embedLink }) => { const getImage = async ({ identifier, name, service }, key, parsedData) => { try { - if(blobUrl?.blobUrl){ - return blobUrl?.blobUrl + if (blobUrl?.blobUrl) { + return blobUrl?.blobUrl; } let numberOfTries = 0; let imageFinalUrl = null; @@ -116,76 +130,76 @@ export const Embed = ({ embedLink }) => { const urlStatus = `${getBaseApiReact()}/arbitrary/resource/status/${service}/${name}/${identifier}?build=true`; const responseStatus = await fetch(urlStatus, { - method: "GET", + method: 'GET', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, }); const responseData = await responseStatus.json(); - if (responseData?.status === "READY") { + if (responseData?.status === 'READY') { if (parsedData?.encryptionType) { const urlData = `${getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}?encoding=base64`; const responseData = await fetch(urlData, { - method: "GET", + method: 'GET', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, }); const data = await responseData.text(); if (data) { - let decryptedData + let decryptedData; try { - if(key && encryptionType === 'private'){ + if (key && encryptionType === 'private') { decryptedData = await window.sendMessage( - "DECRYPT_DATA_WITH_SHARING_KEY", - - { - encryptedData: data, + 'DECRYPT_DATA_WITH_SHARING_KEY', + + { + encryptedData: data, key: decodeURIComponent(key), - } - + } ); } - if(encryptionType === 'group'){ - + if (encryptionType === 'group') { decryptedData = await window.sendMessage( - "DECRYPT_QORTAL_GROUP_DATA", - - { - data64: data, - groupId: selectedGroupId, - } - - ); + 'DECRYPT_QORTAL_GROUP_DATA', - } + { + data64: data, + groupId: selectedGroupId, + } + ); + } } catch (error) { - throw new Error('Unable to decrypt') + throw new Error('Unable to decrypt'); } - - if (!decryptedData || decryptedData?.error) throw new Error("Could not decrypt data"); - imageFinalUrl = base64ToBlobUrl(decryptedData, parsedData?.mimeType ? decodeURIComponent(parsedData?.mimeType) : undefined) - setBlobs((prev=> { + + if (!decryptedData || decryptedData?.error) + throw new Error('Could not decrypt data'); + imageFinalUrl = base64ToBlobUrl( + decryptedData, + parsedData?.mimeType + ? decodeURIComponent(parsedData?.mimeType) + : undefined + ); + setBlobs((prev) => { return { ...prev, [`${service}-${name}-${identifier}`]: { blobUrl: imageFinalUrl, - timestamp: Date.now() - } - } - })) + timestamp: Date.now(), + }, + }; + }); } else { - throw new Error('No data for image') + throw new Error('No data for image'); } - } else { - imageFinalUrl = `${getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}?async=true`; - - // If parsedData is used here, it must be defined somewhere - - } + imageFinalUrl = `${getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}?async=true`; + + // If parsedData is used here, it must be defined somewhere + } } }; @@ -203,18 +217,19 @@ export const Embed = ({ embedLink }) => { } if (imageFinalUrl) { - return imageFinalUrl; } else { setErrorMsg( - "Unable to download IMAGE. Please try again later by clicking the refresh button" + 'Unable to download IMAGE. Please try again later by clicking the refresh button' ); return null; } } catch (error) { - console.error("Error fetching image:", error); + console.error('Error fetching image:', error); setErrorMsg( - error?.error || error?.message || "An unexpected error occurred while trying to download the image" + error?.error || + error?.message || + 'An unexpected error occurred while trying to download the image' ); return null; } @@ -223,25 +238,27 @@ export const Embed = ({ embedLink }) => { const handleImage = async (parsedData) => { try { setIsLoading(true); - setErrorMsg(""); + setErrorMsg(''); if (!parsedData?.name || !parsedData?.service || !parsedData?.identifier) - throw new Error("Invalid image embed link. Missing param."); - let image = await getImage({ - name: parsedData.name, - service: parsedData.service, - identifier: parsedData?.identifier, - }, parsedData?.key, parsedData); - - setImageUrl(image); + throw new Error('Invalid image embed link. Missing param.'); + let image = await getImage( + { + name: parsedData.name, + service: parsedData.service, + identifier: parsedData?.identifier, + }, + parsedData?.key, + parsedData + ); + setImageUrl(image); } catch (error) { - setErrorMsg(error?.message || "Invalid embed link"); + setErrorMsg(error?.message || 'Invalid embed link'); } finally { setIsLoading(false); } }; - const handleLink = () => { try { const parsedData = parseQortalLink(embedLink); @@ -254,28 +271,26 @@ export const Embed = ({ embedLink }) => { setExternal(res); } } - } catch (error) { - - } + } catch (error) {} switch (type) { - case "POLL": + case 'POLL': { handlePoll(parsedData); } break; - case "IMAGE": - setType("IMAGE"); + case 'IMAGE': + setType('IMAGE'); + + break; + case 'ATTACHMENT': + setType('ATTACHMENT'); break; - case "ATTACHMENT": - setType("ATTACHMENT"); - - break; default: break; } } catch (error) { - setErrorMsg(error?.message || "Invalid embed link"); + setErrorMsg(error?.message || 'Invalid embed link'); } }; @@ -284,13 +299,13 @@ export const Embed = ({ embedLink }) => { const parsedData = parseQortalLink(embedLink); handleImage(parsedData); } catch (error) { - setErrorMsg(error?.message || "Invalid embed link"); + setErrorMsg(error?.message || 'Invalid embed link'); } }; const openExternal = () => { - executeEvent("addTab", { data: external }); - executeEvent("open-apps-mode", {}); + executeEvent('addTab', { data: external }); + executeEvent('open-apps-mode', {}); }; useEffect(() => { @@ -299,9 +314,7 @@ export const Embed = ({ embedLink }) => { hasFetched.current = true; }, [embedLink]); - - - const resourceDetails = useRecoilValue(resourceKeySelector(keyIdentifier)); + const resourceDetails = useAtomValue(resourceKeySelector(keyIdentifier)); const { parsedType, encryptionType } = useMemo(() => { let parsedType; @@ -312,15 +325,17 @@ export const Embed = ({ embedLink }) => { parsedType = parsedDataOnTheFly.type; } if (parsedDataOnTheFly?.encryptionType) { - encryptionType = parsedDataOnTheFly?.encryptionType + encryptionType = parsedDataOnTheFly?.encryptionType; } - } catch (error) {} + } catch (error) { + console.log(error); + } return { parsedType, encryptionType }; }, [embedLink]); return (
- {parsedType === "POLL" && ( + {parsedType === 'POLL' && ( { errorMsg={errorMsg} /> )} - {parsedType === "IMAGE" && ( + {parsedType === 'IMAGE' && ( { )} {parsedType === 'ATTACHMENT' && ( {
); }; - - - - - - - - diff --git a/src/components/Embeds/ImageEmbed.tsx b/src/components/Embeds/ImageEmbed.tsx index f1cc859..ec25e25 100644 --- a/src/components/Embeds/ImageEmbed.tsx +++ b/src/components/Embeds/ImageEmbed.tsx @@ -1,265 +1,266 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState } from 'react'; import { Card, CardContent, Typography, - Box, ButtonBase, Divider, Dialog, IconButton, + useTheme, +} from '@mui/material'; -} from "@mui/material"; - -import RefreshIcon from "@mui/icons-material/Refresh"; -import OpenInNewIcon from "@mui/icons-material/OpenInNew"; -import { CustomLoader } from "../../common/CustomLoader"; -import ImageIcon from "@mui/icons-material/Image"; -import CloseIcon from "@mui/icons-material/Close"; -import { decodeIfEncoded } from "../../utils/decode"; +import RefreshIcon from '@mui/icons-material/Refresh'; +import OpenInNewIcon from '@mui/icons-material/OpenInNew'; +import { CustomLoader } from '../../common/CustomLoader'; +import ImageIcon from '@mui/icons-material/Image'; +import CloseIcon from '@mui/icons-material/Close'; +import { decodeIfEncoded } from '../../utils/decode'; export const ImageCard = ({ - image, - fetchImage, - owner, - refresh, - openExternal, - external, - isLoadingParent, - errorMsg, - encryptionType, - }) => { - const [isOpen, setIsOpen] = useState(true); - const [height, setHeight] = useState('400px') - useEffect(() => { - if (isOpen) { - fetchImage(); - } - }, [isOpen]); - - // useEffect(()=> { - // if(errorMsg){ - // setHeight('300px') - // } - // }, [errorMsg]) - - return ( - { + const theme = useTheme(); + const [isOpen, setIsOpen] = useState(true); + const [height, setHeight] = useState('400px'); + useEffect(() => { + if (isOpen) { + fetchImage(); + } + }, [isOpen]); + + // useEffect(()=> { + // if(errorMsg){ + // setHeight('300px') + // } + // }, [errorMsg]) + + return ( + + - - + IMAGE embed + + + + - IMAGE embed - - + + {external && ( - - {external && ( - - - - )} + )} + + + + + Created by {decodeIfEncoded(owner)} + + + {encryptionType === 'private' + ? 'ENCRYPTED' + : encryptionType === 'group' + ? 'GROUP ENCRYPTED' + : 'Not encrypted'} + + + + + {isLoadingParent && isOpen && ( + + {' '} + {' '} - - - - Created by {decodeIfEncoded(owner)} - - - {encryptionType === 'private' ? "ENCRYPTED" : encryptionType === 'group' ? 'GROUP ENCRYPTED' : "Not encrypted"} - - - - - - {isLoadingParent && isOpen && ( - - {" "} - {" "} - - )} - {errorMsg && ( - - {" "} - - {errorMsg} - {" "} - - )} - - - - - - - - - ); - }; + {errorMsg} + {' '} + + )} + - export function ImageViewer({ src, alt = "" }) { - const [isFullscreen, setIsFullscreen] = useState(false); - - const handleOpenFullscreen = () => setIsFullscreen(true); - const handleCloseFullscreen = () => setIsFullscreen(false); - - return ( - <> - {/* Image in container */} + + + + + + + ); +}; + +export function ImageViewer({ src, alt = '' }) { + const [isFullscreen, setIsFullscreen] = useState(false); + + const handleOpenFullscreen = () => setIsFullscreen(true); + const handleCloseFullscreen = () => setIsFullscreen(false); + const theme = useTheme(); + return ( + <> + {/* Image in container */} + + {alt} + + + {/* Fullscreen Viewer */} + + {/* Close Button */} + + + + + {/* Fullscreen Image */} {alt} - - {/* Fullscreen Viewer */} - - - {/* Close Button */} - - - - - {/* Fullscreen Image */} - {alt} - - - - ); - } \ No newline at end of file + + + ); +} diff --git a/src/components/Embeds/PollEmbed.tsx b/src/components/Embeds/PollEmbed.tsx index 3da02c3..8ca2fd4 100644 --- a/src/components/Embeds/PollEmbed.tsx +++ b/src/components/Embeds/PollEmbed.tsx @@ -1,5 +1,5 @@ -import React, { useContext, useEffect, useState } from "react"; -import { MyContext } from "../../App"; +import React, { useContext, useEffect, useState } from 'react'; +import { MyContext } from '../../App'; import { Card, CardContent, @@ -12,384 +12,379 @@ import { Box, ButtonBase, Divider, - -} from "@mui/material"; -import { getNameInfo } from "../Group/Group"; -import PollIcon from "@mui/icons-material/Poll"; -import { getFee } from "../../background"; -import RefreshIcon from "@mui/icons-material/Refresh"; -import { Spacer } from "../../common/Spacer"; -import OpenInNewIcon from "@mui/icons-material/OpenInNew"; -import { CustomLoader } from "../../common/CustomLoader"; - + useTheme, +} from '@mui/material'; +import { getNameInfo } from '../Group/Group'; +import PollIcon from '@mui/icons-material/Poll'; +import { getFee } from '../../background'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import { Spacer } from '../../common/Spacer'; +import OpenInNewIcon from '@mui/icons-material/OpenInNew'; +import { CustomLoader } from '../../common/CustomLoader'; export const PollCard = ({ - poll, - setInfoSnack, - setOpenSnack, - refresh, - openExternal, - external, - isLoadingParent, - errorMsg, - }) => { - const [selectedOption, setSelectedOption] = useState(""); - const [ownerName, setOwnerName] = useState(""); - const [showResults, setShowResults] = useState(false); - const [isOpen, setIsOpen] = useState(false); - const { show, userInfo } = useContext(MyContext); - const [isLoadingSubmit, setIsLoadingSubmit] = useState(false); - const handleVote = async () => { - const fee = await getFee("VOTE_ON_POLL"); - - await show({ - message: `Do you accept this VOTE_ON_POLL transaction? POLLS are public!`, - publishFee: fee.fee + " QORT", - }); - setIsLoadingSubmit(true); - - window - .sendMessage( - "voteOnPoll", - { - pollName: poll?.info?.pollName, - optionIndex: +selectedOption, - }, - 60000 - ) - .then((response) => { - setIsLoadingSubmit(false); - if (response.error) { - setInfoSnack({ - type: "error", - message: response?.error || "Unable to vote.", - }); - setOpenSnack(true); - return; - } else { - setInfoSnack({ - type: "success", - message: - "Successfully voted. Please wait a couple minutes for the network to propogate the changes.", - }); - setOpenSnack(true); - } - }) - .catch((error) => { - setIsLoadingSubmit(false); + poll, + setInfoSnack, + setOpenSnack, + refresh, + openExternal, + external, + isLoadingParent, + errorMsg, +}) => { + const [selectedOption, setSelectedOption] = useState(''); + const [ownerName, setOwnerName] = useState(''); + const [showResults, setShowResults] = useState(false); + const [isOpen, setIsOpen] = useState(false); + const { show, userInfo } = useContext(MyContext); + const [isLoadingSubmit, setIsLoadingSubmit] = useState(false); + const theme = useTheme(); + const handleVote = async () => { + const fee = await getFee('VOTE_ON_POLL'); + + await show({ + message: `Do you accept this VOTE_ON_POLL transaction? POLLS are public!`, + publishFee: fee.fee + ' QORT', + }); + setIsLoadingSubmit(true); + + window + .sendMessage( + 'voteOnPoll', + { + pollName: poll?.info?.pollName, + optionIndex: +selectedOption, + }, + 60000 + ) + .then((response) => { + setIsLoadingSubmit(false); + if (response.error) { setInfoSnack({ - type: "error", - message: error?.message || "Unable to vote.", + type: 'error', + message: response?.error || 'Unable to vote.', + }); + setOpenSnack(true); + return; + } else { + setInfoSnack({ + type: 'success', + message: + 'Successfully voted. Please wait a couple minutes for the network to propogate the changes.', }); setOpenSnack(true); - }); - }; - - const getName = async (owner) => { - try { - const res = await getNameInfo(owner); - if (res) { - setOwnerName(res); } - } catch (error) {} - }; - - useEffect(() => { - if (poll?.info?.owner) { - getName(poll.info.owner); + }) + .catch((error) => { + setIsLoadingSubmit(false); + setInfoSnack({ + type: 'error', + message: error?.message || 'Unable to vote.', + }); + setOpenSnack(true); + }); + }; + + const getName = async (owner) => { + try { + const res = await getNameInfo(owner); + if (res) { + setOwnerName(res); } - }, [poll?.info?.owner]); - - return ( - { + if (poll?.info?.owner) { + getName(poll.info.owner); + } + }, [poll?.info?.owner]); + + return ( + + - - + POLL embed + + + + - POLL embed - - + + {external && ( - - {external && ( - - - - )} - + )} - + + - + + + + {!isOpen && !errorMsg && ( + <> + + + + )} + {isLoadingParent && isOpen && ( + - Created by {ownerName || poll?.info?.owner} - - - - - {!isOpen && !errorMsg && ( - <> - - - - )} - {isLoadingParent && isOpen && ( - - {" "} - {" "} - - )} - {errorMsg && ( - - {" "} - - {errorMsg} - {" "} - - )} - - - - {' '} + + )} + {errorMsg && ( + - + > + {' '} - Options - - setSelectedOption(e.target.value)} - > - {poll?.info?.pollOptions?.map((option, index) => ( - - } - label={option?.optionName} - sx={{ - "& .MuiFormControlLabel-label": { - fontSize: "14px", - - }, - }} - /> - ))} - - - - {' '} + + )} + + + + + + + Options + + setSelectedOption(e.target.value)} + > + {poll?.info?.pollOptions?.map((option, index) => ( + } + label={option?.optionName} sx={{ - fontSize: "14px", - fontStyle: "italic", + '& .MuiFormControlLabel-label': { + fontSize: '14px', + }, + }} + /> + ))} + + + + + {' '} + {`${poll?.votes?.totalVotes} ${ + poll?.votes?.totalVotes === 1 ? ' vote' : ' votes' + }`} + + + + + item?.voterPublicKey === userInfo?.publicKey + ) + ? 'visible' + : 'hidden', + }} + > + You've already voted. + + + {isLoadingSubmit && ( + + Is processing transaction, please wait... + + )} + { + setShowResults((prev) => !prev); + }} + > + {showResults ? 'hide ' : 'show '} results + + + {showResults && } + + + ); +}; + +const PollResults = ({ votes }) => { + const maxVotes = Math.max( + ...votes?.voteCounts?.map((option) => option.voteCount) + ); + const options = votes?.voteCounts; + return ( + + {options + .sort((a, b) => b.voteCount - a.voteCount) // Sort options by votes (highest first) + .map((option, index) => ( + + + - {" "} - {`${poll?.votes?.totalVotes} ${ - poll?.votes?.totalVotes === 1 ? " vote" : " votes" - }`} + {`${index + 1}. ${option.optionName}`} + + + {option.voteCount} votes - - - item?.voterPublicKey === userInfo?.publicKey - ) - ? "visible" - : "hidden", + mt: 1, + height: 10, + backgroundColor: '#e0e0e0', + borderRadius: 5, + overflow: 'hidden', }} > - You've already voted. - - - {isLoadingSubmit && ( - - Is processing transaction, please wait... - - )} - { - setShowResults((prev) => !prev); - }} - > - {showResults ? "hide " : "show "} results - - - {showResults && } - - - ); - }; - - const PollResults = ({ votes }) => { - const maxVotes = Math.max( - ...votes?.voteCounts?.map((option) => option.voteCount) - ); - const options = votes?.voteCounts; - return ( - - {options - .sort((a, b) => b.voteCount - a.voteCount) // Sort options by votes (highest first) - .map((option, index) => ( - - - - {`${index + 1}. ${option.optionName}`} - - - {option.voteCount} votes - - - - + /> - ))} - - ); - }; \ No newline at end of file + + ))} + + ); +}; diff --git a/src/components/Embeds/VideoPlayer.tsx b/src/components/Embeds/VideoPlayer.tsx index c28bc99..30991ff 100644 --- a/src/components/Embeds/VideoPlayer.tsx +++ b/src/components/Embeds/VideoPlayer.tsx @@ -1,23 +1,26 @@ -import React, { useContext, useEffect, useMemo, useRef, useState } from 'react' -import ReactDOM from 'react-dom' -import { Box, IconButton, Slider } from '@mui/material' -import { CircularProgress, Typography } from '@mui/material' -import { Key } from 'ts-key-enum' +import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'; +import ReactDOM from 'react-dom'; +import { Box, IconButton, Slider } from '@mui/material'; +import { CircularProgress, Typography } from '@mui/material'; +import { Key } from 'ts-key-enum'; import { PlayArrow, Pause, VolumeUp, Fullscreen, - PictureInPicture, VolumeOff, Calculate -} from '@mui/icons-material' -import { styled } from '@mui/system' -import { Refresh } from '@mui/icons-material' + PictureInPicture, + VolumeOff, + Calculate, +} from '@mui/icons-material'; +import { styled } from '@mui/system'; +import { Refresh } from '@mui/icons-material'; -import { Menu, MenuItem } from '@mui/material' -import { MoreVert as MoreIcon } from '@mui/icons-material' -import { GlobalContext, getBaseApiReact } from '../../App' -import { resourceKeySelector } from '../../atoms/global' -import { useRecoilValue } from 'recoil' +import { Menu, MenuItem } from '@mui/material'; +import { MoreVert as MoreIcon } from '@mui/icons-material'; +import { MyContext, getBaseApiReact } from '../../App'; +import { resourceKeySelector } from '../../atoms/global'; + +import { useAtomValue } from 'jotai'; const VideoContainer = styled(Box)` position: relative; display: flex; @@ -28,14 +31,14 @@ const VideoContainer = styled(Box)` height: 100%; margin: 0px; padding: 0px; -` +`; const VideoElement = styled('video')` width: 100%; height: auto; max-height: calc(100vh - 150px); background: rgb(33, 33, 33); -` +`; const ControlsContainer = styled(Box)` position: absolute; @@ -47,18 +50,18 @@ const ControlsContainer = styled(Box)` right: 0; padding: 8px; background-color: rgba(0, 0, 0, 0.6); -` +`; interface VideoPlayerProps { - src?: string - poster?: string - name?: string - identifier?: string - service?: string - autoplay?: boolean - from?: string | null - customStyle?: any - user?: string + src?: string; + poster?: string; + name?: string; + identifier?: string; + service?: string; + autoplay?: boolean; + from?: string | null; + customStyle?: any; + user?: string; } export const VideoPlayer: React.FC = ({ @@ -69,33 +72,32 @@ export const VideoPlayer: React.FC = ({ autoplay = true, from = null, customStyle = {}, - node + node, }) => { - - const keyIdentifier = useMemo(()=> { - - if(name && identifier && service){ - return `${service}-${name}-${identifier}` + const keyIdentifier = useMemo(() => { + if (name && identifier && service) { + return `${service}-${name}-${identifier}`; } else { - return undefined + return undefined; } - }, [service, name, identifier]) - const download = useRecoilValue(resourceKeySelector(keyIdentifier)); - const { downloadResource } = useContext(GlobalContext); + }, [service, name, identifier]); - const videoRef = useRef(null) - const [playing, setPlaying] = useState(false) - const [volume, setVolume] = useState(1) - const [mutedVolume, setMutedVolume] = useState(1) - const [isMuted, setIsMuted] = useState(false) - const [progress, setProgress] = useState(0) - const [isLoading, setIsLoading] = useState(false) - const [canPlay, setCanPlay] = useState(false) - const [startPlay, setStartPlay] = useState(false) - const [isMobileView, setIsMobileView] = useState(false) - const [playbackRate, setPlaybackRate] = useState(1) - const [anchorEl, setAnchorEl] = useState(null) - const reDownload = useRef(false) + const download = useAtomValue(resourceKeySelector(keyIdentifier)); + + const { downloadResource } = useContext(MyContext); + + const videoRef = useRef(null); + const [playing, setPlaying] = useState(false); + const [volume, setVolume] = useState(1); + const [mutedVolume, setMutedVolume] = useState(1); + const [isMuted, setIsMuted] = useState(false); + const [progress, setProgress] = useState(0); + const [isLoading, setIsLoading] = useState(false); + const [canPlay, setCanPlay] = useState(false); + const [startPlay, setStartPlay] = useState(false); + const [playbackRate, setPlaybackRate] = useState(1); + const [anchorEl, setAnchorEl] = useState(null); + const reDownload = useRef(false); const resetVideoState = () => { // Reset all states to their initial values @@ -107,10 +109,9 @@ export const VideoPlayer: React.FC = ({ setIsLoading(false); setCanPlay(false); setStartPlay(false); - setIsMobileView(false); setPlaybackRate(1); setAnchorEl(null); - + // Reset refs to their initial values if (videoRef.current) { videoRef.current.pause(); // Ensure the video is paused @@ -120,18 +121,19 @@ export const VideoPlayer: React.FC = ({ }; const src = useMemo(() => { - if(name && identifier && service){ - return `${node || getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}` - } - return '' - }, [service, name, identifier]) + if (name && identifier && service) { + return `${node || getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}`; + } + return ''; + }, [service, name, identifier]); + + useEffect(() => { + resetVideoState(); + }, [keyIdentifier]); - useEffect(()=> { - resetVideoState() - }, [keyIdentifier]) const resourceStatus = useMemo(() => { - return download?.status || {} - }, [download]) + return download?.status || {}; + }, [download]); const minSpeed = 0.25; const maxSpeed = 4.0; @@ -139,306 +141,339 @@ export const VideoPlayer: React.FC = ({ const updatePlaybackRate = (newSpeed: number) => { if (videoRef.current) { - if (newSpeed > maxSpeed || newSpeed < minSpeed) - newSpeed = minSpeed - videoRef.current.playbackRate = newSpeed - setPlaybackRate(newSpeed) + if (newSpeed > maxSpeed || newSpeed < minSpeed) newSpeed = minSpeed; + videoRef.current.playbackRate = newSpeed; + setPlaybackRate(newSpeed); } - } + }; const increaseSpeed = (wrapOverflow = true) => { - const changedSpeed = playbackRate + speedChange - let newSpeed = wrapOverflow ? changedSpeed : Math.min(changedSpeed, maxSpeed) - + const changedSpeed = playbackRate + speedChange; + let newSpeed = wrapOverflow + ? changedSpeed + : Math.min(changedSpeed, maxSpeed); if (videoRef.current) { updatePlaybackRate(newSpeed); } - } + }; const decreaseSpeed = () => { if (videoRef.current) { updatePlaybackRate(playbackRate - speedChange); } - } - + }; const togglePlay = async () => { - if (!videoRef.current) return - setStartPlay(true) + if (!videoRef.current) return; + setStartPlay(true); if (!src || resourceStatus?.status !== 'READY') { ReactDOM.flushSync(() => { - setIsLoading(true) - }) - getSrc() + setIsLoading(true); + }); + getSrc(); } if (playing) { - videoRef.current.pause() + videoRef.current.pause(); } else { - videoRef.current.play() + videoRef.current.play(); } - setPlaying(!playing) - } - + setPlaying(!playing); + }; const onVolumeChange = (_: any, value: number | number[]) => { - if (!videoRef.current) return - videoRef.current.volume = value as number - setVolume(value as number) - setIsMuted(false) - } + if (!videoRef.current) return; + videoRef.current.volume = value as number; + setVolume(value as number); + setIsMuted(false); + }; const onProgressChange = (_: any, value: number | number[]) => { - if (!videoRef.current) return - videoRef.current.currentTime = value as number - setProgress(value as number) + if (!videoRef.current) return; + videoRef.current.currentTime = value as number; + setProgress(value as number); if (!playing) { - videoRef.current.play() - setPlaying(true) + videoRef.current.play(); + setPlaying(true); } - } + }; const handleEnded = () => { - setPlaying(false) - } + setPlaying(false); + }; const updateProgress = () => { - if (!videoRef.current) return - setProgress(videoRef.current.currentTime) - } + if (!videoRef.current) return; + setProgress(videoRef.current.currentTime); + }; - const [isFullscreen, setIsFullscreen] = useState(false) + const [isFullscreen, setIsFullscreen] = useState(false); const enterFullscreen = () => { - if (!videoRef.current) return + if (!videoRef.current) return; if (videoRef.current.requestFullscreen) { - videoRef.current.requestFullscreen() + videoRef.current.requestFullscreen(); } - } + }; const exitFullscreen = () => { if (document.exitFullscreen) { - document.exitFullscreen() + document.exitFullscreen(); } - } + }; const toggleFullscreen = () => { - isFullscreen ? exitFullscreen() : enterFullscreen() - } - + isFullscreen ? exitFullscreen() : enterFullscreen(); + }; useEffect(() => { const handleFullscreenChange = () => { - setIsFullscreen(!!document.fullscreenElement) - } + setIsFullscreen(!!document.fullscreenElement); + }; - document.addEventListener('fullscreenchange', handleFullscreenChange) + document.addEventListener('fullscreenchange', handleFullscreenChange); return () => { - document.removeEventListener('fullscreenchange', handleFullscreenChange) - } - }, []) - - + document.removeEventListener('fullscreenchange', handleFullscreenChange); + }; + }, []); const handleCanPlay = () => { - setIsLoading(false) - setCanPlay(true) - } + setIsLoading(false); + setCanPlay(true); + }; const getSrc = React.useCallback(async () => { - if (!name || !identifier || !service) return + if (!name || !identifier || !service) return; try { downloadResource({ name, service, - identifier - }) - } catch (error) { - console.error(error) + identifier, + }); + } catch (error) { + console.error(error); } - }, [identifier, name, service]) - - - + }, [identifier, name, service]); function formatTime(seconds: number): string { - seconds = Math.floor(seconds) - let minutes: number | string = Math.floor(seconds / 60) - let hours: number | string = Math.floor(minutes / 60) + seconds = Math.floor(seconds); + let minutes: number | string = Math.floor(seconds / 60); + let hours: number | string = Math.floor(minutes / 60); - let remainingSeconds: number | string = seconds % 60 - let remainingMinutes: number | string = minutes % 60 + let remainingSeconds: number | string = seconds % 60; + let remainingMinutes: number | string = minutes % 60; if (remainingSeconds < 10) { - remainingSeconds = '0' + remainingSeconds + remainingSeconds = '0' + remainingSeconds; } if (remainingMinutes < 10) { - remainingMinutes = '0' + remainingMinutes + remainingMinutes = '0' + remainingMinutes; } if (hours === 0) { - hours = '' - } - else { - hours = hours + ':' + hours = ''; + } else { + hours = hours + ':'; } - return hours + remainingMinutes + ':' + remainingSeconds + return hours + remainingMinutes + ':' + remainingSeconds; } const reloadVideo = () => { - if (!videoRef.current) return - const currentTime = videoRef.current.currentTime - videoRef.current.src = src - videoRef.current.load() - videoRef.current.currentTime = currentTime + if (!videoRef.current) return; + const currentTime = videoRef.current.currentTime; + videoRef.current.src = src; + videoRef.current.load(); + videoRef.current.currentTime = currentTime; if (playing) { - videoRef.current.play() + videoRef.current.play(); } - } + }; useEffect(() => { if ( resourceStatus?.status === 'DOWNLOADED' && reDownload?.current === false ) { - getSrc() - reDownload.current = true + getSrc(); + reDownload.current = true; } - }, [getSrc, resourceStatus]) + }, [getSrc, resourceStatus]); const handleMenuOpen = (event: any) => { - setAnchorEl(event.currentTarget) - } + setAnchorEl(event.currentTarget); + }; const handleMenuClose = () => { - setAnchorEl(null) - } + setAnchorEl(null); + }; useEffect(() => { - const videoWidth = videoRef?.current?.offsetWidth - if (videoWidth && videoWidth <= 600) { - setIsMobileView(true) - } - }, [canPlay]) + const videoWidth = videoRef?.current?.offsetWidth; + }, [canPlay]); const getDownloadProgress = (current: number, total: number) => { - const progress = current / total * 100; - return Number.isNaN(progress) ? '' : progress.toFixed(0) + '%' - } + const progress = (current / total) * 100; + return Number.isNaN(progress) ? '' : progress.toFixed(0) + '%'; + }; const mute = () => { - setIsMuted(true) - setMutedVolume(volume) - setVolume(0) - if (videoRef.current) videoRef.current.volume = 0 - } + setIsMuted(true); + setMutedVolume(volume); + setVolume(0); + if (videoRef.current) videoRef.current.volume = 0; + }; const unMute = () => { - setIsMuted(false) - setVolume(mutedVolume) - if (videoRef.current) videoRef.current.volume = mutedVolume - } + setIsMuted(false); + setVolume(mutedVolume); + if (videoRef.current) videoRef.current.volume = mutedVolume; + }; const toggleMute = () => { isMuted ? unMute() : mute(); - } + }; const changeVolume = (volumeChange: number) => { if (videoRef.current) { const minVolume = 0; const maxVolume = 1; + let newVolume = volumeChange + volume; - let newVolume = volumeChange + volume + newVolume = Math.max(newVolume, minVolume); + newVolume = Math.min(newVolume, maxVolume); - newVolume = Math.max(newVolume, minVolume) - newVolume = Math.min(newVolume, maxVolume) - - setIsMuted(false) - setMutedVolume(newVolume) - videoRef.current.volume = newVolume + setIsMuted(false); + setMutedVolume(newVolume); + videoRef.current.volume = newVolume; setVolume(newVolume); } - - } + }; const setProgressRelative = (secondsChange: number) => { if (videoRef.current) { - const currentTime = videoRef.current?.currentTime - const minTime = 0 - const maxTime = videoRef.current?.duration || 100 + const currentTime = videoRef.current?.currentTime; + const minTime = 0; + const maxTime = videoRef.current?.duration || 100; let newTime = currentTime + secondsChange; - newTime = Math.max(newTime, minTime) - newTime = Math.min(newTime, maxTime) + newTime = Math.max(newTime, minTime); + newTime = Math.min(newTime, maxTime); videoRef.current.currentTime = newTime; setProgress(newTime); } - } + }; const setProgressAbsolute = (videoPercent: number) => { if (videoRef.current) { - videoPercent = Math.min(videoPercent, 100) - videoPercent = Math.max(videoPercent, 0) - const finalTime = videoRef.current?.duration * videoPercent / 100 - videoRef.current.currentTime = finalTime + videoPercent = Math.min(videoPercent, 100); + videoPercent = Math.max(videoPercent, 0); + const finalTime = (videoRef.current?.duration * videoPercent) / 100; + videoRef.current.currentTime = finalTime; setProgress(finalTime); } - } - + }; const keyboardShortcutsDown = (e: React.KeyboardEvent) => { - e.preventDefault() + e.preventDefault(); switch (e.key) { - case Key.Add: increaseSpeed(false); break; - case '+': increaseSpeed(false); break; - case '>': increaseSpeed(false); break; + case Key.Add: + increaseSpeed(false); + break; + case '+': + increaseSpeed(false); + break; + case '>': + increaseSpeed(false); + break; - case Key.Subtract: decreaseSpeed(); break; - case '-': decreaseSpeed(); break; - case '<': decreaseSpeed(); break; + case Key.Subtract: + decreaseSpeed(); + break; + case '-': + decreaseSpeed(); + break; + case '<': + decreaseSpeed(); + break; - case Key.ArrowLeft: { - if (e.shiftKey) setProgressRelative(-300); - else if (e.ctrlKey) setProgressRelative(-60); - else if (e.altKey) setProgressRelative(-10); - else setProgressRelative(-5); - } break; + case Key.ArrowLeft: + { + if (e.shiftKey) setProgressRelative(-300); + else if (e.ctrlKey) setProgressRelative(-60); + else if (e.altKey) setProgressRelative(-10); + else setProgressRelative(-5); + } + break; - case Key.ArrowRight: { - if (e.shiftKey) setProgressRelative(300); - else if (e.ctrlKey) setProgressRelative(60); - else if (e.altKey) setProgressRelative(10); - else setProgressRelative(5); - } break; + case Key.ArrowRight: + { + if (e.shiftKey) setProgressRelative(300); + else if (e.ctrlKey) setProgressRelative(60); + else if (e.altKey) setProgressRelative(10); + else setProgressRelative(5); + } + break; - case Key.ArrowDown: changeVolume(-0.05); break; - case Key.ArrowUp: changeVolume(0.05); break; + case Key.ArrowDown: + changeVolume(-0.05); + break; + case Key.ArrowUp: + changeVolume(0.05); + break; } - } + }; const keyboardShortcutsUp = (e: React.KeyboardEvent) => { - e.preventDefault() + e.preventDefault(); switch (e.key) { - case ' ': togglePlay(); break; - case 'm': toggleMute(); break; + case ' ': + togglePlay(); + break; + case 'm': + toggleMute(); + break; - case 'f': enterFullscreen(); break; - case Key.Escape: exitFullscreen(); break; + case 'f': + enterFullscreen(); + break; + case Key.Escape: + exitFullscreen(); + break; - case '0': setProgressAbsolute(0); break; - case '1': setProgressAbsolute(10); break; - case '2': setProgressAbsolute(20); break; - case '3': setProgressAbsolute(30); break; - case '4': setProgressAbsolute(40); break; - case '5': setProgressAbsolute(50); break; - case '6': setProgressAbsolute(60); break; - case '7': setProgressAbsolute(70); break; - case '8': setProgressAbsolute(80); break; - case '9': setProgressAbsolute(90); break; + case '0': + setProgressAbsolute(0); + break; + case '1': + setProgressAbsolute(10); + break; + case '2': + setProgressAbsolute(20); + break; + case '3': + setProgressAbsolute(30); + break; + case '4': + setProgressAbsolute(40); + break; + case '5': + setProgressAbsolute(50); + break; + case '6': + setProgressAbsolute(60); + break; + case '7': + setProgressAbsolute(70); + break; + case '8': + setProgressAbsolute(80); + break; + case '9': + setProgressAbsolute(90); + break; } - } + }; return ( = ({ height: '100%', }} > - {isLoading && ( = ({ sx={{ display: 'flex', flexDirection: 'column', - gap: '10px' + gap: '10px', }} > - - - {resourceStatus?.status === 'REFETCHING' ? ( - <> - <> - {getDownloadProgress(resourceStatus?.localChunkCount, resourceStatus?.totalChunkCount)} - - <> Refetching data in 25 seconds - - ) : resourceStatus?.status === 'DOWNLOADED' ? ( - <>Download Completed: building tutorial video... - ) : resourceStatus?.status !== 'READY' ? ( + + {resourceStatus?.status === 'REFETCHING' ? ( + <> <> - {getDownloadProgress(resourceStatus?.localChunkCount || 0, resourceStatus?.totalChunkCount || 100)} - + {getDownloadProgress( + resourceStatus?.localChunkCount, + resourceStatus?.totalChunkCount + )} - ) : ( - <>Fetching tutorial from the Qortal Network... - )} - - + + <> Refetching data in 25 seconds + + ) : resourceStatus?.status === 'DOWNLOADED' ? ( + <>Download Completed: building tutorial video... + ) : resourceStatus?.status !== 'READY' ? ( + <> + {getDownloadProgress( + resourceStatus?.localChunkCount || 0, + resourceStatus?.totalChunkCount || 100 + )} + + ) : ( + <>Fetching tutorial from the Qortal Network... + )} + )} {((!src && !isLoading) || !startPlay) && ( @@ -516,59 +554,61 @@ export const VideoPlayer: React.FC = ({ zIndex={500} bgcolor="rgba(0, 0, 0, 0.6)" onClick={() => { - togglePlay() + togglePlay(); }} sx={{ - cursor: 'pointer' + cursor: 'pointer', }} > )} - - + > + - {isMobileView && canPlay ? ( + {canPlay ? ( <> @@ -577,77 +617,7 @@ export const VideoPlayer: React.FC = ({ - - - - - - - - - - - - increaseSpeed()}> - - Speed: {playbackRate}x - - - - - - - - ) : canPlay ? ( - <> - - {playing ? : } - - @@ -669,7 +639,7 @@ export const VideoPlayer: React.FC = ({ !videoRef.current?.duration || !progress ? 'hidden' : 'visible', - flexShrink: 0 + flexShrink: 0, }} > {progress && videoRef.current?.duration && formatTime(progress)}/ @@ -680,7 +650,7 @@ export const VideoPlayer: React.FC = ({ @@ -694,14 +664,14 @@ export const VideoPlayer: React.FC = ({ step={0.01} sx={{ maxWidth: '100px', - color: 'var(--Mail-Background)' + color: 'var(--Mail-Background)', }} /> increaseSpeed()} > @@ -709,7 +679,7 @@ export const VideoPlayer: React.FC = ({ @@ -719,5 +689,5 @@ export const VideoPlayer: React.FC = ({ ) : null} - ) -} + ); +}; diff --git a/src/components/Explore/Explore.tsx b/src/components/Explore/Explore.tsx index 61037d7..266fdd5 100644 --- a/src/components/Explore/Explore.tsx +++ b/src/components/Explore/Explore.tsx @@ -1,129 +1,130 @@ -import { Box, ButtonBase, Typography } from "@mui/material"; -import React from "react"; -import ChatIcon from "@mui/icons-material/Chat"; -import qTradeLogo from "../../assets/Icons/q-trade-logo.webp"; -import AppsIcon from "@mui/icons-material/Apps"; -import { executeEvent } from "../../utils/events"; +import { Box, ButtonBase, Typography, useTheme } from '@mui/material'; +import ChatIcon from '@mui/icons-material/Chat'; +import qTradeLogo from '../../assets/Icons/q-trade-logo.webp'; +import AppsIcon from '@mui/icons-material/Apps'; +import { executeEvent } from '../../utils/events'; import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet'; +import { useTranslation } from 'react-i18next'; +export const Explore = ({ setDesktopViewMode }) => { + const theme = useTheme(); + const { t } = useTranslation(['core', 'tutorial']); -export const Explore = ({setDesktopViewMode}) => { return ( { - executeEvent("addTab", { - data: { service: "APP", name: "q-trade" }, - }); - executeEvent("open-apps-mode", {}); - }} + executeEvent('addTab', { + data: { service: 'APP', name: 'q-trade' }, + }); + executeEvent('open-apps-mode', {}); + }} > - Trade QORT + {t('tutorial:initial.trade_qort', { postProcess: 'capitalize' })} + { + setDesktopViewMode('apps'); }} - onClick={()=> { - setDesktopViewMode('apps') - - }} > - See Apps + {t('tutorial:initial.see_apps', { postProcess: 'capitalize' })} + { - executeEvent("openGroupMessage", { - from: "0" , - }); - }} + executeEvent('openGroupMessage', { + from: '0', + }); + }} > - General Chat + {t('tutorial:initial.general_chat', { postProcess: 'capitalize' })} { - executeEvent("openWalletsApp", { - - }); - }} + executeEvent('openWalletsApp', {}); + }} > - Wallets + {t('core:wallet.wallet_other', { postProcess: 'capitalize' })} diff --git a/src/components/GeneralNotifications.tsx b/src/components/GeneralNotifications.tsx index c7336dc..c7a6072 100644 --- a/src/components/GeneralNotifications.tsx +++ b/src/components/GeneralNotifications.tsx @@ -1,5 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; - +import { useState } from 'react'; import { Box, ButtonBase, @@ -8,58 +7,79 @@ import { Popover, Tooltip, Typography, -} from "@mui/material"; -import NotificationsIcon from "@mui/icons-material/Notifications"; -import AccountBalanceWalletIcon from "@mui/icons-material/AccountBalanceWallet"; -import { formatDate } from "../utils/time"; -import { useHandlePaymentNotification } from "../hooks/useHandlePaymentNotification"; -import { executeEvent } from "../utils/events"; + useTheme, +} from '@mui/material'; +import NotificationsIcon from '@mui/icons-material/Notifications'; +import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet'; +import { formatDate } from '../utils/time'; +import { useHandlePaymentNotification } from '../hooks/useHandlePaymentNotification'; +import { executeEvent } from '../utils/events'; +import { useTranslation } from 'react-i18next'; export const GeneralNotifications = ({ address }) => { const [anchorEl, setAnchorEl] = useState(null); - const {latestTx, + + const { + latestTx, getNameOrAddressOfSenderMiddle, - hasNewPayment, setLastEnteredTimestampPayment, nameAddressOfSender} = useHandlePaymentNotification(address) - + hasNewPayment, + setLastEnteredTimestampPayment, + nameAddressOfSender, + } = useHandlePaymentNotification(address); + const handlePopupClick = (event) => { event.stopPropagation(); // Prevent parent onClick from firing setAnchorEl(event.currentTarget); }; + const { t } = useTranslation(['core']); + const theme = useTheme(); + return ( <> { handlePopupClick(e); - - }} style={{}} > - PAYMENT NOTIFICATION} - placement="left" - arrow - sx={{ fontSize: "24" }} - slotProps={{ - tooltip: { - sx: { - color: "#ffffff", - backgroundColor: "#444444", - }, - }, - arrow: { - sx: { - color: "#444444", - }, - }, - }} - > - + {t('core:payment_notification')} + + } + placement="left" + arrow + sx={{ fontSize: '24' }} + slotProps={{ + tooltip: { + sx: { + color: theme.palette.text.primary, + backgroundColor: theme.palette.background.paper, + }, + }, + arrow: { + sx: { + color: theme.palette.text.primary, + }, + }, }} - /> + > + @@ -67,81 +87,93 @@ export const GeneralNotifications = ({ address }) => { open={!!anchorEl} anchorEl={anchorEl} onClose={() => { - if(hasNewPayment){ - setLastEnteredTimestampPayment(Date.now()) + if (hasNewPayment) { + setLastEnteredTimestampPayment(Date.now()); } - setAnchorEl(null) - + setAnchorEl(null); }} // Close popover on click outside > - {!hasNewPayment && No new notifications} + {!hasNewPayment && ( + + No new notifications + + )} {hasNewPayment && ( { - setAnchorEl(null) - executeEvent('openWalletsApp', {}) + onClick={() => { + setAnchorEl(null); + executeEvent('openWalletsApp', {}); }} > - - - {" "} - {formatDate(latestTx?.timestamp)} - - - - {latestTx?.amount} - - {nameAddressOfSender.current[latestTx?.creatorAddress] || getNameOrAddressOfSenderMiddle(latestTx?.creatorAddress)} - + > + {' '} + {formatDate(latestTx?.timestamp)} + + + + {latestTx?.amount} + + + + {nameAddressOfSender.current[latestTx?.creatorAddress] || + getNameOrAddressOfSenderMiddle(latestTx?.creatorAddress)} + )} diff --git a/src/components/GlobalActions/GlobalActions.tsx b/src/components/GlobalActions/GlobalActions.tsx index 6dd845c..e181df4 100644 --- a/src/components/GlobalActions/GlobalActions.tsx +++ b/src/components/GlobalActions/GlobalActions.tsx @@ -1,10 +1,10 @@ -import React from 'react' -import { JoinGroup } from './JoinGroup' +import React from 'react'; +import { JoinGroup } from './JoinGroup'; -export const GlobalActions = ({memberGroups}) => { +export const GlobalActions = () => { return ( <> - + - ) -} + ); +}; diff --git a/src/components/GlobalActions/JoinGroup.tsx b/src/components/GlobalActions/JoinGroup.tsx index f03430b..6503de4 100644 --- a/src/components/GlobalActions/JoinGroup.tsx +++ b/src/components/GlobalActions/JoinGroup.tsx @@ -1,5 +1,5 @@ -import React, { useContext, useEffect, useMemo, useState } from "react"; -import { subscribeToEvent, unsubscribeFromEvent } from "../../utils/events"; +import React, { useContext, useEffect, useMemo, useState } from 'react'; +import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events'; import { Box, Button, @@ -9,20 +9,26 @@ import { DialogActions, DialogContent, Typography, -} from "@mui/material"; -import { CustomButton, CustomButtonAccept } from "../../App-styles"; -import { getBaseApiReact, MyContext } from "../../App"; -import { getFee } from "../../background"; -import { CustomizedSnackbars } from "../Snackbar/Snackbar"; -import { FidgetSpinner } from "react-loader-spinner"; + useTheme, +} from '@mui/material'; +import { CustomButton, CustomButtonAccept } from '../../styles/App-styles'; +import { getBaseApiReact, MyContext } from '../../App'; +import { getFee } from '../../background'; +import { CustomizedSnackbars } from '../Snackbar/Snackbar'; +import { FidgetSpinner } from 'react-loader-spinner'; +import { useAtom, useSetAtom } from 'jotai'; +import { memberGroupsAtom, txListAtom } from '../../atoms/global'; -export const JoinGroup = ({ memberGroups }) => { - const { show, setTxList } = useContext(MyContext); +export const JoinGroup = () => { + const { show } = useContext(MyContext); + const setTxList = useSetAtom(txListAtom); + const [memberGroups] = useAtom(memberGroupsAtom); const [openSnack, setOpenSnack] = useState(false); const [infoSnack, setInfoSnack] = useState(null); const [groupInfo, setGroupInfo] = useState(null); const [isLoadingInfo, setIsLoadingInfo] = useState(false); const [isOpen, setIsOpen] = useState(false); + const theme = useTheme(); const [isLoadingJoinGroup, setIsLoadingJoinGroup] = useState(false); const handleJoinGroup = async (e) => { setGroupInfo(null); @@ -42,43 +48,45 @@ export const JoinGroup = ({ memberGroups }) => { }; useEffect(() => { - subscribeToEvent("globalActionJoinGroup", handleJoinGroup); + subscribeToEvent('globalActionJoinGroup', handleJoinGroup); return () => { - unsubscribeFromEvent("globalActionJoinGroup", handleJoinGroup); + unsubscribeFromEvent('globalActionJoinGroup', handleJoinGroup); }; }, []); - const isInGroup = useMemo(()=> { - return !!memberGroups.find((item)=> +item?.groupId === +groupInfo?.groupId) - }, [memberGroups, groupInfo]) + const isInGroup = useMemo(() => { + return !!memberGroups.find( + (item) => +item?.groupId === +groupInfo?.groupId + ); + }, [memberGroups, groupInfo]); const joinGroup = async (group, isOpen) => { try { const groupId = group.groupId; - const fee = await getFee("JOIN_GROUP"); + const fee = await getFee('JOIN_GROUP'); await show({ - message: "Would you like to perform an JOIN_GROUP transaction?", - publishFee: fee.fee + " QORT", + message: 'Would you like to perform an JOIN_GROUP transaction?', + publishFee: fee.fee + ' QORT', }); setIsLoadingJoinGroup(true); await new Promise((res, rej) => { window - .sendMessage("joinGroup", { + .sendMessage('joinGroup', { groupId, }) .then((response) => { if (!response?.error) { setInfoSnack({ - type: "success", + type: 'success', message: - "Successfully requested to join group. It may take a couple of minutes for the changes to propagate", + 'Successfully requested to join group. It may take a couple of minutes for the changes to propagate', }); if (isOpen) { setTxList((prev) => [ { ...response, - type: "joined-group", + type: 'joined-group', label: `Joined Group ${group?.groupName}: awaiting confirmation`, labelDone: `Joined Group ${group?.groupName}: success!`, done: false, @@ -90,7 +98,7 @@ export const JoinGroup = ({ memberGroups }) => { setTxList((prev) => [ { ...response, - type: "joined-group-request", + type: 'joined-group-request', label: `Requested to join Group ${group?.groupName}: awaiting confirmation`, labelDone: `Requested to join Group ${group?.groupName}: success!`, done: false, @@ -105,7 +113,7 @@ export const JoinGroup = ({ memberGroups }) => { return; } else { setInfoSnack({ - type: "error", + type: 'error', message: response?.error, }); setOpenSnack(true); @@ -114,8 +122,8 @@ export const JoinGroup = ({ memberGroups }) => { }) .catch((error) => { setInfoSnack({ - type: "error", - message: error.message || "An error occurred", + type: 'error', + message: error.message || 'An error occurred', }); setOpenSnack(true); rej(error); @@ -138,37 +146,37 @@ export const JoinGroup = ({ memberGroups }) => { {!groupInfo && ( - {" "} + {' '} {" "} + />{' '} )} @@ -176,7 +184,7 @@ export const JoinGroup = ({ memberGroups }) => { @@ -185,7 +193,7 @@ export const JoinGroup = ({ memberGroups }) => { {groupInfo?.description && ( @@ -193,19 +201,19 @@ export const JoinGroup = ({ memberGroups }) => { )} {isInGroup && ( - - *You are already in this group! - + + *You are already in this group! + )} {!isInGroup && groupInfo?.isOpen === false && ( @@ -216,32 +224,34 @@ export const JoinGroup = ({ memberGroups }) => { - { + { joinGroup(groupInfo, groupInfo?.isOpen); setIsOpen(false); - }} disabled={isInGroup}> - - Join - + + Join + - + setIsOpen(false)} > @@ -259,14 +269,14 @@ export const JoinGroup = ({ memberGroups }) => { {isLoadingJoinGroup && ( ` +export const Label = styled('label')` + display: block; font-family: 'IBM Plex Sans', sans-serif; font-size: 14px; - display: block; - margin-bottom: 4px; font-weight: 400; - ` -); -const Transition = React.forwardRef(function Transition( + margin-bottom: 4px; +`; + +const Transition = forwardRef(function Transition( props: TransitionProps & { - children: React.ReactElement; + children: ReactElement; }, - ref: React.Ref + ref: Ref ) { return ; }); export const AddGroup = ({ address, open, setOpen }) => { - const {show, setTxList} = React.useContext(MyContext) + const { show } = useContext(MyContext); + const setTxList = useSetAtom(txListAtom); - const [tab, setTab] = React.useState("create"); - const [openAdvance, setOpenAdvance] = React.useState(false); + const [openAdvance, setOpenAdvance] = useState(false); + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + const [groupType, setGroupType] = useState('1'); + const [approvalThreshold, setApprovalThreshold] = useState('40'); + const [minBlock, setMinBlock] = useState('5'); + const [maxBlock, setMaxBlock] = useState('21600'); + const [value, setValue] = useState(0); + const [openSnack, setOpenSnack] = useState(false); + const [infoSnack, setInfoSnack] = useState(null); - const [name, setName] = React.useState(""); - const [description, setDescription] = React.useState(""); - const [groupType, setGroupType] = React.useState("1"); - const [approvalThreshold, setApprovalThreshold] = React.useState("40"); - const [minBlock, setMinBlock] = React.useState("5"); - const [maxBlock, setMaxBlock] = React.useState("21600"); - const [value, setValue] = React.useState(0); - const [openSnack, setOpenSnack] = React.useState(false); - const [infoSnack, setInfoSnack] = React.useState(null); - - const handleChange = (event: React.SyntheticEvent, newValue: number) => { + const handleChange = (event: SyntheticEvent, newValue: number) => { setValue(newValue); }; + const handleClose = () => { setOpen(false); }; @@ -89,400 +97,530 @@ export const AddGroup = ({ address, open, setOpen }) => { setMaxBlock(event.target.value as string); }; - + const { t } = useTranslation(['core', 'group']); + const theme = useTheme(); const handleCreateGroup = async () => { try { - if(!name) throw new Error('Please provide a name') - if(!description) throw new Error('Please provide a description') + if (!name) + throw new Error( + t('group:message.error.name_required', { + postProcess: 'capitalize', + }) + ); + if (!description) + throw new Error( + t('group:message.error.description_required', { + postProcess: 'capitalize', + }) + ); + + const fee = await getFee('CREATE_GROUP'); - const fee = await getFee('CREATE_GROUP') await show({ - message: "Would you like to perform an CREATE_GROUP transaction?" , - publishFee: fee.fee + ' QORT' - }) + message: t('group:question.create_group', { + postProcess: 'capitalize', + }), + publishFee: fee.fee + ' QORT', + }); - await new Promise((res, rej) => { - window.sendMessage("createGroup", { - groupName: name, - groupDescription: description, - groupType: +groupType, - groupApprovalThreshold: +approvalThreshold, - minBlock: +minBlock, - maxBlock: +maxBlock, - }) - .then((response) => { - if (!response?.error) { - setInfoSnack({ - type: "success", - message: "Successfully created group. It may take a couple of minutes for the changes to propagate", + await new Promise((res, rej) => { + window + .sendMessage('createGroup', { + groupName: name, + groupDescription: description, + groupType: +groupType, + groupApprovalThreshold: +approvalThreshold, + minBlock: +minBlock, + maxBlock: +maxBlock, + }) + .then((response) => { + if (!response?.error) { + setInfoSnack({ + type: 'success', + message: t('group:message.success.group_creation', { + postProcess: 'capitalize', + }), + }); + setOpenSnack(true); + setTxList((prev) => [ + { + ...response, + type: 'created-group', + label: t('group:message.success.group_creation_name', { + group_name: name, + postProcess: 'capitalize', + }), + labelDone: t('group:message.success.group_creation_label', { + group_name: name, + postProcess: 'capitalize', + }), + done: false, + }, + ...prev, + ]); + res(response); + return; + } + rej({ message: response.error }); + }) + .catch((error) => { + rej({ + message: + error.message || + t('core:message.error.generic', { postProcess: 'capitalize' }), }); - setOpenSnack(true); - setTxList((prev) => [ - { - ...response, - type: 'created-group', - label: `Created group ${name}: awaiting confirmation`, - labelDone: `Created group ${name}: success!`, - done: false, - }, - ...prev, - ]); - res(response); - return; - } - rej({ message: response.error }); - }) - .catch((error) => { - rej({ message: error.message || "An error occurred" }); - }); - + }); }); } catch (error) { setInfoSnack({ - type: "error", + type: 'error', message: error?.message, }); setOpenSnack(true); } }; - function CustomTabPanel(props: TabPanelProps) { - const { children, value, index, ...other } = props; - - return ( - - ); - } - function a11yProps(index: number) { return { id: `simple-tab-${index}`, - "aria-controls": `simple-tabpanel-${index}`, + 'aria-controls': `simple-tabpanel-${index}`, }; } + const openGroupInvitesRequestFunc = () => { + setValue(2); + }; - const openGroupInvitesRequestFunc = ()=> { - setValue(2) - } - - React.useEffect(() => { - subscribeToEvent("openGroupInvitesRequest", openGroupInvitesRequestFunc); + useEffect(() => { + subscribeToEvent('openGroupInvitesRequest', openGroupInvitesRequestFunc); return () => { - unsubscribeFromEvent("openGroupInvitesRequest", openGroupInvitesRequestFunc); + unsubscribeFromEvent( + 'openGroupInvitesRequest', + openGroupInvitesRequestFunc + ); }; }, []); + if (!open) return null; + return ( - + - + - - Group Mgmt + + {t('group:group.management', { postProcess: 'capitalize' })} - - {/* */} + - - - - - - + + + + + + - + {value === 0 && ( - - - setName(e.target.value)} - /> - - - - - setDescription(e.target.value)} - /> - - - - - - setOpenAdvance((prev) => !prev)} - > - Advanced options - - {openAdvance ? : } - - + setName(e.target.value)} + /> + + + - + setDescription(e.target.value)} + /> + + + setOpenAdvance((prev) => !prev)} + > + + {t('group:advanced_options', { + postProcess: 'capitalize', + })} + + + {openAdvance ? : } + + + + + + + + + + + + + + + + + - - + {t('group.action.create', { + postProcess: 'capitalize', + })} + - - - - )} {value === 1 && ( - - - + + - )} - - {value === 2 && ( - - - - )} - + {value === 2 && ( + + + + )} - + + - + ); }; diff --git a/src/components/Group/AddGroupList.tsx b/src/components/Group/AddGroupList.tsx index ccd4850..971f6e0 100644 --- a/src/components/Group/AddGroupList.tsx +++ b/src/components/Group/AddGroupList.tsx @@ -1,50 +1,58 @@ import { Box, - Button, ListItem, ListItemButton, ListItemText, Popover, TextField, Typography, -} from "@mui/material"; -import React, { + useTheme, +} from '@mui/material'; +import { useCallback, useContext, useEffect, useMemo, useRef, useState, -} from "react"; +} from 'react'; import { AutoSizer, CellMeasurer, CellMeasurerCache, List, -} from "react-virtualized"; -import _ from "lodash"; -import { MyContext, getBaseApiReact } from "../../App"; -import { LoadingButton } from "@mui/lab"; -import { getBaseApi, getFee } from "../../background"; +} from 'react-virtualized'; +import _ from 'lodash'; +import { MyContext, getBaseApiReact } from '../../App'; +import { LoadingButton } from '@mui/lab'; +import { getFee } from '../../background'; import LockIcon from '@mui/icons-material/Lock'; import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred'; -import { Spacer } from "../../common/Spacer"; +import { Spacer } from '../../common/Spacer'; +import { useTranslation } from 'react-i18next'; +import { useAtom, useSetAtom } from 'jotai'; +import { memberGroupsAtom, txListAtom } from '../../atoms/global'; + const cache = new CellMeasurerCache({ fixedWidth: true, defaultHeight: 50, }); export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => { - const { memberGroups, show, setTxList } = useContext(MyContext); + const { show } = useContext(MyContext); + const [memberGroups] = useAtom(memberGroupsAtom); + const setTxList = useSetAtom(txListAtom); + + const { t } = useTranslation(['core', 'group']); const [groups, setGroups] = useState([]); const [popoverAnchor, setPopoverAnchor] = useState(null); // Track which list item the popover is anchored to const [openPopoverIndex, setOpenPopoverIndex] = useState(null); // Track which list item has the popover open const listRef = useRef(); - const [inputValue, setInputValue] = useState(""); + const [inputValue, setInputValue] = useState(''); const [filteredItems, setFilteredItems] = useState(groups); const [isLoading, setIsLoading] = useState(false); - + const theme = useTheme(); const handleFilter = useCallback( (query) => { if (query) { @@ -72,9 +80,7 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => { const getGroups = async () => { try { - const response = await fetch( - `${getBaseApiReact()}/groups/?limit=0` - ); + const response = await fetch(`${getBaseApiReact()}/groups/?limit=0`); const groupData = await response.json(); const filteredGroup = groupData.filter( (item) => !memberGroups.find((group) => group.groupId === item.groupId) @@ -103,30 +109,44 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => { const handleJoinGroup = async (group, isOpen) => { try { const groupId = group.groupId; - const fee = await getFee('JOIN_GROUP') - await show({ - message: "Would you like to perform an JOIN_GROUP transaction?" , - publishFee: fee.fee + ' QORT' - }) + + const fee = await getFee('JOIN_GROUP'); + + await show({ + message: t('group:question.join_group', { + postProcess: 'capitalize', + }), + publishFee: fee.fee + ' QORT', + }); setIsLoading(true); + await new Promise((res, rej) => { - window.sendMessage("joinGroup", { - groupId, - }) + window + .sendMessage('joinGroup', { + groupId, + }) .then((response) => { if (!response?.error) { setInfoSnack({ - type: "success", - message: "Successfully requested to join group. It may take a couple of minutes for the changes to propagate", + type: 'success', + message: t('group:message.success.join_group', { + postProcess: 'capitalize', + }), }); - + if (isOpen) { setTxList((prev) => [ { ...response, type: 'joined-group', - label: `Joined Group ${group?.groupName}: awaiting confirmation`, - labelDone: `Joined Group ${group?.groupName}: success!`, + label: t('group:message.success.group_join_label', { + group_name: group?.groupName, + postProcess: 'capitalize', + }), + labelDone: t('group:message.success.group_join_label', { + group_name: group?.groupName, + postProcess: 'capitalize', + }), done: false, groupId, }, @@ -145,14 +165,14 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => { ...prev, ]); } - + setOpenSnack(true); handlePopoverClose(); res(response); return; } else { setInfoSnack({ - type: "error", + type: 'error', message: response?.error, }); setOpenSnack(true); @@ -161,18 +181,18 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => { }) .catch((error) => { setInfoSnack({ - type: "error", - message: error.message || "An error occurred", + type: 'error', + message: error.message || 'An error occurred', }); setOpenSnack(true); rej(error); }); - }); setIsLoading(false); - } catch (error) {} finally { + } catch (error) { + console.log(error); + } finally { setIsLoading(false); - } }; @@ -195,30 +215,33 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => { anchorEl={popoverAnchor} onClose={handlePopoverClose} anchorOrigin={{ - vertical: "bottom", - horizontal: "center", + vertical: 'bottom', + horizontal: 'center', }} transformOrigin={{ - vertical: "top", - horizontal: "center", + vertical: 'top', + horizontal: 'center', }} - style={{ marginTop: "8px" }} + style={{ marginTop: '8px' }} > - Join {group?.groupName} + + {t('core:action.join', { postProcess: 'capitalize' })}{' '} + {group?.groupName} + {group?.isOpen === false && - "This is a closed/private group, so you will need to wait until an admin accepts your request"} + 'This is a closed/private group, so you will need to wait until an admin accepts your request'} { variant="contained" onClick={() => handleJoinGroup(group, group?.isOpen)} > - Join group + {t('group:action.join_group', { + postProcess: 'capitalize', + })} @@ -234,16 +259,20 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => { onClick={(event) => handlePopoverOpen(event, index)} > {group?.isOpen === false && ( - - )} - {group?.isOpen === true && ( - - )} - + + )} + {group?.isOpen === true && ( + + )} + { }; return ( - +

Groups list

{ />
diff --git a/src/components/Group/BlockedUsersModal.tsx b/src/components/Group/BlockedUsersModal.tsx index 24b3f01..17837fc 100644 --- a/src/components/Group/BlockedUsersModal.tsx +++ b/src/components/Group/BlockedUsersModal.tsx @@ -8,64 +8,79 @@ import { DialogTitle, TextField, Typography, -} from "@mui/material"; -import React, { useContext, useEffect, useState } from "react"; -import { getBaseApiReact, MyContext } from "../../App"; -import { Spacer } from "../../common/Spacer"; -import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "../../utils/events"; -import { validateAddress } from "../../utils/validateAddress"; -import { getNameInfo, requestQueueMemberNames } from "./Group"; -import { useModal } from "../../common/useModal"; -import { useRecoilState } from "recoil"; -import { isOpenBlockedModalAtom } from "../../atoms/global"; + useTheme, +} from '@mui/material'; +import { useContext, useEffect, useState } from 'react'; +import { getBaseApiReact, MyContext } from '../../App'; +import { Spacer } from '../../common/Spacer'; +import { + executeEvent, + subscribeToEvent, + unsubscribeFromEvent, +} from '../../utils/events'; +import { validateAddress } from '../../utils/validateAddress'; +import { getNameInfo, requestQueueMemberNames } from './Group'; +import { useModal } from '../../common/useModal'; +import { isOpenBlockedModalAtom } from '../../atoms/global'; import InfoIcon from '@mui/icons-material/Info'; +import { useAtom } from 'jotai'; + export const BlockedUsersModal = () => { - const [isOpenBlockedModal, setIsOpenBlockedModal] = useRecoilState(isOpenBlockedModalAtom) + const theme = useTheme(); + const [isOpenBlockedModal, setIsOpenBlockedModal] = useAtom( + isOpenBlockedModalAtom + ); + const [hasChanged, setHasChanged] = useState(false); - const [value, setValue] = useState(""); - const [addressesWithNames, setAddressesWithNames] = useState({}) + const [value, setValue] = useState(''); + const [addressesWithNames, setAddressesWithNames] = useState({}); const { isShow, onCancel, onOk, show, message } = useModal(); - const { getAllBlockedUsers, removeBlockFromList, addToBlockList, setOpenSnackGlobal, setInfoSnackCustom } = - useContext(MyContext); + const { + getAllBlockedUsers, + removeBlockFromList, + addToBlockList, + setOpenSnackGlobal, + setInfoSnackCustom, + } = useContext(MyContext); + const [blockedUsers, setBlockedUsers] = useState({ addresses: {}, names: {}, }); + const fetchBlockedUsers = () => { setBlockedUsers(getAllBlockedUsers()); }; useEffect(() => { - if(!isOpenBlockedModal) return + if (!isOpenBlockedModal) return; fetchBlockedUsers(); }, [isOpenBlockedModal]); - const getNames = async () => { + const getNames = async () => { // const validApi = await findUsableApi(); - const addresses = Object.keys(blockedUsers?.addresses) - const addressNames = {} + const addresses = Object.keys(blockedUsers?.addresses); + const addressNames = {}; - const getMemNames = addresses.map(async (address) => { - const name = await requestQueueMemberNames.enqueue(() => { - return getNameInfo(address); - }); - if (name) { - addressNames[address] = name - } - - + const name = await requestQueueMemberNames.enqueue(() => { + return getNameInfo(address); + }); + if (name) { + addressNames[address] = name; + } + return true; }); - + await Promise.all(getMemNames); - - setAddressesWithNames(addressNames) + + setAddressesWithNames(addressNames); }; const blockUser = async (e, user?: string) => { try { - const valUser = user || value + const valUser = user || value; if (!valUser) return; const isAddress = validateAddress(valUser); let userName = null; @@ -80,62 +95,66 @@ export const BlockedUsersModal = () => { if (!isAddress) { const response = await fetch(`${getBaseApiReact()}/names/${valUser}`); const data = await response.json(); - if (!data?.owner) throw new Error("Name does not exist"); + if (!data?.owner) throw new Error('Name does not exist'); if (data?.owner) { userAddress = data.owner; userName = valUser; } } - if(!userName){ + if (!userName) { await addToBlockList(userAddress, null); fetchBlockedUsers(); setHasChanged(true); - executeEvent('updateChatMessagesWithBlocks', true) - setValue('') - return + executeEvent('updateChatMessagesWithBlocks', true); + setValue(''); + return; } const responseModal = await show({ userName, userAddress, }); - if (responseModal === "both") { + if (responseModal === 'both') { await addToBlockList(userAddress, userName); - } else if (responseModal === "address") { + } else if (responseModal === 'address') { await addToBlockList(userAddress, null); - } else if (responseModal === "name") { + } else if (responseModal === 'name') { await addToBlockList(null, userName); } fetchBlockedUsers(); setHasChanged(true); - setValue('') - if(user){ - setIsOpenBlockedModal(false) + setValue(''); + if (user) { + setIsOpenBlockedModal(false); } - if(responseModal === 'both' || responseModal === 'address'){ - executeEvent('updateChatMessagesWithBlocks', true) + if (responseModal === 'both' || responseModal === 'address') { + executeEvent('updateChatMessagesWithBlocks', true); } } catch (error) { setOpenSnackGlobal(true); - - setInfoSnackCustom({ - type: "error", - message: error?.message || "Unable to block user", - }); + setInfoSnackCustom({ + type: 'error', + message: error?.message || 'Unable to block user', + }); } }; + const blockUserFromOutsideModalFunc = (e) => { - const user = e.detail?.user; - setIsOpenBlockedModal(true) - blockUser(null, user) + const user = e.detail?.user; + setIsOpenBlockedModal(true); + blockUser(null, user); + }; + + useEffect(() => { + subscribeToEvent('blockUserFromOutside', blockUserFromOutsideModalFunc); + + return () => { + unsubscribeFromEvent( + 'blockUserFromOutside', + blockUserFromOutsideModalFunc + ); }; - - useEffect(() => { - subscribeToEvent("blockUserFromOutside", blockUserFromOutsideModalFunc); - - return () => { - unsubscribeFromEvent("blockUserFromOutside", blockUserFromOutsideModalFunc); - }; - }, []); + }, []); + return ( { Blocked Users { Blocked addresses- blocks processing of txs - + )} {Object.entries(blockedUsers?.addresses || {})?.map( @@ -197,11 +218,11 @@ export const BlockedUsersModal = () => { return ( {addressesWithNames[key] || key} @@ -215,7 +236,7 @@ export const BlockedUsersModal = () => { try { await removeBlockFromList(key, undefined); setHasChanged(true); - setValue(""); + setValue(''); fetchBlockedUsers(); } catch (error) { console.error(error); @@ -241,20 +262,20 @@ export const BlockedUsersModal = () => { {Object.entries(blockedUsers?.names || {})?.map(([key, value]) => { return ( {key} @@ -284,20 +305,20 @@ export const BlockedUsersModal = () => { + - + + + {combinedListTempAndReal.map((message, index, list) => { let fullMessage = message; if (hashMapMailMessages[message?.identifier]) { fullMessage = hashMapMailMessages[message.identifier]; - if (fullMessage?.error) { return ( + + {formatTimestampForum(message?.created)} + {fullMessage?.error} - ); @@ -855,23 +863,23 @@ export const Thread = ({ return ( + + {formatTimestampForum(message?.created)} + - Downloading from QDN + {t('core:downloading_qdn', { postProcess: 'capitalize' })} - ); })} - + {!hasLastPage && !isLoading && ( <> )} - - 4 ? 'visible' : 'hidden' - }}> - - 4 ? 'visible' : 'hidden', + }} + > + + + - - - - - + {t('core:page.first', { postProcess: 'capitalize' })} + + + + - -
+ + + +
diff --git a/src/components/Group/Forum/texteditor.css b/src/components/Group/Forum/texteditor.css deleted file mode 100644 index e3bbd50..0000000 --- a/src/components/Group/Forum/texteditor.css +++ /dev/null @@ -1,71 +0,0 @@ -.ql-editor { - min-height: 200px; - width: 100%; - color: black; - font-size: 16px; - font-family: Roboto; - max-height: 225px; - overflow-y: scroll; - padding: 0px !important; - } - - .ql-editor::-webkit-scrollbar-track { - background-color: transparent; - cursor: default; - } - .ql-editor::-webkit-scrollbar-track:hover { - background-color: transparent; - } - - .ql-editor::-webkit-scrollbar { - width: 16px; - height: 10px; - background-color: rgba(229, 229, 229, 0.70); - } - - .ql-editor::-webkit-scrollbar-thumb { - background-color: #B0B0B0; - border-radius: 8px; - background-clip: content-box; - border: 4px solid transparent; - } - - - .ql-editor img { - cursor: default; - } - - .ql-editor-display { - min-height: 20px; - width: 100%; - color: black; - font-size: 16px; - font-family: Roboto; - padding: 0px !important; - } - - .ql-editor-display img { - cursor: default; - } - - .ql-container { - font-size: 16px - } - - .ql-toolbar .ql-stroke { - fill: none !important; - stroke: black !important; -} - -.ql-toolbar .ql-fill { - fill: black !important; - stroke: none !important; -} - -.ql-toolbar .ql-picker { - color: black !important; -} - -.ql-toolbar .ql-picker-options { - background-color: white !important; -} \ No newline at end of file diff --git a/src/components/Group/Group.tsx b/src/components/Group/Group.tsx index 9f95a3a..2f2a314 100644 --- a/src/components/Group/Group.tsx +++ b/src/components/Group/Group.tsx @@ -7,97 +7,98 @@ import { ListItemAvatar, ListItemText, Typography, -} from "@mui/material"; + useTheme, +} from '@mui/material'; import React, { useCallback, - useContext, useEffect, useMemo, useRef, useState, -} from "react"; -import { ChatGroup } from "../Chat/ChatGroup"; -import { CreateCommonSecret } from "../Chat/CreateCommonSecret"; -import { base64ToUint8Array } from "../../qdn/encryption/group-encryption"; -import { uint8ArrayToObject } from "../../backgroundFunctions/encryption"; -import CampaignIcon from "@mui/icons-material/Campaign"; -import { AddGroup } from "./AddGroup"; -import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline"; -import CreateIcon from "@mui/icons-material/Create"; - - +} from 'react'; +import { ChatGroup } from '../Chat/ChatGroup'; +import { CreateCommonSecret } from '../Chat/CreateCommonSecret'; +import { base64ToUint8Array } from '../../qdn/encryption/group-encryption'; +import { uint8ArrayToObject } from '../../backgroundFunctions/encryption'; +import CampaignIcon from '@mui/icons-material/Campaign'; +import { AddGroup } from './AddGroup'; +import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; +import CreateIcon from '@mui/icons-material/Create'; import { AuthenticatedContainerInnerRight, CustomButton, -} from "../../App-styles"; -import { Spacer } from "../../common/Spacer"; -import { ManageMembers } from "./ManageMembers"; -import MarkChatUnreadIcon from "@mui/icons-material/MarkChatUnread"; +} from '../../styles/App-styles'; +import { Spacer } from '../../common/Spacer'; +import { ManageMembers } from './ManageMembers'; +import MarkChatUnreadIcon from '@mui/icons-material/MarkChatUnread'; import { - MyContext, clearAllQueues, getArbitraryEndpointReact, getBaseApiReact, - isMobile, pauseAllQueues, resumeAllQueues, -} from "../../App"; -import { ChatDirect } from "../Chat/ChatDirect"; -import { CustomizedSnackbars } from "../Snackbar/Snackbar"; -import { LoadingButton } from "@mui/lab"; -import { LoadingSnackbar } from "../Snackbar/LoadingSnackbar"; -import { GroupAnnouncements } from "../Chat/GroupAnnouncements"; - - - -import { GroupForum } from "../Chat/GroupForum"; +} from '../../App'; +import { ChatDirect } from '../Chat/ChatDirect'; +import { CustomizedSnackbars } from '../Snackbar/Snackbar'; +import { LoadingButton } from '@mui/lab'; +import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar'; +import { GroupAnnouncements } from '../Chat/GroupAnnouncements'; +import { GroupForum } from '../Chat/GroupForum'; import { executeEvent, subscribeToEvent, unsubscribeFromEvent, -} from "../../utils/events"; -import { RequestQueueWithPromise } from "../../utils/queue/queue"; -import { WebSocketActive } from "./WebsocketActive"; -import { useMessageQueue } from "../../MessageQueueContext"; -import { isExtMsg, isUpdateMsg } from "../../background"; -import { ContextMenu } from "../ContextMenu"; +} from '../../utils/events'; +import { RequestQueueWithPromise } from '../../utils/queue/queue'; +import { WebSocketActive } from './WebsocketActive'; +import { useMessageQueue } from '../../MessageQueueContext'; +import { ContextMenu } from '../ContextMenu'; +import { HomeDesktop } from './HomeDesktop'; +import { IconWrapper } from '../Desktop/DesktopFooter'; +import { DesktopHeader } from '../Desktop/DesktopHeader'; +import { AppsDesktop } from '../Apps/AppsDesktop'; +import { AppsDevMode } from '../Apps/AppsDevMode'; +import { DesktopSideBar } from '../DesktopSideBar'; +import { HubsIcon } from '../../assets/Icons/HubsIcon'; +import { MessagingIcon } from '../../assets/Icons/MessagingIcon'; +import { formatEmailDate } from './QMailMessages'; +import { AdminSpace } from '../Chat/AdminSpace'; -import { ReturnIcon } from "../../assets/Icons/ReturnIcon"; -import { ExitIcon } from "../../assets/Icons/ExitIcon"; -import { HomeDesktop } from "./HomeDesktop"; -import { IconWrapper } from "../Desktop/DesktopFooter"; -import { DesktopHeader } from "../Desktop/DesktopHeader"; -import { AppsDesktop } from "../Apps/AppsDesktop"; -import { AppsDevMode } from "../Apps/AppsDevMode"; -import { DesktopSideBar } from "../DesktopSideBar"; -import { HubsIcon } from "../../assets/Icons/HubsIcon"; -import { MessagingIcon } from "../../assets/Icons/MessagingIcon"; -import { formatEmailDate } from "./QMailMessages"; -import { AdminSpace } from "../Chat/AdminSpace"; -import { useRecoilState, useSetRecoilState } from "recoil"; -import { addressInfoControllerAtom, groupsPropertiesAtom, isOpenBlockedModalAtom, selectedGroupIdAtom } from "../../atoms/global"; -import { sortArrayByTimestampAndGroupName } from "../../utils/time"; +import { + addressInfoControllerAtom, + groupAnnouncementsAtom, + groupChatTimestampsAtom, + groupsOwnerNamesAtom, + groupsPropertiesAtom, + isOpenBlockedModalAtom, + memberGroupsAtom, + mutedGroupsAtom, + selectedGroupIdAtom, + timestampEnterDataAtom, +} from '../../atoms/global'; +import { sortArrayByTimestampAndGroupName } from '../../utils/time'; import PersonOffIcon from '@mui/icons-material/PersonOff'; import LockIcon from '@mui/icons-material/Lock'; import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred'; -import { BlockedUsersModal } from "./BlockedUsersModal"; -import { WalletsAppWrapper } from "./WalletsAppWrapper"; - +import { BlockedUsersModal } from './BlockedUsersModal'; +import { WalletsAppWrapper } from './WalletsAppWrapper'; +import { useTranslation } from 'react-i18next'; +import { GroupList } from './GroupList'; +import { useAtom, useSetAtom } from 'jotai'; export const getPublishesFromAdmins = async (admins: string[], groupId) => { - const queryString = admins.map((name) => `name=${name}`).join("&"); + const queryString = admins.map((name) => `name=${name}`).join('&'); const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT_PRIVATE&identifier=symmetric-qchat-group-${ groupId }&exactmatchnames=true&limit=0&reverse=true&${queryString}&prefix=true`; const response = await fetch(url); if (!response.ok) { - throw new Error("network error"); + throw new Error('network error'); } const adminData = await response.json(); const filterId = adminData.filter( - (data: any) => - data.identifier === `symmetric-qchat-group-${groupId}` + (data: any) => data.identifier === `symmetric-qchat-group-${groupId}` ); if (filterId?.length === 0) { return false; @@ -111,9 +112,9 @@ export const getPublishesFromAdmins = async (admins: string[], groupId) => { return dateB.getTime() - dateA.getTime(); }); - return sortedData[0]; }; + interface GroupProps { myAddress: string; isFocused: boolean; @@ -121,7 +122,7 @@ interface GroupProps { balance: number; } -const timeDifferenceForNotificationChats = 900000; +export const timeDifferenceForNotificationChats = 900000; export const requestQueueMemberNames = new RequestQueueWithPromise(5); export const requestQueueAdminMemberNames = new RequestQueueWithPromise(5); @@ -135,7 +136,7 @@ export const getGroupAdminsAddress = async (groupNumber: number) => { `${getBaseApiReact()}/groups/members/${groupNumber}?limit=0&onlyAdmins=true` ); const groupData = await response.json(); - let members: any = []; + const members: any = []; if (groupData && Array.isArray(groupData?.members)) { for (const member of groupData.members) { if (member.member) { @@ -149,12 +150,12 @@ export const getGroupAdminsAddress = async (groupNumber: number) => { export function validateSecretKey(obj) { // Check if the input is an object - if (typeof obj !== "object" || obj === null) { + if (typeof obj !== 'object' || obj === null) { return false; } // Iterate over each key in the object - for (let key in obj) { + for (const key in obj) { // Ensure the key is a string representation of a positive integer if (!/^\d+$/.test(key)) { return false; @@ -164,19 +165,19 @@ export function validateSecretKey(obj) { const value = obj[key]; // Check that value is an object and not null - if (typeof value !== "object" || value === null) { + if (typeof value !== 'object' || value === null) { return false; } - // Check for messageKey - if (!value.hasOwnProperty("messageKey")) { + // Check for messageKey + if (!value.hasOwnProperty('messageKey')) { return false; } // Ensure messageKey and nonce are non-empty strings if ( - typeof value.messageKey !== "string" || - value.messageKey.trim() === "" + typeof value.messageKey !== 'string' || + value.messageKey.trim() === '' ) { return false; } @@ -196,45 +197,49 @@ export const getGroupMembers = async (groupNumber: number) => { return groupData; }; - export const decryptResource = async (data: string, fromQortalRequest) => { try { return new Promise((res, rej) => { - window.sendMessage("decryptGroupEncryption", { - data, - }) + window + .sendMessage('decryptGroupEncryption', { + data, + }) .then((response) => { if (!response?.error) { res(response); return; } - if(fromQortalRequest){ - rej({error: response.error, message: response?.error}); + if (fromQortalRequest) { + rej({ error: response.error, message: response?.error }); } else { rej(response.error); - } }) .catch((error) => { - if(fromQortalRequest){ - rej({message: error.message || "An error occurred", error: error.message || "An error occurred"}); + if (fromQortalRequest) { + rej({ + message: error.message || 'An error occurred', + error: error.message || 'An error occurred', + }); } else { - rej(error.message || "An error occurred",); + rej(error.message || 'An error occurred'); } }); - }); - } catch (error) {} + } catch (error) { + console.log(error); + } }; export const addDataPublishesFunc = async (data: string, groupId, type) => { try { return new Promise((res, rej) => { - window.sendMessage("addDataPublishes", { - data, - groupId, - type, - }) + window + .sendMessage('addDataPublishes', { + data, + groupId, + type, + }) .then((response) => { if (!response?.error) { res(response); @@ -243,20 +248,22 @@ export const addDataPublishesFunc = async (data: string, groupId, type) => { rej(response.error); }) .catch((error) => { - rej(error.message || "An error occurred"); + rej(error.message || 'An error occurred'); }); - }); - } catch (error) {} + } catch (error) { + console.log(error); + } }; export const getDataPublishesFunc = async (groupId, type) => { try { return new Promise((res, rej) => { - window.sendMessage("getDataPublishes", { - groupId, - type, - }) + window + .sendMessage('getDataPublishes', { + groupId, + type, + }) .then((response) => { if (!response?.error) { res(response); @@ -265,11 +272,12 @@ export const getDataPublishesFunc = async (groupId, type) => { rej(response.error); }) .catch((error) => { - rej(error.message || "An error occurred"); + rej(error.message || 'An error occurred'); }); - }); - } catch (error) {} + } catch (error) { + console.log(error); + } }; export async function getNameInfo(address: string) { @@ -279,7 +287,7 @@ export async function getNameInfo(address: string) { if (nameData?.length > 0) { return nameData[0]?.name; } else { - return ""; + return ''; } } @@ -294,7 +302,6 @@ export const getGroupAdmins = async (groupNumber: number) => { let membersAddresses = []; let both = []; - const getMemNames = groupData?.members?.map(async (member) => { if (member?.member) { const name = await requestQueueAdminMemberNames.enqueue(() => { @@ -327,7 +334,7 @@ export const getNames = async (listOfMembers) => { if (name) { members.push({ ...member, name }); } else { - members.push({ ...member, name: "" }); + members.push({ ...member, name: '' }); } } @@ -338,8 +345,8 @@ export const getNames = async (listOfMembers) => { return members; }; -export const getNamesForAdmins = async (admins) => { +export const getNamesForAdmins = async (admins) => { let members: any = []; const getMemNames = admins?.map(async (admin) => { @@ -379,9 +386,9 @@ export const Group = ({ balance, setIsOpenDrawerProfile, setDesktopViewMode, - desktopViewMode + desktopViewMode, }: GroupProps) => { - const [desktopSideView, setDesktopSideView] = useState('groups') + const [desktopSideView, setDesktopSideView] = useState('groups'); const [secretKey, setSecretKey] = useState(null); const [secretKeyPublishDate, setSecretKeyPublishDate] = useState(null); @@ -405,10 +412,15 @@ export const Group = ({ const [openAddGroup, setOpenAddGroup] = useState(false); const [isInitialGroups, setIsInitialGroups] = useState(false); const [openManageMembers, setOpenManageMembers] = useState(false); - const { setMemberGroups, rootHeight, isRunningPublicNode } = useContext(MyContext); + + const setMemberGroups = useSetAtom(memberGroupsAtom); + const lastGroupNotification = useRef(null); - const [timestampEnterData, setTimestampEnterData] = useState({}); - const [chatMode, setChatMode] = useState("groups"); + const [timestampEnterData, setTimestampEnterData] = useAtom( + timestampEnterDataAtom + ); + + const [chatMode, setChatMode] = useState('groups'); const [newChat, setNewChat] = useState(false); const [openSnack, setOpenSnack] = React.useState(false); const [infoSnack, setInfoSnack] = React.useState(null); @@ -417,18 +429,22 @@ export const Group = ({ const [isLoadingGroup, setIsLoadingGroup] = React.useState(false); const [firstSecretKeyInCreation, setFirstSecretKeyInCreation] = React.useState(false); - const [groupSection, setGroupSection] = React.useState("home"); - const [groupAnnouncements, setGroupAnnouncements] = React.useState({}); + const [groupSection, setGroupSection] = React.useState('home'); + const [groupAnnouncements, setGroupAnnouncements] = useAtom( + groupAnnouncementsAtom + ); + const [defaultThread, setDefaultThread] = React.useState(null); const [isOpenDrawer, setIsOpenDrawer] = React.useState(false); - const setIsOpenBlockedUserModal = useSetRecoilState(isOpenBlockedModalAtom) + const setIsOpenBlockedUserModal = useSetAtom(isOpenBlockedModalAtom); const [hideCommonKeyPopup, setHideCommonKeyPopup] = React.useState(false); - const [isLoadingGroupMessage, setIsLoadingGroupMessage] = React.useState(""); - const [drawerMode, setDrawerMode] = React.useState("groups"); - const [mutedGroups, setMutedGroups] = useState([]); - const [mobileViewMode, setMobileViewMode] = useState("home"); - const [mobileViewModeKeepOpen, setMobileViewModeKeepOpen] = useState(""); + const [isLoadingGroupMessage, setIsLoadingGroupMessage] = React.useState(''); + const [drawerMode, setDrawerMode] = React.useState('groups'); + const setMutedGroups = useSetAtom(mutedGroupsAtom); + + const [mobileViewMode, setMobileViewMode] = useState('home'); + const [mobileViewModeKeepOpen, setMobileViewModeKeepOpen] = useState(''); const isFocusedRef = useRef(true); const timestampEnterDataRef = useRef({}); const selectedGroupRef = useRef(null); @@ -440,68 +456,72 @@ export const Group = ({ const settimeoutForRefetchSecretKey = useRef(null); const { clearStatesMessageQueueProvider } = useMessageQueue(); const initiatedGetMembers = useRef(false); - const [groupChatTimestamps, setGroupChatTimestamps] = React.useState({}); - const [appsMode, setAppsMode] = useState('home') - const [appsModeDev, setAppsModeDev] = useState('home') - const [isOpenSideViewDirects, setIsOpenSideViewDirects] = useState(false) - const [isOpenSideViewGroups, setIsOpenSideViewGroups] = useState(false) - const [isForceShowCreationKeyPopup, setIsForceShowCreationKeyPopup] = useState(false) + const [groupChatTimestamps, setGroupChatTimestamps] = useAtom( + groupChatTimestampsAtom + ); - const [groupsProperties, setGroupsProperties] = useRecoilState(groupsPropertiesAtom) - const setUserInfoForLevels = useSetRecoilState(addressInfoControllerAtom); + const [appsMode, setAppsMode] = useState('home'); + const [appsModeDev, setAppsModeDev] = useState('home'); + const [isOpenSideViewDirects, setIsOpenSideViewDirects] = useState(false); + const [isOpenSideViewGroups, setIsOpenSideViewGroups] = useState(false); + const [isForceShowCreationKeyPopup, setIsForceShowCreationKeyPopup] = + useState(false); + const groupsOwnerNamesRef = useRef({}); + const { t } = useTranslation(['core', 'group']); - const isPrivate = useMemo(()=> { - if(selectedGroup?.groupId === '0') return false - if(!selectedGroup?.groupId || !groupsProperties[selectedGroup?.groupId]) return null - if(groupsProperties[selectedGroup?.groupId]?.isOpen === true) return false - if(groupsProperties[selectedGroup?.groupId]?.isOpen === false) return true - return null - }, [selectedGroup]) + const [groupsProperties, setGroupsProperties] = useAtom(groupsPropertiesAtom); + const setGroupsOwnerNames = useSetAtom(groupsOwnerNamesAtom); - + const setUserInfoForLevels = useSetAtom(addressInfoControllerAtom); + const isPrivate = useMemo(() => { + if (selectedGroup?.groupId === '0') return false; + if (!selectedGroup?.groupId || !groupsProperties[selectedGroup?.groupId]) + return null; + if (groupsProperties[selectedGroup?.groupId]?.isOpen === true) return false; + if (groupsProperties[selectedGroup?.groupId]?.isOpen === false) return true; + return null; + }, [selectedGroup]); - const setSelectedGroupId = useSetRecoilState(selectedGroupIdAtom) - const toggleSideViewDirects = ()=> { - if(isOpenSideViewGroups){ - setIsOpenSideViewGroups(false) + const setSelectedGroupId = useSetAtom(selectedGroupIdAtom); + + const toggleSideViewDirects = () => { + if (isOpenSideViewGroups) { + setIsOpenSideViewGroups(false); } - setIsOpenSideViewDirects((prev)=> !prev) - } - const toggleSideViewGroups = ()=> { - if(isOpenSideViewDirects){ - setIsOpenSideViewDirects(false) + setIsOpenSideViewDirects((prev) => !prev); + }; + const toggleSideViewGroups = () => { + if (isOpenSideViewDirects) { + setIsOpenSideViewDirects(false); } - setIsOpenSideViewGroups((prev)=> !prev) - } - useEffect(()=> { - timestampEnterDataRef.current = timestampEnterData - }, [timestampEnterData]) + setIsOpenSideViewGroups((prev) => !prev); + }; + useEffect(() => { + timestampEnterDataRef.current = timestampEnterData; + }, [timestampEnterData]); useEffect(() => { isFocusedRef.current = isFocused; }, [isFocused]); useEffect(() => { groupSectionRef.current = groupSection; }, [groupSection]); - useEffect(() => { selectedGroupRef.current = selectedGroup; - setSelectedGroupId(selectedGroup?.groupId) + setSelectedGroupId(selectedGroup?.groupId); }, [selectedGroup]); - useEffect(() => { selectedDirectRef.current = selectedDirect; }, [selectedDirect]); - - - const getUserSettings = async () => { + const getUserSettings = useCallback(async () => { try { return new Promise((res, rej) => { - window.sendMessage("getUserSettings", { - key: "mutedGroups", - }) + window + .sendMessage('getUserSettings', { + key: 'mutedGroups', + }) .then((response) => { if (!response?.error) { setMutedGroups(response || []); @@ -511,72 +531,75 @@ export const Group = ({ rej(response.error); }) .catch((error) => { - rej(error.message || "An error occurred"); + rej(error.message || 'An error occurred'); }); - }); } catch (error) { - console.log("error", error); + console.log('error', error); } - }; + }, [setMutedGroups]); useEffect(() => { getUserSettings(); - }, []); + }, [getUserSettings]); - const getTimestampEnterChat = async () => { + const getTimestampEnterChat = useCallback(async () => { try { return new Promise((res, rej) => { - window.sendMessage("getTimestampEnterChat") - .then((response) => { - if (!response?.error) { - setTimestampEnterData(response); - res(response); - return; - } - rej(response.error); - }) - .catch((error) => { - rej(error.message || "An error occurred"); - }); - + window + .sendMessage('getTimestampEnterChat') + .then((response) => { + if (!response?.error) { + setTimestampEnterData(response); + res(response); + return; + } + rej(response.error); + }) + .catch((error) => { + rej(error.message || 'An error occurred'); + }); }); - } catch (error) {} - }; + } catch (error) { + console.log(error); + } + }, []); const refreshHomeDataFunc = () => { - setGroupSection("default"); + setGroupSection('default'); setTimeout(() => { - setGroupSection("home"); + setGroupSection('home'); }, 300); }; const getGroupAnnouncements = async () => { try { return new Promise((res, rej) => { - window.sendMessage("getGroupNotificationTimestamp") - .then((response) => { - if (!response?.error) { - setGroupAnnouncements(response); - res(response); - return; - } - rej(response.error); - }) - .catch((error) => { - rej(error.message || "An error occurred"); - }); - + window + .sendMessage('getGroupNotificationTimestamp') + .then((response) => { + if (!response?.error) { + setGroupAnnouncements(response); + res(response); + return; + } + rej(response.error); + }) + .catch((error) => { + rej(error.message || 'An error occurred'); + }); }); - } catch (error) {} + } catch (error) { + console.log(error); + } }; - useEffect(()=> { - if(myAddress){ - getGroupAnnouncements() - getTimestampEnterChat() + useEffect(() => { + if (myAddress) { + getGroupAnnouncements(); + getTimestampEnterChat(); } - }, [myAddress]) + }, [myAddress]); const getGroupOwner = async (groupId) => { try { @@ -589,12 +612,11 @@ export const Group = ({ data.name = name; } setGroupOwner(data); - } catch (error) {} + } catch (error) { + console.log(error); + } }; - - - const directChatHasUnread = useMemo(() => { let hasUnread = false; directs.forEach((direct) => { @@ -615,13 +637,12 @@ export const Group = ({ const groupChatHasUnread = useMemo(() => { let hasUnread = false; groups.forEach((group) => { - - if ( group?.data && group?.sender !== myAddress && - group?.timestamp && groupChatTimestamps[group?.groupId] && - ((!timestampEnterData[group?.groupId] && + group?.timestamp && + groupChatTimestamps[group?.groupId] && + ((!timestampEnterData[group?.groupId] && Date.now() - group?.timestamp < timeDifferenceForNotificationChats) || timestampEnterData[group?.groupId] < group?.timestamp) ) { @@ -644,285 +665,352 @@ export const Group = ({ return hasUnread; }, [groupAnnouncements, groups]); + const getSecretKey = useCallback( + async (loadingGroupParam?: boolean, secretKeyToPublish?: boolean) => { + try { + setIsLoadingGroupMessage('Locating encryption keys'); + pauseAllQueues(); + let dataFromStorage; + let publishFromStorage; + let adminsFromStorage; - - const getSecretKey = async ( - loadingGroupParam?: boolean, - secretKeyToPublish?: boolean - ) => { - try { - setIsLoadingGroupMessage("Locating encryption keys"); - pauseAllQueues(); - let dataFromStorage; - let publishFromStorage; - let adminsFromStorage; - if ( - secretKeyToPublish && - secretKey && - lastFetchedSecretKey.current - && - Date.now() - lastFetchedSecretKey.current < 600000 - ) - return secretKey; - if (loadingGroupParam) { - setIsLoadingGroup(true); - } - if (selectedGroup?.groupId !== selectedGroupRef.current.groupId) { - if (settimeoutForRefetchSecretKey.current) { - clearTimeout(settimeoutForRefetchSecretKey.current); + if ( + secretKeyToPublish && + secretKey && + lastFetchedSecretKey.current && + Date.now() - lastFetchedSecretKey.current < 600000 + ) { + return secretKey; } - return; + + if (loadingGroupParam) { + setIsLoadingGroup(true); + } + + if (selectedGroup?.groupId !== selectedGroupRef.current.groupId) { + if (settimeoutForRefetchSecretKey.current) { + clearTimeout(settimeoutForRefetchSecretKey.current); + } + return; + } + + const prevGroupId = selectedGroupRef.current.groupId; + + const { names, addresses, both } = + adminsFromStorage || (await getGroupAdmins(selectedGroup?.groupId)); + setAdmins(addresses); + setAdminsWithNames(both); + + if (!names.length) throw new Error('Network error'); + + const publish = + publishFromStorage || + (await getPublishesFromAdmins(names, selectedGroup?.groupId)); + + if (prevGroupId !== selectedGroupRef.current.groupId) { + if (settimeoutForRefetchSecretKey.current) { + clearTimeout(settimeoutForRefetchSecretKey.current); + } + return; + } + + if (publish === false) { + setTriedToFetchSecretKey(true); + settimeoutForRefetchSecretKey.current = setTimeout(() => { + getSecretKey(); + }, 120000); + return false; + } + + setSecretKeyPublishDate(publish?.updated || publish?.created); + + let data; + if (dataFromStorage) { + data = dataFromStorage; + } else { + setIsLoadingGroupMessage('Downloading encryption keys'); + const res = await fetch( + `${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${publish.identifier}?encoding=base64&rebuild=true` + ); + data = await res.text(); + } + + const decryptedKey: any = await decryptResource(data); + const dataint8Array = base64ToUint8Array(decryptedKey.data); + const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); + + if (!validateSecretKey(decryptedKeyToObject)) { + throw new Error('SecretKey is not valid'); + } + + setSecretKeyDetails(publish); + setSecretKey(decryptedKeyToObject); + lastFetchedSecretKey.current = Date.now(); + setMemberCountFromSecretKeyData(decryptedKey.count); + + window + .sendMessage('setGroupData', { + groupId: selectedGroup?.groupId, + secretKeyData: data, + secretKeyResource: publish, + admins: { names, addresses, both }, + }) + .catch((error) => { + console.error( + 'Failed to set group data:', + error.message || 'An error occurred' + ); + }); + + if (decryptedKeyToObject) { + setTriedToFetchSecretKey(true); + setFirstSecretKeyInCreation(false); + return decryptedKeyToObject; + } else { + setTriedToFetchSecretKey(true); + } + } catch (error) { + if (error === 'Unable to decrypt data') { + setTriedToFetchSecretKey(true); + settimeoutForRefetchSecretKey.current = setTimeout(() => { + getSecretKey(); + }, 120000); + } + } finally { + setIsLoadingGroup(false); + setIsLoadingGroupMessage(''); + resumeAllQueues(); } - const prevGroupId = selectedGroupRef.current.groupId; - // const validApi = await findUsableApi(); - const { names, addresses, both } = - adminsFromStorage || (await getGroupAdmins(selectedGroup?.groupId)); + }, + [ + secretKey, + selectedGroup?.groupId, + setIsLoadingGroup, + setIsLoadingGroupMessage, + setSecretKey, + setSecretKeyDetails, + setTriedToFetchSecretKey, + setFirstSecretKeyInCreation, + setMemberCountFromSecretKeyData, + setAdmins, + setAdminsWithNames, + setSecretKeyPublishDate, + ] + ); + + const getAdminsForPublic = async (selectedGroup) => { + try { + const { names, addresses, both } = await getGroupAdmins( + selectedGroup?.groupId + ); setAdmins(addresses); setAdminsWithNames(both); - if (!names.length) { - throw new Error("Network error"); - } - const publish = - publishFromStorage || (await getPublishesFromAdmins(names, selectedGroup?.groupId)); - - if (prevGroupId !== selectedGroupRef.current.groupId) { - if (settimeoutForRefetchSecretKey.current) { - clearTimeout(settimeoutForRefetchSecretKey.current); - } - return; - } - if (publish === false) { - setTriedToFetchSecretKey(true); - settimeoutForRefetchSecretKey.current = setTimeout(() => { - getSecretKey(); - }, 120000); - return false; - } - setSecretKeyPublishDate(publish?.updated || publish?.created); - let data; - if (dataFromStorage) { - data = dataFromStorage; - } else { - // const shouldRebuild = !secretKeyPublishDate || (publish?.update && publish?.updated > secretKeyPublishDate) - setIsLoadingGroupMessage("Downloading encryption keys"); - const res = await fetch( - `${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${ - publish.identifier - }?encoding=base64&rebuild=true` - ); - data = await res.text(); - } - const decryptedKey: any = await decryptResource(data); - const dataint8Array = base64ToUint8Array(decryptedKey.data); - const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); - if (!validateSecretKey(decryptedKeyToObject)) - throw new Error("SecretKey is not valid"); - setSecretKeyDetails(publish); - setSecretKey(decryptedKeyToObject); - lastFetchedSecretKey.current = Date.now(); - setMemberCountFromSecretKeyData(decryptedKey.count); - window.sendMessage("setGroupData", { - groupId: selectedGroup?.groupId, - secretKeyData: data, - secretKeyResource: publish, - admins: { names, addresses, both }, - }).catch((error) => { - console.error("Failed to set group data:", error.message || "An error occurred"); - }); - - if (decryptedKeyToObject) { - setTriedToFetchSecretKey(true); - setFirstSecretKeyInCreation(false); - return decryptedKeyToObject; - } else { - setTriedToFetchSecretKey(true); - } } catch (error) { - if (error === "Unable to decrypt data") { - setTriedToFetchSecretKey(true); - settimeoutForRefetchSecretKey.current = setTimeout(() => { - getSecretKey(); - }, 120000); - } - } finally { - setIsLoadingGroup(false); - setIsLoadingGroupMessage(""); - resumeAllQueues(); + console.log(error); } }; - - const getAdminsForPublic = async(selectedGroup)=> { - try { - const { names, addresses, both } = - await getGroupAdmins(selectedGroup?.groupId) - setAdmins(addresses); - setAdminsWithNames(both); - } catch (error) { - //error - } - } - useEffect(() => { if (selectedGroup && isPrivate !== null) { - if(isPrivate){ + if (isPrivate) { setTriedToFetchSecretKey(false); getSecretKey(true); } - + getGroupOwner(selectedGroup?.groupId); } - if(isPrivate === false){ + if (isPrivate === false) { setTriedToFetchSecretKey(true); - if(selectedGroup?.groupId !== '0'){ - getAdminsForPublic(selectedGroup) + if (selectedGroup?.groupId !== '0') { + getAdminsForPublic(selectedGroup); } - - } }, [selectedGroup, isPrivate]); - - - - - const getCountNewMesg = async (groupId, after)=> { + const getCountNewMesg = async (groupId, after) => { try { const response = await fetch( `${getBaseApiReact()}/chat/messages?after=${after}&txGroupId=${groupId}&haschatreference=false&encoding=BASE64&limit=1` ); const data = await response.json(); - if(data && data[0]) return data[0].timestamp + if (data && data[0]) return data[0].timestamp; } catch (error) { - + console.log(error); } - } + }; - const getLatestRegularChat = async (groups)=> { + const getLatestRegularChat = async (groups) => { try { - - const groupData = {} + const groupData = {}; - const getGroupData = groups.map(async(group)=> { - if(!group.groupId || !group?.timestamp) return null - if((!groupData[group.groupId] || groupData[group.groupId] < group.timestamp)){ - const hasMoreRecentMsg = await getCountNewMesg(group.groupId, timestampEnterDataRef.current[group?.groupId] || Date.now() - 24 * 60 * 60 * 1000) - if(hasMoreRecentMsg){ - groupData[group.groupId] = hasMoreRecentMsg + const getGroupData = groups.map(async (group) => { + if (!group.groupId || !group?.timestamp) return null; + if ( + !groupData[group.groupId] || + groupData[group.groupId] < group.timestamp + ) { + const hasMoreRecentMsg = await getCountNewMesg( + group.groupId, + timestampEnterDataRef.current[group?.groupId] || + Date.now() - 24 * 60 * 60 * 1000 + ); + if (hasMoreRecentMsg) { + groupData[group.groupId] = hasMoreRecentMsg; } } else { - return null + return null; } - }) + }); - await Promise.all(getGroupData) - setGroupChatTimestamps(groupData) + await Promise.all(getGroupData); + setGroupChatTimestamps(groupData); } catch (error) { - + console.log(error); } - } + }; - const getGroupsProperties = useCallback(async(address)=> { + const getOwnerNameForGroup = async (owner: string, groupId: string) => { + try { + if (!owner) return; + if (groupsOwnerNamesRef.current[groupId]) return; + const name = await requestQueueMemberNames.enqueue(() => { + return getNameInfo(owner); + }); + if (name) { + groupsOwnerNamesRef.current[groupId] = name; + setGroupsOwnerNames((prev) => { + return { ...prev, [groupId]: name }; + }); + } + } catch (error) { + console.error(error); + } + }; + + const getGroupsProperties = useCallback(async (address) => { try { const url = `${getBaseApiReact()}/groups/member/${address}`; const response = await fetch(url); - if(!response.ok) throw new Error('Cannot get group properties') + if (!response.ok) throw new Error('Cannot get group properties'); let data = await response.json(); - const transformToObject = data.reduce((result, item) => { - - result[item.groupId] = item - return result; - }, {}); - setGroupsProperties(transformToObject) + const transformToObject = data.reduce((result, item) => { + result[item.groupId] = item; + return result; + }, {}); + setGroupsProperties(transformToObject); + Object.keys(transformToObject).forEach((key) => { + getOwnerNameForGroup(transformToObject[key]?.owner || '', key); + }); } catch (error) { - // error + console.log(error); } - }, []) + }, []); - - useEffect(()=> { - if(!myAddress) return - if(areKeysEqual(groups?.map((grp)=> grp?.groupId), Object.keys(groupsProperties))){ + useEffect(() => { + if (!myAddress) return; + if ( + areKeysEqual( + groups?.map((grp) => grp?.groupId), + Object.keys(groupsProperties) + ) + ) { + // TODO: empty block. Check it! } else { - getGroupsProperties(myAddress) + getGroupsProperties(myAddress); } - }, [groups, myAddress]) - - + }, [groups, myAddress]); useEffect(() => { // Handler function for incoming messages const messageHandler = (event) => { if (event.origin !== window.location.origin) { - return; + return; } const message = event.data; - if (message?.action === "SET_GROUPS") { - + if (message?.action === 'SET_GROUPS') { // Update the component state with the received 'sendqort' state setGroups(sortArrayByTimestampAndGroupName(message.payload)); getLatestRegularChat(message.payload); - setMemberGroups(message.payload?.filter((item)=> item?.groupId !== '0')); - - if (selectedGroupRef.current && groupSectionRef.current === "chat") { - window.sendMessage("addTimestampEnterChat", { - timestamp: Date.now(), - groupId: selectedGroupRef.current.groupId, - }).catch((error) => { - console.error("Failed to add timestamp:", error.message || "An error occurred"); - }); + setMemberGroups( + message.payload?.filter((item) => item?.groupId !== '0') + ); + + if (selectedGroupRef.current && groupSectionRef.current === 'chat') { + window + .sendMessage('addTimestampEnterChat', { + timestamp: Date.now(), + groupId: selectedGroupRef.current.groupId, + }) + .catch((error) => { + console.error( + 'Failed to add timestamp:', + error.message || 'An error occurred' + ); + }); } - + if (selectedDirectRef.current) { - window.sendMessage("addTimestampEnterChat", { - timestamp: Date.now(), - groupId: selectedDirectRef.current.address, - }).catch((error) => { - console.error("Failed to add timestamp:", error.message || "An error occurred"); - }); + window + .sendMessage('addTimestampEnterChat', { + timestamp: Date.now(), + groupId: selectedDirectRef.current.address, + }) + .catch((error) => { + console.error( + 'Failed to add timestamp:', + error.message || 'An error occurred' + ); + }); } - + setTimeout(() => { getTimestampEnterChat(); }, 600); } - - if (message?.action === "SET_GROUP_ANNOUNCEMENTS") { + + if (message?.action === 'SET_GROUP_ANNOUNCEMENTS') { // Update the component state with the received 'sendqort' state setGroupAnnouncements(message.payload); - - if (selectedGroupRef.current && groupSectionRef.current === "announcement") { - window.sendMessage("addGroupNotificationTimestamp", { - timestamp: Date.now(), - groupId: selectedGroupRef.current.groupId, - }).catch((error) => { - console.error("Failed to add group notification timestamp:", error.message || "An error occurred"); - }); - + + if ( + selectedGroupRef.current && + groupSectionRef.current === 'announcement' + ) { + window + .sendMessage('addGroupNotificationTimestamp', { + timestamp: Date.now(), + groupId: selectedGroupRef.current.groupId, + }) + .catch((error) => { + console.error( + 'Failed to add group notification timestamp:', + error.message || 'An error occurred' + ); + }); + setTimeout(() => { getGroupAnnouncements(); }, 200); } } - - if (message?.action === "SET_DIRECTS") { + + if (message?.action === 'SET_DIRECTS') { // Update the component state with the received 'sendqort' state setDirects(message.payload); - } else if (message?.action === "PLAY_NOTIFICATION_SOUND") { + } else if (message?.action === 'PLAY_NOTIFICATION_SOUND') { // audio.play(); } }; - + // Attach the event listener - window.addEventListener("message", messageHandler); - + window.addEventListener('message', messageHandler); + // Clean up the event listener on component unmount return () => { - window.removeEventListener("message", messageHandler); + window.removeEventListener('message', messageHandler); }; }, []); - useEffect(() => { if ( @@ -933,11 +1021,12 @@ export const Group = ({ ) return; - window.sendMessage("setupGroupWebsocket", {}) - .catch((error) => { - console.error("Failed to setup group websocket:", error.message || "An error occurred"); - }); - + window.sendMessage('setupGroupWebsocket', {}).catch((error) => { + console.error( + 'Failed to setup group websocket:', + error.message || 'An error occurred' + ); + }); hasInitializedWebsocket.current = true; }, [myAddress, groups]); @@ -947,14 +1036,18 @@ export const Group = ({ const res = await getGroupMembers(groupId); if (groupId !== selectedGroupRef.current?.groupId) return; setMembers(res); - } catch (error) {} + } catch (error) { + console.log(error); + } }; + useEffect(() => { if ( !initiatedGetMembers.current && selectedGroup?.groupId && secretKey && - admins.includes(myAddress) && selectedGroup?.groupId !== '0' + admins.includes(myAddress) && + selectedGroup?.groupId !== '0' ) { // getAdmins(selectedGroup?.groupId); getMembers(selectedGroup?.groupId); @@ -1000,10 +1093,11 @@ export const Group = ({ try { setIsLoadingNotifyAdmin(true); await new Promise((res, rej) => { - window.sendMessage("notifyAdminRegenerateSecretKey", { - adminAddress: admin.address, - groupName: selectedGroup?.groupName, - }) + window + .sendMessage('notifyAdminRegenerateSecretKey', { + adminAddress: admin.address, + groupName: selectedGroup?.groupName, + }) .then((response) => { if (!response?.error) { res(response); @@ -1012,19 +1106,18 @@ export const Group = ({ rej(response.error); }) .catch((error) => { - rej(error.message || "An error occurred"); + rej(error.message || 'An error occurred'); }); - }); setInfoSnack({ - type: "success", - message: "Successfully sent notification.", + type: 'success', + message: 'Successfully sent notification.', }); setOpenSnack(true); } catch (error) { setInfoSnack({ - type: "error", - message: "Unable to send notification", + type: 'error', + message: 'Unable to send notification', }); } finally { setIsLoadingNotifyAdmin(false); @@ -1038,7 +1131,8 @@ export const Group = ({ if (!findGroup) return false; if (!findGroup?.data) return false; return ( - findGroup?.timestamp && groupChatTimestamps[findGroup?.groupId] && + findGroup?.timestamp && + groupChatTimestamps[findGroup?.groupId] && ((!timestampEnterData[selectedGroup?.groupId] && Date.now() - findGroup?.timestamp < timeDifferenceForNotificationChats) || @@ -1066,23 +1160,23 @@ export const Group = ({ return; } if (findDirect) { - if(!isMobile){ - setDesktopSideView("directs"); - setDesktopViewMode('home') - } else { - setMobileViewModeKeepOpen("messaging"); - } + setDesktopSideView('directs'); + setDesktopViewMode('home'); setSelectedDirect(null); setNewChat(false); - window.sendMessage("addTimestampEnterChat", { - timestamp: Date.now(), - groupId: findDirect.address, - }).catch((error) => { - console.error("Failed to add timestamp:", error.message || "An error occurred"); + window + .sendMessage('addTimestampEnterChat', { + timestamp: Date.now(), + groupId: findDirect.address, + }) + .catch((error) => { + console.error( + 'Failed to add timestamp:', + error.message || 'An error occurred' + ); }); - setTimeout(() => { setSelectedDirect(findDirect); @@ -1102,36 +1196,32 @@ export const Group = ({ ); if (findDirect) { - if(!isMobile){ - setDesktopSideView("directs"); - } else { - setMobileViewModeKeepOpen("messaging"); - } + setDesktopSideView('directs'); setSelectedDirect(null); setNewChat(false); - window.sendMessage("addTimestampEnterChat", { - timestamp: Date.now(), - groupId: findDirect.address, - }).catch((error) => { - console.error("Failed to add timestamp:", error.message || "An error occurred"); + window + .sendMessage('addTimestampEnterChat', { + timestamp: Date.now(), + groupId: findDirect.address, + }) + .catch((error) => { + console.error( + 'Failed to add timestamp:', + error.message || 'An error occurred' + ); }); - setTimeout(() => { setSelectedDirect(findDirect); getTimestampEnterChat(); }, 200); } else { - if(!isMobile){ - setDesktopSideView("directs"); - } else { - setMobileViewModeKeepOpen("messaging"); - } + setDesktopSideView('directs'); setNewChat(true); setTimeout(() => { - executeEvent("setDirectToValueNewChat", { + executeEvent('setDirectToValueNewChat', { directToValue: name || directAddress, }); }, 500); @@ -1139,41 +1229,50 @@ export const Group = ({ }; useEffect(() => { - subscribeToEvent("openDirectMessageInternal", openDirectChatFromInternal); + subscribeToEvent('openDirectMessageInternal', openDirectChatFromInternal); return () => { unsubscribeFromEvent( - "openDirectMessageInternal", + 'openDirectMessageInternal', openDirectChatFromInternal ); }; }, [directs, selectedDirect]); useEffect(() => { - subscribeToEvent("openDirectMessage", openDirectChatFromNotification); + subscribeToEvent('openDirectMessage', openDirectChatFromNotification); return () => { - unsubscribeFromEvent("openDirectMessage", openDirectChatFromNotification); + unsubscribeFromEvent('openDirectMessage', openDirectChatFromNotification); }; }, [directs, selectedDirect]); const handleMarkAsRead = (e) => { const { groupId } = e.detail; - window.sendMessage("addTimestampEnterChat", { - timestamp: Date.now(), - groupId, - }).catch((error) => { - console.error("Failed to add timestamp:", error.message || "An error occurred"); - }); - - - window.sendMessage("addGroupNotificationTimestamp", { + window + .sendMessage('addTimestampEnterChat', { timestamp: Date.now(), groupId, - }).catch((error) => { - console.error("Failed to add group notification timestamp:", error.message || "An error occurred"); - }); - + }) + .catch((error) => { + console.error( + 'Failed to add timestamp:', + error.message || 'An error occurred' + ); + }); + + window + .sendMessage('addGroupNotificationTimestamp', { + timestamp: Date.now(), + groupId, + }) + .catch((error) => { + console.error( + 'Failed to add group notification timestamp:', + error.message || 'An error occurred' + ); + }); + setTimeout(() => { getGroupAnnouncements(); getTimestampEnterChat(); @@ -1181,10 +1280,10 @@ export const Group = ({ }; useEffect(() => { - subscribeToEvent("markAsRead", handleMarkAsRead); + subscribeToEvent('markAsRead', handleMarkAsRead); return () => { - unsubscribeFromEvent("markAsRead", handleMarkAsRead); + unsubscribeFromEvent('markAsRead', handleMarkAsRead); }; }, []); @@ -1196,7 +1295,7 @@ export const Group = ({ setSecretKeyDetails(null); setNewEncryptionNotification(null); setMemberCountFromSecretKeyData(null); - setIsForceShowCreationKeyPopup(false) + setIsForceShowCreationKeyPopup(false); setSelectedGroup(null); setSelectedDirect(null); setGroups([]); @@ -1210,9 +1309,8 @@ export const Group = ({ setOpenAddGroup(false); setIsInitialGroups(false); setOpenManageMembers(false); - setMemberGroups([]); // Assuming you're clearing the context here as well setTimestampEnterData({}); - setChatMode("groups"); + setChatMode('groups'); setNewChat(false); setOpenSnack(false); setInfoSnack(null); @@ -1220,10 +1318,10 @@ export const Group = ({ setIsLoadingGroups(false); setIsLoadingGroup(false); setFirstSecretKeyInCreation(false); - setGroupSection("home"); + setGroupSection('home'); setGroupAnnouncements({}); setDefaultThread(null); - setMobileViewMode("home"); + setMobileViewMode('home'); // Reset all useRef values to their initial states hasInitialized.current = false; hasInitializedWebsocket.current = false; @@ -1237,9 +1335,7 @@ export const Group = ({ setupGroupWebsocketInterval.current = null; settimeoutForRefetchSecretKey.current = null; initiatedGetMembers.current = false; - if(!isMobile){ - setDesktopViewMode('home') - } + setDesktopViewMode('home'); }; const logoutEventFunc = () => { @@ -1248,57 +1344,25 @@ export const Group = ({ }; useEffect(() => { - subscribeToEvent("logout-event", logoutEventFunc); + subscribeToEvent('logout-event', logoutEventFunc); return () => { - unsubscribeFromEvent("logout-event", logoutEventFunc); + unsubscribeFromEvent('logout-event', logoutEventFunc); }; }, []); const openAppsMode = () => { - if (isMobile) { - setMobileViewMode("apps"); - } - if (!isMobile) { - setDesktopViewMode('apps') - - } - if(isMobile){ - setIsOpenSideViewDirects(false) - setIsOpenSideViewGroups(false) - setGroupSection("default"); - setSelectedGroup(null); - setNewChat(false); - setSelectedDirect(null); - setSecretKey(null); - setGroupOwner(null) - lastFetchedSecretKey.current = null; - initiatedGetMembers.current = false; - setSecretKeyPublishDate(null); - setAdmins([]); - setSecretKeyDetails(null); - setAdminsWithNames([]); - setMembers([]); - setMemberCountFromSecretKeyData(null); - setTriedToFetchSecretKey(false); - setFirstSecretKeyInCreation(false); - setIsOpenSideViewDirects(false) - setIsOpenSideViewGroups(false) - } - - + setDesktopViewMode('apps'); }; useEffect(() => { - subscribeToEvent("open-apps-mode", openAppsMode); + subscribeToEvent('open-apps-mode', openAppsMode); return () => { - unsubscribeFromEvent("open-apps-mode", openAppsMode); + unsubscribeFromEvent('open-apps-mode', openAppsMode); }; }, []); - - const openGroupChatFromNotification = (e) => { if (isLoadingOpenSectionFromNotification.current) return; @@ -1306,18 +1370,18 @@ export const Group = ({ const findGroup = groups?.find((group) => +group?.groupId === +groupId); if (findGroup?.groupId === selectedGroup?.groupId) { isLoadingOpenSectionFromNotification.current = false; - setChatMode("groups"); - setDesktopViewMode('chat') + setChatMode('groups'); + setDesktopViewMode('chat'); return; } if (findGroup) { - setChatMode("groups"); + setChatMode('groups'); setSelectedGroup(null); setSelectedDirect(null); setNewChat(false); setSecretKey(null); - setGroupOwner(null) + setGroupOwner(null); lastFetchedSecretKey.current = null; initiatedGetMembers.current = false; setSecretKeyPublishDate(null); @@ -1326,26 +1390,28 @@ export const Group = ({ setAdminsWithNames([]); setMembers([]); setMemberCountFromSecretKeyData(null); - setIsForceShowCreationKeyPopup(false) + setIsForceShowCreationKeyPopup(false); setTriedToFetchSecretKey(false); setFirstSecretKeyInCreation(false); - setGroupSection("chat"); - if(!isMobile){ - setDesktopViewMode('chat') - } + setGroupSection('chat'); + setDesktopViewMode('chat'); - window.sendMessage("addTimestampEnterChat", { - timestamp: Date.now(), - groupId: findGroup.groupId, - }).catch((error) => { - console.error("Failed to add timestamp:", error.message || "An error occurred"); + window + .sendMessage('addTimestampEnterChat', { + timestamp: Date.now(), + groupId: findGroup.groupId, + }) + .catch((error) => { + console.error( + 'Failed to add timestamp:', + error.message || 'An error occurred' + ); }); - setTimeout(() => { setSelectedGroup(findGroup); - setMobileViewMode("group"); - setDesktopSideView('groups') + setMobileViewMode('group'); + setDesktopSideView('groups'); getTimestampEnterChat(); isLoadingOpenSectionFromNotification.current = false; }, 350); @@ -1355,10 +1421,10 @@ export const Group = ({ }; useEffect(() => { - subscribeToEvent("openGroupMessage", openGroupChatFromNotification); + subscribeToEvent('openGroupMessage', openGroupChatFromNotification); return () => { - unsubscribeFromEvent("openGroupMessage", openGroupChatFromNotification); + unsubscribeFromEvent('openGroupMessage', openGroupChatFromNotification); }; }, [groups, selectedGroup]); @@ -1368,10 +1434,10 @@ export const Group = ({ const findGroup = groups?.find((group) => +group?.groupId === +groupId); if (findGroup?.groupId === selectedGroup?.groupId) return; if (findGroup) { - setChatMode("groups"); + setChatMode('groups'); setSelectedGroup(null); setSecretKey(null); - setGroupOwner(null) + setGroupOwner(null); lastFetchedSecretKey.current = null; initiatedGetMembers.current = false; setSecretKeyPublishDate(null); @@ -1380,24 +1446,27 @@ export const Group = ({ setAdminsWithNames([]); setMembers([]); setMemberCountFromSecretKeyData(null); - setIsForceShowCreationKeyPopup(false) + setIsForceShowCreationKeyPopup(false); setTriedToFetchSecretKey(false); setFirstSecretKeyInCreation(false); - setGroupSection("announcement"); - if(!isMobile){ - setDesktopViewMode('chat') - } - window.sendMessage("addGroupNotificationTimestamp", { - timestamp: Date.now(), - groupId: findGroup.groupId, - }).catch((error) => { - console.error("Failed to add group notification timestamp:", error.message || "An error occurred"); + setGroupSection('announcement'); + setDesktopViewMode('chat'); + window + .sendMessage('addGroupNotificationTimestamp', { + timestamp: Date.now(), + groupId: findGroup.groupId, + }) + .catch((error) => { + console.error( + 'Failed to add group notification timestamp:', + error.message || 'An error occurred' + ); }); - + setTimeout(() => { setSelectedGroup(findGroup); - setMobileViewMode("group"); - setDesktopSideView('groups') + setMobileViewMode('group'); + setDesktopSideView('groups'); getGroupAnnouncements(); }, 350); } @@ -1405,13 +1474,13 @@ export const Group = ({ useEffect(() => { subscribeToEvent( - "openGroupAnnouncement", + 'openGroupAnnouncement', openGroupAnnouncementFromNotification ); return () => { unsubscribeFromEvent( - "openGroupAnnouncement", + 'openGroupAnnouncement', openGroupAnnouncementFromNotification ); }; @@ -1422,16 +1491,16 @@ export const Group = ({ const { groupId } = data; const findGroup = groups?.find((group) => +group?.groupId === +groupId); if (findGroup?.groupId === selectedGroup?.groupId) { - setGroupSection("forum"); + setGroupSection('forum'); setDefaultThread(data); return; } if (findGroup) { - setChatMode("groups"); + setChatMode('groups'); setSelectedGroup(null); setSecretKey(null); - setGroupOwner(null) + setGroupOwner(null); lastFetchedSecretKey.current = null; initiatedGetMembers.current = false; setSecretKeyPublishDate(null); @@ -1440,54 +1509,45 @@ export const Group = ({ setAdminsWithNames([]); setMembers([]); setMemberCountFromSecretKeyData(null); - setIsForceShowCreationKeyPopup(false) + setIsForceShowCreationKeyPopup(false); setTriedToFetchSecretKey(false); setFirstSecretKeyInCreation(false); - setGroupSection("forum"); + setGroupSection('forum'); setDefaultThread(data); - if(!isMobile){ - setDesktopViewMode('chat') - } + setDesktopViewMode('chat'); setTimeout(() => { setSelectedGroup(findGroup); - setMobileViewMode("group"); - setDesktopSideView('groups') + setMobileViewMode('group'); + setDesktopSideView('groups'); getGroupAnnouncements(); }, 350); } }; useEffect(() => { - subscribeToEvent("openThreadNewPost", openThreadNewPostFunc); + subscribeToEvent('openThreadNewPost', openThreadNewPostFunc); return () => { - unsubscribeFromEvent("openThreadNewPost", openThreadNewPostFunc); + unsubscribeFromEvent('openThreadNewPost', openThreadNewPostFunc); }; }, [groups, selectedGroup]); - const handleSecretKeyCreationInProgress = () => { + const handleSecretKeyCreationInProgress = useCallback(() => { setFirstSecretKeyInCreation(true); - }; + }, []); const goToHome = async () => { - if (isMobile) { - setMobileViewMode("home"); - } - if (!isMobile) { - } - setDesktopViewMode('home') - + setDesktopViewMode('home'); await new Promise((res) => { setTimeout(() => { res(null); }, 200); }); - }; const goToAnnouncements = async () => { - setGroupSection("default"); + setGroupSection('default'); await new Promise((res) => { setTimeout(() => { res(null); @@ -1495,14 +1555,19 @@ export const Group = ({ }); setSelectedDirect(null); setNewChat(false); - setGroupSection("announcement"); - window.sendMessage("addGroupNotificationTimestamp", { - timestamp: Date.now(), - groupId: selectedGroupRef.current.groupId, - }).catch((error) => { - console.error("Failed to add group notification timestamp:", error.message || "An error occurred"); + setGroupSection('announcement'); + window + .sendMessage('addGroupNotificationTimestamp', { + timestamp: Date.now(), + groupId: selectedGroupRef.current.groupId, + }) + .catch((error) => { + console.error( + 'Failed to add group notification timestamp:', + error.message || 'An error occurred' + ); }); - + setTimeout(() => { getGroupAnnouncements(); }, 200); @@ -1510,33 +1575,37 @@ export const Group = ({ const openDrawerGroups = () => { setIsOpenDrawer(true); - setDrawerMode("groups"); + setDrawerMode('groups'); }; const goToThreads = () => { setSelectedDirect(null); setNewChat(false); - setGroupSection("forum"); + setGroupSection('forum'); }; const goToChat = async () => { - setGroupSection("default"); + setGroupSection('default'); await new Promise((res) => { setTimeout(() => { res(null); }, 200); }); - setGroupSection("chat"); + setGroupSection('chat'); setNewChat(false); setSelectedDirect(null); if (selectedGroupRef.current) { - window.sendMessage("addTimestampEnterChat", { - timestamp: Date.now(), - groupId: selectedGroupRef.current.groupId, - }).catch((error) => { - console.error("Failed to add timestamp:", error.message || "An error occurred"); + window + .sendMessage('addTimestampEnterChat', { + timestamp: Date.now(), + groupId: selectedGroupRef.current.groupId, + }) + .catch((error) => { + console.error( + 'Failed to add timestamp:', + error.message || 'An error occurred' + ); }); - setTimeout(() => { getTimestampEnterChat(); @@ -1544,96 +1613,105 @@ export const Group = ({ } }; - + const theme = useTheme(); const renderDirects = () => { return (
- {!isMobile && ( - - { - setDesktopSideView("groups"); - }} - > - - - - - { - setDesktopSideView("directs"); - }} - > - + { + setDesktopSideView('groups'); + }} + > + - - - - - )} - + > + + + + { + setDesktopSideView('directs'); + }} + > + + + + + +
{directs.map((direct: any) => ( { - console.error("Failed to add timestamp:", error.message || "An error occurred"); + window + .sendMessage('addTimestampEnterChat', { + timestamp: Date.now(), + groupId: direct.address, + }) + .catch((error) => { + console.error( + 'Failed to add timestamp:', + error.message || 'An error occurred' + ); }); - + setTimeout(() => { setSelectedDirect(direct); @@ -1657,29 +1740,29 @@ export const Group = ({ }, 200); }} sx={{ - display: "flex", - width: "100%", - flexDirection: "column", - cursor: "pointer", - border: "1px #232428 solid", - padding: "2px", - borderRadius: "2px", + display: 'flex', + width: '100%', + flexDirection: 'column', + cursor: 'pointer', + padding: '2px', + borderRadius: '2px', background: - direct?.address === selectedDirect?.address && "white", + direct?.address === selectedDirect?.address && + theme.palette.background.default, }} > @@ -1688,28 +1771,33 @@ export const Group = ({ {direct?.sender !== myAddress && @@ -1721,7 +1809,7 @@ export const Group = ({ direct?.timestamp) && ( )} @@ -1732,10 +1820,10 @@ export const Group = ({
New Chat @@ -1757,354 +1845,105 @@ export const Group = ({ ); }; + const selectGroupFunc = useCallback((group) => { + setMobileViewMode('group'); + setDesktopSideView('groups'); + initiatedGetMembers.current = false; + clearAllQueues(); + setSelectedDirect(null); + setTriedToFetchSecretKey(false); + setNewChat(false); + setSelectedGroup(null); + setUserInfoForLevels({}); + setSecretKey(null); + lastFetchedSecretKey.current = null; + setSecretKeyPublishDate(null); + setAdmins([]); + setSecretKeyDetails(null); + setAdminsWithNames([]); + setGroupOwner(null); + setMembers([]); + setMemberCountFromSecretKeyData(null); + setHideCommonKeyPopup(false); + setFirstSecretKeyInCreation(false); + setGroupSection('chat'); + setIsOpenDrawer(false); + setIsForceShowCreationKeyPopup(false); + setTimeout(() => { + setSelectedGroup(group); + }, 200); + }, []); - const renderGroups = () => { - return ( -
- {!isMobile && ( - - { - setDesktopSideView("groups"); - }} - > - - - - - { - setDesktopSideView("directs"); - }} - > - - - - - - )} - -
- {groups.map((group: any) => ( - - { - setMobileViewMode("group"); - setDesktopSideView('groups') - initiatedGetMembers.current = false; - clearAllQueues(); - setSelectedDirect(null); - setTriedToFetchSecretKey(false); - setNewChat(false); - setSelectedGroup(null); - setUserInfoForLevels({}) - setSecretKey(null); - lastFetchedSecretKey.current = null; - setSecretKeyPublishDate(null); - setAdmins([]); - setSecretKeyDetails(null); - setAdminsWithNames([]); - setGroupOwner(null) - setMembers([]); - setMemberCountFromSecretKeyData(null); - setHideCommonKeyPopup(false); - setFirstSecretKeyInCreation(false); - setGroupSection("chat"); - setIsOpenDrawer(false); - setIsForceShowCreationKeyPopup(false) - setTimeout(() => { - setSelectedGroup(group); - - }, 200); - }} - sx={{ - display: "flex", - width: "100%", - flexDirection: "column", - cursor: "pointer", - border: "1px #232428 solid", - padding: "2px", - borderRadius: "2px", - background: - group?.groupId === selectedGroup?.groupId && "white", - }} - > - - - - {groupsProperties[group?.groupId]?.isOpen === false ? ( - - - - ): ( - - - - // - // {group.groupName?.charAt(0)} - // - )} - - - - {groupAnnouncements[group?.groupId] && - !groupAnnouncements[group?.groupId]?.seentimestamp && ( - - )} - {group?.data && - groupChatTimestamps[group?.groupId] && - group?.sender !== myAddress && - group?.timestamp && - ((!timestampEnterData[group?.groupId] && - Date.now() - group?.timestamp < - timeDifferenceForNotificationChats) || - timestampEnterData[group?.groupId] < - group?.timestamp) && ( - - )} - - - - - ))} -
-
- {chatMode === "groups" && ( - <> - { - setOpenAddGroup(true); - }} - > - - Group Mgmt - - {!isRunningPublicNode && ( - { - setIsOpenBlockedUserModal(true); - }} - sx={{ - minWidth: 'unset', - padding: '10px' - }} - > - - - )} - - - )} - {chatMode === "directs" && ( - { - setNewChat(true); - setSelectedDirect(null); - setIsOpenDrawer(false); - }} - > - - New Chat - - )} -
-
- ); - }; - return ( <> + - - -
- {!isMobile && ((desktopViewMode !== 'apps' && desktopViewMode !== 'dev') || isOpenSideViewGroups) && ( - + {((desktopViewMode !== 'apps' && desktopViewMode !== 'dev') || + isOpenSideViewGroups) && ( + )} - {!isMobile && desktopViewMode === 'chat' && desktopSideView !== 'directs' && renderGroups()} - {!isMobile && desktopViewMode === 'chat' && desktopSideView === 'directs' && renderDirects()} + {desktopViewMode === 'chat' && desktopSideView !== 'directs' && ( + + )} + + {desktopViewMode === 'chat' && + desktopSideView === 'directs' && + renderDirects()} - {newChat && ( <> - {isMobile && ( - - - - { - close() - }} - > - - - - - - { - setSelectedDirect(null) - setMobileViewModeKeepOpen('') - }} - > - - - - - - )} { setSelectedDirect(null); - setNewChat(false); }} setMobileViewModeKeepOpen={setMobileViewModeKeepOpen} @@ -2208,292 +1986,312 @@ export const Group = ({ )} {desktopViewMode === 'chat' && !selectedGroup && ( - - No group selected - - - )} - -
- {!isMobile && ( - - - - )} - - - - - {triedToFetchSecretKey && ( - + + )} + +
+ + + + {triedToFetchSecretKey && ( + + )} + {isPrivate && + firstSecretKeyInCreation && + triedToFetchSecretKey && + !secretKeyPublishDate && ( +
+ {' '} + + {t('group:message.generic.encryption_key', { + postProcess: 'capitalize', + })} + +
+ )} + {isPrivate && + !admins.includes(myAddress) && + !secretKey && + triedToFetchSecretKey ? ( + <> + {secretKeyPublishDate || + (!secretKeyPublishDate && !firstSecretKeyInCreation) ? ( +
+ {' '} + + {t('group:message.generic.not_part_group', { + postProcess: 'capitalize', + })} + + + + + {t('group:message.generic.only_encrypted', { + postProcess: 'capitalize', + })} + + + + + {t('group:message.generic.notify_admins', { + postProcess: 'capitalize', + })} + + + {adminsWithNames.map((admin) => { + return ( + + {admin?.name} + notifyAdmin(admin)} + > + {t('core:action.notify', { + postProcess: 'capitalize', + })} + + + ); + })} +
+ ) : null} + + ) : admins.includes(myAddress) && + !secretKey && + isPrivate && + triedToFetchSecretKey ? null : !triedToFetchSecretKey ? null : ( + <> + + + {groupSection === 'adminSpace' && ( + + )} + + )} + + + {((isPrivate && + admins.includes(myAddress) && + shouldReEncrypt && + triedToFetchSecretKey && + !firstSecretKeyInCreation && + !hideCommonKeyPopup) || + isForceShowCreationKeyPopup) && ( + )} - {isPrivate && firstSecretKeyInCreation && - triedToFetchSecretKey && - !secretKeyPublishDate && ( -
- {" "} - - The group's first common encryption key is in the - process of creation. Please wait a few minutes for it to - be retrieved by the network. Checking every 2 minutes... - -
- )} - {isPrivate && !admins.includes(myAddress) && - !secretKey && - triedToFetchSecretKey ? ( - <> - {secretKeyPublishDate || - (!secretKeyPublishDate && !firstSecretKeyInCreation) ? ( -
- {" "} - - You are not part of the encrypted group of members. - Wait until an admin re-encrypts the keys. - - - - Only unencrypted messages will be displayed. - - - - Try notifying an admin from the list of admins below: - - - {adminsWithNames.map((admin) => { - return ( - - {admin?.name} - notifyAdmin(admin)} - > - Notify - - - ); - })} -
- ) : null} - - ) : admins.includes(myAddress) && - (!secretKey && isPrivate) && - triedToFetchSecretKey ? null : !triedToFetchSecretKey ? null : ( - <> - - - {groupSection === "adminSpace" && ( - - )} - - - )} - - - {((isPrivate && admins.includes(myAddress) && - shouldReEncrypt && - triedToFetchSecretKey && - !firstSecretKeyInCreation && - !hideCommonKeyPopup) || isForceShowCreationKeyPopup) && ( - - )} -
- {openManageMembers && ( - - )} -
- - + + {openManageMembers && ( + + )} +
+ {selectedDirect && !newChat && ( <> )} - - {!isMobile && ( - - )} - {!isMobile && ( - - )} - - - {!isMobile && ( - - - - )} + - + + + - + - - + > @@ -2583,5 +2406,3 @@ export const Group = ({ ); }; - - diff --git a/src/components/Group/GroupInvites.tsx b/src/components/Group/GroupInvites.tsx index 13e5850..ca3f32c 100644 --- a/src/components/Group/GroupInvites.tsx +++ b/src/components/Group/GroupInvites.tsx @@ -1,30 +1,24 @@ -import * as React from "react"; -import List from "@mui/material/List"; -import ListItem from "@mui/material/ListItem"; -import ListItemButton from "@mui/material/ListItemButton"; -import ListItemIcon from "@mui/material/ListItemIcon"; -import ListItemText from "@mui/material/ListItemText"; -import Checkbox from "@mui/material/Checkbox"; -import IconButton from "@mui/material/IconButton"; -import CommentIcon from "@mui/icons-material/Comment"; -import InfoIcon from "@mui/icons-material/Info"; -import GroupAddIcon from "@mui/icons-material/GroupAdd"; -import { executeEvent } from "../../utils/events"; -import { Box, ButtonBase, Collapse, Typography } from "@mui/material"; -import { Spacer } from "../../common/Spacer"; -import { getGroupNames } from "./UserListOfInvites"; -import { CustomLoader } from "../../common/CustomLoader"; -import { getBaseApiReact, isMobile } from "../../App"; -import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; -import ExpandLessIcon from "@mui/icons-material/ExpandLess"; +import { useEffect, useState } from 'react'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemText from '@mui/material/ListItemText'; +import IconButton from '@mui/material/IconButton'; +import GroupAddIcon from '@mui/icons-material/GroupAdd'; +import { executeEvent } from '../../utils/events'; +import { Box, ButtonBase, Collapse, Typography, useTheme } from '@mui/material'; +import { getGroupNames } from './UserListOfInvites'; +import { CustomLoader } from '../../common/CustomLoader'; +import { getBaseApiReact } from '../../App'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ExpandLessIcon from '@mui/icons-material/ExpandLess'; +import { useTranslation } from 'react-i18next'; export const GroupInvites = ({ myAddress, setOpenAddGroup }) => { - const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState( - [] - ); - const [isExpanded, setIsExpanded] = React.useState(false); + const [groupsWithJoinRequests, setGroupsWithJoinRequests] = useState([]); + const [isExpanded, setIsExpanded] = useState(false); - const [loading, setLoading] = React.useState(true); + const [loading, setLoading] = useState(true); const getJoinRequests = async () => { try { @@ -37,12 +31,16 @@ export const GroupInvites = ({ myAddress, setOpenAddGroup }) => { setGroupsWithJoinRequests(resMoreData); } catch (error) { + console.log(error); } finally { setLoading(false); } }; - React.useEffect(() => { + const { t } = useTranslation(['core', 'group']); + const theme = useTheme(); + + useEffect(() => { if (myAddress) { getJoinRequests(); } @@ -51,57 +49,65 @@ export const GroupInvites = ({ myAddress, setOpenAddGroup }) => { return ( setIsExpanded((prev)=> !prev)} + onClick={() => setIsExpanded((prev) => !prev)} > - Group Invites {groupsWithJoinRequests?.length > 0 && ` (${groupsWithJoinRequests?.length})`} + {t('group:group.invites', { postProcess: 'capitalize' })}{' '} + {groupsWithJoinRequests?.length > 0 && + ` (${groupsWithJoinRequests?.length})`} - {isExpanded ? : ( - - )} + {isExpanded ? ( + + ) : ( + + )} + {loading && groupsWithJoinRequests.length === 0 && ( @@ -110,31 +116,33 @@ export const GroupInvites = ({ myAddress, setOpenAddGroup }) => { {!loading && groupsWithJoinRequests.length === 0 && ( - Nothing to display + {t('group:message.generic.no_display', { + postProcess: 'capitalize', + })} )} @@ -142,13 +150,13 @@ export const GroupInvites = ({ myAddress, setOpenAddGroup }) => { return ( { setOpenAddGroup(true); setTimeout(() => { - executeEvent("openGroupInvitesRequest", {}); + executeEvent('openGroupInvitesRequest', {}); }, 300); }} disablePadding @@ -156,8 +164,8 @@ export const GroupInvites = ({ myAddress, setOpenAddGroup }) => { @@ -166,12 +174,15 @@ export const GroupInvites = ({ myAddress, setOpenAddGroup }) => { diff --git a/src/components/Group/GroupJoinRequests.tsx b/src/components/Group/GroupJoinRequests.tsx index 76f958a..15304f5 100644 --- a/src/components/Group/GroupJoinRequests.tsx +++ b/src/components/Group/GroupJoinRequests.tsx @@ -1,244 +1,282 @@ -import * as React from "react"; -import List from "@mui/material/List"; -import ListItem from "@mui/material/ListItem"; -import ListItemButton from "@mui/material/ListItemButton"; -import ListItemIcon from "@mui/material/ListItemIcon"; -import ListItemText from "@mui/material/ListItemText"; -import Checkbox from "@mui/material/Checkbox"; -import IconButton from "@mui/material/IconButton"; -import CommentIcon from "@mui/icons-material/Comment"; -import InfoIcon from "@mui/icons-material/Info"; -import { RequestQueueWithPromise } from "../../utils/queue/queue"; +import * as React from 'react'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemText from '@mui/material/ListItemText'; +import IconButton from '@mui/material/IconButton'; +import { RequestQueueWithPromise } from '../../utils/queue/queue'; import GroupAddIcon from '@mui/icons-material/GroupAdd'; -import { executeEvent } from "../../utils/events"; -import { Box, ButtonBase, Collapse, Typography } from "@mui/material"; -import { Spacer } from "../../common/Spacer"; -import { CustomLoader } from "../../common/CustomLoader"; -import { getBaseApi } from "../../background"; -import { MyContext, getBaseApiReact, isMobile } from "../../App"; -import { myGroupsWhereIAmAdminAtom } from "../../atoms/global"; -import { useSetRecoilState } from "recoil"; +import { executeEvent } from '../../utils/events'; +import { Box, ButtonBase, Collapse, Typography, useTheme } from '@mui/material'; +import { CustomLoader } from '../../common/CustomLoader'; +import { getBaseApiReact } from '../../App'; +import { myGroupsWhereIAmAdminAtom, txListAtom } from '../../atoms/global'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandLessIcon from '@mui/icons-material/ExpandLess'; -export const requestQueueGroupJoinRequests = new RequestQueueWithPromise(2) +import { useTranslation } from 'react-i18next'; +import { useAtom, useSetAtom } from 'jotai'; +export const requestQueueGroupJoinRequests = new RequestQueueWithPromise(2); -export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, getTimestampEnterChat, setSelectedGroup, setGroupSection, setMobileViewMode, setDesktopViewMode }) => { - const [isExpanded, setIsExpanded] = React.useState(false) - - const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState([]) - const [loading, setLoading] = React.useState(true) - const {txList, setTxList} = React.useContext(MyContext) - const setMyGroupsWhereIAmAdmin = useSetRecoilState( - myGroupsWhereIAmAdminAtom +export const GroupJoinRequests = ({ + myAddress, + groups, + setOpenManageMembers, + getTimestampEnterChat, + setSelectedGroup, + setGroupSection, + setMobileViewMode, + setDesktopViewMode, +}) => { + const [isExpanded, setIsExpanded] = React.useState(false); + const { t } = useTranslation(['core', 'group']); + const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState( + [] ); + const [loading, setLoading] = React.useState(true); + const [txList] = useAtom(txListAtom); + const setMyGroupsWhereIAmAdmin = useSetAtom(myGroupsWhereIAmAdminAtom); - const getJoinRequests = async ()=> { + const theme = useTheme(); + const getJoinRequests = async () => { try { - setLoading(true) - - let groupsAsAdmin = [] - const getAllGroupsAsAdmin = groups.filter((item)=> item.groupId !== '0').map(async (group)=> { - - const isAdminResponse = await requestQueueGroupJoinRequests.enqueue(()=> { - return fetch( - `${getBaseApiReact()}/groups/members/${group.groupId}?limit=0&onlyAdmins=true` + setLoading(true); + + let groupsAsAdmin = []; + const getAllGroupsAsAdmin = groups + .filter((item) => item.groupId !== '0') + .map(async (group) => { + const isAdminResponse = await requestQueueGroupJoinRequests.enqueue( + () => { + return fetch( + `${getBaseApiReact()}/groups/members/${group.groupId}?limit=0&onlyAdmins=true` + ); + } ); - }) - const isAdminData = await isAdminResponse.json() - + const isAdminData = await isAdminResponse.json(); - const findMyself = isAdminData?.members?.find((member)=> member.member === myAddress) - - if(findMyself){ - groupsAsAdmin.push(group) - } - return true - }) + const findMyself = isAdminData?.members?.find( + (member) => member.member === myAddress + ); - - await Promise.all(getAllGroupsAsAdmin) - setMyGroupsWhereIAmAdmin(groupsAsAdmin) - const res = await Promise.all(groupsAsAdmin.map(async (group)=> { + if (findMyself) { + groupsAsAdmin.push(group); + } + return true; + }); - const joinRequestResponse = await requestQueueGroupJoinRequests.enqueue(()=> { - return fetch( - `${getBaseApiReact()}/groups/joinrequests/${group.groupId}` - ); - }) + await Promise.all(getAllGroupsAsAdmin); + setMyGroupsWhereIAmAdmin(groupsAsAdmin); + const res = await Promise.all( + groupsAsAdmin.map(async (group) => { + const joinRequestResponse = + await requestQueueGroupJoinRequests.enqueue(() => { + return fetch( + `${getBaseApiReact()}/groups/joinrequests/${group.groupId}` + ); + }); - const joinRequestData = await joinRequestResponse.json() - return { - group, - data: joinRequestData - } - })) - setGroupsWithJoinRequests(res) + const joinRequestData = await joinRequestResponse.json(); + return { + group, + data: joinRequestData, + }; + }) + ); + setGroupsWithJoinRequests(res); } catch (error) { - + console.log(error); } finally { - setLoading(false) + setLoading(false); } - } + }; React.useEffect(() => { if (myAddress && groups.length > 0) { - getJoinRequests() + getJoinRequests(); } else { - setLoading(false) + setLoading(false); } }, [myAddress, groups]); - const filteredJoinRequests = React.useMemo(()=> { - return groupsWithJoinRequests.map((group)=> { - const filteredGroupRequests = group?.data?.filter((gd)=> { - const findJoinRequsetInTxList = txList?.find((tx)=> tx?.groupId === group?.group?.groupId && tx?.qortalAddress === gd?.joiner && tx?.type === 'join-request-accept') + const filteredJoinRequests = React.useMemo(() => { + return groupsWithJoinRequests.map((group) => { + const filteredGroupRequests = group?.data?.filter((gd) => { + const findJoinRequsetInTxList = txList?.find( + (tx) => + tx?.groupId === group?.group?.groupId && + tx?.qortalAddress === gd?.joiner && + tx?.type === 'join-request-accept' + ); - if(findJoinRequsetInTxList) return false - return true - }) + if (findJoinRequsetInTxList) return false; + return true; + }); return { ...group, - data: filteredGroupRequests - } - }) - }, [groupsWithJoinRequests, txList]) - - + data: filteredGroupRequests, + }; + }); + }, [groupsWithJoinRequests, txList]); return ( - + setIsExpanded((prev)=> !prev)} + onClick={() => setIsExpanded((prev) => !prev)} > - Join Requests {filteredJoinRequests?.filter((group)=> group?.data?.length > 0)?.length > 0 && ` (${filteredJoinRequests?.filter((group)=> group?.data?.length > 0)?.length})`} + {t('group:join_requests', { postProcess: 'capitalize' })}{' '} + {filteredJoinRequests?.filter((group) => group?.data?.length > 0) + ?.length > 0 && + ` (${filteredJoinRequests?.filter((group) => group?.data?.length > 0)?.length})`} - {isExpanded ? : ( - - )} + {isExpanded ? ( + + ) : ( + + )} - - - {loading && filteredJoinRequests.length === 0 && ( - - - - )} - {!loading && (filteredJoinRequests.length === 0 || filteredJoinRequests?.filter((group)=> group?.data?.length > 0).length === 0) && ( - - - Nothing to display - - - )} - - {filteredJoinRequests?.map((group)=> { - if(group?.data?.length === 0) return null - return ( - { - setSelectedGroup(group?.group) - setMobileViewMode('group') - getTimestampEnterChat() - setGroupSection("announcement") - setOpenManageMembers(true) - if(!isMobile){ - setDesktopViewMode('chat') - } - setTimeout(() => { - executeEvent("openGroupJoinRequest", {}); - - }, 300); - }} + + - - - } > - - - + + + )} + {!loading && + (filteredJoinRequests.length === 0 || + filteredJoinRequests?.filter((group) => group?.data?.length > 0) + .length === 0) && ( + + - - - ) - - })} - - - - - - + color: 'rgba(255, 255, 255, 0.2)', + }} + > + {t('group:message.generic.no_display', { + postProcess: 'capitalize', + })} + + + )} + + {filteredJoinRequests?.map((group) => { + if (group?.data?.length === 0) return null; + return ( + { + setSelectedGroup(group?.group); + setMobileViewMode('group'); + getTimestampEnterChat(); + setGroupSection('announcement'); + setOpenManageMembers(true); + setDesktopViewMode('chat'); + setTimeout(() => { + executeEvent('openGroupJoinRequest', {}); + }, 300); + }} + sx={{ + marginBottom: '20px', + }} + disablePadding + secondaryAction={ + + + + } + > + + + + + ); + })} + + + ); }; diff --git a/src/components/Group/GroupList.tsx b/src/components/Group/GroupList.tsx new file mode 100644 index 0000000..1057281 --- /dev/null +++ b/src/components/Group/GroupList.tsx @@ -0,0 +1,352 @@ +import { + Avatar, + Box, + ButtonBase, + List, + ListItem, + ListItemAvatar, + ListItemText, + useTheme, +} from '@mui/material'; +import React, { useCallback } from 'react'; +import { IconWrapper } from '../Desktop/DesktopFooter'; +import { HubsIcon } from '../../assets/Icons/HubsIcon'; +import { MessagingIcon } from '../../assets/Icons/MessagingIcon'; +import { ContextMenu } from '../ContextMenu'; +import { getBaseApiReact } from '../../App'; +import { formatEmailDate } from './QMailMessages'; +import CampaignIcon from '@mui/icons-material/Campaign'; +import MarkChatUnreadIcon from '@mui/icons-material/MarkChatUnread'; +import LockIcon from '@mui/icons-material/Lock'; +import { CustomButton } from '../../styles/App-styles'; +import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; +import PersonOffIcon from '@mui/icons-material/PersonOff'; +import { + groupAnnouncementSelector, + groupChatTimestampSelector, + groupPropertySelector, + groupsOwnerNamesSelector, + isRunningPublicNodeAtom, + timestampEnterDataSelector, +} from '../../atoms/global'; + +import { timeDifferenceForNotificationChats } from './Group'; +import { useAtom, useAtomValue } from 'jotai'; + +export const GroupList = ({ + selectGroupFunc, + setDesktopSideView, + groupChatHasUnread, + groupsAnnHasUnread, + desktopSideView, + directChatHasUnread, + chatMode, + groups, + selectedGroup, + getUserSettings, + setOpenAddGroup, + setIsOpenBlockedUserModal, + myAddress, +}) => { + const theme = useTheme(); + const [isRunningPublicNode] = useAtom(isRunningPublicNodeAtom); + + return ( +
+ + { + setDesktopSideView('groups'); + }} + > + + + + + { + setDesktopSideView('directs'); + }} + > + + + + + + +
+ + {groups.map((group: any) => ( + + ))} + +
+
+ <> + { + setOpenAddGroup(true); + }} + > + + Group + + + {!isRunningPublicNode && ( + { + setIsOpenBlockedUserModal(true); + }} + sx={{ + minWidth: 'unset', + padding: '10px', + }} + > + + + )} + +
+
+ ); +}; + +const GroupItem = React.memo( + ({ selectGroupFunc, group, selectedGroup, getUserSettings, myAddress }) => { + const theme = useTheme(); + const ownerName = useAtomValue(groupsOwnerNamesSelector(group?.groupId)); + const announcement = useAtomValue( + groupAnnouncementSelector(group?.groupId) + ); + const groupProperty = useAtomValue(groupPropertySelector(group?.groupId)); + const groupChatTimestamp = useAtomValue( + groupChatTimestampSelector(group?.groupId) + ); + const timestampEnterData = useAtomValue( + timestampEnterDataSelector(group?.groupId) + ); + + const selectGroupHandler = useCallback(() => { + selectGroupFunc(group); + }, [group, selectGroupFunc]); + + return ( + + + + + {ownerName ? ( + + {group?.groupName?.charAt(0).toUpperCase()} + + ) : ( + + {' '} + {group?.groupName?.charAt(0).toUpperCase() || 'G'} + + )} + + + {announcement && !announcement?.seentimestamp && ( + + )} + + {group?.data && + groupChatTimestamp && + group?.sender !== myAddress && + group?.timestamp && + ((!timestampEnterData && + Date.now() - group?.timestamp < + timeDifferenceForNotificationChats) || + timestampEnterData < group?.timestamp) && ( + + )} + {groupProperty?.isOpen === false && ( + + )} + + + + + ); + } +); diff --git a/src/components/Group/GroupMenu.tsx b/src/components/Group/GroupMenu.tsx deleted file mode 100644 index a44c480..0000000 --- a/src/components/Group/GroupMenu.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import React, { useState } from "react"; -import { - Button, - Menu, - MenuItem, - ListItemIcon, - ListItemText, - Badge, - Box, -} from "@mui/material"; -import ForumIcon from "@mui/icons-material/Forum"; -import GroupIcon from "@mui/icons-material/Group"; -import { ArrowDownIcon } from "../../assets/Icons/ArrowDownIcon"; -import { NotificationIcon2 } from "../../assets/Icons/NotificationIcon2"; -import { ChatIcon } from "../../assets/Icons/ChatIcon"; -import { ThreadsIcon } from "../../assets/Icons/ThreadsIcon"; -import { MembersIcon } from "../../assets/Icons/MembersIcon"; - -export const GroupMenu = ({ setGroupSection, groupSection, setOpenManageMembers, goToAnnouncements, goToChat, hasUnreadChat, hasUnreadAnnouncements }) => { - const [anchorEl, setAnchorEl] = useState(null); - const open = Boolean(anchorEl); - - const handleClick = (event) => { - setAnchorEl(event.currentTarget); - }; - - const handleClose = () => { - setAnchorEl(null); - }; - - return ( - - - - { - goToChat() - handleClose(); - }} - > - - - - - - { - goToAnnouncements() - handleClose(); - }} - > - - - - - - { - setGroupSection("forum"); - handleClose(); - }} - > - - - - - - - { - setOpenManageMembers(true) - handleClose(); - }} - > - - - - - - - - - ); -}; diff --git a/src/components/Group/Home.tsx b/src/components/Group/Home.tsx deleted file mode 100644 index a8c804c..0000000 --- a/src/components/Group/Home.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { Box, Button, Typography } from "@mui/material"; -import React from "react"; -import { Spacer } from "../../common/Spacer"; -import { ListOfThreadPostsWatched } from "./ListOfThreadPostsWatched"; -import { ThingsToDoInitial } from "./ThingsToDoInitial"; -import { GroupJoinRequests } from "./GroupJoinRequests"; -import { GroupInvites } from "./GroupInvites"; -import RefreshIcon from "@mui/icons-material/Refresh"; - -export const Home = ({ - refreshHomeDataFunc, - myAddress, - isLoadingGroups, - balance, - userInfo, - groups, - setGroupSection, - setSelectedGroup, - getTimestampEnterChat, - setOpenManageMembers, - setOpenAddGroup, - setMobileViewMode, - setDesktopViewMode -}) => { - return ( - - - 15 ? "16px" : "20px", - padding: '10px' - }} - > - Welcome - {userInfo?.name ? ( - {`, ${userInfo?.name}`} - ) : null} - - - - {/* - - */} - {!isLoadingGroups && ( - - - - - - - - )} - - - ); -}; diff --git a/src/components/Group/HomeDesktop.tsx b/src/components/Group/HomeDesktop.tsx index d83f774..83f8828 100644 --- a/src/components/Group/HomeDesktop.tsx +++ b/src/components/Group/HomeDesktop.tsx @@ -1,16 +1,16 @@ -import { Box, Button, Divider, Typography } from "@mui/material"; -import React from "react"; -import { Spacer } from "../../common/Spacer"; -import { ListOfThreadPostsWatched } from "./ListOfThreadPostsWatched"; -import { ThingsToDoInitial } from "./ThingsToDoInitial"; -import { GroupJoinRequests } from "./GroupJoinRequests"; -import { GroupInvites } from "./GroupInvites"; -import RefreshIcon from "@mui/icons-material/Refresh"; -import { ListOfGroupPromotions } from "./ListOfGroupPromotions"; -import { QortPrice } from "../Home/QortPrice"; -import ExploreIcon from "@mui/icons-material/Explore"; -import { Explore } from "../Explore/Explore"; -import { NewUsersCTA } from "../Home/NewUsersCTA"; +import { Box, Divider, Typography, useTheme } from '@mui/material'; +import React from 'react'; +import { Spacer } from '../../common/Spacer'; +import { ThingsToDoInitial } from './ThingsToDoInitial'; +import { GroupJoinRequests } from './GroupJoinRequests'; +import { GroupInvites } from './GroupInvites'; +import { ListOfGroupPromotions } from './ListOfGroupPromotions'; +import { QortPrice } from '../Home/QortPrice'; +import ExploreIcon from '@mui/icons-material/Explore'; +import { Explore } from '../Explore/Explore'; +import { NewUsersCTA } from '../Home/NewUsersCTA'; +import { useTranslation } from 'react-i18next'; + export const HomeDesktop = ({ refreshHomeDataFunc, myAddress, @@ -30,93 +30,97 @@ export const HomeDesktop = ({ }) => { const [checked1, setChecked1] = React.useState(false); const [checked2, setChecked2] = React.useState(false); - React.useEffect(() => { - if (balance && +balance >= 6) { - setChecked1(true); - } - }, [balance]); - - - React.useEffect(() => { - if (name) setChecked2(true); - }, [name]); - - - const isLoaded = React.useMemo(()=> { - if(userInfo !== null) return true - return false - }, [ userInfo]) - - const hasDoneNameAndBalanceAndIsLoaded = React.useMemo(()=> { - if(isLoaded && checked1 && checked2) return true - return false - }, [checked1, isLoaded, checked2]) - + const { t } = useTranslation(['core']); + const theme = useTheme(); + + React.useEffect(() => { + if (balance && +balance >= 6) { + setChecked1(true); + } + }, [balance]); + + React.useEffect(() => { + if (name) setChecked2(true); + }, [name]); + + const isLoaded = React.useMemo(() => { + if (userInfo !== null) return true; + return false; + }, [userInfo]); + + const hasDoneNameAndBalanceAndIsLoaded = React.useMemo(() => { + if (isLoaded && checked1 && checked2) return true; + return false; + }, [checked1, isLoaded, checked2]); + return ( + 15 ? "16px" : "20px", - padding: "10px", + fontSize: userInfo?.name?.length > 15 ? '16px' : '20px', + padding: '10px', }} > - Welcome + {t('core:welcome', { postProcess: 'capitalize' })} {userInfo?.name ? ( {`, ${userInfo?.name}`} ) : null} + + {!isLoadingGroups && ( item?.groupId !== "0").length !== 0 + groups?.filter((item) => item?.groupId !== '0').length !== 0 } /> - - {desktopViewMode === "home" && ( + + {desktopViewMode === 'home' && ( <> {/* */} - {hasDoneNameAndBalanceAndIsLoaded && ( - <> - - - - - - - - )} - + {hasDoneNameAndBalanceAndIsLoaded && ( + <> + + + + + + + + + )} )} )} - + {!isLoadingGroups && ( <> - - - {" "} - - Explore - {" "} - - - {!hasDoneNameAndBalanceAndIsLoaded && ( - - )} - - {hasDoneNameAndBalanceAndIsLoaded && ( - - - )} - - - - - - - - )} - - - - {/* - - */} + {t('tutorial:initial.explore', { postProcess: 'capitalize' })} + {' '} + + + + {!hasDoneNameAndBalanceAndIsLoaded && } + + {hasDoneNameAndBalanceAndIsLoaded && } + + + + + + + )} +
diff --git a/src/components/Group/InviteMember.tsx b/src/components/Group/InviteMember.tsx index 147c9bb..db01b59 100644 --- a/src/components/Group/InviteMember.tsx +++ b/src/components/Group/InviteMember.tsx @@ -1,50 +1,52 @@ -import { LoadingButton } from "@mui/lab"; -import { - Box, - Button, - Input, - MenuItem, - Select, - SelectChangeEvent, -} from "@mui/material"; -import React, { useState } from "react"; -import { Spacer } from "../../common/Spacer"; -import { Label } from "./AddGroup"; -import { getFee } from "../../background"; +import { LoadingButton } from '@mui/lab'; +import { Box, Input, MenuItem, Select, SelectChangeEvent } from '@mui/material'; +import { useState } from 'react'; +import { Spacer } from '../../common/Spacer'; +import { Label } from './AddGroup'; +import { getFee } from '../../background'; +import { useTranslation } from 'react-i18next'; export const InviteMember = ({ groupId, setInfoSnack, setOpenSnack, show }) => { - const [value, setValue] = useState(""); + const [value, setValue] = useState(''); const [expiryTime, setExpiryTime] = useState('259200'); - const [isLoadingInvite, setIsLoadingInvite] = useState(false) + const [isLoadingInvite, setIsLoadingInvite] = useState(false); + const { t } = useTranslation(['core', 'group']); + const inviteMember = async () => { try { - const fee = await getFee('GROUP_INVITE') + const fee = await getFee('GROUP_INVITE'); await show({ - message: "Would you like to perform a GROUP_INVITE transaction?" , - publishFee: fee.fee + ' QORT' - }) - setIsLoadingInvite(true) + message: t('group:question.group_invite', { + postProcess: 'capitalize', + }), + publishFee: fee.fee + ' QORT', + }); + setIsLoadingInvite(true); if (!expiryTime || !value) return; new Promise((res, rej) => { - window.sendMessage("inviteToGroup", { - groupId, - qortalAddress: value, - inviteTime: +expiryTime, - }) + window + .sendMessage('inviteToGroup', { + groupId, + qortalAddress: value, + inviteTime: +expiryTime, + }) .then((response) => { if (!response?.error) { setInfoSnack({ - type: "success", - message: `Successfully invited ${value}. It may take a couple of minutes for the changes to propagate`, + type: 'success', + message: t('group:message.success.group_invite', { + value: value, + postProcess: 'capitalize', + }), }); setOpenSnack(true); res(response); - - setValue(""); + + setValue(''); return; } setInfoSnack({ - type: "error", + type: 'error', message: response?.error, }); setOpenSnack(true); @@ -52,16 +54,17 @@ export const InviteMember = ({ groupId, setInfoSnack, setOpenSnack, show }) => { }) .catch((error) => { setInfoSnack({ - type: "error", - message: error?.message || "An error occurred", + type: 'error', + message: error?.message || 'An error occurred', }); setOpenSnack(true); rej(error); }); - }); - } catch (error) {} finally { - setIsLoadingInvite(false) + } catch (error) { + console.log(error); + } finally { + setIsLoadingInvite(false); } }; @@ -72,40 +75,48 @@ export const InviteMember = ({ groupId, setInfoSnack, setOpenSnack, show }) => { return ( - Invite member + {t('group:action.invite_member', { postProcess: 'capitalize' })} setValue(e.target.value)} /> - - - + + - Invite + + {t('core:action.invite', { postProcess: 'capitalize' })} + ); }; diff --git a/src/components/Group/ListOfBans.tsx b/src/components/Group/ListOfBans.tsx index db4c6ba..355f1d8 100644 --- a/src/components/Group/ListOfBans.tsx +++ b/src/components/Group/ListOfBans.tsx @@ -1,16 +1,32 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { Avatar, Box, Button, ListItem, ListItemAvatar, ListItemButton, ListItemText, Popover } from '@mui/material'; -import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from 'react-virtualized'; +import { useEffect, useRef, useState } from 'react'; +import { + Avatar, + Box, + ListItem, + ListItemAvatar, + ListItemButton, + ListItemText, + Popover, +} from '@mui/material'; +import { + AutoSizer, + CellMeasurer, + CellMeasurerCache, + List, +} from 'react-virtualized'; import { getNameInfo } from './Group'; -import { getBaseApi, getFee } from '../../background'; +import { getFee } from '../../background'; import { LoadingButton } from '@mui/lab'; import { getBaseApiReact } from '../../App'; +import { useTranslation } from 'react-i18next'; export const getMemberInvites = async (groupNumber) => { - const response = await fetch(`${getBaseApiReact()}/groups/bans/${groupNumber}?limit=0`); + const response = await fetch( + `${getBaseApiReact()}/groups/bans/${groupNumber}?limit=0` + ); const groupData = await response.json(); return groupData; -} +}; const getNames = async (listOfMembers, includeNoNames) => { let members = []; @@ -20,14 +36,14 @@ const getNames = async (listOfMembers, includeNoNames) => { const name = await getNameInfo(member.offender); if (name) { members.push({ ...member, name }); - } else if(includeNoNames){ - members.push({ ...member, name: name || "" }); + } else if (includeNoNames) { + members.push({ ...member, name: name || '' }); } } } } return members; -} +}; const cache = new CellMeasurerCache({ fixedWidth: true, @@ -40,6 +56,7 @@ export const ListOfBans = ({ groupId, setInfoSnack, setOpenSnack, show }) => { const [openPopoverIndex, setOpenPopoverIndex] = useState(null); // Track which list item has the popover open const listRef = useRef(); const [isLoadingUnban, setIsLoadingUnban] = useState(false); + const { t } = useTranslation(['core', 'group']); const getInvites = async (groupId) => { try { @@ -49,7 +66,7 @@ export const ListOfBans = ({ groupId, setInfoSnack, setOpenSnack, show }) => { } catch (error) { console.error(error); } - } + }; useEffect(() => { if (groupId) { @@ -67,33 +84,36 @@ export const ListOfBans = ({ groupId, setInfoSnack, setOpenSnack, show }) => { setOpenPopoverIndex(null); }; - const handleCancelBan = async (address)=> { + const handleCancelBan = async (address) => { try { - const fee = await getFee('CANCEL_GROUP_BAN') + const fee = await getFee('CANCEL_GROUP_BAN'); await show({ - message: "Would you like to perform a CANCEL_GROUP_BAN transaction?" , - publishFee: fee.fee + ' QORT' - }) - setIsLoadingUnban(true) - new Promise((res, rej)=> { - window.sendMessage("cancelBan", { - groupId, - qortalAddress: address, - }) + message: t('group:question.cancel_ban', { postProcess: 'capitalize' }), + publishFee: fee.fee + ' QORT', + }); + setIsLoadingUnban(true); + new Promise((res, rej) => { + window + .sendMessage('cancelBan', { + groupId, + qortalAddress: address, + }) .then((response) => { if (!response?.error) { res(response); setIsLoadingUnban(false); setInfoSnack({ - type: "success", - message: "Successfully unbanned user. It may take a couple of minutes for the changes to propagate", + type: 'success', + message: t('group:message.success.unbanned_user', { + postProcess: 'capitalize', + }), }); handlePopoverClose(); setOpenSnack(true); return; } setInfoSnack({ - type: "error", + type: 'error', message: response?.error, }); setOpenSnack(true); @@ -101,24 +121,23 @@ export const ListOfBans = ({ groupId, setInfoSnack, setOpenSnack, show }) => { }) .catch((error) => { setInfoSnack({ - type: "error", - message: error.message || "An error occurred", + type: 'error', + message: error.message || 'An error occurred', }); setOpenSnack(true); rej(error); }); - - }) + }); } catch (error) { - + console.log(error); } finally { - setIsLoadingUnban(false) + setIsLoadingUnban(false); } - } + }; const rowRenderer = ({ index, key, parent, style }) => { const member = bans[index]; - + return ( { anchorEl={popoverAnchor} onClose={handlePopoverClose} anchorOrigin={{ - vertical: "bottom", - horizontal: "center", + vertical: 'bottom', + horizontal: 'center', }} transformOrigin={{ - vertical: "top", - horizontal: "center", + vertical: 'top', + horizontal: 'center', }} - style={{ marginTop: "8px" }} + style={{ marginTop: '8px' }} > - - handleCancelBan(member?.offender)}>Cancel Ban + variant="contained" + onClick={() => handleCancelBan(member?.offender)} + > + {t('group:action.cancel_ban', { + postProcess: 'capitalize', + })} + - handlePopoverOpen(event, index)}> + + handlePopoverOpen(event, index)} + > @@ -178,8 +211,17 @@ export const ListOfBans = ({ groupId, setInfoSnack, setOpenSnack, show }) => { return (
-

Ban list

-
+

{t('group:ban_list', { postProcess: 'capitalize' })}

+
{({ height, width }) => ( {
); -} +}; diff --git a/src/components/Group/ListOfGroupPromotions.tsx b/src/components/Group/ListOfGroupPromotions.tsx index afeff14..c7c9ecf 100644 --- a/src/components/Group/ListOfGroupPromotions.tsx +++ b/src/components/Group/ListOfGroupPromotions.tsx @@ -4,7 +4,7 @@ import React, { useEffect, useRef, useState, -} from "react"; +} from 'react'; import { Avatar, Box, @@ -16,53 +16,48 @@ import { DialogContent, DialogContentText, DialogTitle, - ListItem, - ListItemAvatar, - ListItemButton, - ListItemText, MenuItem, Popover, Select, TextField, Typography, -} from "@mui/material"; - -import { getNameInfo } from "./Group"; -import { getBaseApi, getFee } from "../../background"; -import { LoadingButton } from "@mui/lab"; -import LockIcon from "@mui/icons-material/Lock"; -import NoEncryptionGmailerrorredIcon from "@mui/icons-material/NoEncryptionGmailerrorred"; + useTheme, +} from '@mui/material'; +import { LoadingButton } from '@mui/lab'; +import LockIcon from '@mui/icons-material/Lock'; +import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred'; import { MyContext, getArbitraryEndpointReact, getBaseApiReact, - isMobile, -} from "../../App"; -import { Spacer } from "../../common/Spacer"; -import { CustomLoader } from "../../common/CustomLoader"; -import { RequestQueueWithPromise } from "../../utils/queue/queue"; -import { useRecoilState } from "recoil"; +} from '../../App'; +import { Spacer } from '../../common/Spacer'; +import { CustomLoader } from '../../common/CustomLoader'; +import { RequestQueueWithPromise } from '../../utils/queue/queue'; + import { myGroupsWhereIAmAdminAtom, promotionTimeIntervalAtom, promotionsAtom, -} from "../../atoms/global"; -import { Label } from "./AddGroup"; -import ShortUniqueId from "short-unique-id"; -import { CustomizedSnackbars } from "../Snackbar/Snackbar"; -import { getGroupNames } from "./UserListOfInvites"; -import { WrapperUserAction } from "../WrapperUserAction"; -import { useVirtualizer } from "@tanstack/react-virtual"; -import ErrorBoundary from "../../common/ErrorBoundary"; -import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; -import ExpandLessIcon from "@mui/icons-material/ExpandLess"; -export const requestQueuePromos = new RequestQueueWithPromise(20); + txListAtom, +} from '../../atoms/global'; +import { Label } from './AddGroup'; +import ShortUniqueId from 'short-unique-id'; +import { CustomizedSnackbars } from '../Snackbar/Snackbar'; +import { getGroupNames } from './UserListOfInvites'; +import { useVirtualizer } from '@tanstack/react-virtual'; +import ErrorBoundary from '../../common/ErrorBoundary'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ExpandLessIcon from '@mui/icons-material/ExpandLess'; +import { getFee } from '../../background'; +import { useAtom, useSetAtom } from 'jotai'; +export const requestQueuePromos = new RequestQueueWithPromise(3); export function utf8ToBase64(inputString: string): string { // Encode the string as UTF-8 const utf8String = encodeURIComponent(inputString).replace( /%([0-9A-F]{2})/g, - (match, p1) => String.fromCharCode(Number("0x" + p1)) + (match, p1) => String.fromCharCode(Number('0x' + p1)) ); // Convert the UTF-8 encoded string to base64 @@ -83,14 +78,15 @@ export const ListOfGroupPromotions = () => { const [selectedGroup, setSelectedGroup] = useState(null); const [loading, setLoading] = useState(false); const [isShowModal, setIsShowModal] = useState(false); - const [text, setText] = useState(""); - const [myGroupsWhereIAmAdmin, setMyGroupsWhereIAmAdmin] = useRecoilState( + const [text, setText] = useState(''); + const [myGroupsWhereIAmAdmin, setMyGroupsWhereIAmAdmin] = useAtom( myGroupsWhereIAmAdminAtom ); - const [promotions, setPromotions] = useRecoilState(promotionsAtom); - const [promotionTimeInterval, setPromotionTimeInterval] = useRecoilState( + const [promotions, setPromotions] = useAtom(promotionsAtom); + const [promotionTimeInterval, setPromotionTimeInterval] = useAtom( promotionTimeIntervalAtom ); + const [isExpanded, setIsExpanded] = React.useState(false); const [openSnack, setOpenSnack] = useState(false); @@ -98,8 +94,10 @@ export const ListOfGroupPromotions = () => { const [fee, setFee] = useState(null); const [isLoadingJoinGroup, setIsLoadingJoinGroup] = useState(false); const [isLoadingPublish, setIsLoadingPublish] = useState(false); - const { show, setTxList } = useContext(MyContext); + const { show } = useContext(MyContext); + const setTxList = useSetAtom(txListAtom); + const theme = useTheme(); const listRef = useRef(); const rowVirtualizer = useVirtualizer({ count: promotions.length, @@ -115,10 +113,12 @@ export const ListOfGroupPromotions = () => { useEffect(() => { try { (async () => { - const feeRes = await getFee("ARBITRARY"); + const feeRes = await getFee('ARBITRARY'); setFee(feeRes?.fee); })(); - } catch (error) {} + } catch (error) { + console.log(error); + } }, []); const getPromotions = useCallback(async () => { try { @@ -126,9 +126,9 @@ export const ListOfGroupPromotions = () => { const identifier = `group-promotions-ui24-`; const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=100&includemetadata=false&reverse=true&prefix=true`; const response = await fetch(url, { - method: "GET", + method: 'GET', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, }); const responseData = await response.json(); @@ -142,7 +142,7 @@ export const ListOfGroupPromotions = () => { promo.name }/${promo.identifier}`; const response = await fetch(url, { - method: "GET", + method: 'GET', }); try { @@ -164,7 +164,7 @@ export const ListOfGroupPromotions = () => { } } } catch (error) { - console.error("Error fetching promo:", error); + console.error('Error fetching promo:', error); } }); } @@ -222,10 +222,10 @@ export const ListOfGroupPromotions = () => { await new Promise((res, rej) => { window - .sendMessage("publishOnQDN", { + .sendMessage('publishOnQDN', { data: data, identifier: identifier, - service: "DOCUMENT", + service: 'DOCUMENT', }) .then((response) => { if (!response?.error) { @@ -235,23 +235,23 @@ export const ListOfGroupPromotions = () => { rej(response.error); }) .catch((error) => { - rej(error.message || "An error occurred"); + rej(error.message || 'An error occurred'); }); - }); + }); // TODO translate setInfoSnack({ - type: "success", + type: 'success', message: - "Successfully published promotion. It may take a couple of minutes for the promotion to appear", + 'Successfully published promotion. It may take a couple of minutes for the promotion to appear', }); setOpenSnack(true); - setText(""); + setText(''); setSelectedGroup(null); setIsShowModal(false); } catch (error) { setInfoSnack({ - type: "error", + type: 'error', message: - error?.message || "Error publishing the promotion. Please try again", + error?.message || 'Error publishing the promotion. Please try again', }); setOpenSnack(true); } finally { @@ -262,30 +262,30 @@ export const ListOfGroupPromotions = () => { const handleJoinGroup = async (group, isOpen) => { try { const groupId = group.groupId; - const fee = await getFee("JOIN_GROUP"); + const fee = await getFee('JOIN_GROUP'); await show({ - message: "Would you like to perform an JOIN_GROUP transaction?", - publishFee: fee.fee + " QORT", + message: 'Would you like to perform an JOIN_GROUP transaction?', + publishFee: fee.fee + ' QORT', }); setIsLoadingJoinGroup(true); await new Promise((res, rej) => { window - .sendMessage("joinGroup", { + .sendMessage('joinGroup', { groupId, }) .then((response) => { if (!response?.error) { setInfoSnack({ - type: "success", + type: 'success', message: - "Successfully requested to join group. It may take a couple of minutes for the changes to propagate", + 'Successfully requested to join group. It may take a couple of minutes for the changes to propagate', }); if (isOpen) { setTxList((prev) => [ { ...response, - type: "joined-group", + type: 'joined-group', label: `Joined Group ${group?.groupName}: awaiting confirmation`, labelDone: `Joined Group ${group?.groupName}: success!`, done: false, @@ -297,7 +297,7 @@ export const ListOfGroupPromotions = () => { setTxList((prev) => [ { ...response, - type: "joined-group-request", + type: 'joined-group-request', label: `Requested to join Group ${group?.groupName}: awaiting confirmation`, labelDone: `Requested to join Group ${group?.groupName}: success!`, done: false, @@ -313,7 +313,7 @@ export const ListOfGroupPromotions = () => { return; } else { setInfoSnack({ - type: "error", + type: 'error', message: response?.error, }); setOpenSnack(true); @@ -322,8 +322,8 @@ export const ListOfGroupPromotions = () => { }) .catch((error) => { setInfoSnack({ - type: "error", - message: error.message || "An error occurred", + type: 'error', + message: error.message || 'An error occurred', }); setOpenSnack(true); rej(error); @@ -339,55 +339,59 @@ export const ListOfGroupPromotions = () => { return ( - + setIsExpanded((prev) => !prev)} > - Group promotions {promotions.length > 0 && ` (${promotions.length})`} + Group promotions{' '} + {promotions.length > 0 && ` (${promotions.length})`} {isExpanded ? ( ) : ( )} + @@ -396,57 +400,59 @@ export const ListOfGroupPromotions = () => { <> + + {loading && promotions.length === 0 && ( @@ -455,18 +461,18 @@ export const ListOfGroupPromotions = () => { {!loading && promotions.length === 0 && ( Nothing to display @@ -475,11 +481,11 @@ export const ListOfGroupPromotions = () => { )}
{ className="scrollable-container" style={{ flexGrow: 1, - overflow: "auto", - position: "relative", - display: "flex", - height: "0px", + overflow: 'auto', + position: 'relative', + display: 'flex', + height: '0px', }} >
{rowVirtualizer.getVirtualItems().map((virtualRow) => { @@ -516,17 +522,17 @@ export const ListOfGroupPromotions = () => { ref={rowVirtualizer.measureElement} //measure dynamic row height key={promotion?.identifier} style={{ - position: "absolute", + position: 'absolute', top: 0, - left: "50%", // Move to the center horizontally + left: '50%', // Move to the center horizontally transform: `translateY(${virtualRow.start}px) translateX(-50%)`, // Adjust for centering - width: "100%", // Control width (90% of the parent) - padding: "10px 0", - display: "flex", - alignItems: "center", - overscrollBehavior: "none", - flexDirection: "column", - gap: "5px", + width: '100%', // Control width (90% of the parent) + padding: '10px 0', + display: 'flex', + alignItems: 'center', + overscrollBehavior: 'none', + flexDirection: 'column', + gap: '5px', }} > { > { - if (reason === "backdropClick") { + if (reason === 'backdropClick') { // Prevent closing on backdrop click return; } handlePopoverClose(); // Close only on other events like Esc key press }} anchorOrigin={{ - vertical: "top", - horizontal: "center", + vertical: 'top', + horizontal: 'center', }} transformOrigin={{ - vertical: "bottom", - horizontal: "center", + vertical: 'bottom', + horizontal: 'center', }} - style={{ marginTop: "8px" }} + style={{ marginTop: '8px' }} > Group name: {` ${promotion?.groupName}`} + - Number of members:{" "} + Number of members:{' '} {` ${promotion?.memberCount}`} + {promotion?.description && ( {promotion?.description} )} + {promotion?.isOpen === false && ( @@ -615,14 +624,16 @@ export const ListOfGroupPromotions = () => { your request )} + + { > Close + { { > {promotion?.name?.charAt(0)} + {promotion?.name} + {promotion?.groupName} + + {promotion?.isOpen === false && ( )} {promotion?.isOpen === true && ( )} {promotion?.isOpen - ? "Public group" - : "Private group"} + ? 'Public group' + : 'Private group'} + + {promotion?.data} + + +
@@ -774,6 +792,7 @@ export const ListOfGroupPromotions = () => { + {isShowModal && ( @@ -783,7 +802,7 @@ export const ListOfGroupPromotions = () => { aria-describedby="alert-dialog-description" > - {"Promote your group to non-members"} + {'Promote your group to non-members'} @@ -791,14 +810,14 @@ export const ListOfGroupPromotions = () => { group. - Max 200 characters. Publish Fee: {fee && fee} {" QORT"} + Max 200 characters. Publish Fee: {fee && fee} {' QORT'} @@ -832,11 +851,11 @@ export const ListOfGroupPromotions = () => { }} multiline={true} sx={{ - "& .MuiFormLabel-root": { - color: "white", + '& .MuiFormLabel-root': { + color: theme.palette.text.primary, }, - "& .MuiFormLabel-root.Mui-focused": { - color: "white", + '& .MuiFormLabel-root.Mui-focused': { + color: theme.palette.text.primary, }, }} /> diff --git a/src/components/Group/ListOfInvites.tsx b/src/components/Group/ListOfInvites.tsx index a41bc47..a571775 100644 --- a/src/components/Group/ListOfInvites.tsx +++ b/src/components/Group/ListOfInvites.tsx @@ -1,16 +1,31 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { Avatar, Box, Button, ListItem, ListItemAvatar, ListItemButton, ListItemText, Popover } from '@mui/material'; -import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from 'react-virtualized'; +import { useEffect, useRef, useState } from 'react'; +import { + Avatar, + Box, + ListItem, + ListItemAvatar, + ListItemButton, + ListItemText, + Popover, +} from '@mui/material'; +import { + AutoSizer, + CellMeasurer, + CellMeasurerCache, + List, +} from 'react-virtualized'; import { getNameInfo } from './Group'; -import { getBaseApi, getFee } from '../../background'; +import { getFee } from '../../background'; import { LoadingButton } from '@mui/lab'; import { getBaseApiReact } from '../../App'; export const getMemberInvites = async (groupNumber) => { - const response = await fetch(`${getBaseApiReact()}/groups/invites/group/${groupNumber}?limit=0`); + const response = await fetch( + `${getBaseApiReact()}/groups/invites/group/${groupNumber}?limit=0` + ); const groupData = await response.json(); return groupData; -} +}; const getNames = async (listOfMembers, includeNoNames) => { let members = []; @@ -20,21 +35,26 @@ const getNames = async (listOfMembers, includeNoNames) => { const name = await getNameInfo(member.invitee); if (name) { members.push({ ...member, name }); - } else if(includeNoNames){ - members.push({ ...member, name: name || "" }); + } else if (includeNoNames) { + members.push({ ...member, name: name || '' }); } } } } return members; -} +}; const cache = new CellMeasurerCache({ fixedWidth: true, defaultHeight: 50, }); -export const ListOfInvites = ({ groupId, setInfoSnack, setOpenSnack, show }) => { +export const ListOfInvites = ({ + groupId, + setInfoSnack, + setOpenSnack, + show, +}) => { const [invites, setInvites] = useState([]); const [popoverAnchor, setPopoverAnchor] = useState(null); // Track which list item the popover is anchored to const [openPopoverIndex, setOpenPopoverIndex] = useState(null); // Track which list item has the popover open @@ -50,7 +70,7 @@ export const ListOfInvites = ({ groupId, setInfoSnack, setOpenSnack, show }) => } catch (error) { console.error(error); } - } + }; useEffect(() => { if (groupId) { @@ -68,24 +88,27 @@ export const ListOfInvites = ({ groupId, setInfoSnack, setOpenSnack, show }) => setOpenPopoverIndex(null); }; - const handleCancelInvitation = async (address)=> { + const handleCancelInvitation = async (address) => { try { - const fee = await getFee('CANCEL_GROUP_INVITE') + // TODO translate + const fee = await getFee('CANCEL_GROUP_INVITE'); await show({ - message: "Would you like to perform a CANCEL_GROUP_INVITE transaction?" , - publishFee: fee.fee + ' QORT' - }) - setIsLoadingCancelInvite(true) - await new Promise((res, rej)=> { - window.sendMessage("cancelInvitationToGroup", { - groupId, - qortalAddress: address, - }) + message: 'Would you like to perform a CANCEL_GROUP_INVITE transaction?', + publishFee: fee.fee + ' QORT', + }); + setIsLoadingCancelInvite(true); + await new Promise((res, rej) => { + window + .sendMessage('cancelInvitationToGroup', { + groupId, + qortalAddress: address, + }) .then((response) => { if (!response?.error) { setInfoSnack({ - type: "success", - message: "Successfully canceled invitation. It may take a couple of minutes for the changes to propagate", + type: 'success', + message: + 'Successfully canceled invitation. It may take a couple of minutes for the changes to propagate', }); setOpenSnack(true); handlePopoverClose(); @@ -94,7 +117,7 @@ export const ListOfInvites = ({ groupId, setInfoSnack, setOpenSnack, show }) => return; } setInfoSnack({ - type: "error", + type: 'error', message: response?.error, }); setOpenSnack(true); @@ -102,24 +125,22 @@ export const ListOfInvites = ({ groupId, setInfoSnack, setOpenSnack, show }) => }) .catch((error) => { setInfoSnack({ - type: "error", - message: error.message || "An error occurred", + type: 'error', + message: error.message || 'An error occurred', }); setOpenSnack(true); rej(error); }); - - }) + }); } catch (error) { - } finally { - setIsLoadingCancelInvite(false) + setIsLoadingCancelInvite(false); } - } + }; const rowRenderer = ({ index, key, parent, style }) => { const member = invites[index]; - + return ( anchorEl={popoverAnchor} onClose={handlePopoverClose} anchorOrigin={{ - vertical: "bottom", - horizontal: "center", + vertical: 'bottom', + horizontal: 'center', }} transformOrigin={{ - vertical: "top", - horizontal: "center", + vertical: 'top', + horizontal: 'center', }} - style={{ marginTop: "8px" }} + style={{ marginTop: '8px' }} > - - handleCancelInvitation(member?.invitee)}>Cancel Invitation + variant="contained" + onClick={() => handleCancelInvitation(member?.invitee)} + > + Cancel Invitation + - handlePopoverOpen(event, index)}> + handlePopoverOpen(event, index)} + > @@ -180,7 +212,16 @@ export const ListOfInvites = ({ groupId, setInfoSnack, setOpenSnack, show }) => return (

Invitees list

-
+
{({ height, width }) => (
); -} +}; diff --git a/src/components/Group/ListOfJoinRequests.tsx b/src/components/Group/ListOfJoinRequests.tsx index 0f4bd81..8c51ee2 100644 --- a/src/components/Group/ListOfJoinRequests.tsx +++ b/src/components/Group/ListOfJoinRequests.tsx @@ -1,16 +1,33 @@ -import React, { useContext, useEffect, useRef, useState } from 'react'; -import { Avatar, Box, Button, ListItem, ListItemAvatar, ListItemButton, ListItemText, Popover } from '@mui/material'; -import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from 'react-virtualized'; +import { useEffect, useRef, useState } from 'react'; +import { + Avatar, + Box, + ListItem, + ListItemAvatar, + ListItemButton, + ListItemText, + Popover, +} from '@mui/material'; +import { + AutoSizer, + CellMeasurer, + CellMeasurerCache, + List, +} from 'react-virtualized'; import { getNameInfo } from './Group'; import { getBaseApi, getFee } from '../../background'; import { LoadingButton } from '@mui/lab'; -import { MyContext, getBaseApiReact } from '../../App'; +import { getBaseApiReact } from '../../App'; +import { txListAtom } from '../../atoms/global'; +import { useAtom } from 'jotai'; export const getMemberInvites = async (groupNumber) => { - const response = await fetch(`${getBaseApiReact()}/groups/joinrequests/${groupNumber}?limit=0`); + const response = await fetch( + `${getBaseApiReact()}/groups/joinrequests/${groupNumber}?limit=0` + ); const groupData = await response.json(); return groupData; -} +}; const getNames = async (listOfMembers, includeNoNames) => { let members = []; @@ -19,24 +36,29 @@ const getNames = async (listOfMembers, includeNoNames) => { if (member.joiner) { const name = await getNameInfo(member.joiner); if (name) { - members.push({ ...member, name: name || "" }); - } else if(includeNoNames){ - members.push({ ...member, name: name || "" }); + members.push({ ...member, name: name || '' }); + } else if (includeNoNames) { + members.push({ ...member, name: name || '' }); } } } } return members; -} +}; const cache = new CellMeasurerCache({ fixedWidth: true, defaultHeight: 50, }); -export const ListOfJoinRequests = ({ groupId, setInfoSnack, setOpenSnack, show }) => { +export const ListOfJoinRequests = ({ + groupId, + setInfoSnack, + setOpenSnack, + show, +}) => { const [invites, setInvites] = useState([]); - const {txList, setTxList} = useContext(MyContext) + const [txList, setTxList] = useAtom(txListAtom); const [popoverAnchor, setPopoverAnchor] = useState(null); // Track which list item the popover is anchored to const [openPopoverIndex, setOpenPopoverIndex] = useState(null); // Track which list item has the popover open @@ -51,7 +73,7 @@ export const ListOfJoinRequests = ({ groupId, setInfoSnack, setOpenSnack, show } } catch (error) { console.error(error); } - } + }; useEffect(() => { if (groupId) { @@ -69,31 +91,33 @@ export const ListOfJoinRequests = ({ groupId, setInfoSnack, setOpenSnack, show } setOpenPopoverIndex(null); }; - const handleAcceptJoinRequest = async (address)=> { + const handleAcceptJoinRequest = async (address) => { try { - const fee = await getFee('GROUP_INVITE') + const fee = await getFee('GROUP_INVITE'); // TODO translate await show({ - message: "Would you like to perform a GROUP_INVITE transaction?" , - publishFee: fee.fee + ' QORT' - }) - setIsLoadingAccept(true) - await new Promise((res, rej)=> { - window.sendMessage("inviteToGroup", { - groupId, - qortalAddress: address, - inviteTime: 10800, - }) + message: 'Would you like to perform a GROUP_INVITE transaction?', + publishFee: fee.fee + ' QORT', + }); + setIsLoadingAccept(true); + await new Promise((res, rej) => { + window + .sendMessage('inviteToGroup', { + groupId, + qortalAddress: address, + inviteTime: 10800, + }) .then((response) => { if (!response?.error) { setIsLoadingAccept(false); setInfoSnack({ - type: "success", - message: "Successfully accepted join request. It may take a couple of minutes for the changes to propagate", + type: 'success', + message: + 'Successfully accepted join request. It may take a couple of minutes for the changes to propagate', }); setOpenSnack(true); handlePopoverClose(); res(response); - + setTxList((prev) => [ { ...response, @@ -106,12 +130,12 @@ export const ListOfJoinRequests = ({ groupId, setInfoSnack, setOpenSnack, show } }, ...prev, ]); - + return; } - + setInfoSnack({ - type: "error", + type: 'error', message: response?.error, }); setOpenSnack(true); @@ -119,25 +143,28 @@ export const ListOfJoinRequests = ({ groupId, setInfoSnack, setOpenSnack, show } }) .catch((error) => { setInfoSnack({ - type: "error", - message: error?.message || "An error occurred", + type: 'error', + message: error?.message || 'An error occurred', }); setOpenSnack(true); rej(error); }); - - }) + }); } catch (error) { - } finally { - setIsLoadingAccept(false) + setIsLoadingAccept(false); } - } + }; const rowRenderer = ({ index, key, parent, style }) => { const member = invites[index]; - const findJoinRequsetInTxList = txList?.find((tx)=> tx?.groupId === groupId && tx?.qortalAddress === member?.joiner && tx?.type === 'join-request-accept') - if(findJoinRequsetInTxList) return null + const findJoinRequsetInTxList = txList?.find( + (tx) => + tx?.groupId === groupId && + tx?.qortalAddress === member?.joiner && + tx?.type === 'join-request-accept' + ); + if (findJoinRequsetInTxList) return null; return ( - - handleAcceptJoinRequest(member?.joiner)}>Accept + variant="contained" + onClick={() => handleAcceptJoinRequest(member?.joiner)} + > + Accept + - handlePopoverOpen(event, index)}> + handlePopoverOpen(event, index)} + > @@ -198,7 +236,16 @@ export const ListOfJoinRequests = ({ groupId, setInfoSnack, setOpenSnack, show } return (

Join request list

-
+
{({ height, width }) => (
); -} +}; diff --git a/src/components/Group/ListOfMembers.tsx b/src/components/Group/ListOfMembers.tsx index 9910453..fb45945 100644 --- a/src/components/Group/ListOfMembers.tsx +++ b/src/components/Group/ListOfMembers.tsx @@ -1,29 +1,30 @@ import { Avatar, Box, - Button, ListItem, ListItemAvatar, ListItemButton, ListItemText, Popover, Typography, -} from "@mui/material"; -import React, { useRef, useState } from "react"; + useTheme, +} from '@mui/material'; +import { useRef, useState } from 'react'; import { AutoSizer, CellMeasurer, CellMeasurerCache, List, -} from "react-virtualized"; -import { LoadingButton } from "@mui/lab"; -import { getBaseApi, getFee } from "../../background"; -import { getBaseApiReact } from "../../App"; +} from 'react-virtualized'; +import { LoadingButton } from '@mui/lab'; +import { getFee } from '../../background'; +import { getBaseApiReact } from '../../App'; const cache = new CellMeasurerCache({ fixedWidth: true, defaultHeight: 50, }); + const ListOfMembers = ({ members, groupId, @@ -39,8 +40,7 @@ const ListOfMembers = ({ const [isLoadingBan, setIsLoadingBan] = useState(false); const [isLoadingMakeAdmin, setIsLoadingMakeAdmin] = useState(false); const [isLoadingRemoveAdmin, setIsLoadingRemoveAdmin] = useState(false); - - + const theme = useTheme(); const listRef = useRef(); const handlePopoverOpen = (event, index) => { @@ -55,23 +55,25 @@ const ListOfMembers = ({ const handleKick = async (address) => { try { - const fee = await getFee("GROUP_KICK"); + const fee = await getFee('GROUP_KICK'); await show({ - message: "Would you like to perform a GROUP_KICK transaction?", - publishFee: fee.fee + " QORT", + message: 'Would you like to perform a GROUP_KICK transaction?', + publishFee: fee.fee + ' QORT', }); setIsLoadingKick(true); new Promise((res, rej) => { - window.sendMessage("kickFromGroup", { - groupId, - qortalAddress: address, - }) + window + .sendMessage('kickFromGroup', { + groupId, + qortalAddress: address, + }) .then((response) => { if (!response?.error) { setInfoSnack({ - type: "success", - message: "Successfully kicked member from group. It may take a couple of minutes for the changes to propagate", + type: 'success', + message: + 'Successfully kicked member from group. It may take a couple of minutes for the changes to propagate', }); setOpenSnack(true); handlePopoverClose(); @@ -79,7 +81,7 @@ const ListOfMembers = ({ return; } setInfoSnack({ - type: "error", + type: 'error', message: response?.error, }); setOpenSnack(true); @@ -87,38 +89,40 @@ const ListOfMembers = ({ }) .catch((error) => { setInfoSnack({ - type: "error", - message: error.message || "An error occurred", + type: 'error', + message: error.message || 'An error occurred', }); setOpenSnack(true); rej(error); }); - }); } catch (error) { + console.log(error); } finally { setIsLoadingKick(false); } }; const handleBan = async (address) => { try { - const fee = await getFee("GROUP_BAN"); + const fee = await getFee('GROUP_BAN'); // TODO translate await show({ - message: "Would you like to perform a GROUP_BAN transaction?", - publishFee: fee.fee + " QORT", + message: 'Would you like to perform a GROUP_BAN transaction?', + publishFee: fee.fee + ' QORT', }); setIsLoadingBan(true); await new Promise((res, rej) => { - window.sendMessage("banFromGroup", { - groupId, - qortalAddress: address, - rBanTime: 0, - }) + window + .sendMessage('banFromGroup', { + groupId, + qortalAddress: address, + rBanTime: 0, + }) .then((response) => { if (!response?.error) { setInfoSnack({ - type: "success", - message: "Successfully banned member from group. It may take a couple of minutes for the changes to propagate", + type: 'success', + message: + 'Successfully banned member from group. It may take a couple of minutes for the changes to propagate', }); setOpenSnack(true); handlePopoverClose(); @@ -126,7 +130,7 @@ const ListOfMembers = ({ return; } setInfoSnack({ - type: "error", + type: 'error', message: response?.error, }); setOpenSnack(true); @@ -134,13 +138,12 @@ const ListOfMembers = ({ }) .catch((error) => { setInfoSnack({ - type: "error", - message: error.message || "An error occurred", + type: 'error', + message: error.message || 'An error occurred', }); setOpenSnack(true); rej(error); }); - }); } catch (error) { } finally { @@ -150,22 +153,24 @@ const ListOfMembers = ({ const makeAdmin = async (address) => { try { - const fee = await getFee("ADD_GROUP_ADMIN"); + const fee = await getFee('ADD_GROUP_ADMIN'); await show({ - message: "Would you like to perform a ADD_GROUP_ADMIN transaction?", - publishFee: fee.fee + " QORT", + message: 'Would you like to perform a ADD_GROUP_ADMIN transaction?', + publishFee: fee.fee + ' QORT', }); setIsLoadingMakeAdmin(true); await new Promise((res, rej) => { - window.sendMessage("makeAdmin", { - groupId, - qortalAddress: address, - }) + window + .sendMessage('makeAdmin', { + groupId, + qortalAddress: address, + }) .then((response) => { if (!response?.error) { setInfoSnack({ - type: "success", - message: "Successfully made member an admin. It may take a couple of minutes for the changes to propagate", + type: 'success', + message: + 'Successfully made member an admin. It may take a couple of minutes for the changes to propagate', }); setOpenSnack(true); handlePopoverClose(); @@ -173,7 +178,7 @@ const ListOfMembers = ({ return; } setInfoSnack({ - type: "error", + type: 'error', message: response?.error, }); setOpenSnack(true); @@ -181,13 +186,12 @@ const ListOfMembers = ({ }) .catch((error) => { setInfoSnack({ - type: "error", - message: error.message || "An error occurred", + type: 'error', + message: error.message || 'An error occurred', }); setOpenSnack(true); rej(error); }); - }); } catch (error) { } finally { @@ -197,22 +201,24 @@ const ListOfMembers = ({ const removeAdmin = async (address) => { try { - const fee = await getFee("REMOVE_GROUP_ADMIN"); + const fee = await getFee('REMOVE_GROUP_ADMIN'); await show({ - message: "Would you like to perform a REMOVE_GROUP_ADMIN transaction?", - publishFee: fee.fee + " QORT", + message: 'Would you like to perform a REMOVE_GROUP_ADMIN transaction?', + publishFee: fee.fee + ' QORT', }); setIsLoadingRemoveAdmin(true); await new Promise((res, rej) => { - window.sendMessage("removeAdmin", { - groupId, - qortalAddress: address, - }) + window + .sendMessage('removeAdmin', { + groupId, + qortalAddress: address, + }) .then((response) => { if (!response?.error) { setInfoSnack({ - type: "success", - message: "Successfully removed member as an admin. It may take a couple of minutes for the changes to propagate", + type: 'success', + message: + 'Successfully removed member as an admin. It may take a couple of minutes for the changes to propagate', }); setOpenSnack(true); handlePopoverClose(); @@ -220,7 +226,7 @@ const ListOfMembers = ({ return; } setInfoSnack({ - type: "error", + type: 'error', message: response?.error, }); setOpenSnack(true); @@ -228,13 +234,12 @@ const ListOfMembers = ({ }) .catch((error) => { setInfoSnack({ - type: "error", - message: error.message || "An error occurred", + type: 'error', + message: error.message || 'An error occurred', }); setOpenSnack(true); rej(error); }); - }); } catch (error) { } finally { @@ -260,24 +265,24 @@ const ListOfMembers = ({ anchorEl={popoverAnchor} onClose={handlePopoverClose} anchorOrigin={{ - vertical: "bottom", - horizontal: "center", + vertical: 'bottom', + horizontal: 'center', }} transformOrigin={{ - vertical: "top", - horizontal: "center", + vertical: 'top', + horizontal: 'center', }} - style={{ marginTop: "8px" }} + style={{ marginTop: '8px' }} > {isOwner && ( @@ -336,21 +341,28 @@ const ListOfMembers = ({ {member?.isAdmin && ( - Admin - )} + + Admin + + )} -
)} @@ -363,11 +375,11 @@ const ListOfMembers = ({

Member list

diff --git a/src/components/Group/ListOfThreadPostsWatched.tsx b/src/components/Group/ListOfThreadPostsWatched.tsx index 6e24ba3..1cf0d2a 100644 --- a/src/components/Group/ListOfThreadPostsWatched.tsx +++ b/src/components/Group/ListOfThreadPostsWatched.tsx @@ -1,21 +1,14 @@ -import * as React from "react"; -import List from "@mui/material/List"; -import ListItem from "@mui/material/ListItem"; -import ListItemButton from "@mui/material/ListItemButton"; -import ListItemIcon from "@mui/material/ListItemIcon"; -import ListItemText from "@mui/material/ListItemText"; -import Checkbox from "@mui/material/Checkbox"; -import IconButton from "@mui/material/IconButton"; -import CommentIcon from "@mui/icons-material/Comment"; -import InfoIcon from "@mui/icons-material/Info"; -import GroupAddIcon from "@mui/icons-material/GroupAdd"; -import { executeEvent } from "../../utils/events"; -import { Box, Typography } from "@mui/material"; -import { Spacer } from "../../common/Spacer"; -import { getGroupNames } from "./UserListOfInvites"; -import { CustomLoader } from "../../common/CustomLoader"; -import VisibilityIcon from "@mui/icons-material/Visibility"; -import { isMobile } from "../../App"; +import * as React from 'react'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemText from '@mui/material/ListItemText'; +import IconButton from '@mui/material/IconButton'; +import { executeEvent } from '../../utils/events'; +import { Box, Typography } from '@mui/material'; +import { Spacer } from '../../common/Spacer'; +import { CustomLoader } from '../../common/CustomLoader'; +import VisibilityIcon from '@mui/icons-material/Visibility'; export const ListOfThreadPostsWatched = () => { const [posts, setPosts] = React.useState([]); @@ -24,33 +17,33 @@ export const ListOfThreadPostsWatched = () => { const getPosts = async () => { try { await new Promise((res, rej) => { - window.sendMessage("getThreadActivity", {}) - .then((response) => { - if (!response?.error) { - if (!response) { - res(null); + window + .sendMessage('getThreadActivity', {}) + .then((response) => { + if (!response?.error) { + if (!response) { + res(null); + return; + } + const uniquePosts = response.reduce((acc, current) => { + const x = acc.find( + (item) => item?.thread?.threadId === current?.thread?.threadId + ); + if (!x) { + return acc.concat([current]); + } else { + return acc; + } + }, []); + setPosts(uniquePosts); + res(uniquePosts); return; } - const uniquePosts = response.reduce((acc, current) => { - const x = acc.find( - (item) => item?.thread?.threadId === current?.thread?.threadId - ); - if (!x) { - return acc.concat([current]); - } else { - return acc; - } - }, []); - setPosts(uniquePosts); - res(uniquePosts); - return; - } - rej(response.error); - }) - .catch((error) => { - rej(error.message || "An error occurred"); - }); - + rej(response.error); + }) + .catch((error) => { + rej(error.message || 'An error occurred'); // TODO translate + }); }); } catch (error) { } finally { @@ -63,49 +56,50 @@ export const ListOfThreadPostsWatched = () => { }, []); return ( - + New Thread Posts: - + {loading && posts.length === 0 && ( @@ -114,19 +108,18 @@ export const ListOfThreadPostsWatched = () => { {!loading && posts.length === 0 && ( Nothing to display @@ -134,47 +127,46 @@ export const ListOfThreadPostsWatched = () => { )} {posts?.length > 0 && ( - - {posts?.map((post) => { - return ( - { - executeEvent("openThreadNewPost", { - data: post, - }); - }} - disablePadding - secondaryAction={ - - - - } - > - - - - - ); - })} - + + {posts?.map((post) => { + return ( + { + executeEvent('openThreadNewPost', { + data: post, + }); + }} + disablePadding + secondaryAction={ + + + + } + > + + + + + ); + })} + )} - ); diff --git a/src/components/Group/ManageMembers.tsx b/src/components/Group/ManageMembers.tsx index b1a35f0..6956693 100644 --- a/src/components/Group/ManageMembers.tsx +++ b/src/components/Group/ManageMembers.tsx @@ -1,36 +1,35 @@ -import * as React from "react"; -import Button from "@mui/material/Button"; -import Dialog from "@mui/material/Dialog"; -import ListItemText from "@mui/material/ListItemText"; -import ListItemButton from "@mui/material/ListItemButton"; -import List from "@mui/material/List"; -import Divider from "@mui/material/Divider"; -import AppBar from "@mui/material/AppBar"; -import Toolbar from "@mui/material/Toolbar"; -import IconButton from "@mui/material/IconButton"; -import Typography from "@mui/material/Typography"; -import CloseIcon from "@mui/icons-material/Close"; -import Slide from "@mui/material/Slide"; -import { TransitionProps } from "@mui/material/transitions"; -import ListOfMembers from "./ListOfMembers"; -import { InviteMember } from "./InviteMember"; -import { ListOfInvites } from "./ListOfInvites"; -import { ListOfBans } from "./ListOfBans"; -import { ListOfJoinRequests } from "./ListOfJoinRequests"; -import { Box, ButtonBase, Card, Tab, Tabs } from "@mui/material"; -import { CustomizedSnackbars } from "../Snackbar/Snackbar"; -import { MyContext, getBaseApiReact, isMobile } from "../../App"; -import { getGroupMembers, getNames } from "./Group"; -import { LoadingSnackbar } from "../Snackbar/LoadingSnackbar"; -import { getFee } from "../../background"; -import { LoadingButton } from "@mui/lab"; -import { subscribeToEvent, unsubscribeFromEvent } from "../../utils/events"; -import { Spacer } from "../../common/Spacer"; +import * as React from 'react'; +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import AppBar from '@mui/material/AppBar'; +import Toolbar from '@mui/material/Toolbar'; +import IconButton from '@mui/material/IconButton'; +import Typography from '@mui/material/Typography'; +import CloseIcon from '@mui/icons-material/Close'; +import Slide from '@mui/material/Slide'; +import { TransitionProps } from '@mui/material/transitions'; +import ListOfMembers from './ListOfMembers'; +import { InviteMember } from './InviteMember'; +import { ListOfInvites } from './ListOfInvites'; +import { ListOfBans } from './ListOfBans'; +import { ListOfJoinRequests } from './ListOfJoinRequests'; +import { Box, ButtonBase, Card, Tab, Tabs, useTheme } from '@mui/material'; +import { CustomizedSnackbars } from '../Snackbar/Snackbar'; +import { MyContext, getBaseApiReact } from '../../App'; +import { getGroupMembers, getNames } from './Group'; +import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar'; +import { getFee } from '../../background'; +import { LoadingButton } from '@mui/lab'; +import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events'; +import { Spacer } from '../../common/Spacer'; import InsertLinkIcon from '@mui/icons-material/InsertLink'; +import { useSetAtom } from 'jotai'; +import { txListAtom } from '../../atoms/global'; + function a11yProps(index: number) { return { id: `simple-tab-${index}`, - "aria-controls": `simple-tabpanel-${index}`, + 'aria-controls': `simple-tabpanel-${index}`, }; } @@ -50,39 +49,41 @@ export const ManageMembers = ({ selectedGroup, isAdmin, - isOwner + isOwner, }) => { const [membersWithNames, setMembersWithNames] = React.useState([]); - const [tab, setTab] = React.useState("create"); + const [tab, setTab] = React.useState('create'); const [value, setValue] = React.useState(0); const [openSnack, setOpenSnack] = React.useState(false); const [infoSnack, setInfoSnack] = React.useState(null); - const [isLoadingMembers, setIsLoadingMembers] = React.useState(false) - const [isLoadingLeave, setIsLoadingLeave] = React.useState(false) - const [groupInfo, setGroupInfo] = React.useState(null) + const [isLoadingMembers, setIsLoadingMembers] = React.useState(false); + const [isLoadingLeave, setIsLoadingLeave] = React.useState(false); + const [groupInfo, setGroupInfo] = React.useState(null); const handleChange = (event: React.SyntheticEvent, newValue: number) => { setValue(newValue); }; - const { show, setTxList } = React.useContext(MyContext); + const theme = useTheme(); + const { show } = React.useContext(MyContext); + const setTxList = useSetAtom(txListAtom); const handleClose = () => { setOpen(false); }; - const handleLeaveGroup = async () => { try { - setIsLoadingLeave(true) - const fee = await getFee('LEAVE_GROUP') + setIsLoadingLeave(true); + const fee = await getFee('LEAVE_GROUP'); await show({ - message: "Would you like to perform an LEAVE_GROUP transaction?" , - publishFee: fee.fee + ' QORT' - }) + message: 'Would you like to perform an LEAVE_GROUP transaction?', + publishFee: fee.fee + ' QORT', + }); await new Promise((res, rej) => { - window.sendMessage("leaveGroup", { - groupId: selectedGroup?.groupId, - }) + window + .sendMessage('leaveGroup', { + groupId: selectedGroup?.groupId, + }) .then((response) => { if (!response?.error) { setTxList((prev) => [ @@ -98,8 +99,9 @@ export const ManageMembers = ({ ]); res(response); setInfoSnack({ - type: "success", - message: "Successfully requested to leave group. It may take a couple of minutes for the changes to propagate", + type: 'success', + message: + 'Successfully requested to leave group. It may take a couple of minutes for the changes to propagate', }); setOpenSnack(true); return; @@ -107,57 +109,62 @@ export const ManageMembers = ({ rej(response.error); }) .catch((error) => { - rej(error.message || "An error occurred"); + rej(error.message || 'An error occurred'); // TODO translate }); - }); - } catch (error) {} finally { - setIsLoadingLeave(false) + } catch (error) { + console.log(error); + } finally { + setIsLoadingLeave(false); } }; - const getMembersWithNames = React.useCallback(async (groupId) => { + const getMembersWithNames = React.useCallback(async (groupId) => { try { - setIsLoadingMembers(true) + setIsLoadingMembers(true); const res = await getGroupMembers(groupId); const resWithNames = await getNames(res.members); setMembersWithNames(resWithNames); - setIsLoadingMembers(false) - } catch (error) {} + setIsLoadingMembers(false); + } catch (error) { + console.log(error); + } }, []); const getMembers = async (groupId) => { try { const res = await getGroupMembers(groupId); setMembersWithNames(res?.members || []); - } catch (error) {} + } catch (error) { + console.log(error); + } }; const getGroupInfo = async (groupId) => { try { - const response = await fetch( - `${getBaseApiReact()}/groups/${groupId}` - ); - const groupData = await response.json(); - setGroupInfo(groupData) - } catch (error) {} + const response = await fetch(`${getBaseApiReact()}/groups/${groupId}`); + const groupData = await response.json(); + setGroupInfo(groupData); + } catch (error) { + console.log(error); + } }; - React.useEffect(()=> { - if(selectedGroup?.groupId){ - getMembers(selectedGroup?.groupId) - getGroupInfo(selectedGroup?.groupId) + React.useEffect(() => { + if (selectedGroup?.groupId) { + getMembers(selectedGroup?.groupId); + getGroupInfo(selectedGroup?.groupId); } - }, [selectedGroup?.groupId]) + }, [selectedGroup?.groupId]); - const openGroupJoinRequestFunc = ()=> { - setValue(4) - } + const openGroupJoinRequestFunc = () => { + setValue(4); + }; React.useEffect(() => { - subscribeToEvent("openGroupJoinRequest", openGroupJoinRequestFunc); + subscribeToEvent('openGroupJoinRequest', openGroupJoinRequestFunc); return () => { - unsubscribeFromEvent("openGroupJoinRequest", openGroupJoinRequestFunc); + unsubscribeFromEvent('openGroupJoinRequest', openGroupJoinRequestFunc); }; }, []); @@ -169,9 +176,14 @@ export const ManageMembers = ({ onClose={handleClose} TransitionComponent={Transition} > - + - + Manage Members - - - - - - - - + + + + + + + + + + + + - + + - GroupId: {groupInfo?.groupId} - GroupName: {groupInfo?.groupName} - Number of members: {groupInfo?.memberCount} - { - const link = `qortal://use-group/action-join/groupid-${groupInfo?.groupId}` - await navigator.clipboard.writeText(link); - }}> Join Group Link + GroupId: {groupInfo?.groupId} + + GroupName: {groupInfo?.groupName} + + + Number of members: {groupInfo?.memberCount} + + + { + const link = `qortal://use-group/action-join/groupid-${groupInfo?.groupId}`; + await navigator.clipboard.writeText(link); + }} + > + Join Group Link + - - {selectedGroup?.groupId && !isOwner && ( - - Leave Group - - )} + + + + {selectedGroup?.groupId && !isOwner && ( + + Leave Group + + )} {value === 0 && ( - + + + )} - {value === 1 && ( + {value === 1 && ( - + )} {value === 2 && ( - - + )} {value === 3 && ( - + )} - + {value === 4 && ( - + )} - + + +
- ); }; diff --git a/src/components/Group/QMailMessages.tsx b/src/components/Group/QMailMessages.tsx index c2d7370..72a23d5 100644 --- a/src/components/Group/QMailMessages.tsx +++ b/src/components/Group/QMailMessages.tsx @@ -1,279 +1,304 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react' -import List from "@mui/material/List"; -import ListItem from "@mui/material/ListItem"; -import ListItemButton from "@mui/material/ListItemButton"; -import ListItemIcon from "@mui/material/ListItemIcon"; -import ListItemText from "@mui/material/ListItemText"; -import moment from 'moment' -import { Box, ButtonBase, Collapse, Typography } from "@mui/material"; -import { Spacer } from "../../common/Spacer"; -import { getBaseApiReact, isMobile } from "../../App"; -import { MessagingIcon } from '../../assets/Icons/MessagingIcon'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import moment from 'moment'; +import { Box, ButtonBase, Collapse, Typography, useTheme } from '@mui/material'; +import { getBaseApiReact } from '../../App'; import MailIcon from '@mui/icons-material/Mail'; import MailOutlineIcon from '@mui/icons-material/MailOutline'; import { executeEvent } from '../../utils/events'; import { CustomLoader } from '../../common/CustomLoader'; -import { useRecoilState } from 'recoil'; + import { mailsAtom, qMailLastEnteredTimestampAtom } from '../../atoms/global'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandLessIcon from '@mui/icons-material/ExpandLess'; import MarkEmailUnreadIcon from '@mui/icons-material/MarkEmailUnread'; +import { useAtom } from 'jotai'; + export const isLessThanOneWeekOld = (timestamp) => { // Current time in milliseconds const now = Date.now(); - + // One week ago in milliseconds (7 days * 24 hours * 60 minutes * 60 seconds * 1000 milliseconds) - const oneWeekAgo = now - (7 * 24 * 60 * 60 * 1000); - + const oneWeekAgo = now - 7 * 24 * 60 * 60 * 1000; + // Check if the timestamp is newer than one week ago return timestamp > oneWeekAgo; }; + export function formatEmailDate(timestamp: number) { - const date = moment(timestamp); - const now = moment(); + const date = moment(timestamp); + const now = moment(); - if (date.isSame(now, 'day')) { - // If the email was received today, show the time - return date.format('h:mm A'); - } else if (date.isSame(now, 'year')) { - // If the email was received this year, show the month and day - return date.format('MMM D'); - } else { - // For older emails, show the full date - return date.format('MMM D, YYYY'); - } + if (date.isSame(now, 'day')) { + // If the email was received today, show the time + return date.format('h:mm A'); + } else if (date.isSame(now, 'year')) { + // If the email was received this year, show the month and day + return date.format('MMM D'); + } else { + // For older emails, show the full date + return date.format('MMM D, YYYY'); + } } -export const QMailMessages = ({userName, userAddress}) => { - const [isExpanded, setIsExpanded] = useState(false) - const [mails, setMails] = useRecoilState(mailsAtom) - const [lastEnteredTimestamp, setLastEnteredTimestamp] = useRecoilState(qMailLastEnteredTimestampAtom) - const [loading, setLoading] = useState(true) - const getMails = useCallback(async () => { - try { - setLoading(true) - const query = `qortal_qmail_${userName.slice( - 0, - 20 - )}_${userAddress.slice(-6)}_mail_` - const response = await fetch(`${getBaseApiReact()}/arbitrary/resources/search?service=MAIL_PRIVATE&query=${query}&limit=10&includemetadata=false&offset=0&reverse=true&excludeblocked=true&mode=ALL`); - const mailData = await response.json(); - - - setMails(mailData); - } catch (error) { - console.error(error); - } finally { - setLoading(false) +export const QMailMessages = ({ userName, userAddress }) => { + const [isExpanded, setIsExpanded] = useState(false); + const [mails, setMails] = useAtom(mailsAtom); + const [lastEnteredTimestamp, setLastEnteredTimestamp] = useAtom( + qMailLastEnteredTimestampAtom + ); - } - }, []) + const [loading, setLoading] = useState(true); + const theme = useTheme(); - const getTimestamp = async () => { - try { - return new Promise((res, rej) => { - window.sendMessage("getEnteredQmailTimestamp") - .then((response) => { - if (!response?.error) { - if(response?.timestamp){ - setLastEnteredTimestamp(response?.timestamp) - } + const getMails = useCallback(async () => { + try { + setLoading(true); + const query = `qortal_qmail_${userName.slice( + 0, + 20 + )}_${userAddress.slice(-6)}_mail_`; + const response = await fetch( + `${getBaseApiReact()}/arbitrary/resources/search?service=MAIL_PRIVATE&query=${query}&limit=10&includemetadata=false&offset=0&reverse=true&excludeblocked=true&mode=ALL` + ); + const mailData = await response.json(); + + setMails(mailData); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + }, []); + + const getTimestamp = async () => { + try { + return new Promise((res, rej) => { + window + .sendMessage('getEnteredQmailTimestamp') + .then((response) => { + if (!response?.error) { + if (response?.timestamp) { + setLastEnteredTimestamp(response?.timestamp); } - rej(response.error); - }) - .catch((error) => { - rej(error.message || "An error occurred"); - }); - + } + rej(response.error); + }) + .catch((error) => { + rej(error.message || 'An error occurred'); // TODO translate }); - } catch (error) {} - }; - - useEffect(() => { - getTimestamp() - if(!userName || !userAddress) return - getMails(); + }); + } catch (error) { + console.log(error); + } + }; - const interval = setInterval(() => { - getTimestamp() - getMails(); - }, 300000); - - return () => clearInterval(interval); - - }, [getMails, userName, userAddress]); + useEffect(() => { + getTimestamp(); + if (!userName || !userAddress) return; + getMails(); - const anyUnread = useMemo(()=> { - let unread = false - - mails.forEach((mail)=> { - if(!lastEnteredTimestamp && isLessThanOneWeekOld(mail?.created) || (lastEnteredTimestamp && isLessThanOneWeekOld(mail?.created) && lastEnteredTimestamp < mail?.created)){ - unread = true - } - }) - return unread - }, [mails, lastEnteredTimestamp]) + const interval = setInterval(() => { + getTimestamp(); + getMails(); + }, 300000); + + return () => clearInterval(interval); + }, [getMails, userName, userAddress]); + + const anyUnread = useMemo(() => { + let unread = false; + + mails.forEach((mail) => { + if ( + (!lastEnteredTimestamp && isLessThanOneWeekOld(mail?.created)) || + (lastEnteredTimestamp && + isLessThanOneWeekOld(mail?.created) && + lastEnteredTimestamp < mail?.created) + ) { + unread = true; + } + }); + return unread; + }, [mails, lastEnteredTimestamp]); return ( - - setIsExpanded((prev)=> !prev)} > - setIsExpanded((prev) => !prev)} > - Latest Q-Mails - - - {isExpanded ? : ( - - )} - - - - {loading && mails.length === 0 && ( - + Latest Q-Mails + + + {isExpanded ? ( + - - + /> + ) : ( + )} - {!loading && mails.length === 0 && ( - - + + + + {loading && mails.length === 0 && ( + - Nothing to display - - - )} - - - {mails?.map((mail)=> { - return ( - { - executeEvent("addTab", { data: { service: 'APP', name: 'q-mail' } }); - executeEvent("open-apps-mode", { }); - setLastEnteredTimestamp(Date.now()) + + + )} + {!loading && mails.length === 0 && ( + + + Nothing to display + + + )} + + {mails?.map((mail) => { + return ( + { + executeEvent('addTab', { + data: { service: 'APP', name: 'q-mail' }, + }); + executeEvent('open-apps-mode', {}); + setLastEnteredTimestamp(Date.now()); + }} + > + - + - - - {!lastEnteredTimestamp && isLessThanOneWeekOld(mail?.created) ? ( - - ) : !lastEnteredTimestamp ? ( - - ): (lastEnteredTimestamp < mail?.created) && isLessThanOneWeekOld(mail?.created) ? ( - - ) : ( - - ) - } - - - - - - - ) + {!lastEnteredTimestamp && + isLessThanOneWeekOld(mail?.created) ? ( + + ) : !lastEnteredTimestamp ? ( + + ) : lastEnteredTimestamp < mail?.created && + isLessThanOneWeekOld(mail?.created) ? ( + + ) : ( + + )} + + + + ); })} - - - - - + + + - - - ) -} + ); +}; diff --git a/src/components/Group/Settings.tsx b/src/components/Group/Settings.tsx index 2aba6f6..0279d2b 100644 --- a/src/components/Group/Settings.tsx +++ b/src/components/Group/Settings.tsx @@ -1,39 +1,25 @@ -import * as React from "react"; -import Button from "@mui/material/Button"; -import Dialog from "@mui/material/Dialog"; -import ListItemText from "@mui/material/ListItemText"; -import ListItemButton from "@mui/material/ListItemButton"; -import List from "@mui/material/List"; -import Divider from "@mui/material/Divider"; -import AppBar from "@mui/material/AppBar"; -import Toolbar from "@mui/material/Toolbar"; -import IconButton from "@mui/material/IconButton"; -import Typography from "@mui/material/Typography"; -import CloseIcon from "@mui/icons-material/Close"; -import Slide from "@mui/material/Slide"; -import { TransitionProps } from "@mui/material/transitions"; -import ListOfMembers from "./ListOfMembers"; -import { InviteMember } from "./InviteMember"; -import { ListOfInvites } from "./ListOfInvites"; -import { ListOfBans } from "./ListOfBans"; -import { ListOfJoinRequests } from "./ListOfJoinRequests"; -import { Box, FormControlLabel, Switch, Tab, Tabs, styled } from "@mui/material"; -import { CustomizedSnackbars } from "../Snackbar/Snackbar"; -import { MyContext, isMobile } from "../../App"; -import { getGroupMembers, getNames } from "./Group"; -import { LoadingSnackbar } from "../Snackbar/LoadingSnackbar"; -import { getFee } from "../../background"; -import { LoadingButton } from "@mui/lab"; -import { subscribeToEvent, unsubscribeFromEvent } from "../../utils/events"; -import { enabledDevModeAtom } from "../../atoms/global"; -import { useRecoilState } from "recoil"; +import { + ChangeEvent, + forwardRef, + Fragment, + ReactElement, + Ref, + useEffect, + useState, +} from 'react'; +import Dialog from '@mui/material/Dialog'; +import AppBar from '@mui/material/AppBar'; +import Toolbar from '@mui/material/Toolbar'; +import IconButton from '@mui/material/IconButton'; +import Typography from '@mui/material/Typography'; +import CloseIcon from '@mui/icons-material/Close'; +import Slide from '@mui/material/Slide'; +import { TransitionProps } from '@mui/material/transitions'; +import { Box, FormControlLabel, Switch, styled, useTheme } from '@mui/material'; +import { enabledDevModeAtom } from '../../atoms/global'; -function a11yProps(index: number) { - return { - id: `simple-tab-${index}`, - "aria-controls": `simple-tabpanel-${index}`, - }; -} +import ThemeManager from '../Theme/ThemeManager'; +import { useAtom } from 'jotai'; const LocalNodeSwitch = styled(Switch)(({ theme }) => ({ padding: 8, @@ -49,13 +35,13 @@ const LocalNodeSwitch = styled(Switch)(({ theme }) => ({ }, '&::before': { backgroundImage: `url('data:image/svg+xml;utf8,')`, left: 12, }, '&::after': { backgroundImage: `url('data:image/svg+xml;utf8,')`, right: 12, }, @@ -68,44 +54,43 @@ const LocalNodeSwitch = styled(Switch)(({ theme }) => ({ }, })); -const Transition = React.forwardRef(function Transition( +const Transition = forwardRef(function Transition( props: TransitionProps & { - children: React.ReactElement; + children: ReactElement; }, - ref: React.Ref + ref: Ref ) { return ; }); -export const Settings = ({ - address, - open, - setOpen, -}) => { - const [checked, setChecked] = React.useState(false); - const [isEnabledDevMode, setIsEnabledDevMode] = useRecoilState(enabledDevModeAtom) +export const Settings = ({ address, open, setOpen }) => { + const [checked, setChecked] = useState(false); + const [isEnabledDevMode, setIsEnabledDevMode] = useAtom(enabledDevModeAtom); - + const theme = useTheme(); - const handleChange = (event: React.ChangeEvent) => { + const handleChange = (event: ChangeEvent) => { setChecked(event.target.checked); - window.sendMessage("addUserSettings", { - keyValue: { - key: 'disable-push-notifications', - value: event.target.checked, - }, - }) + window + .sendMessage('addUserSettings', { + keyValue: { + key: 'disable-push-notifications', + value: event.target.checked, + }, + }) .then((response) => { if (response?.error) { - console.error("Error adding user settings:", response.error); + console.error('Error adding user settings:', response.error); } else { - console.log("User settings added successfully"); + console.log('User settings added successfully'); // TODO translate } }) .catch((error) => { - console.error("Failed to add user settings:", error.message || "An error occurred"); + console.error( + 'Failed to add user settings:', + error.message || 'An error occurred' + ); }); - }; const handleClose = () => { @@ -115,9 +100,10 @@ export const Settings = ({ const getUserSettings = async () => { try { return new Promise((res, rej) => { - window.sendMessage("getUserSettings", { - key: "disable-push-notifications", - }) + window + .sendMessage('getUserSettings', { + key: 'disable-push-notifications', + }) .then((response) => { if (!response?.error) { setChecked(response || false); @@ -127,34 +113,32 @@ export const Settings = ({ rej(response.error); }) .catch((error) => { - rej(error.message || "An error occurred"); + rej(error.message || 'An error occurred'); }); - }); } catch (error) { - console.log("error", error); + console.log('error', error); } }; - React.useEffect(() => { + useEffect(() => { getUserSettings(); }, []); - - return ( - + - + - + General Settings + + @@ -188,20 +172,24 @@ export const Settings = ({ /> {window?.electronAPI && ( { - setIsEnabledDevMode(e.target.checked) - localStorage.setItem('isEnabledDevMode', JSON.stringify(e.target.checked)) - }} /> - } - label="Enable dev mode" - /> + control={ + { + setIsEnabledDevMode(e.target.checked); + localStorage.setItem( + 'isEnabledDevMode', + JSON.stringify(e.target.checked) + ); + }} + /> + } + label="Enable dev mode" + /> )} + - + ); }; diff --git a/src/components/Group/ThingsToDoInitial.tsx b/src/components/Group/ThingsToDoInitial.tsx index 367b3b1..84c806b 100644 --- a/src/components/Group/ThingsToDoInitial.tsx +++ b/src/components/Group/ThingsToDoInitial.tsx @@ -1,28 +1,26 @@ -import * as React from "react"; -import List from "@mui/material/List"; -import ListItem from "@mui/material/ListItem"; -import ListItemButton from "@mui/material/ListItemButton"; -import ListItemIcon from "@mui/material/ListItemIcon"; -import ListItemText from "@mui/material/ListItemText"; -import Checkbox from "@mui/material/Checkbox"; -import IconButton from "@mui/material/IconButton"; -import CommentIcon from "@mui/icons-material/Comment"; -import InfoIcon from "@mui/icons-material/Info"; -import { Box, Typography } from "@mui/material"; -import { Spacer } from "../../common/Spacer"; -import { isMobile } from "../../App"; -import { QMailMessages } from "./QMailMessages"; -import { executeEvent } from "../../utils/events"; +import * as React from 'react'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import { Box, Typography, useTheme } from '@mui/material'; +import { Spacer } from '../../common/Spacer'; +import { QMailMessages } from './QMailMessages'; +import { executeEvent } from '../../utils/events'; +import { useTranslation } from 'react-i18next'; -export const ThingsToDoInitial = ({ myAddress, name, hasGroups, balance, userInfo }) => { +export const ThingsToDoInitial = ({ + myAddress, + name, + hasGroups, + balance, + userInfo, +}) => { const [checked1, setChecked1] = React.useState(false); const [checked2, setChecked2] = React.useState(false); - // const [checked3, setChecked3] = React.useState(false); - - // React.useEffect(() => { - // if (hasGroups) setChecked3(true); - // }, [hasGroups]); - + const { t } = useTranslation(['core', 'tutorial']); + const theme = useTheme(); React.useEffect(() => { if (balance && +balance >= 6) { @@ -30,201 +28,171 @@ export const ThingsToDoInitial = ({ myAddress, name, hasGroups, balance, userInf } }, [balance]); - React.useEffect(() => { if (name) setChecked2(true); }, [name]); + const isLoaded = React.useMemo(() => { + if (userInfo !== null) return true; + return false; + }, [userInfo]); - const isLoaded = React.useMemo(()=> { - if(userInfo !== null) return true - return false - }, [ userInfo]) + const hasDoneNameAndBalanceAndIsLoaded = React.useMemo(() => { + if (isLoaded && checked1 && checked2) return true; + return false; + }, [checked1, isLoaded, checked2]); - const hasDoneNameAndBalanceAndIsLoaded = React.useMemo(()=> { - if(isLoaded && checked1 && checked2) return true - return false -}, [checked1, isLoaded, checked2]) - -if(hasDoneNameAndBalanceAndIsLoaded){ - return ( - - ); -} -if(!isLoaded) return null + if (hasDoneNameAndBalanceAndIsLoaded) { + return ( + + ); + } + if (!isLoaded) return null; return ( - {!isLoaded ? 'Loading...' : 'Getting Started' } + {!isLoaded + ? t('core:loading', { postProcess: 'capitalize' }) + : t('tutorial:initial.getting_started', { + postProcess: 'capitalize', + })} + {isLoaded && ( - - - { - executeEvent("openBuyQortInfo", {}) - }} - > - - - - {/* */} - - - - - // - // - // } - disablePadding - > - - - { - executeEvent('openRegisterName', {}) - }} sx={{ - "& .MuiTypography-root": { - fontSize: "1rem", - fontWeight: 400, - }, - }} primary={`Register a name`} /> - - - - - - {/* - - - - - - - - */} - + + + { + executeEvent('openBuyQortInfo', {}); + }} + > + + + + + + + + + { + executeEvent('openRegisterName', {}); + }} + sx={{ + '& .MuiTypography-root': { + fontSize: '1rem', + fontWeight: 400, + }, + }} + primary={t('tutorial:initial.register_name', { + postProcess: 'capitalize', + })} + /> + + + + + + )} - ); diff --git a/src/components/Group/UserListOfInvites.tsx b/src/components/Group/UserListOfInvites.tsx index 2a63896..a0362be 100644 --- a/src/components/Group/UserListOfInvites.tsx +++ b/src/components/Group/UserListOfInvites.tsx @@ -1,240 +1,271 @@ -import { Box, Button, ListItem, ListItemButton, ListItemText, Popover, Typography } from '@mui/material'; -import React, { useContext, useEffect, useRef, useState } from 'react' -import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from 'react-virtualized'; +import { + Box, + ListItem, + ListItemButton, + ListItemText, + Popover, + Typography, + useTheme, +} from '@mui/material'; +import { useContext, useEffect, useRef, useState } from 'react'; +import { + AutoSizer, + CellMeasurer, + CellMeasurerCache, + List, +} from 'react-virtualized'; import { MyContext, getBaseApiReact } from '../../App'; import { LoadingButton } from '@mui/lab'; -import { getBaseApi, getFee } from '../../background'; +import { getFee } from '../../background'; import LockIcon from '@mui/icons-material/Lock'; import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred'; -import { Spacer } from "../../common/Spacer"; +import { Spacer } from '../../common/Spacer'; +import { useSetAtom } from 'jotai'; +import { txListAtom } from '../../atoms/global'; const cache = new CellMeasurerCache({ - fixedWidth: true, - defaultHeight: 50, - }); + fixedWidth: true, + defaultHeight: 50, +}); - - -const getGroupInfo = async (groupId)=> { +const getGroupInfo = async (groupId) => { const response = await fetch(`${getBaseApiReact()}/groups/` + groupId); const groupData = await response.json(); if (groupData) { - return groupData - } -} - export const getGroupNames = async (listOfGroups) => { - let groups = []; - if (listOfGroups && Array.isArray(listOfGroups)) { - for (const group of listOfGroups) { - - const groupInfo = await getGroupInfo(group.groupId); - if (groupInfo) { - groups.push({ ...group, ...groupInfo }); - - } - } - } - return groups; + return groupData; } - -export const UserListOfInvites = ({myAddress, setInfoSnack, setOpenSnack}) => { - const {txList, setTxList, show} = useContext(MyContext) - const [invites, setInvites] = useState([]); - const [isLoading, setIsLoading] = useState(false); - - const [popoverAnchor, setPopoverAnchor] = useState(null); // Track which list item the popover is anchored to - const [openPopoverIndex, setOpenPopoverIndex] = useState(null); // Track which list item has the popover open - const listRef = useRef(); - - const getRequests = async () => { - try { - const response = await fetch(`${getBaseApiReact()}/groups/invites/${myAddress}/?limit=0`); - const inviteData = await response.json(); - - const resMoreData = await getGroupNames(inviteData) - setInvites(resMoreData); - } catch (error) { - console.error(error); +}; +export const getGroupNames = async (listOfGroups) => { + let groups = []; + if (listOfGroups && Array.isArray(listOfGroups)) { + for (const group of listOfGroups) { + const groupInfo = await getGroupInfo(group.groupId); + if (groupInfo) { + groups.push({ ...group, ...groupInfo }); } } - - useEffect(() => { - - getRequests(); - - }, []); - - const handlePopoverOpen = (event, index) => { - setPopoverAnchor(event.currentTarget); - setOpenPopoverIndex(index); - }; - - const handlePopoverClose = () => { - setPopoverAnchor(null); - setOpenPopoverIndex(null); - }; - - const handleJoinGroup = async (groupId, groupName)=> { - try { - - const fee = await getFee('JOIN_GROUP') - await show({ - message: "Would you like to perform an JOIN_GROUP transaction?" , - publishFee: fee.fee + ' QORT' - }) + } + return groups; +}; - setIsLoading(true); +export const UserListOfInvites = ({ + myAddress, + setInfoSnack, + setOpenSnack, +}) => { + const { show } = useContext(MyContext); + const setTxList = useSetAtom(txListAtom); - await new Promise((res, rej)=> { - window.sendMessage("joinGroup", { + const [invites, setInvites] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const theme = useTheme(); + const [popoverAnchor, setPopoverAnchor] = useState(null); // Track which list item the popover is anchored to + const [openPopoverIndex, setOpenPopoverIndex] = useState(null); // Track which list item has the popover open + const listRef = useRef(); + + const getRequests = async () => { + try { + const response = await fetch( + `${getBaseApiReact()}/groups/invites/${myAddress}/?limit=0` + ); + const inviteData = await response.json(); + + const resMoreData = await getGroupNames(inviteData); + setInvites(resMoreData); + } catch (error) { + console.error(error); + } + }; + + useEffect(() => { + getRequests(); + }, []); + + const handlePopoverOpen = (event, index) => { + setPopoverAnchor(event.currentTarget); + setOpenPopoverIndex(index); + }; + + const handlePopoverClose = () => { + setPopoverAnchor(null); + setOpenPopoverIndex(null); + }; + + const handleJoinGroup = async (groupId, groupName) => { + try { + const fee = await getFee('JOIN_GROUP'); // TODO translate + await show({ + message: 'Would you like to perform an JOIN_GROUP transaction?', + publishFee: fee.fee + ' QORT', + }); + + setIsLoading(true); + + await new Promise((res, rej) => { + window + .sendMessage('joinGroup', { groupId, }) - .then((response) => { - if (!response?.error) { - setTxList((prev) => [ - { - ...response, - type: 'joined-group', - label: `Joined Group ${groupName}: awaiting confirmation`, - labelDone: `Joined Group ${groupName}: success!`, - done: false, - groupId, - }, - ...prev, - ]); - res(response); - setInfoSnack({ - type: "success", - message: "Successfully requested to join group. It may take a couple of minutes for the changes to propagate", - }); - setOpenSnack(true); - handlePopoverClose(); - return; - } + .then((response) => { + if (!response?.error) { + setTxList((prev) => [ + { + ...response, + type: 'joined-group', + label: `Joined Group ${groupName}: awaiting confirmation`, + labelDone: `Joined Group ${groupName}: success!`, + done: false, + groupId, + }, + ...prev, + ]); + res(response); setInfoSnack({ - type: "error", - message: response?.error, + type: 'success', + message: + 'Successfully requested to join group. It may take a couple of minutes for the changes to propagate', }); setOpenSnack(true); - rej(response.error); - }) - .catch((error) => { - setInfoSnack({ - type: "error", - message: error.message || "An error occurred", - }); - setOpenSnack(true); - rej(error); + handlePopoverClose(); + return; + } + setInfoSnack({ + type: 'error', + message: response?.error, }); - - }) - - } catch (error) { - - } finally { - setIsLoading(false); - - } + setOpenSnack(true); + rej(response.error); + }) + .catch((error) => { + setInfoSnack({ + type: 'error', + message: error.message || 'An error occurred', + }); + setOpenSnack(true); + rej(error); + }); + }); + } catch (error) { + } finally { + setIsLoading(false); } - - const rowRenderer = ({ index, key, parent, style }) => { - const invite = invites[index]; - - return ( - - {({ measure }) => ( -
- - - { + const invite = invites[index]; + + return ( + + {({ measure }) => ( +
+ + + - Join {invite?.groupName} - Join {invite?.groupName} + handleJoinGroup(invite?.groupId, invite?.groupName)}>Join group - - - handlePopoverOpen(event, index)}> + variant="contained" + onClick={() => + handleJoinGroup(invite?.groupId, invite?.groupName) + } + > + Join group + + + + handlePopoverOpen(event, index)} + > {invite?.isOpen === false && ( - + + )} + {invite?.isOpen === true && ( + + )} + + + + +
)} - {invite?.isOpen === true && ( - - )} - - - -
-
- )} -
- ); - }; - - return ( - + ); + }; + + return ( + -

Invite list

-
+

Invite list

+
- - {({ height, width }) => ( - - )} - -
- - ); -} + + {({ height, width }) => ( + + )} + +
+
+ ); +}; diff --git a/src/components/Group/WalletsAppWrapper.tsx b/src/components/Group/WalletsAppWrapper.tsx index 788a36a..7e44eb6 100644 --- a/src/components/Group/WalletsAppWrapper.tsx +++ b/src/components/Group/WalletsAppWrapper.tsx @@ -1,37 +1,32 @@ -import { Box, ButtonBase, Divider, Typography } from "@mui/material"; -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -import CloseIcon from "@mui/icons-material/Close"; -import AppViewerContainer from "../Apps/AppViewerContainer"; +import { Box, ButtonBase, Divider, Typography, useTheme } from '@mui/material'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import CloseIcon from '@mui/icons-material/Close'; +import AppViewerContainer from '../Apps/AppViewerContainer'; import { executeEvent, subscribeToEvent, unsubscribeFromEvent, -} from "../../utils/events"; -import { useRecoilState } from "recoil"; -import { navigationControllerAtom } from "../../atoms/global"; -import { AppsNavBarLeft, AppsNavBarParent } from "../Apps/Apps-styles"; -import NavBack from "../../assets/svgs/NavBack.svg"; -import RefreshIcon from "@mui/icons-material/Refresh"; +} from '../../utils/events'; +import { navigationControllerAtom } from '../../atoms/global'; +import { AppsNavBarLeft, AppsNavBarParent } from '../Apps/Apps-styles'; +import { NavBack } from '../../assets/Icons/NavBack.tsx'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import { useAtom } from 'jotai'; export const WalletsAppWrapper = () => { const iframeRef = useRef(null); const [isOpen, setIsOpen] = useState(false); - const [navigationController, setNavigationController] = useRecoilState( + const [navigationController, setNavigationController] = useAtom( navigationControllerAtom ); - const [selectedTab, setSelectedTab] = useState({ - tabId: "5558589", - name: "Q-Wallets", - service: "APP", - path: 'qortal?authOnMount=true' - }); + const [selectedTab, setSelectedTab] = useState({ + tabId: '5558589', + name: 'Q-Wallets', + service: 'APP', + path: 'qortal?authOnMount=true', + }); + const theme = useTheme(); const isDisableBackButton = useMemo(() => { if (selectedTab && navigationController[selectedTab?.tabId]?.hasBack) return false; @@ -48,64 +43,64 @@ export const WalletsAppWrapper = () => { ); useEffect(() => { - subscribeToEvent("openWalletsApp", openWalletsAppFunc); + subscribeToEvent('openWalletsApp', openWalletsAppFunc); return () => { - unsubscribeFromEvent("openWalletsApp", openWalletsAppFunc); + unsubscribeFromEvent('openWalletsApp', openWalletsAppFunc); }; }, [openWalletsAppFunc]); - const handleClose = ()=> { + const handleClose = () => { setIsOpen(false); - iframeRef.current = null - } + iframeRef.current = null; + }; return ( <> {isOpen && ( Q-Wallets - + + + { skipAuth={true} /> - + { executeEvent(`navigateBackApp-${selectedTab?.tabId}`, {}); @@ -124,31 +121,30 @@ export const WalletsAppWrapper = () => { disabled={isDisableBackButton} sx={{ opacity: !isDisableBackButton ? 1 : 0.1, - cursor: !isDisableBackButton ? "pointer" : "default", + cursor: !isDisableBackButton ? 'pointer' : 'default', }} > - + - { - if (selectedTab?.refreshFunc) { - selectedTab.refreshFunc(selectedTab?.tabId); - - } else { - executeEvent("refreshApp", { - tabId: selectedTab?.tabId, - }); - } - - - }}> - + { + if (selectedTab?.refreshFunc) { + selectedTab.refreshFunc(selectedTab?.tabId); + } else { + executeEvent('refreshApp', { + tabId: selectedTab?.tabId, + }); + } + }} + > + diff --git a/src/components/Group/WebsocketActive.tsx b/src/components/Group/WebsocketActive.tsx index b9da4d2..01f3ead 100644 --- a/src/components/Group/WebsocketActive.tsx +++ b/src/components/Group/WebsocketActive.tsx @@ -1,12 +1,16 @@ -import React, { useEffect, useRef } from 'react'; -import { getBaseApiReactSocket, pauseAllQueues, resumeAllQueues } from '../../App'; +import { useEffect, useRef } from 'react'; +import { + getBaseApiReactSocket, + pauseAllQueues, + resumeAllQueues, +} from '../../App'; import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events'; export const WebSocketActive = ({ myAddress, setIsLoadingGroups }) => { const socketRef = useRef(null); // WebSocket reference const timeoutIdRef = useRef(null); // Timeout ID reference const groupSocketTimeoutRef = useRef(null); // Group Socket Timeout reference - const initiateRef = useRef(null) + const initiateRef = useRef(null); const forceCloseWebSocket = () => { if (socketRef.current) { clearTimeout(timeoutIdRef.current); @@ -17,21 +21,20 @@ export const WebSocketActive = ({ myAddress, setIsLoadingGroups }) => { }; const logoutEventFunc = () => { - forceCloseWebSocket() + forceCloseWebSocket(); }; useEffect(() => { - subscribeToEvent("logout-event", logoutEventFunc); + subscribeToEvent('logout-event', logoutEventFunc); return () => { - unsubscribeFromEvent("logout-event", logoutEventFunc); + unsubscribeFromEvent('logout-event', logoutEventFunc); }; }, []); useEffect(() => { if (!myAddress) return; // Only proceed if myAddress is set - const pingHeads = () => { try { if (socketRef.current?.readyState === WebSocket.OPEN) { @@ -53,10 +56,9 @@ export const WebSocketActive = ({ myAddress, setIsLoadingGroups }) => { const currentAddress = myAddress; try { - if(!initiateRef.current) { - setIsLoadingGroups(true) - pauseAllQueues() - + if (!initiateRef.current) { + setIsLoadingGroups(true); + pauseAllQueues(); } const socketLink = `${getBaseApiReactSocket()}/websockets/chat/active/${currentAddress}?encoding=BASE64&haschatreference=false`; socketRef.current = new WebSocket(socketLink); @@ -71,34 +73,45 @@ export const WebSocketActive = ({ myAddress, setIsLoadingGroups }) => { clearTimeout(timeoutIdRef.current); groupSocketTimeoutRef.current = setTimeout(pingHeads, 45000); // Ping every 45 seconds } else { - if(!initiateRef.current) { - setIsLoadingGroups(false) - initiateRef.current = true - resumeAllQueues() - + if (!initiateRef.current) { + setIsLoadingGroups(false); + initiateRef.current = true; + resumeAllQueues(); } const data = JSON.parse(e.data); - const copyGroups = [...(data?.groups || [])] - const findIndex = copyGroups?.findIndex(item => item?.groupId === 0) - if(findIndex !== -1){ + const copyGroups = [...(data?.groups || [])]; + const findIndex = copyGroups?.findIndex( + (item) => item?.groupId === 0 + ); + if (findIndex !== -1) { copyGroups[findIndex] = { ...(copyGroups[findIndex] || {}), - groupId: "0" - } + groupId: '0', + }; } - const filteredGroups = copyGroups - const sortedGroups = filteredGroups.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); - const sortedDirects = (data?.direct || []).filter(item => - item?.name !== 'extension-proxy' && item?.address !== 'QSMMGSgysEuqDCuLw3S4cHrQkBrh3vP3VH' - ).sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); + const filteredGroups = copyGroups; + const sortedGroups = filteredGroups.sort( + (a, b) => (b.timestamp || 0) - (a.timestamp || 0) + ); + const sortedDirects = (data?.direct || []) + .filter( + (item) => + item?.name !== 'extension-proxy' && + item?.address !== 'QSMMGSgysEuqDCuLw3S4cHrQkBrh3vP3VH' + ) + .sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); - window.sendMessage("handleActiveGroupDataFromSocket", { - groups: sortedGroups, - directs: sortedDirects, - }).catch((error) => { - console.error("Failed to handle active group data from socket:", error.message || "An error occurred"); + window + .sendMessage('handleActiveGroupDataFromSocket', { + groups: sortedGroups, + directs: sortedDirects, + }) + .catch((error) => { + console.error( + 'Failed to handle active group data from socket:', + error.message || 'An error occurred' + ); }); - } } catch (error) { console.error('Error parsing onmessage data:', error); @@ -127,9 +140,7 @@ export const WebSocketActive = ({ myAddress, setIsLoadingGroups }) => { } }; - - initWebsocketMessageGroup(); // Initialize WebSocket on component mount - + initWebsocketMessageGroup(); // Initialize WebSocket on component mount return () => { forceCloseWebSocket(); // Clean up WebSocket on component unmount diff --git a/src/components/Group/useBlockUsers.tsx b/src/components/Group/useBlockUsers.tsx index eeb5361..e82d0c6 100644 --- a/src/components/Group/useBlockUsers.tsx +++ b/src/components/Group/useBlockUsers.tsx @@ -1,218 +1,203 @@ -import React, { useCallback, useEffect, useRef } from "react"; -import { getBaseApiReact } from "../../App"; -import { truncate } from "lodash"; - - +import { useCallback, useEffect, useMemo, useRef } from 'react'; export const useBlockedAddresses = () => { - const userBlockedRef = useRef({}) - const userNamesBlockedRef = useRef({}) - - const getAllBlockedUsers = useCallback(()=> { + const userBlockedRef = useRef({}); + const userNamesBlockedRef = useRef({}); + const getAllBlockedUsers = useCallback(() => { return { names: userNamesBlockedRef.current, - addresses: userBlockedRef.current - } - }, []) + addresses: userBlockedRef.current, + }; + }, []); - const isUserBlocked = useCallback((address, name)=> { + const isUserBlocked = useCallback((address, name) => { try { - if(!address) return false - if(userBlockedRef.current[address]) return true - return false - - + if (!address) return false; + if (userBlockedRef.current[address]) return true; + return false; } catch (error) { - //error + console.log(error); } - }, []) + }, []); - useEffect(()=> { - const fetchBlockedList = async ()=> { + useEffect(() => { + const fetchBlockedList = async () => { try { - const response = await new Promise((res, rej) => { - window.sendMessage("listActions", { - + const response = await new Promise((res, rej) => { + window + .sendMessage('listActions', { type: 'get', listName: `blockedAddresses`, - - }) - .then((response) => { - if (response.error) { - rej(response?.message); - return; - } else { - res(response); - } - }) - .catch((error) => { - console.error("Failed qortalRequest", error); - }); - }) - const blockedUsers = {} - response?.forEach((item)=> { - blockedUsers[item] = true - }) - userBlockedRef.current = blockedUsers + }) + .then((response) => { + if (response.error) { + rej(response?.message); + return; + } else { + res(response); + } + }) + .catch((error) => { + console.error('Failed qortalRequest', error); + }); + }); - const response2 = await new Promise((res, rej) => { - window.sendMessage("listActions", { - + const blockedUsers = {}; + + response?.forEach((item) => { + blockedUsers[item] = true; + }); + + userBlockedRef.current = blockedUsers; + + const response2 = await new Promise((res, rej) => { + window + .sendMessage('listActions', { type: 'get', listName: `blockedNames`, - + }) + .then((response) => { + if (response.error) { + rej(response?.message); + return; + } else { + res(response); + } + }) + .catch((error) => { + console.error('Failed qortalRequest', error); + }); + }); + + const blockedUsers2 = {}; + + response2?.forEach((item) => { + blockedUsers2[item] = true; + }); + + userNamesBlockedRef.current = blockedUsers2; + } catch (error) { + console.error(error); + } + }; + fetchBlockedList(); + }, []); + + const removeBlockFromList = useCallback(async (address, name) => { + if (name) { + await new Promise((res, rej) => { + window + .sendMessage('listActions', { + type: 'remove', + items: [name], + listName: 'blockedNames', }) .then((response) => { if (response.error) { rej(response?.message); return; } else { + const copyObject = { ...userNamesBlockedRef.current }; + delete copyObject[name]; + userNamesBlockedRef.current = copyObject; + res(response); } }) .catch((error) => { - console.error("Failed qortalRequest", error); + console.error('Failed qortalRequest', error); }); - }) - const blockedUsers2 = {} - response2?.forEach((item)=> { - blockedUsers2[item] = true - }) - userNamesBlockedRef.current = blockedUsers2 - - - } catch (error) { - console.error(error) - } - } - fetchBlockedList() - }, []) - - const removeBlockFromList = useCallback(async (address, name)=> { - if(name){ - await new Promise((res, rej) => { - window.sendMessage("listActions", { - - type: 'remove', - items: [name] , - listName: 'blockedNames' - - }) - .then((response) => { - if (response.error) { - rej(response?.message); - return; - } else { - - const copyObject = {...userNamesBlockedRef.current} - delete copyObject[name] - userNamesBlockedRef.current = copyObject - - - res(response); - } - }) - .catch((error) => { - console.error("Failed qortalRequest", error); - }); - }) + }); } - if(address){ + if (address) { await new Promise((res, rej) => { - window.sendMessage("listActions", { - + window + .sendMessage('listActions', { type: 'remove', items: [address], - listName: 'blockedAddresses' - - }) - .then((response) => { - if (response.error) { - rej(response?.message); - return; - } else { - - const copyObject = {...userBlockedRef.current} - delete copyObject[address] - userBlockedRef.current = copyObject - - - res(response); - } - }) - .catch((error) => { - console.error("Failed qortalRequest", error); - }); - }) - } - - - }, []) + listName: 'blockedAddresses', + }) + .then((response) => { + if (response.error) { + rej(response?.message); + return; + } else { + const copyObject = { ...userBlockedRef.current }; + delete copyObject[address]; + userBlockedRef.current = copyObject; - const addToBlockList = useCallback(async (address, name)=> { - if(name){ + res(response); + } + }) + .catch((error) => { + console.error('Failed qortalRequest', error); + }); + }); + } + }, []); + + const addToBlockList = useCallback(async (address, name) => { + if (name) { await new Promise((res, rej) => { - window.sendMessage("listActions", { - + window + .sendMessage('listActions', { type: 'add', items: [name], - listName: 'blockedNames' - - }) - .then((response) => { - if (response.error) { - rej(response?.message); - return; - } else { - const copyObject = {...userNamesBlockedRef.current} - copyObject[name] = true - userNamesBlockedRef.current = copyObject - - - res(response); - } - }) - .catch((error) => { - console.error("Failed qortalRequest", error); - }); - }) + listName: 'blockedNames', + }) + .then((response) => { + if (response.error) { + rej(response?.message); + return; + } else { + const copyObject = { ...userNamesBlockedRef.current }; + copyObject[name] = true; + userNamesBlockedRef.current = copyObject; + + res(response); + } + }) + .catch((error) => { + console.error('Failed qortalRequest', error); + }); + }); } - if(address){ + if (address) { await new Promise((res, rej) => { - window.sendMessage("listActions", { - + window + .sendMessage('listActions', { type: 'add', items: [address], - listName: 'blockedAddresses' - - }) - .then((response) => { - if (response.error) { - rej(response?.message); - return; - } else { - - const copyObject = {...userBlockedRef.current} - copyObject[address] = true - userBlockedRef.current = copyObject - - res(response); - } - }) - .catch((error) => { - console.error("Failed qortalRequest", error); - }); - }) - } - - }, []) + listName: 'blockedAddresses', + }) + .then((response) => { + if (response.error) { + rej(response?.message); + return; + } else { + const copyObject = { ...userBlockedRef.current }; + copyObject[address] = true; + userBlockedRef.current = copyObject; - return { - isUserBlocked, - addToBlockList, - removeBlockFromList, - getAllBlockedUsers - }; + res(response); + } + }) + .catch((error) => { + console.error('Failed qortalRequest', error); + }); + }); + } + }, []); + + return useMemo( + () => ({ + isUserBlocked, + addToBlockList, + removeBlockFromList, + getAllBlockedUsers, + }), + [isUserBlocked, addToBlockList, removeBlockFromList, getAllBlockedUsers] + ); }; diff --git a/src/components/Group/useHandleUserInfo.tsx b/src/components/Group/useHandleUserInfo.tsx index a497259..8b8796f 100644 --- a/src/components/Group/useHandleUserInfo.tsx +++ b/src/components/Group/useHandleUserInfo.tsx @@ -1,34 +1,30 @@ -import React, { useCallback, useRef } from "react"; -import { getBaseApiReact } from "../../App"; - - +import { useCallback, useRef } from 'react'; +import { getBaseApiReact } from '../../App'; export const useHandleUserInfo = () => { - const userInfoRef = useRef({}) + const userInfoRef = useRef({}); - - const getIndividualUserInfo = useCallback(async (address)=> { + const getIndividualUserInfo = useCallback(async (address) => { try { - if(!address) return null - if(userInfoRef.current[address] !== undefined) return userInfoRef.current[address] + if (!address) return null; + if (userInfoRef.current[address] !== undefined) + return userInfoRef.current[address]; const url = `${getBaseApiReact()}/addresses/${address}`; - const response = await fetch(url); - if (!response.ok) { - throw new Error("network error"); - } - const data = await response.json(); - userInfoRef.current = { - ...userInfoRef.current, - [address]: data?.level - } - return data?.level + const response = await fetch(url); + if (!response.ok) { + throw new Error('network error'); + } + const data = await response.json(); + userInfoRef.current = { + ...userInfoRef.current, + [address]: data?.level, + }; + return data?.level; } catch (error) { - //error + console.log(error); } - }, []) + }, []); - return { - getIndividualUserInfo, - }; + return getIndividualUserInfo; }; diff --git a/src/components/GroupAvatar.tsx b/src/components/GroupAvatar.tsx new file mode 100644 index 0000000..3364b8f --- /dev/null +++ b/src/components/GroupAvatar.tsx @@ -0,0 +1,293 @@ +import React, { useCallback, useContext, useEffect, useState } from 'react'; +import Logo2 from '../assets/svgs/Logo2.svg'; +import { MyContext, getArbitraryEndpointReact, getBaseApiReact } from '../App'; +import { + Avatar, + Box, + Button, + ButtonBase, + Popover, + Typography, + useTheme, +} from '@mui/material'; +import { Spacer } from '../common/Spacer'; +import ImageUploader from '../common/ImageUploader'; +import { getFee } from '../background'; +import { fileToBase64 } from '../utils/fileReading'; +import { LoadingButton } from '@mui/lab'; +import ErrorIcon from '@mui/icons-material/Error'; + +export const GroupAvatar = ({ + myName, + balance, + setOpenSnack, + setInfoSnack, + groupId, +}) => { + const [hasAvatar, setHasAvatar] = useState(false); + const [avatarFile, setAvatarFile] = useState(null); + const [tempAvatar, setTempAvatar] = useState(null); + const { show } = useContext(MyContext); + + const [anchorEl, setAnchorEl] = useState(null); + const [isLoading, setIsLoading] = useState(false); + // Handle child element click to open Popover + const handleChildClick = (event) => { + event.stopPropagation(); // Prevent parent onClick from firing + setAnchorEl(event.currentTarget); + }; + + // Handle closing the Popover + const handleClose = () => { + setAnchorEl(null); + }; + + // Determine if the popover is open + const open = Boolean(anchorEl); + const id = open ? 'avatar-img' : undefined; + + const checkIfAvatarExists = useCallback(async (name, groupId) => { + try { + const identifier = `qortal_group_avatar_${groupId}`; + const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=THUMBNAIL&identifier=${identifier}&limit=1&name=${name}&includemetadata=false&prefix=true`; + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + const responseData = await response.json(); + if (responseData?.length > 0) { + setHasAvatar(true); + } + } catch (error) { + console.log(error); + } + }, []); + useEffect(() => { + if (!myName || !groupId) return; + checkIfAvatarExists(myName, groupId); + }, [myName, groupId, checkIfAvatarExists]); + + const publishAvatar = async () => { + try { + if (!groupId) return; + const fee = await getFee('ARBITRARY'); + if (+balance < +fee.fee) + throw new Error(`Publishing an Avatar requires ${fee.fee}`); + await show({ + message: 'Would you like to publish an avatar?', + publishFee: fee.fee + ' QORT', + }); + setIsLoading(true); + const avatarBase64 = await fileToBase64(avatarFile); + await new Promise((res, rej) => { + window + .sendMessage('publishOnQDN', { + data: avatarBase64, + identifier: `qortal_group_avatar_${groupId}`, + service: 'THUMBNAIL', + }) + .then((response) => { + if (!response?.error) { + res(response); + return; + } + rej(response.error); + }) + .catch((error) => { + rej(error.message || 'An error occurred'); + }); + }); + setAvatarFile(null); + setTempAvatar(`data:image/webp;base64,${avatarBase64}`); + handleClose(); + } catch (error) { + if (error?.message) { + setOpenSnack(true); + setInfoSnack({ + type: 'error', + message: error?.message, + }); + } + } finally { + setIsLoading(false); + } + }; + + if (tempAvatar) { + return ( + <> + + {myName?.charAt(0)} + + + + change avatar + + + + + ); + } + + if (hasAvatar) { + return ( + <> + + {myName?.charAt(0)} + + + + change avatar + + + + + ); + } + + return ( + <> + + + + set avatar + + + + + ); +}; + +const PopoverComp = ({ + avatarFile, + setAvatarFile, + id, + open, + anchorEl, + handleClose, + publishAvatar, + isLoading, + myName, +}) => { + const theme = useTheme(); + return ( + + + + (500 KB max. for GIFS){' '} + + setAvatarFile(file)}> + + + {avatarFile?.name} + + {!myName && ( + + + + A registered name is required to set an avatar + + + )} + + + + Publish avatar + + + + ); +}; diff --git a/src/components/Home/NewUsersCTA.tsx b/src/components/Home/NewUsersCTA.tsx index 4ad5c79..44666d6 100644 --- a/src/components/Home/NewUsersCTA.tsx +++ b/src/components/Home/NewUsersCTA.tsx @@ -1,84 +1,91 @@ -import { Box, ButtonBase, Typography } from "@mui/material"; -import React from "react"; -import { Spacer } from "../../common/Spacer"; +import { Box, ButtonBase, Typography } from '@mui/material'; +import { Spacer } from '../../common/Spacer'; +import { useTranslation } from 'react-i18next'; export const NewUsersCTA = ({ balance }) => { + const { t } = useTranslation(['core']); + if (balance === undefined || +balance > 0) return null; + return ( - Are you a new user? + {t('core:new_user', { postProcess: 'capitalize' })} + + - Please message us on Telegram or Discord if you need 4 QORT to start - chatting without any limitations + {t('core:message_us', { postProcess: 'capitalize' })} + + { if (window?.electronAPI?.openExternal) { window.electronAPI.openExternal( - "https://link.qortal.dev/telegram-invite" + 'https://link.qortal.dev/telegram-invite' ); } else { window.open( - "https://link.qortal.dev/telegram-invite", - "_blank" + 'https://link.qortal.dev/telegram-invite', + '_blank' ); } }} > Telegram + { if (window?.electronAPI?.openExternal) { window.electronAPI.openExternal( - "https://link.qortal.dev/discord-invite" + 'https://link.qortal.dev/discord-invite' ); } else { - window.open("https://link.qortal.dev/discord-invite", "_blank"); + window.open('https://link.qortal.dev/discord-invite', '_blank'); } }} > diff --git a/src/components/Home/QortPrice.tsx b/src/components/Home/QortPrice.tsx index f4586fb..ec74b11 100644 --- a/src/components/Home/QortPrice.tsx +++ b/src/components/Home/QortPrice.tsx @@ -1,8 +1,9 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { getBaseApiReact } from "../../App"; -import { Box, Tooltip, Typography } from "@mui/material"; -import { BarSpinner } from "../../common/Spinners/BarSpinner/BarSpinner"; -import { formatDate } from "../../utils/time"; +import { useCallback, useEffect, useState } from 'react'; +import { getBaseApiReact } from '../../App'; +import { Box, Tooltip, Typography, useTheme } from '@mui/material'; +import { BarSpinner } from '../../common/Spinners/BarSpinner/BarSpinner'; +import { formatDate } from '../../utils/time'; +import { useTranslation } from 'react-i18next'; function getAverageLtcPerQort(trades) { let totalQort = 0; @@ -38,6 +39,8 @@ export const QortPrice = () => { const [supply, setSupply] = useState(null); const [lastBlock, setLastBlock] = useState(null); const [loading, setLoading] = useState(true); + const { t } = useTranslation(['core', 'tutorial']); + const theme = useTheme(); const getPrice = useCallback(async () => { try { @@ -101,64 +104,63 @@ export const QortPrice = () => { return () => clearInterval(interval); }, [getPrice]); - return ( + Based on the latest 20 trades } placement="bottom" arrow - sx={{ fontSize: "24" }} + sx={{ fontSize: '24' }} slotProps={{ tooltip: { sx: { - color: "#ffffff", - backgroundColor: "#444444", + color: theme.palette.text.primary, + backgroundColor: theme.palette.background.paper, }, }, arrow: { sx: { - color: "#444444", + color: theme.palette.text.primary, }, }, }} > - Price + {t('core:price', { postProcess: 'capitalize' })} + {!ltcPerQort ? ( - + ) : ( {ltcPerQort} LTC/QORT @@ -168,28 +170,28 @@ export const QortPrice = () => { - Supply + {t('core:supply', { postProcess: 'capitalize' })} + {!supply ? ( - + ) : ( {supply} QORT @@ -197,60 +199,58 @@ export const QortPrice = () => { )} - {lastBlock?.timestamp && formatDate(lastBlock?.timestamp)} - - } - placement="bottom" - arrow - sx={{ fontSize: "24" }} - slotProps={{ - tooltip: { - sx: { - color: "#ffffff", - backgroundColor: "#444444", - }, + title={ + + {lastBlock?.timestamp && formatDate(lastBlock?.timestamp)} + + } + placement="bottom" + arrow + sx={{ fontSize: '24' }} + slotProps={{ + tooltip: { + sx: { + color: theme.palette.text.primary, + backgroundColor: theme.palette.background.paper, }, - arrow: { - sx: { - color: "#444444", - }, + }, + arrow: { + sx: { + color: theme.palette.text.primary, }, - }} - > - - + - Last height + {t('core:last_height', { postProcess: 'capitalize' })} + {!lastBlock?.height ? ( - + ) : ( {lastBlock?.height} )} - - + ); diff --git a/src/components/Language/LanguageSelector.tsx b/src/components/Language/LanguageSelector.tsx new file mode 100644 index 0000000..751c0a9 --- /dev/null +++ b/src/components/Language/LanguageSelector.tsx @@ -0,0 +1,92 @@ +import { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { supportedLanguages } from '../../../i18n'; +import { Tooltip, useTheme } from '@mui/material'; + +const LanguageSelector = () => { + const { i18n, t } = useTranslation(['core']); + const [showSelect, setShowSelect] = useState(false); + const theme = useTheme(); + const selectorRef = useRef(null); + + const handleChange = (e) => { + const newLang = e.target.value; + i18n.changeLanguage(newLang); + setShowSelect(false); + }; + + const currentLang = i18n.language; + const { name, flag } = + supportedLanguages[currentLang] || supportedLanguages['en']; + + // Detect clicks outside the component + useEffect(() => { + const handleClickOutside = (event) => { + if (selectorRef.current && !selectorRef.current.contains(event.target)) { + setShowSelect(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + return ( +
+ + {showSelect ? ( + + ) : ( + + )} + +
+ ); +}; + +export default LanguageSelector; diff --git a/src/components/MainAvatar.tsx b/src/components/MainAvatar.tsx index faca10a..04d7172 100644 --- a/src/components/MainAvatar.tsx +++ b/src/components/MainAvatar.tsx @@ -1,22 +1,30 @@ -import React, { useContext, useEffect, useState } from "react"; -import Logo2 from "../assets/svgs/Logo2.svg"; -import { MyContext, getArbitraryEndpointReact, getBaseApiReact } from "../App"; -import { Avatar, Box, Button, ButtonBase, Popover, Typography } from "@mui/material"; -import { Spacer } from "../common/Spacer"; -import ImageUploader from "../common/ImageUploader"; -import { getFee } from "../background"; -import { fileToBase64 } from "../utils/fileReading"; -import { LoadingButton } from "@mui/lab"; +import React, { useContext, useEffect, useState } from 'react'; +import Logo2 from '../assets/svgs/Logo2.svg'; +import { MyContext, getArbitraryEndpointReact, getBaseApiReact } from '../App'; +import { + Avatar, + Box, + Button, + ButtonBase, + Popover, + Typography, + useTheme, +} from '@mui/material'; +import { Spacer } from '../common/Spacer'; +import ImageUploader from '../common/ImageUploader'; +import { getFee } from '../background'; +import { fileToBase64 } from '../utils/fileReading'; +import { LoadingButton } from '@mui/lab'; import ErrorIcon from '@mui/icons-material/Error'; export const MainAvatar = ({ myName, balance, setOpenSnack, setInfoSnack }) => { const [hasAvatar, setHasAvatar] = useState(false); const [avatarFile, setAvatarFile] = useState(null); - const [tempAvatar, setTempAvatar] = useState(null) + const [tempAvatar, setTempAvatar] = useState(null); const { show } = useContext(MyContext); const [anchorEl, setAnchorEl] = useState(null); -const [isLoading, setIsLoading] = useState(false) + const [isLoading, setIsLoading] = useState(false); // Handle child element click to open Popover const handleChildClick = (event) => { event.stopPropagation(); // Prevent parent onClick from firing @@ -37,93 +45,105 @@ const [isLoading, setIsLoading] = useState(false) const identifier = `qortal_avatar`; const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=THUMBNAIL&identifier=${identifier}&limit=1&name=${myName}&includemetadata=false&prefix=true`; const response = await fetch(url, { - method: "GET", + method: 'GET', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, }); const responseData = await response.json(); if (responseData?.length > 0) { setHasAvatar(true); } - } catch (error) {} + } catch (error) { + console.log(error); + } }; useEffect(() => { if (!myName) return; checkIfAvatarExists(); }, [myName]); - - const publishAvatar = async ()=> { + const publishAvatar = async () => { try { - const fee = await getFee('ARBITRARY') - if(+balance < +fee.fee) throw new Error(`Publishing an Avatar requires ${fee.fee}`) - await show({ - message: "Would you like to publish an avatar?" , - publishFee: fee.fee + ' QORT' - }) - setIsLoading(true); - const avatarBase64 = await fileToBase64(avatarFile) - await new Promise((res, rej) => { - window.sendMessage("publishOnQDN", { - data: avatarBase64, - identifier: "qortal_avatar", - service: "THUMBNAIL", - }) - .then((response) => { - if (!response?.error) { - res(response); - return; - } - rej(response.error); - }) - .catch((error) => { - rej(error.message || "An error occurred"); - }); - - }); - setAvatarFile(null); - setTempAvatar(`data:image/webp;base64,${avatarBase64}`) - handleClose() + const fee = await getFee('ARBITRARY'); + if (+balance < +fee.fee) + throw new Error(`Publishing an Avatar requires ${fee.fee}`); + await show({ + message: 'Would you like to publish an avatar?', + publishFee: fee.fee + ' QORT', + }); + setIsLoading(true); + const avatarBase64 = await fileToBase64(avatarFile); + await new Promise((res, rej) => { + window + .sendMessage('publishOnQDN', { + data: avatarBase64, + identifier: 'qortal_avatar', + service: 'THUMBNAIL', + }) + .then((response) => { + if (!response?.error) { + res(response); + return; + } + rej(response.error); + }) + .catch((error) => { + rej(error.message || 'An error occurred'); + }); + }); + setAvatarFile(null); + setTempAvatar(`data:image/webp;base64,${avatarBase64}`); + handleClose(); } catch (error) { if (error?.message) { - setOpenSnack(true) - setInfoSnack({ - type: "error", - message: error?.message, - }); - } + setOpenSnack(true); + setInfoSnack({ + type: 'error', + message: error?.message, + }); + } } finally { - setIsLoading(false); + setIsLoading(false); } - } + }; - if(tempAvatar){ + if (tempAvatar) { return ( - <> - + + {myName?.charAt(0)} + + + - {myName?.charAt(0)} - - - - change avatar - - - - - ); + change avatar + +
+ + + ); } if (hasAvatar) { @@ -131,8 +151,8 @@ const [isLoading, setIsLoading] = useState(false) <> change avatar - + ); } @@ -160,42 +190,62 @@ const [isLoading, setIsLoading] = useState(false) set avatar - + ); }; - -const PopoverComp = ({avatarFile, setAvatarFile, id, open, anchorEl, handleClose, publishAvatar, isLoading, myName}) => { - return ( - { + const theme = useTheme(); + return ( + - + - (500 KB max. for GIFS){" "} + (500 KB max. for GIFS){' '} setAvatarFile(file)}> @@ -203,23 +253,34 @@ const PopoverComp = ({avatarFile, setAvatarFile, id, open, anchorEl, handleClose {avatarFile?.name} {!myName && ( - - - A registered name is required to set an avatar + alignItems: 'center', + }} + > + + + A registered name is required to set an avatar + )} - - - + + + Publish avatar - ) - }; \ No newline at end of file + ); +}; diff --git a/src/components/Minting/Minting.tsx b/src/components/Minting/Minting.tsx index af63ece..fe05c5b 100644 --- a/src/components/Minting/Minting.tsx +++ b/src/components/Minting/Minting.tsx @@ -9,72 +9,64 @@ import { DialogTitle, Divider, IconButton, - InputBase, - InputLabel, Snackbar, Typography, -} from "@mui/material"; -import React, { - useCallback, - useContext, - useEffect, - useMemo, - useState, -} from "react"; -import CloseIcon from "@mui/icons-material/Close"; -import { MyContext, getBaseApiReact } from "../../App"; + useTheme, +} from '@mui/material'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import CloseIcon from '@mui/icons-material/Close'; +import { getBaseApiReact } from '../../App'; import { executeEvent, subscribeToEvent, unsubscribeFromEvent, -} from "../../utils/events"; -import { getFee, getNameOrAddress } from "../../background"; -import CopyToClipboard from "react-copy-to-clipboard"; -import { AddressBox } from "../../App-styles"; -import { Spacer } from "../../common/Spacer"; -import Copy from "../../assets/svgs/Copy.svg"; -import { Loader } from "../Loader"; -import { FidgetSpinner } from "react-loader-spinner"; -import { useModal } from "../../common/useModal"; +} from '../../utils/events'; +import { getFee, getNameOrAddress } from '../../background'; +import { Spacer } from '../../common/Spacer'; +import { FidgetSpinner } from 'react-loader-spinner'; +import { useModal } from '../../common/useModal'; +import { useAtom, useSetAtom } from 'jotai'; +import { memberGroupsAtom, txListAtom } from '../../atoms/global'; + +export const Minting = ({ setIsOpenMinting, myAddress, show }) => { + const setTxList = useSetAtom(txListAtom); + const [groups] = useAtom(memberGroupsAtom); -export const Minting = ({ - setIsOpenMinting, - myAddress, - groups, - show, - setTxList, - txList, -}) => { const [mintingAccounts, setMintingAccounts] = useState([]); const [accountInfo, setAccountInfo] = useState(null); - const [rewardSharePublicKey, setRewardSharePublicKey] = useState(""); - const [mintingKey, setMintingKey] = useState(""); - const [rewardsharekey, setRewardsharekey] = useState(""); + const [rewardSharePublicKey, setRewardSharePublicKey] = useState(''); + const [mintingKey, setMintingKey] = useState(''); + const [rewardsharekey, setRewardsharekey] = useState(''); const [rewardShares, setRewardShares] = useState([]); const [nodeInfos, setNodeInfos] = useState({}); const [openSnack, setOpenSnack] = useState(false); const [isLoading, setIsLoading] = useState(false); - const { show: showKey, message } = useModal(); - const { isShow: isShowNext, onOk, show: showNext } = useModal(); + const { show: showKey, message } = useModal(); + const { isShow: isShowNext, onOk, show: showNext } = useModal(); + const theme = useTheme(); const [info, setInfo] = useState(null); const [names, setNames] = useState({}); const [accountInfos, setAccountInfos] = useState({}); - const [showWaitDialog, setShowWaitDialog] = useState(false) + const [showWaitDialog, setShowWaitDialog] = useState(false); + const isPartOfMintingGroup = useMemo(() => { if (groups?.length === 0) return false; - return !!groups?.find((item) => item?.groupId?.toString() === "694"); + return !!groups?.find((item) => item?.groupId?.toString() === '694'); }, [groups]); + const getMintingAccounts = useCallback(async () => { try { const url = `${getBaseApiReact()}/admin/mintingaccounts`; const response = await fetch(url); if (!response.ok) { - throw new Error("network error"); + throw new Error('network error'); } const data = await response.json(); setMintingAccounts(data); - } catch (error) {} + } catch (error) { + console.log(error); + } }, []); const accountIsMinting = useMemo(() => { @@ -105,7 +97,7 @@ export const Minting = ({ }); } } catch (error) { - // error + console.log(error); } }; @@ -117,7 +109,7 @@ export const Minting = ({ const url = `${getBaseApiReact()}/addresses/${address}`; const response = await fetch(url); if (!response.ok) { - throw new Error("network error"); + throw new Error('network error'); } const data = await response.json(); if (others) { @@ -131,6 +123,7 @@ export const Minting = ({ setAccountInfo(data); } } catch (error) { + console.log(error); } finally { if (!others) { setIsLoading(false); @@ -144,10 +137,10 @@ export const Minting = ({ }; useEffect(() => { - subscribeToEvent("refresh-rewardshare-list", refreshRewardShare); + subscribeToEvent('refresh-rewardshare-list', refreshRewardShare); return () => { - unsubscribeFromEvent("refresh-rewardshare-list", refreshRewardShare); + unsubscribeFromEvent('refresh-rewardshare-list', refreshRewardShare); }; }, [myAddress]); @@ -177,15 +170,15 @@ export const Minting = ({ try { const url = `${getBaseApiReact()}/admin/status`; const response = await fetch(url, { - method: "GET", + method: 'GET', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, }); const data = await response.json(); setNodeInfos(data); } catch (error) { - console.error("Request failed", error); + console.error('Request failed', error); } }; @@ -194,12 +187,14 @@ export const Minting = ({ const url = `${getBaseApiReact()}/addresses/rewardshares?involving=${address}`; const response = await fetch(url); if (!response.ok) { - throw new Error("network error"); + throw new Error('network error'); } const data = await response.json(); setRewardShares(data); - return data - } catch (error) {} + return data; + } catch (error) { + console.log(error); + } }, []); const addMintingAccount = useCallback(async (val) => { @@ -208,10 +203,9 @@ export const Minting = ({ return await new Promise((res, rej) => { window .sendMessage( - "ADMIN_ACTION", - + 'ADMIN_ACTION', { - type: "addmintingaccount", + type: 'addmintingaccount', value: val, }, 180000, @@ -220,7 +214,7 @@ export const Minting = ({ .then((response) => { if (!response?.error) { res(response); - setMintingKey(""); + setMintingKey(''); setTimeout(() => { getMintingAccounts(); }, 300); @@ -229,13 +223,13 @@ export const Minting = ({ rej({ message: response.error }); }) .catch((error) => { - rej({ message: error.message || "An error occurred" }); + rej({ message: error.message || 'An error occurred' }); }); }); } catch (error) { setInfo({ - type: "error", - message: error?.message || "Unable to add minting account", + type: 'error', + message: error?.message || 'Unable to add minting account', }); setOpenSnack(true); } finally { @@ -249,10 +243,9 @@ export const Minting = ({ return await new Promise((res, rej) => { window .sendMessage( - "ADMIN_ACTION", - + 'ADMIN_ACTION', { - type: "removemintingaccount", + type: 'removemintingaccount', value: val, }, 180000, @@ -270,13 +263,13 @@ export const Minting = ({ rej({ message: response.error }); }) .catch((error) => { - rej({ message: error.message || "An error occurred" }); + rej({ message: error.message || 'An error occurred' }); }); }); } catch (error) { setInfo({ - type: "error", - message: error?.message || "Unable to remove minting account", + type: 'error', + message: error?.message || 'Unable to remove minting account', }); setOpenSnack(true); } finally { @@ -285,14 +278,14 @@ export const Minting = ({ }, []); const createRewardShare = useCallback(async (publicKey, recipient) => { - const fee = await getFee("REWARD_SHARE"); + const fee = await getFee('REWARD_SHARE'); // TODO translate await show({ - message: "Would you like to perform an REWARD_SHARE transaction?", - publishFee: fee.fee + " QORT", + message: 'Would you like to perform an REWARD_SHARE transaction?', + publishFee: fee.fee + ' QORT', }); return await new Promise((res, rej) => { window - .sendMessage("createRewardShare", { + .sendMessage('createRewardShare', { recipientPublicKey: publicKey, }) .then((response) => { @@ -301,7 +294,7 @@ export const Minting = ({ { recipient, ...response, - type: "add-rewardShare", + type: 'add-rewardShare', label: `Add rewardshare: awaiting confirmation`, labelDone: `Add rewardshare: success!`, done: false, @@ -314,7 +307,7 @@ export const Minting = ({ rej({ message: response.error }); }) .catch((error) => { - rej({ message: error.message || "An error occurred" }); + rej({ message: error.message || 'An error occurred' }); }); }); }, []); @@ -322,7 +315,7 @@ export const Minting = ({ const getRewardSharePrivateKey = useCallback(async (publicKey) => { return await new Promise((res, rej) => { window - .sendMessage("getRewardSharePrivateKey", { + .sendMessage('getRewardSharePrivateKey', { recipientPublicKey: publicKey, }) .then((response) => { @@ -333,7 +326,7 @@ export const Minting = ({ rej({ message: response.error }); }) .catch((error) => { - rej({ message: error.message || "An error occurred" }); + rej({ message: error.message || 'An error occurred' }); }); }); }, []); @@ -341,26 +334,23 @@ export const Minting = ({ const waitUntilRewardShareIsConfirmed = async (timeoutMs = 600000) => { const pollingInterval = 30000; const startTime = Date.now(); - + const sleep = (ms) => new Promise((res) => setTimeout(res, ms)); - + while (Date.now() - startTime < timeoutMs) { - - const rewardShares = await getRewardShares(myAddress); - const findRewardShare = rewardShares?.find( - (item) => - item?.recipient === myAddress && item?.mintingAccount === myAddress - ); - - if (findRewardShare) { - return true; // Exit early if found - } - - + const rewardShares = await getRewardShares(myAddress); + const findRewardShare = rewardShares?.find( + (item) => + item?.recipient === myAddress && item?.mintingAccount === myAddress + ); + + if (findRewardShare) { + return true; // Exit early if found + } await sleep(pollingInterval); // Wait before the next poll } - - throw new Error("Timeout waiting for reward share confirmation"); + + throw new Error('Timeout waiting for reward share confirmation'); }; const startMinting = async () => { @@ -377,23 +367,22 @@ export const Minting = ({ addMintingAccount(privateRewardShare); } else { await createRewardShare(accountInfo?.publicKey, myAddress); - setShowWaitDialog(true) - await waitUntilRewardShareIsConfirmed() + setShowWaitDialog(true); + await waitUntilRewardShareIsConfirmed(); await showNext({ - message: '' - }) + message: '', + }); const privateRewardShare = await getRewardSharePrivateKey( accountInfo?.publicKey ); - setShowWaitDialog(false) + setShowWaitDialog(false); addMintingAccount(privateRewardShare); - } } catch (error) { - setShowWaitDialog(false) + setShowWaitDialog(false); setInfo({ - type: "error", - message: error?.message || "Unable to start minting", + type: 'error', + message: error?.message || 'Unable to start minting', }); setOpenSnack(true); } finally { @@ -412,13 +401,13 @@ export const Minting = ({ const url = `${getBaseApiReact()}/groups/member/${address}`; const response = await fetch(url); const data = await response.json(); - return !!data?.find((grp) => grp?.groupId?.toString() === "694"); + return !!data?.find((grp) => grp?.groupId?.toString() === '694'); }; const removeRewardShare = useCallback(async (rewardShare) => { return await new Promise((res, rej) => { window - .sendMessage("removeRewardShare", { + .sendMessage('removeRewardShare', { rewardShareKeyPairPublicKey: rewardShare.rewardSharePublicKey, recipient: rewardShare.recipient, percentageShare: -1, @@ -430,7 +419,7 @@ export const Minting = ({ { ...rewardShare, ...response, - type: "remove-rewardShare", + type: 'remove-rewardShare', label: `Remove rewardshare: awaiting confirmation`, labelDone: `Remove rewardshare: success!`, done: false, @@ -442,7 +431,7 @@ export const Minting = ({ rej({ message: response.error }); }) .catch((error) => { - rej({ message: error.message || "An error occurred" }); + rej({ message: error.message || 'An error occurred' }); }); }); }, []); @@ -454,8 +443,8 @@ export const Minting = ({ const privateRewardShare = await removeRewardShare(rewardShare); } catch (error) { setInfo({ - type: "error", - message: error?.message || "Unable to remove reward share", + type: 'error', + message: error?.message || 'Unable to remove reward share', }); setOpenSnack(true); } finally { @@ -468,9 +457,9 @@ export const Minting = ({ setIsLoading(true); const confirmReceiver = await getNameOrAddress(receiver); if (confirmReceiver.error) - throw new Error("Invalid receiver address or name"); + throw new Error('Invalid receiver address or name'); const isInMinterGroup = await checkIfMinterGroup(confirmReceiver); - if (!isInMinterGroup) throw new Error("Account not in Minter Group"); + if (!isInMinterGroup) throw new Error('Account not in Minter Group'); const publicKey = await getPublicKeyFromAddress(confirmReceiver); const findRewardShare = rewardShares?.find( (item) => @@ -487,8 +476,8 @@ export const Minting = ({ } } catch (error) { setInfo({ - type: "error", - message: error?.message || "Unable to create reward share", + type: 'error', + message: error?.message || 'Unable to create reward share', }); setOpenSnack(true); } finally { @@ -510,7 +499,7 @@ export const Minting = ({ const _blocksNeed = () => { if (accountInfo?.level === 0) { - return 7200; + return 7200; // TODO manage these magic numbers in a proper location } else if (accountInfo?.level === 1) { return 72000; } else if (accountInfo?.level === 2) { @@ -550,11 +539,9 @@ export const Minting = ({ (accountInfo?.blocksMinted + accountInfo?.blocksMintedAdjustment); let countBlocksString = countBlocks.toString(); - return "" + countBlocksString; + return '' + countBlocksString; }; - - return ( - {"Manage your minting"} + {'Manage your minting'} + {isLoading && ( )} Account: {handleNames(accountInfo?.address)} + Level: {accountInfo?.level} + blocks remaining until next level: {_levelUpBlocks()} + This node is minting: {nodeInfos?.isMintingPossible?.toString()} @@ -631,11 +622,11 @@ export const Minting = ({ {isPartOfMintingGroup && !accountIsMinting && ( + + ))} @@ -745,17 +738,17 @@ export const Minting = ({ {!isPartOfMintingGroup && ( @@ -768,22 +761,22 @@ export const Minting = ({ - - )} @@ -837,7 +834,7 @@ export const Minting = ({ {info?.message} diff --git a/src/components/Mobile/MobileFooter.tsx b/src/components/Mobile/MobileFooter.tsx deleted file mode 100644 index 52e5c7c..0000000 --- a/src/components/Mobile/MobileFooter.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import * as React from "react"; -import { - BottomNavigation, - BottomNavigationAction, - ButtonBase, - Typography, -} from "@mui/material"; -import { Home, Groups, Message, ShowChart } from "@mui/icons-material"; -import Box from "@mui/material/Box"; -import BottomLogo from "../../assets/svgs/BottomLogo5.svg"; -import LogoSelected from "../../assets/svgs/LogoSelected.svg"; -import { Browser } from '@capacitor/browser'; - -import { CustomSvg } from "../../common/CustomSvg"; -import { WalletIcon } from "../../assets/Icons/WalletIcon"; -import { HubsIcon } from "../../assets/Icons/HubsIcon"; -import { TradingIcon } from "../../assets/Icons/TradingIcon"; -import { MessagingIcon } from "../../assets/Icons/MessagingIcon"; -import { executeEvent } from "../../utils/events"; - -const IconWrapper = ({ children, label, color }) => { - return ( - - {children} - - {label} - - - ); -}; - -export const MobileFooter = ({ - selectedGroup, - groupSection, - isUnread, - goToAnnouncements, - isUnreadChat, - goToChat, - goToThreads, - setOpenManageMembers, - groupChatHasUnread, - groupsAnnHasUnread, - directChatHasUnread, - chatMode, - openDrawerGroups, - goToHome, - setIsOpenDrawerProfile, - mobileViewMode, - setMobileViewMode, - setMobileViewModeKeepOpen, - hasUnreadGroups, - hasUnreadDirects -}) => { - const [value, setValue] = React.useState(0); - return ( - - setValue(newValue)} - sx={{ backgroundColor: "transparent", flexGrow: 1 }} - > - { - // setMobileViewMode('wallet') - setIsOpenDrawerProfile(true); - }} - icon={ - - - - } - sx={{ color: value === 0 ? "white" : "gray", padding: "0px 10px" }} - /> - { - setMobileViewMode("groups"); - }} - icon={ - - - - } - sx={{ - color: value === 0 ? "white" : "gray", - paddingLeft: "10px", - paddingRight: "42px", - }} - /> - - - {/* Floating Center Button */} - - { - if(mobileViewMode === 'home'){ - setMobileViewMode('apps') - - } else { - setMobileViewMode('home') - - } - }}> - - {/* Custom Center Icon */} - center-icon - - - - - setValue(newValue)} - sx={{ backgroundColor: "transparent", flexGrow: 1 }} - > - { - setMobileViewModeKeepOpen("messaging"); - }} - icon={ - - - - } - sx={{ - color: value === 2 ? "white" : "gray", - paddingLeft: "55px", - paddingRight: "10px", - }} - /> - { - executeEvent("addTab", { data: { service: 'APP', name: 'q-trade' } }); - executeEvent("open-apps-mode", { }); - }} - - icon={ - - - - } - sx={{ color: value === 3 ? "white" : "gray", padding: "0px 10px" }} - /> - - - ); -}; diff --git a/src/components/Mobile/MobileHeader.tsx b/src/components/Mobile/MobileHeader.tsx deleted file mode 100644 index cd98ba9..0000000 --- a/src/components/Mobile/MobileHeader.tsx +++ /dev/null @@ -1,472 +0,0 @@ -import React, { useState } from "react"; -import { - AppBar, - Toolbar, - IconButton, - Typography, - Box, - MenuItem, - Select, - ButtonBase, - Menu, - ListItemIcon, - ListItemText, -} from "@mui/material"; -import { HomeIcon } from "../../assets/Icons/HomeIcon"; -import { LogoutIcon } from "../../assets/Icons/LogoutIcon"; -import { NotificationIcon } from "../../assets/Icons/NotificationIcon"; -import { ArrowDownIcon } from "../../assets/Icons/ArrowDownIcon"; -import { MessagingIcon } from "../../assets/Icons/MessagingIcon"; -import { MessagingIcon2 } from "../../assets/Icons/MessagingIcon2"; -import { HubsIcon } from "../../assets/Icons/HubsIcon"; -import { Save } from "../Save/Save"; -import CloseFullscreenIcon from '@mui/icons-material/CloseFullscreen'; -import { useRecoilState } from "recoil"; -import { fullScreenAtom, hasSettingsChangedAtom } from "../../atoms/global"; -import { useAppFullScreen } from "../../useAppFullscreen"; - -const Header = ({ - logoutFunc, - goToHome, - setIsOpenDrawerProfile, - isThin, - setMobileViewModeKeepOpen, - hasUnreadGroups, - hasUnreadDirects, - setMobileViewMode, - myName, - setSelectedDirect, - setNewChat -}) => { - const [anchorEl, setAnchorEl] = useState(null); - const open = Boolean(anchorEl); - const [fullScreen, setFullScreen] = useRecoilState(fullScreenAtom); - const {exitFullScreen} = useAppFullScreen(setFullScreen) - const handleClick = (event) => { - setAnchorEl(event.currentTarget); - }; - - const handleClose = () => { - setAnchorEl(null); - }; - - if (isThin) { - return ( - - - {/* Left Home Icon */} - - { - setMobileViewModeKeepOpen(""); - goToHome(); - }} - // onClick={onHomeClick} - > - - - - - - {fullScreen && ( - { - exitFullScreen() - setFullScreen(false) - }}> - - - )} - - - - {/* Center Title */} - - QORTAL - - - {/* Right Logout Icon */} - - { - setMobileViewModeKeepOpen("messaging"); - }} - > - - - - - - - - - - { - setSelectedDirect(null) - setNewChat(false) - setMobileViewMode("groups"); - setMobileViewModeKeepOpen("") - handleClose(); - }} - > - - - - - - { - setMobileViewModeKeepOpen("messaging"); - - handleClose(); - }} - > - - - - - - - - ); - } - return ( - <> - {/* Main Header */} - - - {/* Left Home Icon */} - - - - - {fullScreen && ( - { - exitFullScreen() - setFullScreen(false) - }}> - - - )} - - {/* Center Title */} - - QORTAL - - - {/* Right Logout Icon */} - - - - - - - - - {/* Secondary Section */} - - - - {myName} - - {/* - */} - - - - - - - - - {/* Right Dropdown */} - {/* { - setIsOpenDrawerProfile(true); - }} - > - - - View Wallet - - - - - */} - - - { - setMobileViewMode("groups"); - setMobileViewModeKeepOpen("") - handleClose(); - }} - > - - - - - - { - setMobileViewModeKeepOpen("messaging"); - - handleClose(); - }} - > - - - - - - - - ); -}; - -export default Header; diff --git a/src/components/PasswordField/PasswordField.tsx b/src/components/PasswordField/PasswordField.tsx index 89bc8b4..b59538e 100644 --- a/src/components/PasswordField/PasswordField.tsx +++ b/src/components/PasswordField/PasswordField.tsx @@ -1,67 +1,90 @@ -import { Button, ButtonBase, InputAdornment, TextField, TextFieldProps, styled } from "@mui/material"; -import { forwardRef, useState } from 'react' +import { + ButtonBase, + InputAdornment, + TextField, + TextFieldProps, + styled, +} from '@mui/material'; +import { forwardRef, useState } from 'react'; import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; import VisibilityIcon from '@mui/icons-material/Visibility'; -export const CustomInput = styled(TextField)({ - width: "183px", // Adjust the width as needed - borderRadius: "5px", - // backgroundColor: "rgba(30, 30, 32, 1)", - outline: "none", - input: { - fontSize: 10, - fontFamily: "Inter", - fontWeight: 400, - color: "white", - "&::placeholder": { - fontSize: 16, - color: "rgba(255, 255, 255, 0.2)", - }, - outline: "none", - padding: "10px", - }, - "& .MuiOutlinedInput-root": { - "& fieldset": { - border: '0.5px solid rgba(255, 255, 255, 0.5)', - }, - "&:hover fieldset": { - border: '0.5px solid rgba(255, 255, 255, 0.5)', - }, - "&.Mui-focused fieldset": { - border: '0.5px solid rgba(255, 255, 255, 0.5)', - }, - }, - "& .MuiInput-underline:before": { - borderBottom: "none", - }, - "& .MuiInput-underline:hover:not(.Mui-disabled):before": { - borderBottom: "none", - }, - "& .MuiInput-underline:after": { - borderBottom: "none", - }, -}); +export const CustomInput = styled(TextField)(({ theme }) => ({ + width: '183px', + borderRadius: '5px', + backgroundColor: theme.palette.background.paper, + outline: 'none', + input: { + fontSize: 10, + fontFamily: 'Inter', + fontWeight: 400, + color: theme.palette.text.primary, + '&::placeholder': { + fontSize: 16, + color: theme.palette.text.disabled, + }, + outline: 'none', + padding: '10px', + }, + '& .MuiOutlinedInput-root': { + '& fieldset': { + border: `0.5px solid ${theme.palette.divider}`, + }, + '&:hover fieldset': { + border: `0.5px solid ${theme.palette.divider}`, + }, + '&.Mui-focused fieldset': { + border: `0.5px solid ${theme.palette.divider}`, + }, + }, + '& .MuiInput-underline:before': { + borderBottom: 'none', + }, + '& .MuiInput-underline:hover:not(.Mui-disabled):before': { + borderBottom: 'none', + }, + '& .MuiInput-underline:after': { + borderBottom: 'none', + }, +})); -export const PasswordField = forwardRef( ({ ...props }, ref) => { +export const PasswordField = forwardRef( + ({ ...props }, ref) => { const [canViewPassword, setCanViewPassword] = useState(false); + return ( - { - setCanViewPassword((prevState) => !prevState) - }}> - {canViewPassword ? : } - - ) - }} - inputRef={ref} - {...props} - /> - ) -}); \ No newline at end of file + { + setCanViewPassword((prevState) => !prevState); + }} + > + {canViewPassword ? ( + + + + ) : ( + + + + )} + + ), + }} + inputRef={ref} + {...props} + /> + ); + } +); diff --git a/src/components/QMailStatus.tsx b/src/components/QMailStatus.tsx index cb1642a..deac6db 100644 --- a/src/components/QMailStatus.tsx +++ b/src/components/QMailStatus.tsx @@ -1,63 +1,98 @@ -import React, { useMemo } from 'react' -import QMailLogo from '../assets/QMailLogo.png' -import { useRecoilState } from 'recoil' -import { mailsAtom, qMailLastEnteredTimestampAtom } from '../atoms/global' -import { isLessThanOneWeekOld } from './Group/QMailMessages' -import { ButtonBase, Tooltip } from '@mui/material' -import { executeEvent } from '../utils/events' -export const QMailStatus = () => { - const [lastEnteredTimestamp, setLastEnteredTimestamp] = useRecoilState(qMailLastEnteredTimestampAtom) - const [mails, setMails] = useRecoilState(mailsAtom) +import { useMemo } from 'react'; +import { mailsAtom, qMailLastEnteredTimestampAtom } from '../atoms/global'; +import { isLessThanOneWeekOld } from './Group/QMailMessages'; +import { ButtonBase, Tooltip, useTheme } from '@mui/material'; +import { executeEvent } from '../utils/events'; +import { Mail } from '@mui/icons-material'; +import { useTranslation } from 'react-i18next'; +import { useAtom } from 'jotai'; + +export const QMailStatus = () => { + const { t } = useTranslation(['core']); + const theme = useTheme(); + + const [lastEnteredTimestamp, setLastEnteredTimestamp] = useAtom( + qMailLastEnteredTimestampAtom + ); + const [mails, setMails] = useAtom(mailsAtom); + + const hasNewMail = useMemo(() => { + if (mails?.length === 0) return false; + const latestMail = mails[0]; + if (!lastEnteredTimestamp && isLessThanOneWeekOld(latestMail?.created)) + return true; + if ( + lastEnteredTimestamp < latestMail?.created && + isLessThanOneWeekOld(latestMail?.created) + ) + return true; + return false; + }, [lastEnteredTimestamp, mails]); - const hasNewMail = useMemo(()=> { - if(mails?.length === 0) return false - const latestMail = mails[0] - if(!lastEnteredTimestamp && isLessThanOneWeekOld(latestMail?.created)) return true - if((lastEnteredTimestamp < latestMail?.created) && isLessThanOneWeekOld(latestMail?.created)) return true - return false - }, [lastEnteredTimestamp, mails]) return ( - { - executeEvent("addTab", { data: { service: 'APP', name: 'q-mail' } }); - executeEvent("open-apps-mode", { }); - setLastEnteredTimestamp(Date.now()) - }} style={{ - position: 'relative' - }}> + { + executeEvent('addTab', { data: { service: 'APP', name: 'q-mail' } }); + executeEvent('open-apps-mode', {}); + setLastEnteredTimestamp(Date.now()); + }} + style={{ + position: 'relative', + }} + > {hasNewMail && ( -
+ height: '15px', + outline: '1px solid white', + position: 'absolute', + right: '-7px', + top: '-7px', + width: '15px', + zIndex: 1, + }} + /> )} Q-MAIL} + title={ + + {t('core:q_mail', { + postProcess: 'capitalize', + })} + + } placement="left" arrow - sx={{ fontSize: "24" }} + sx={{ fontSize: '24' }} slotProps={{ tooltip: { sx: { - color: "#ffffff", - backgroundColor: "#444444", + color: theme.palette.text.primary, + backgroundColor: theme.palette.background.paper, }, }, arrow: { sx: { - color: "#444444", + color: theme.palette.text.primary, }, }, }} > - + - - ) -} + + ); +}; diff --git a/src/components/QortPayment.tsx b/src/components/QortPayment.tsx index 6add9b2..d9baa65 100644 --- a/src/components/QortPayment.tsx +++ b/src/components/QortPayment.tsx @@ -1,167 +1,192 @@ -import { Box, CircularProgress } from '@mui/material'; -import React, { useEffect, useState } from 'react' -import { CustomButton, CustomInput, CustomLabel, TextP } from '../App-styles'; +import { Box, CircularProgress, useTheme } from '@mui/material'; +import { useState } from 'react'; +import { + CustomButton, + CustomInput, + CustomLabel, + TextP, +} from '../styles/App-styles'; import { Spacer } from '../common/Spacer'; import BoundedNumericTextField from '../common/BoundedNumericTextField'; import { PasswordField } from './PasswordField/PasswordField'; import { ErrorText } from './ErrorText/ErrorText'; import { getFee } from '../background'; -export const QortPayment = ({balance, show, onSuccess, defaultPaymentTo}) => { - const [paymentTo, setPaymentTo] = useState(defaultPaymentTo); - const [paymentAmount, setPaymentAmount] = useState(0); - const [paymentPassword, setPaymentPassword] = useState(""); - const [sendPaymentError, setSendPaymentError] = useState(""); - const [sendPaymentSuccess, setSendPaymentSuccess] = useState(""); - const [isLoadingSendCoin, setIsLoadingSendCoin] = useState(false); +export const QortPayment = ({ balance, show, onSuccess, defaultPaymentTo }) => { + const theme = useTheme(); + const [paymentTo, setPaymentTo] = useState(defaultPaymentTo); + const [paymentAmount, setPaymentAmount] = useState(0); + const [paymentPassword, setPaymentPassword] = useState(''); + const [sendPaymentError, setSendPaymentError] = useState(''); + const [sendPaymentSuccess, setSendPaymentSuccess] = useState(''); + const [isLoadingSendCoin, setIsLoadingSendCoin] = useState(false); + const sendCoinFunc = async () => { + try { + setSendPaymentError(''); + setSendPaymentSuccess(''); + if (!paymentTo) { + setSendPaymentError('Please enter a recipient'); + return; + } + if (!paymentAmount) { + setSendPaymentError('Please enter an amount greater than 0'); + return; + } + if (!paymentPassword) { + setSendPaymentError('Please enter your wallet password'); + return; + } + const fee = await getFee('PAYMENT'); + await show({ + message: `Would you like to transfer ${Number(paymentAmount)} QORT?`, + paymentFee: fee.fee + ' QORT', + }); - const sendCoinFunc = async() => { - try { - setSendPaymentError(""); - setSendPaymentSuccess(""); - if (!paymentTo) { - setSendPaymentError("Please enter a recipient"); - return; + setIsLoadingSendCoin(true); + + window + .sendMessage('sendCoin', { + amount: Number(paymentAmount), + receiver: paymentTo.trim(), + password: paymentPassword, + }) + .then((response) => { + if (response?.error) { + setSendPaymentError(response.error); + } else { + onSuccess(); } - if (!paymentAmount) { - setSendPaymentError("Please enter an amount greater than 0"); - return; - } - if (!paymentPassword) { - setSendPaymentError("Please enter your wallet password"); - return; - } - const fee = await getFee('PAYMENT') - - await show({ - message: `Would you like to transfer ${Number(paymentAmount)} QORT?` , - paymentFee: fee.fee + ' QORT' - }) - setIsLoadingSendCoin(true); - window - .sendMessage("sendCoin", { - amount: Number(paymentAmount), - receiver: paymentTo.trim(), - password: paymentPassword, - }) - .then((response) => { - if (response?.error) { - setSendPaymentError(response.error); - } else { - onSuccess() - - } - setIsLoadingSendCoin(false); - }) - .catch((error) => { - console.error("Failed to send coin:", error); - setIsLoadingSendCoin(false); - }); - } catch (error) { - // error - } - }; + setIsLoadingSendCoin(false); + }) + .catch((error) => { + console.error('Failed to send coin:', error); + setIsLoadingSendCoin(false); + }); + } catch (error) { + console.log(error); + } + }; + return ( <> - - Transfer QORT - - - - Balance: - - - {balance?.toFixed(2)} QORT - - - + sx={{ + alignItems: 'flex-start', + display: 'flex', + flexDirection: 'column', + }} + > + + Transfer QORT + - - To - - setPaymentTo(e.target.value)} - autoComplete="off" - /> - - - Amount - - - setPaymentAmount(+e)} - /> - - - Confirm Wallet Password - - - setPaymentPassword(e.target.value)} - autoComplete="off" - /> - - - {sendPaymentError} - {/* {sendPaymentSuccess} */} - - + + + Balance: + + + + {balance?.toFixed(2)} QORT + + + + + + + To + + + + setPaymentTo(e.target.value)} + autoComplete="off" + /> + + + + Amount + + + + setPaymentAmount(+e)} + /> + + + + + Confirm Wallet Password + + + + + setPaymentPassword(e.target.value)} + autoComplete="off" + /> + + + + + {sendPaymentError} + {/* {sendPaymentSuccess} */} + + + + { + if (isLoadingSendCoin) return; + sendCoinFunc(); + }} + > + {isLoadingSendCoin && ( + { - if(isLoadingSendCoin) return - sendCoinFunc(); - }} - > - {isLoadingSendCoin && ( - - )} - Send - + /> + )} + Send + - ) -} + ); +}; diff --git a/src/components/ReactionPicker.css b/src/components/ReactionPicker.css index 89dbe39..6d0a354 100644 --- a/src/components/ReactionPicker.css +++ b/src/components/ReactionPicker.css @@ -1,27 +1,26 @@ .reaction-container { - position: relative; /* Parent must be positioned relatively */ - } - - .emoji-picker { - position: absolute; /* Picker positioned absolutely relative to the parent */ - right: 0; - z-index: 9000000000; /* Ensure picker appears above other content */ - } - - .message-container { - overflow: visible; /* Ensure the message container doesn't cut off the picker */ - } - + position: relative; /* Parent must be positioned relatively */ +} - .reaction-container { - position: relative; - } - - .emoji-picker { - overflow: hidden; - width: auto - } +.emoji-picker { + position: absolute; /* Picker positioned absolutely relative to the parent */ + right: 0; + z-index: 9000000000; /* Ensure picker appears above other content */ +} - .EmojiPickerReact.epr-dark-theme { - --epr-emoji-size: 18px; /* Adjust emoji size for dark mode */ - } \ No newline at end of file +.message-container { + overflow: visible; /* Ensure the message container doesn't cut off the picker */ +} + +.reaction-container { + position: relative; +} + +.emoji-picker { + overflow: hidden; + width: auto; +} + +.EmojiPickerReact.epr-dark-theme { + --epr-emoji-size: 18px; /* Adjust emoji size for dark mode */ +} diff --git a/src/components/ReactionPicker.tsx b/src/components/ReactionPicker.tsx index 2f5f08e..c95adcf 100644 --- a/src/components/ReactionPicker.tsx +++ b/src/components/ReactionPicker.tsx @@ -1,9 +1,8 @@ -import React, { useState, useRef, useEffect } from 'react'; +import { useState, useRef, useEffect } from 'react'; import ReactDOM from 'react-dom'; import Picker, { EmojiStyle, Theme } from 'emoji-picker-react'; import './ReactionPicker.css'; import { ButtonBase } from '@mui/material'; -import { isMobile } from '../App'; export const ReactionPicker = ({ onReaction }) => { const [showPicker, setShowPicker] = useState(false); @@ -30,7 +29,7 @@ export const ReactionPicker = ({ onReaction }) => { } else { // Get the button's position const buttonRect = buttonRef.current.getBoundingClientRect(); - const pickerWidth = isMobile ? 300 : 350; // Adjust based on picker width + const pickerWidth = 350; // Calculate position to align the right edge of the picker with the button's right edge setPickerPosition({ @@ -90,15 +89,15 @@ export const ReactionPicker = ({ onReaction }) => { }} >
, document.body diff --git a/src/components/RegisterName.tsx b/src/components/RegisterName.tsx index 35af458..4b52a45 100644 --- a/src/components/RegisterName.tsx +++ b/src/components/RegisterName.tsx @@ -1,28 +1,29 @@ -import React, { useCallback, useContext, useEffect, useState } from 'react' +import React, { useCallback, useContext, useEffect, useState } from 'react'; import { - Avatar, - Box, - Button, - ButtonBase, - Collapse, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - Input, - ListItem, - ListItemAvatar, - ListItemButton, - ListItemIcon, - ListItemText, - List, - MenuItem, - Popover, - Select, - TextField, - Typography, - } from "@mui/material"; + Avatar, + Box, + Button, + ButtonBase, + Collapse, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Input, + ListItem, + ListItemAvatar, + ListItemButton, + ListItemIcon, + ListItemText, + List, + MenuItem, + Popover, + Select, + TextField, + Typography, + useTheme, +} from '@mui/material'; import { Label } from './Group/AddGroup'; import { Spacer } from '../common/Spacer'; import { LoadingButton } from '@mui/lab'; @@ -33,280 +34,318 @@ import { subscribeToEvent, unsubscribeFromEvent } from '../utils/events'; import { BarSpinner } from '../common/Spinners/BarSpinner/BarSpinner'; import CheckIcon from '@mui/icons-material/Check'; import ErrorIcon from '@mui/icons-material/Error'; +import { useSetAtom } from 'jotai'; +import { txListAtom } from '../atoms/global'; enum Availability { - NULL = 'null', - LOADING = 'loading', - AVAILABLE = 'available', - NOT_AVAILABLE = 'not-available' + NULL = 'null', + LOADING = 'loading', + AVAILABLE = 'available', + NOT_AVAILABLE = 'not-available', } -export const RegisterName = ({setOpenSnack, setInfoSnack, userInfo, show, setTxList, balance}) => { - const [isOpen, setIsOpen] = useState(false) - const [registerNameValue, setRegisterNameValue] = useState('') - const [isLoadingRegisterName, setIsLoadingRegisterName] = useState(false) - const [isNameAvailable, setIsNameAvailable] = useState(Availability.NULL) - const [nameFee, setNameFee] = useState(null) +export const RegisterName = ({ + setOpenSnack, + setInfoSnack, + userInfo, + show, + balance, +}) => { + const setTxList = useSetAtom(txListAtom); - const checkIfNameExisits = async (name)=> { - if(!name?.trim()){ - setIsNameAvailable(Availability.NULL) + const [isOpen, setIsOpen] = useState(false); + const [registerNameValue, setRegisterNameValue] = useState(''); + const [isLoadingRegisterName, setIsLoadingRegisterName] = useState(false); + const [isNameAvailable, setIsNameAvailable] = useState( + Availability.NULL + ); + const [nameFee, setNameFee] = useState(null); + const theme = useTheme(); + const checkIfNameExisits = async (name) => { + if (!name?.trim()) { + setIsNameAvailable(Availability.NULL); - return + return; } - setIsNameAvailable(Availability.LOADING) + setIsNameAvailable(Availability.LOADING); try { - const res = await fetch(`${getBaseApiReact()}/names/` + name); - const data = await res.json() - if(data?.message === 'name unknown'){ - setIsNameAvailable(Availability.AVAILABLE) - } else { - setIsNameAvailable(Availability.NOT_AVAILABLE) - } + const res = await fetch(`${getBaseApiReact()}/names/` + name); + const data = await res.json(); + if (data?.message === 'name unknown') { + setIsNameAvailable(Availability.AVAILABLE); + } else { + setIsNameAvailable(Availability.NOT_AVAILABLE); + } } catch (error) { - console.error(error) + console.error(error); } finally { } - } - // Debounce logic - useEffect(() => { - const handler = setTimeout(() => { - checkIfNameExisits(registerNameValue); - }, 500); - - // Cleanup timeout if searchValue changes before the timeout completes - return () => { - clearTimeout(handler); - }; - }, [registerNameValue]); + }; + // Debounce logic + useEffect(() => { + const handler = setTimeout(() => { + checkIfNameExisits(registerNameValue); + }, 500); - const openRegisterNameFunc = useCallback((e) => { - setIsOpen(true) + // Cleanup timeout if searchValue changes before the timeout completes + return () => { + clearTimeout(handler); + }; + }, [registerNameValue]); - }, [ setIsOpen]); - - useEffect(() => { - subscribeToEvent("openRegisterName", openRegisterNameFunc); - - return () => { - unsubscribeFromEvent("openRegisterName", openRegisterNameFunc); - }; - }, [openRegisterNameFunc]); + const openRegisterNameFunc = useCallback( + (e) => { + setIsOpen(true); + }, + [setIsOpen] + ); - useEffect(()=> { - const nameRegistrationFee = async ()=> { - try { - const fee = await getFee("REGISTER_NAME"); - setNameFee(fee?.fee) - } catch (error) { - console.error(error) - } - } - nameRegistrationFee() - }, []) + useEffect(() => { + subscribeToEvent('openRegisterName', openRegisterNameFunc); - const registerName = async () => { + return () => { + unsubscribeFromEvent('openRegisterName', openRegisterNameFunc); + }; + }, [openRegisterNameFunc]); + + useEffect(() => { + const nameRegistrationFee = async () => { try { - if (!userInfo?.address) throw new Error("Your address was not found"); - if(!registerNameValue) throw new Error('Enter a name') - - const fee = await getFee("REGISTER_NAME"); - await show({ - message: "Would you like to register this name?", - publishFee: fee.fee + " QORT", - }); - setIsLoadingRegisterName(true); - new Promise((res, rej) => { - window - .sendMessage("registerName", { - name: registerNameValue, - }) - .then((response) => { - if (!response?.error) { - res(response); - setIsLoadingRegisterName(false); - setInfoSnack({ - type: "success", - message: - "Successfully registered. It may take a couple of minutes for the changes to propagate", - }); - setIsOpen(false); - setRegisterNameValue(""); - setOpenSnack(true); - setTxList((prev) => [ - { - ...response, - type: "register-name", - label: `Registered name: awaiting confirmation. This may take a couple minutes.`, - labelDone: `Registered name: success!`, - done: false, - }, - ...prev.filter((item) => !item.done), - ]); - return; - } - setInfoSnack({ - type: "error", - message: response?.error, - }); - setOpenSnack(true); - rej(response.error); - }) - .catch((error) => { - setInfoSnack({ - type: "error", - message: error.message || "An error occurred", - }); - setOpenSnack(true); - rej(error); - }); - }); + const fee = await getFee('REGISTER_NAME'); + setNameFee(fee?.fee); } catch (error) { - if (error?.message) { - setOpenSnack(true) - setInfoSnack({ - type: "error", - message: error?.message, - }); - } - } finally { - setIsLoadingRegisterName(false); + console.error(error); } }; + nameRegistrationFee(); + }, []); + + const registerName = async () => { + try { + if (!userInfo?.address) throw new Error('Your address was not found'); + if (!registerNameValue) throw new Error('Enter a name'); + + const fee = await getFee('REGISTER_NAME'); + await show({ + message: 'Would you like to register this name?', + publishFee: fee.fee + ' QORT', + }); + setIsLoadingRegisterName(true); + new Promise((res, rej) => { + window + .sendMessage('registerName', { + name: registerNameValue, + }) + .then((response) => { + if (!response?.error) { + res(response); + setIsLoadingRegisterName(false); + setInfoSnack({ + type: 'success', + message: + 'Successfully registered. It may take a couple of minutes for the changes to propagate', + }); + setIsOpen(false); + setRegisterNameValue(''); + setOpenSnack(true); + setTxList((prev) => [ + { + ...response, + type: 'register-name', + label: `Registered name: awaiting confirmation. This may take a couple minutes.`, + labelDone: `Registered name: success!`, + done: false, + }, + ...prev.filter((item) => !item.done), + ]); + return; + } + setInfoSnack({ + type: 'error', + message: response?.error, + }); + setOpenSnack(true); + rej(response.error); + }) + .catch((error) => { + setInfoSnack({ + type: 'error', + message: error.message || 'An error occurred', + }); + setOpenSnack(true); + rej(error); + }); + }); + } catch (error) { + if (error?.message) { + setOpenSnack(true); + setInfoSnack({ + type: 'error', + message: error?.message, + }); + } + } finally { + setIsLoadingRegisterName(false); + } + }; return ( - - {"Register name"} - - - + {'Register name'} + + setRegisterNameValue(e.target.value)} value={registerNameValue} placeholder="Choose a name" /> - {(!balance || (nameFee && balance && balance < nameFee))&& ( + {(!balance || (nameFee && balance && balance < nameFee)) && ( <> - - - - Your balance is {balance ?? 0} QORT. A name registration requires a {nameFee} QORT fee - - - + + + + + Your balance is {balance ?? 0} QORT. A name registration + requires a {nameFee} QORT fee + + + )} {isNameAvailable === Availability.AVAILABLE && ( - - - {registerNameValue} is available + alignItems: 'center', + }} + > + + {registerNameValue} is available )} - {isNameAvailable === Availability.NOT_AVAILABLE && ( - - - {registerNameValue} is unavailable + alignItems: 'center', + }} + > + + {registerNameValue} is unavailable )} - {isNameAvailable === Availability.LOADING && ( - - - Checking if name already existis + alignItems: 'center', + }} + > + + Checking if name already existis )} - Benefits of a name - - - - - - - - - - - - - - - - - - + + Benefits of a name + + + + + + + + + + + + + + + - - - - - - - ) -} + + + + + + + ); +}; diff --git a/src/components/Save/Save.tsx b/src/components/Save/Save.tsx index 0c01315..d67780a 100644 --- a/src/components/Save/Save.tsx +++ b/src/components/Save/Save.tsx @@ -1,6 +1,5 @@ -import React, { useContext, useEffect, useMemo, useState } from "react"; -import { useRecoilState, useSetRecoilState } from "recoil"; -import isEqual from "lodash/isEqual"; // Import deep comparison utility +import { useContext, useEffect, useMemo, useState } from 'react'; +import isEqual from 'lodash/isEqual'; // TODO Import deep comparison utility import { canSaveSettingToQdnAtom, hasSettingsChangedAtom, @@ -9,21 +8,32 @@ import { settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom, -} from "../../atoms/global"; -import { Box, Button, ButtonBase, Popover, Typography } from "@mui/material"; -import { objectToBase64 } from "../../qdn/encryption/group-encryption"; -import { MyContext } from "../../App"; -import { getFee } from "../../background"; -import { CustomizedSnackbars } from "../Snackbar/Snackbar"; -import { SaveIcon } from "../../assets/svgs/SaveIcon"; -import { IconWrapper } from "../Desktop/DesktopFooter"; -import { Spacer } from "../../common/Spacer"; -import { LoadingButton } from "@mui/lab"; -import { saveToLocalStorage } from "../Apps/AppsNavBar"; -import { decryptData, encryptData } from "../../qortalRequests/get"; -import { saveFileToDiskGeneric } from "../../utils/generateWallet/generateWallet"; -import { base64ToUint8Array, uint8ArrayToObject } from "../../backgroundFunctions/encryption"; - +} from '../../atoms/global'; +import { + Box, + Button, + ButtonBase, + Popover, + Typography, + useTheme, +} from '@mui/material'; +import { objectToBase64 } from '../../qdn/encryption/group-encryption'; +import { MyContext } from '../../App'; +import { getFee } from '../../background'; +import { CustomizedSnackbars } from '../Snackbar/Snackbar'; +import { SaveIcon } from '../../assets/Icons/SaveIcon'; +import { IconWrapper } from '../Desktop/DesktopFooter'; +import { Spacer } from '../../common/Spacer'; +import { LoadingButton } from '@mui/lab'; +import { saveToLocalStorage } from '../Apps/AppsNavBarDesktop'; +import { decryptData, encryptData } from '../../qortalRequests/get'; +import { saveFileToDiskGeneric } from '../../utils/generateWallet/generateWallet'; +import { + base64ToUint8Array, + uint8ArrayToObject, +} from '../../backgroundFunctions/encryption'; +import { useTranslation } from 'react-i18next'; +import { useAtom, useSetAtom } from 'jotai'; export const handleImportClick = async () => { const fileInput = document.createElement('input'); @@ -53,27 +63,28 @@ export const handleImportClick = async () => { // Trigger the file input dialog fileInput.click(); }); - -} +}; export const Save = ({ isDesktop, disableWidth, myName }) => { - const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom); - const [settingsQdnLastUpdated, setSettingsQdnLastUpdated] = useRecoilState( + const [pinnedApps, setPinnedApps] = useAtom(sortablePinnedAppsAtom); + const [settingsQdnLastUpdated, setSettingsQdnLastUpdated] = useAtom( settingsQDNLastUpdatedAtom ); - const [settingsLocalLastUpdated] = useRecoilState( - settingsLocalLastUpdatedAtom + const [settingsLocalLastUpdated] = useAtom(settingsLocalLastUpdatedAtom); + const setHasSettingsChangedAtom = useSetAtom(hasSettingsChangedAtom); + const [isUsingImportExportSettings, setIsUsingImportExportSettings] = useAtom( + isUsingImportExportSettingsAtom ); - const setHasSettingsChangedAtom = useSetRecoilState(hasSettingsChangedAtom); - const [isUsingImportExportSettings, setIsUsingImportExportSettings] = useRecoilState(isUsingImportExportSettingsAtom); - const [canSave] = useRecoilState(canSaveSettingToQdnAtom); const [openSnack, setOpenSnack] = useState(false); const [isLoading, setIsLoading] = useState(false); const [infoSnack, setInfoSnack] = useState(null); - const [oldPinnedApps, setOldPinnedApps] = useRecoilState(oldPinnedAppsAtom); + const [oldPinnedApps, setOldPinnedApps] = useAtom(oldPinnedAppsAtom); + const [anchorEl, setAnchorEl] = useState(null); const { show } = useContext(MyContext); + const theme = useTheme(); + const { t } = useTranslation(['core']); const hasChanged = useMemo(() => { const newChanges = { @@ -104,8 +115,6 @@ export const Save = ({ isDesktop, disableWidth, myName }) => { settingsLocalLastUpdated, ]); - - useEffect(() => { setHasSettingsChangedAtom(hasChanged); }, [hasChanged]); @@ -124,7 +133,7 @@ export const Save = ({ isDesktop, disableWidth, myName }) => { const encryptData = await new Promise((res, rej) => { window .sendMessage( - "ENCRYPT_DATA", + 'ENCRYPT_DATA', { data64, }, @@ -139,23 +148,22 @@ export const Save = ({ isDesktop, disableWidth, myName }) => { } }) .catch((error) => { - console.error("Failed qortalRequest", error); + console.error('Failed qortalRequest', error); }); }); if (encryptData && !encryptData?.error) { - const fee = await getFee("ARBITRARY"); + const fee = await getFee('ARBITRARY'); await show({ - message: - "Would you like to publish your settings to QDN (encrypted) ?", - publishFee: fee.fee + " QORT", + message: t('core:save.publish_qnd', { postProcess: 'capitalize' }), + publishFee: fee.fee + ' QORT', }); const response = await new Promise((res, rej) => { window - .sendMessage("publishOnQDN", { + .sendMessage('publishOnQDN', { data: encryptData, - identifier: "ext_saved_settings", - service: "DOCUMENT_PRIVATE", + identifier: 'ext_saved_settings', + service: 'DOCUMENT_PRIVATE', }) .then((response) => { if (!response?.error) { @@ -165,24 +173,33 @@ export const Save = ({ isDesktop, disableWidth, myName }) => { rej(response.error); }) .catch((error) => { - rej(error.message || "An error occurred"); + rej( + error.message || + t('core:message.error.generic', { postProcess: 'capitalize' }) + ); }); }); if (response?.identifier) { setOldPinnedApps(pinnedApps); setSettingsQdnLastUpdated(Date.now()); setInfoSnack({ - type: "success", - message: "Sucessfully published to QDN", + type: 'success', + message: t('core:message.success.publish_qdn', { + postProcess: 'capitalize', + }), }); setOpenSnack(true); - setAnchorEl(null) + setAnchorEl(null); } } } catch (error) { setInfoSnack({ - type: "error", - message: error?.message || "Unable to save to QDN", + type: 'error', + message: + error?.message || + t('core:message.error.save_qdn', { + postProcess: 'capitalize', + }), }); setOpenSnack(true); } finally { @@ -196,8 +213,8 @@ export const Save = ({ isDesktop, disableWidth, myName }) => { const revertChanges = () => { setPinnedApps(oldPinnedApps); - saveToLocalStorage("ext_saved_settings", "sortablePinnedApps", null); - setAnchorEl(null) + saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', null); + setAnchorEl(null); }; return ( @@ -207,358 +224,403 @@ export const Save = ({ isDesktop, disableWidth, myName }) => { disabled={ // !hasChanged || // !canSave || - isLoading + isLoading // settingsQdnLastUpdated === -100 } > {isDesktop ? ( ) : ( )}
+ setAnchorEl(null)} // Close popover on click outside anchorOrigin={{ - vertical: "bottom", - horizontal: "center", + vertical: 'bottom', + horizontal: 'center', }} transformOrigin={{ - vertical: "top", - horizontal: "center", + vertical: 'top', + horizontal: 'center', }} sx={{ - width: "300px", - maxWidth: "90%", - maxHeight: "80%", - overflow: "auto", + width: '300px', + maxWidth: '90%', + maxHeight: '80%', + overflow: 'auto', }} > {isUsingImportExportSettings && ( - + - You are using the export/import way of saving settings. - + {t('core:save_options.settings', { + postProcess: 'capitalize', + })} + {' '} - - + size="small" + onClick={() => { + saveToLocalStorage( + 'ext_saved_settings_import_export', + 'sortablePinnedApps', + null, + true + ); + setIsUsingImportExportSettings(false); + }} + variant="contained" + sx={{ + backgroundColor: theme.palette.other.danger, + color: 'black', + fontWeight: 'bold', + opacity: 0.7, + '&:hover': { + backgroundColor: theme.palette.other.danger, + color: 'black', + opacity: 1, + }, + }} + > + {t('core:save_options.qdn', { + postProcess: 'capitalize', + })} + + +
)} {!isUsingImportExportSettings && ( - {!myName ? ( - - You need a registered Qortal name to save your pinned apps to QDN. - + + {t('core:save_options.register_name', { + postProcess: 'capitalize', + })} + ) : ( <> - {hasChanged && ( - - - You have unsaved changes to your pinned apps. Save them to QDN. - - - - Save to QDN - - - {!isNaN(settingsQdnLastUpdated) && settingsQdnLastUpdated > 0 && ( - <> + {hasChanged && ( + - Don't like your current local changes? Would you like to - reset to your saved QDN pinned apps? + {t('core:save_options.unsaved_changes', { + postProcess: 'capitalize', + })} + + - Revert to QDN - - - )} - {!isNaN(settingsQdnLastUpdated) && settingsQdnLastUpdated === 0 && ( - <> - - Don't like your current local changes? Would you like to - reset to the default pinned apps? - - - - Revert to default + {t('core:save_options.save_qdn', { + postProcess: 'capitalize', + })} - + + {!isNaN(settingsQdnLastUpdated) && + settingsQdnLastUpdated > 0 && ( + <> + + {t('core:save_options.reset_qdn', { + postProcess: 'capitalize', + })} + + + + {t('core:save_options.revert_qdn', { + postProcess: 'capitalize', + })} + + + )} + {!isNaN(settingsQdnLastUpdated) && + settingsQdnLastUpdated === 0 && ( + <> + + {' '} + {t('core:save_options.reset_pinned', { + postProcess: 'capitalize', + })} + + + + {t('core:save_options.revert_default', { + postProcess: 'capitalize', + })} + + + )} + + )} + {!isNaN(settingsQdnLastUpdated) && + settingsQdnLastUpdated === -100 && + isUsingImportExportSettings !== true && ( + + + {t('core:save_options.overwrite_changes', { + postProcess: 'capitalize', + })} + + + + {t('core:save_options.overwrite_qdn', { + postProcess: 'capitalize', + })} + + + )} + {!hasChanged && ( + + + {t('core:save_options.no_pinned_changes', { + postProcess: 'capitalize', + })} + + )} - - )} - {!isNaN(settingsQdnLastUpdated) && settingsQdnLastUpdated === -100 && isUsingImportExportSettings !== true && ( - - - The app was unable to download your existing QDN-saved pinned - apps. Would you like to overwrite those changes? - - - - Overwrite to QDN - - - )} - {!hasChanged && ( - - - You currently do not have any changes to your pinned apps - - - - )} )} - )} - + - - { - try { - const fileContent = await handleImportClick(); - const decryptedData = await decryptData({ - encryptedData: fileContent, - }); - const decryptToUnit8ArraySubject = - base64ToUint8Array(decryptedData); - const responseData = uint8ArrayToObject( - decryptToUnit8ArraySubject - ); - if(Array.isArray(responseData)){ - saveToLocalStorage("ext_saved_settings_import_export", "sortablePinnedApps", responseData, { - isUsingImportExport: true - }); - setPinnedApps(responseData) - setOldPinnedApps(responseData) - setIsUsingImportExportSettings(true) - } - - } catch (error) { - console.log("error", error); + { + try { + const fileContent = await handleImportClick(); + const decryptedData = await decryptData({ + encryptedData: fileContent, + }); + const decryptToUnit8ArraySubject = + base64ToUint8Array(decryptedData); + const responseData = uint8ArrayToObject( + decryptToUnit8ArraySubject + ); + if (Array.isArray(responseData)) { + saveToLocalStorage( + 'ext_saved_settings_import_export', + 'sortablePinnedApps', + responseData, + { + isUsingImportExport: true, } - }}> - - Import - - { - try { - const data64 = await objectToBase64(pinnedApps); - - const encryptedData = await encryptData({ - data64, - }); - const blob = new Blob([encryptedData], { - type: "text/plain", - }); - - const timestamp = new Date() - .toISOString() - .replace(/:/g, "-"); // Safe timestamp for filenames - const filename = `qortal-new-ui-backup-settings-${timestamp}.txt`; - await saveFileToDiskGeneric(blob, filename) - - } catch (error) { - console.log('error', error) - } - }}> - Export + ); + setPinnedApps(responseData); + setOldPinnedApps(responseData); + setIsUsingImportExportSettings(true); + } + } catch (error) { + console.log('error', error); + } + }} + > + {t('core:action.import', { + postProcess: 'capitalize', + })} - - + + { + try { + const data64 = await objectToBase64(pinnedApps); + + const encryptedData = await encryptData({ + data64, + }); + const blob = new Blob([encryptedData], { + type: 'text/plain', + }); + + const timestamp = new Date().toISOString().replace(/:/g, '-'); // Safe timestamp for filenames + const filename = `qortal-new-ui-backup-settings-${timestamp}.txt`; + await saveFileToDiskGeneric(blob, filename); + } catch (error) { + console.log('error', error); + } + }} + > + {t('core:action.export', { + postProcess: 'capitalize', + })} + + +
{ - - - +export const CustomizedSnackbars = ({ + open, + setOpen, + info, + setInfo, + duration, +}) => { const handleClose = ( event?: React.SyntheticEvent | Event, - reason?: SnackbarCloseReason, + reason?: SnackbarCloseReason ) => { if (reason === 'clickaway') { return; } - setOpen(false); - setInfo(null) + setInfo(null); }; - if(!open) return null + if (!open) return null; + return (
- +
); -} \ No newline at end of file +}; diff --git a/src/components/TaskManager/TaskManger.tsx b/src/components/TaskManager/TaskManager.tsx similarity index 56% rename from src/components/TaskManager/TaskManger.tsx rename to src/components/TaskManager/TaskManager.tsx index a835ae1..d49077e 100644 --- a/src/components/TaskManager/TaskManger.tsx +++ b/src/components/TaskManager/TaskManager.tsx @@ -5,19 +5,26 @@ import { ListItemText, Collapse, IconButton, -} from "@mui/material"; -import React, { useContext, useEffect, useRef } from "react"; -import PendingIcon from "@mui/icons-material/Pending"; -import TaskAltIcon from "@mui/icons-material/TaskAlt"; -import ExpandLess from "@mui/icons-material/ExpandLess"; -import ExpandMore from "@mui/icons-material/ExpandMore"; -import { MyContext, getBaseApiReact, isMobile } from "../../App"; -import { executeEvent } from "../../utils/events"; + useTheme, +} from '@mui/material'; +import React, { useEffect, useRef } from 'react'; +import PendingIcon from '@mui/icons-material/Pending'; +import TaskAltIcon from '@mui/icons-material/TaskAlt'; +import ExpandLess from '@mui/icons-material/ExpandLess'; +import ExpandMore from '@mui/icons-material/ExpandMore'; +import { getBaseApiReact } from '../../App'; +import { executeEvent } from '../../utils/events'; +import { useAtom } from 'jotai'; +import { memberGroupsAtom, txListAtom } from '../../atoms/global'; export const TaskManager = ({ getUserInfo }) => { - const { txList, setTxList, memberGroups } = useContext(MyContext); + const [memberGroups] = useAtom(memberGroupsAtom); + + const [txList, setTxList] = useAtom(txListAtom); + const [open, setOpen] = React.useState(false); const intervals = useRef({}); + const theme = useTheme(); const handleClick = () => { setOpen((prev) => !prev); @@ -58,7 +65,9 @@ export const TaskManager = ({ getUserInfo }) => { } clearInterval(intervals.current[signature]); } - } catch (error) {} + } catch (error) { + console.log(error); + } stop = false; } }; @@ -71,7 +80,7 @@ export const TaskManager = ({ getUserInfo }) => { let previousData = [...prev]; memberGroups.forEach((group) => { const findGroup = txList.findIndex( - (tx) => tx?.type === "joined-group" && tx?.groupId === group.groupId + (tx) => tx?.type === 'joined-group' && tx?.groupId === group.groupId ); if (findGroup !== -1 && !previousData[findGroup]?.done) { previousData[findGroup].done = true; @@ -81,7 +90,7 @@ export const TaskManager = ({ getUserInfo }) => { memberGroups.forEach((group) => { const findGroup = txList.findIndex( (tx) => - tx?.type === "created-group" && tx?.groupName === group.groupName + tx?.type === 'created-group' && tx?.groupName === group.groupName ); if (findGroup !== -1 && !previousData[findGroup]?.done) { previousData[findGroup].done = true; @@ -90,52 +99,54 @@ export const TaskManager = ({ getUserInfo }) => { prev.forEach((tx, index) => { if ( - tx?.type === "leave-group" && - memberGroups.findIndex((group) => tx?.groupId === group.groupId) === -1 + tx?.type === 'leave-group' && + memberGroups.findIndex((group) => tx?.groupId === group.groupId) === + -1 ) { previousData[index].done = true; } }); - - return previousData; }); }, [memberGroups, getUserInfo]); - useEffect(()=> { - - txList.forEach((tx) => { - if ( - ["created-common-secret", "joined-group-request", "join-request-accept"].includes( - tx?.type - ) && - tx?.signature && - !tx.done - ) { - if (!intervals.current[tx.signature]) { - getStatus({ signature: tx.signature }); - } + useEffect(() => { + txList.forEach((tx) => { + if ( + [ + 'created-common-secret', + 'joined-group-request', + 'join-request-accept', + ].includes(tx?.type) && + tx?.signature && + !tx.done + ) { + if (!intervals.current[tx.signature]) { + getStatus({ signature: tx.signature }); } - if (tx?.type === "register-name" && tx?.signature && !tx.done) { - if (!intervals.current[tx.signature]) { - getStatus({ signature: tx.signature }, getUserInfo); - } + } + if (tx?.type === 'register-name' && tx?.signature && !tx.done) { + if (!intervals.current[tx.signature]) { + getStatus({ signature: tx.signature }, getUserInfo); } - if((tx?.type === "remove-rewardShare" || tx?.type === "add-rewardShare") && tx?.signature && !tx.done){ - if (!intervals.current[tx.signature]) { - const sendEventForRewardShare = ()=> { - executeEvent('refresh-rewardshare-list', {}) - } - getStatus({ signature: tx.signature }, sendEventForRewardShare); - } + } + if ( + (tx?.type === 'remove-rewardShare' || tx?.type === 'add-rewardShare') && + tx?.signature && + !tx.done + ) { + if (!intervals.current[tx.signature]) { + const sendEventForRewardShare = () => { + executeEvent('refresh-rewardshare-list', {}); + }; + getStatus({ signature: tx.signature }, sendEventForRewardShare); } - }); + } + }); + }, [txList]); - }, [txList]) - - if (isMobile || txList?.length === 0 || txList.every((item) => item?.done)) - return null; + if (txList?.length === 0 || txList.every((item) => item?.done)) return null; return ( <> @@ -143,43 +154,48 @@ export const TaskManager = ({ getUserInfo }) => { - {txList.some((item) => !item.done) ? : } + {txList.some((item) => !item.done) ? ( + + ) : ( + + )} )} {open && ( - + {txList.some((item) => !item.done) ? ( - + ) : ( - + )} diff --git a/src/components/Theme/ThemeContext.tsx b/src/components/Theme/ThemeContext.tsx new file mode 100644 index 0000000..72d4f3b --- /dev/null +++ b/src/components/Theme/ThemeContext.tsx @@ -0,0 +1,136 @@ +import { + createContext, + useContext, + useState, + useMemo, + useEffect, + useCallback, +} from 'react'; +import { + ThemeProvider as MuiThemeProvider, + createTheme, +} from '@mui/material/styles'; +import { lightThemeOptions } from '../../styles/theme-light'; +import { darkThemeOptions } from '../../styles/theme-dark'; + +const defaultTheme = { + id: 'default', + name: 'Default Theme', + light: lightThemeOptions.palette, + dark: darkThemeOptions.palette, +}; + +const ThemeContext = createContext({ + themeMode: 'dark', + toggleTheme: () => {}, + userThemes: [defaultTheme], + addUserTheme: (themes) => {}, + setUserTheme: (theme) => {}, + currentThemeId: 'default', +}); + +export const ThemeProvider = ({ children }) => { + const [themeMode, setThemeMode] = useState('dark'); + const [userThemes, setUserThemes] = useState([defaultTheme]); + const [currentThemeId, setCurrentThemeId] = useState('default'); + + const currentTheme = + userThemes.find((theme) => theme.id === currentThemeId) || defaultTheme; + + const muiTheme = useMemo(() => { + if (themeMode === 'light') { + return createTheme({ + ...lightThemeOptions, + palette: { + ...currentTheme.light, + }, + }); + } else { + return createTheme({ + ...lightThemeOptions, + palette: { + ...currentTheme.dark, + }, + }); + } + }, [themeMode, currentTheme]); + + const saveSettings = ( + themes = userThemes, + mode = themeMode, + themeId = currentThemeId + ) => { + localStorage.setItem( + 'saved_ui_theme', + JSON.stringify({ + mode, + userThemes: themes, + currentThemeId: themeId, + }) + ); + }; + + const toggleTheme = () => { + setThemeMode((prev) => { + const newMode = prev === 'light' ? 'dark' : 'light'; + saveSettings(userThemes, newMode, currentThemeId); + return newMode; + }); + }; + + const addUserTheme = (themes) => { + setUserThemes(themes); + saveSettings(themes); + }; + + const setUserTheme = (theme) => { + if (theme.id === 'default') { + setCurrentThemeId('default'); + saveSettings(userThemes, themeMode, 'default'); + } else { + setCurrentThemeId(theme.id); + saveSettings(userThemes, themeMode, theme.id); + } + }; + + const loadSettings = useCallback(() => { + const saved = localStorage.getItem('saved_ui_theme'); + if (saved) { + try { + const parsed = JSON.parse(saved); + if (parsed.mode === 'light' || parsed.mode === 'dark') + setThemeMode(parsed.mode); + if (Array.isArray(parsed.userThemes)) { + const filteredThemes = parsed.userThemes.filter( + (theme) => theme.id !== 'default' + ); + setUserThemes([defaultTheme, ...filteredThemes]); + } + if (parsed.currentThemeId) setCurrentThemeId(parsed.currentThemeId); + } catch (error) { + console.error('Failed to parse saved_ui_theme:', error); + } + } + }, []); + + useEffect(() => { + loadSettings(); + }, [loadSettings]); + + return ( + + {children} + + ); +}; + +export const useThemeContext = () => useContext(ThemeContext); diff --git a/src/components/Theme/ThemeManager.tsx b/src/components/Theme/ThemeManager.tsx new file mode 100644 index 0000000..eca0e9f --- /dev/null +++ b/src/components/Theme/ThemeManager.tsx @@ -0,0 +1,403 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { + Box, + Button, + IconButton, + Typography, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + List, + ListItemText, + ListItemSecondaryAction, + TextField, + Tabs, + Tab, + ListItemButton, +} from '@mui/material'; +import { Sketch } from '@uiw/react-color'; +import DeleteIcon from '@mui/icons-material/Delete'; +import EditIcon from '@mui/icons-material/Edit'; +import AddIcon from '@mui/icons-material/Add'; +import CheckIcon from '@mui/icons-material/Check'; +import { useThemeContext } from './ThemeContext'; +import { darkThemeOptions } from '../../styles/theme-dark'; +import { lightThemeOptions } from '../../styles/theme-light'; +import ShortUniqueId from 'short-unique-id'; +import { rgbStringToHsva, rgbaStringToHsva } from '@uiw/color-convert'; +import FileDownloadIcon from '@mui/icons-material/FileDownload'; +import { saveFileToDiskGeneric } from '../../utils/generateWallet/generateWallet'; +import { handleImportClick } from '../../utils/fileReading'; +const uid = new ShortUniqueId({ length: 8 }); + +function detectColorFormat(color) { + if (typeof color !== 'string') return null; + if (color.startsWith('rgba')) return 'rgba'; + if (color.startsWith('rgb')) return 'rgb'; + return null; +} + +const validateTheme = (theme) => { + if (typeof theme !== 'object' || !theme) return false; + if (typeof theme.name !== 'string') return false; + if (!theme.light || typeof theme.light !== 'object') return false; + if (!theme.dark || typeof theme.dark !== 'object') return false; + + // Optional: deeper checks on structure + const requiredKeys = [ + 'primary', + 'secondary', + 'background', + 'text', + 'border', + 'other', + ]; + + for (const mode of ['light', 'dark']) { + const modeTheme = theme[mode]; + if (modeTheme.mode !== mode) return false; + + for (const key of requiredKeys) { + if (!modeTheme[key] || typeof modeTheme[key] !== 'object') { + return false; + } + } + } + + return true; +}; + +export default function ThemeManager() { + const { userThemes, addUserTheme, setUserTheme, currentThemeId } = + useThemeContext(); + const [openEditor, setOpenEditor] = useState(false); + const [themeDraft, setThemeDraft] = useState({ + id: '', + name: '', + light: {}, + dark: {}, + }); + const [currentTab, setCurrentTab] = useState('light'); + const nameInputRef = useRef(null); + + useEffect(() => { + if (openEditor && nameInputRef.current) { + nameInputRef.current.focus(); + } + }, [openEditor]); + + const handleAddTheme = () => { + setThemeDraft({ + id: '', + name: '', + light: structuredClone(lightThemeOptions.palette), + dark: structuredClone(darkThemeOptions.palette), + }); + setOpenEditor(true); + }; + + const handleEditTheme = (themeId) => { + const themeToEdit = userThemes.find((theme) => theme.id === themeId); + if (themeToEdit) { + setThemeDraft({ ...themeToEdit }); + setOpenEditor(true); + } + }; + + const handleSaveTheme = () => { + if (themeDraft.id) { + const updatedThemes = [...userThemes]; + const index = updatedThemes.findIndex( + (theme) => theme.id === themeDraft.id + ); + if (index !== -1) { + updatedThemes[index] = themeDraft; + addUserTheme(updatedThemes); + } + } else { + const newTheme = { ...themeDraft, id: uid.rnd() }; + const updatedThemes = [...userThemes, newTheme]; + addUserTheme(updatedThemes); + setUserTheme(newTheme); + } + setOpenEditor(false); + }; + + const handleDeleteTheme = (id) => { + const updatedThemes = userThemes.filter((theme) => theme.id !== id); + addUserTheme(updatedThemes); + + if (id === currentThemeId) { + // Find the default theme object in the list + const defaultTheme = updatedThemes.find( + (theme) => theme.id === 'default' + ); + + if (defaultTheme) { + setUserTheme(defaultTheme); + } else { + // Emergency fallback + setUserTheme({ + light: lightThemeOptions, + dark: darkThemeOptions, + }); + } + } + }; + + const handleApplyTheme = (theme) => { + setUserTheme(theme); + }; + + const handleColorChange = (mode, fieldPath, color) => { + setThemeDraft((prev) => { + const updated = { ...prev }; + const paths = fieldPath.split('.'); + updated[mode][paths[0]][paths[1]] = color.hex; + return updated; + }); + }; + + const renderColorPicker = (mode, label, fieldPath, currentValue) => { + let color = currentValue || '#ffffff'; + const format = detectColorFormat(currentValue); + if (format === 'rgba') { + color = rgbaStringToHsva(currentValue); + } else if (format === 'rgb') { + color = rgbStringToHsva(currentValue); + } + return ( + + + {label} + + handleColorChange(mode, fieldPath, color)} + /> + + ); + }; + + const exportTheme = async (theme) => { + try { + const copyTheme = structuredClone(theme); + delete copyTheme.id; + const fileName = `ui_theme_${theme.name}.json`; + + const blob = new Blob([JSON.stringify(copyTheme, null, 2)], { + type: 'application/json', + }); + + await saveFileToDiskGeneric(blob, fileName); + } catch (error) { + console.error(error); + } + }; + + const importTheme = async (theme) => { + try { + const fileContent = await handleImportClick('.json'); + const importedTheme = JSON.parse(fileContent); + if (!validateTheme(importedTheme)) { + throw new Error('Invalid theme format'); + } + const newTheme = { ...importedTheme, id: uid.rnd() }; + const updatedThemes = [...userThemes, newTheme]; + addUserTheme(updatedThemes); + setUserTheme(newTheme); + } catch (error) { + console.error(error); + } + }; + + return ( + + + Theme Manager + + + + + + {userThemes?.map((theme, index) => ( + + + + {theme.id !== 'default' && ( + <> + exportTheme(theme)}> + + + handleEditTheme(theme.id)}> + + + handleDeleteTheme(theme.id)}> + + + + )} + handleApplyTheme(theme)}> + + + + + ))} + + + setOpenEditor(false)} + fullWidth + maxWidth="md" + > + + {themeDraft.id ? 'Edit Theme' : 'Add New Theme'} + + + + setThemeDraft((prev) => ({ ...prev, name: e.target.value })) + } + /> + + setCurrentTab(newValue)} + sx={{ mt: 2, mb: 2 }} + > + + + + + + {renderColorPicker( + currentTab, + 'Primary Main', + 'primary.main', + themeDraft[currentTab]?.primary?.main + )} + {renderColorPicker( + currentTab, + 'Primary Dark', + 'primary.dark', + themeDraft[currentTab]?.primary?.dark + )} + {renderColorPicker( + currentTab, + 'Primary Light', + 'primary.light', + themeDraft[currentTab]?.primary?.light + )} + {renderColorPicker( + currentTab, + 'Secondary Main', + 'secondary.main', + themeDraft[currentTab]?.secondary?.main + )} + {renderColorPicker( + currentTab, + 'Background Default', + 'background.default', + themeDraft[currentTab]?.background?.default + )} + {renderColorPicker( + currentTab, + 'Background Paper', + 'background.paper', + themeDraft[currentTab]?.background?.paper + )} + {renderColorPicker( + currentTab, + 'Background Surface', + 'background.surface', + themeDraft[currentTab]?.background?.surface + )} + {renderColorPicker( + currentTab, + 'Text Primary', + 'text.primary', + themeDraft[currentTab]?.text?.primary + )} + {renderColorPicker( + currentTab, + 'Text Secondary', + 'text.secondary', + themeDraft[currentTab]?.text?.secondary + )} + {renderColorPicker( + currentTab, + 'Border Main', + 'border.main', + themeDraft[currentTab]?.border?.main + )} + {renderColorPicker( + currentTab, + 'Border Subtle', + 'border.subtle', + themeDraft[currentTab]?.border?.subtle + )} + {renderColorPicker( + currentTab, + 'Positive', + 'other.positive', + themeDraft[currentTab]?.other?.positive + )} + {renderColorPicker( + currentTab, + 'Danger', + 'other.danger', + themeDraft[currentTab]?.other?.danger + )} + {renderColorPicker( + currentTab, + 'Unread', + 'other.unread', + themeDraft[currentTab]?.other?.unread + )} + + + + + + + + + ); +} diff --git a/src/components/Theme/ThemeSelector.tsx b/src/components/Theme/ThemeSelector.tsx new file mode 100644 index 0000000..bab403c --- /dev/null +++ b/src/components/Theme/ThemeSelector.tsx @@ -0,0 +1,41 @@ +import { useThemeContext } from './ThemeContext'; +import { IconButton, Tooltip } from '@mui/material'; +import LightModeIcon from '@mui/icons-material/LightMode'; +import DarkModeIcon from '@mui/icons-material/DarkMode'; +import { useTranslation } from 'react-i18next'; + +const ThemeSelector = () => { + const { t } = useTranslation(['core']); + + const { themeMode, toggleTheme } = useThemeContext(); + + return ( +
+ + + {themeMode === 'dark' ? : } + + +
+ ); +}; + +export default ThemeSelector; diff --git a/src/components/Theme/themeManager.css b/src/components/Theme/themeManager.css new file mode 100644 index 0000000..85a4538 --- /dev/null +++ b/src/components/Theme/themeManager.css @@ -0,0 +1,39 @@ +[data-color-mode*='dark'] .w-color-sketch { + --sketch-background: #323232 !important; +} + +[data-color-mode*='dark'] .w-color-swatch { + --sketch-swatch-border-top: 1px solid #525252 !important; +} + +[data-color-mode*='dark'] .w-color-block { + --block-background-color: #323232 !important; + --block-box-shadow: rgb(0 0 0 / 10%) 0 1px !important; +} + +[data-color-mode*='dark'] .w-color-editable-input { + --editable-input-label-color: #757575 !important; + --editable-input-box-shadow: #616161 0px 0px 0px 1px inset !important; + --editable-input-color: #bbb !important; +} + +[data-color-mode*='dark'] .w-color-github { + --github-border: 1px solid rgba(0, 0, 0, 0.2) !important; + --github-background-color: #323232 !important; + --github-box-shadow: rgb(0 0 0 / 15%) 0px 3px 12px !important; + --github-arrow-border-color: rgba(0, 0, 0, 0.15) !important; +} + +[data-color-mode*='dark'] .w-color-compact { + --compact-background-color: #323232 !important; +} + +[data-color-mode*='dark'] .w-color-material { + --material-background-color: #323232 !important; + --material-border-bottom-color: #707070 !important; +} + +[data-color-mode*='dark'] .w-color-alpha { + --alpha-pointer-background-color: #6a6a6a !important; + --alpha-pointer-box-shadow: rgb(0 0 0 / 37%) 0px 1px 4px 0px !important; +} diff --git a/src/components/Tutorials/Tutorials.tsx b/src/components/Tutorials/Tutorials.tsx index 4f9ea49..f1a5fe5 100644 --- a/src/components/Tutorials/Tutorials.tsx +++ b/src/components/Tutorials/Tutorials.tsx @@ -1,72 +1,103 @@ -import React, { useContext, useState } from 'react' -import { GlobalContext, MyContext } from '../../App'; -import { Button, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Tab, Tabs, Typography } from '@mui/material'; +import { useContext, useState } from 'react'; +import { MyContext } from '../../App'; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + Tab, + Tabs, + useTheme, +} from '@mui/material'; import CloseIcon from '@mui/icons-material/Close'; import { VideoPlayer } from '../Embeds/VideoPlayer'; +import { useTranslation } from 'react-i18next'; export const Tutorials = () => { - const { openTutorialModal, setOpenTutorialModal } = useContext(GlobalContext); - const [multiNumber, setMultiNumber] = useState(0) - const handleClose = ()=> { - setOpenTutorialModal(null) - setMultiNumber(0) - } - if(!openTutorialModal) return null - if(openTutorialModal?.multi){ - const selectedTutorial = openTutorialModal?.multi[multiNumber] - return ( - { + setOpenTutorialModal(null); + setMultiNumber(0); + }; + + if (!openTutorialModal) return null; + + if (openTutorialModal?.multi) { + const selectedTutorial = openTutorialModal?.multi[multiNumber]; + return ( + - setMultiNumber(value)} aria-label="basic tabs example"> - {openTutorialModal?.multi?.map((item, index)=> { - return ( - - - ) - })} - - - {selectedTutorial?.title} {` Tutorial`} - + setMultiNumber(value)} + aria-label="basic tabs example" + > + {openTutorialModal?.multi?.map((item, index) => { + return ( + + ); + })} + + + {selectedTutorial?.title} + ({ + sx={{ position: 'absolute', right: 8, top: 8, - color: theme.palette.grey[500], - })} + color: theme.palette.text.primary, + }} > - - + + - ) - } + ); + } + return ( <> { fullWidth={true} maxWidth="xl" > - + {openTutorialModal?.title} {` Tutorial`} + ({ + sx={{ position: 'absolute', right: 8, top: 8, - color: theme.palette.grey[500], - })} + color: theme.palette.text.primary, + }} > - - + + + - ) -} + ); +}; diff --git a/src/components/Tutorials/useHandleTutorials.tsx b/src/components/Tutorials/useHandleTutorials.tsx index 2fabfa0..597199e 100644 --- a/src/components/Tutorials/useHandleTutorials.tsx +++ b/src/components/Tutorials/useHandleTutorials.tsx @@ -1,192 +1,212 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { saveToLocalStorage } from "../Apps/AppsNavBar"; -import creationImg from './img/creation.webp' -import dashboardImg from './img/dashboard.webp' -import groupsImg from './img/groups.webp' -import importantImg from './img/important.webp' -import navigationImg from './img/navigation.webp' -import overviewImg from './img/overview.webp' -import startedImg from './img/started.webp' -import obtainingImg from './img/obtaining-qort.jpg' +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { saveToLocalStorage } from '../Apps/AppsNavBarDesktop'; +import creationImg from './img/creation.webp'; +import dashboardImg from './img/dashboard.webp'; +import groupsImg from './img/groups.webp'; +import importantImg from './img/important.webp'; +import navigationImg from './img/navigation.webp'; +import overviewImg from './img/overview.webp'; +import startedImg from './img/started.webp'; +import obtainingImg from './img/obtaining-qort.jpg'; +import { useTranslation } from 'react-i18next'; const checkIfGatewayIsOnline = async () => { - try { - const url = `https://ext-node.qortal.link/admin/status`; - const response = await fetch(url, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); - const data = await response.json(); - if (data?.height) { - return true - } - return false - - } catch (error) { - return false - - } + try { + const url = `https://ext-node.qortal.link/admin/status`; + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + const data = await response.json(); + if (data?.height) { + return true; + } + return false; + } catch (error) { + return false; } +}; + export const useHandleTutorials = () => { const [openTutorialModal, setOpenTutorialModal] = useState(null); -const [shownTutorials, setShowTutorials] = useState(null) + const [shownTutorials, setShowTutorials] = useState(null); + const { t } = useTranslation(['core', 'tutorial']); -useEffect(()=> { + useEffect(() => { try { - const storedData = localStorage.getItem('shown-tutorials'); + const storedData = localStorage.getItem('shown-tutorials'); - - if (storedData) { - setShowTutorials(JSON.parse(storedData)); - } else { - setShowTutorials({}) - } + if (storedData) { + setShowTutorials(JSON.parse(storedData)); + } else { + setShowTutorials({}); + } } catch (error) { - //error + //error } -}, []) + }, []); - const saveShowTutorial = useCallback((type)=> { + const saveShowTutorial = useCallback((type) => { try { - - setShowTutorials((prev)=> { - return { - ...(prev || {}), - [type]: true - } - }) - saveToLocalStorage('shown-tutorials', type, true) + setShowTutorials((prev) => { + return { + ...(prev || {}), + [type]: true, + }; + }); + saveToLocalStorage('shown-tutorials', type, true); } catch (error) { - //error + //error } - }, []) - const showTutorial = useCallback(async (type, isForce) => { - try { - const isOnline = await checkIfGatewayIsOnline() - if(!isOnline) return + }, []); + const showTutorial = useCallback( + async (type, isForce) => { + try { + const isOnline = await checkIfGatewayIsOnline(); + if (!isOnline) return; switch (type) { - case "create-account": - { - if((shownTutorials || {})['create-account'] && !isForce) return - saveShowTutorial('create-account') - setOpenTutorialModal({ - title: "Account Creation", - resource: { - name: "a-test", - service: "VIDEO", - identifier: "account-creation-hub", - poster: creationImg - }, - }); - } - break; - case "important-information": - { - if((shownTutorials || {})['important-information'] && !isForce) return - saveShowTutorial('important-information') + case 'create-account': + { + if ((shownTutorials || {})['create-account'] && !isForce) return; + saveShowTutorial('create-account'); + setOpenTutorialModal({ + title: 'Account Creation', + resource: { + name: 'a-test', + service: 'VIDEO', + identifier: 'account-creation-hub', + poster: creationImg, + }, + }); + } + break; + case 'important-information': + { + if ((shownTutorials || {})['important-information'] && !isForce) + return; + saveShowTutorial('important-information'); - setOpenTutorialModal({ - title: "Important Information!", - resource: { - name: "a-test", - service: "VIDEO", - identifier: "important-information-hub", - poster: importantImg - }, - }); - } - break; - case "getting-started": - { - if((shownTutorials || {})['getting-started'] && !isForce) return - saveShowTutorial('getting-started') + setOpenTutorialModal({ + title: 'Important Information!', + resource: { + name: 'a-test', + service: 'VIDEO', + identifier: 'important-information-hub', + poster: importantImg, + }, + }); + } + break; + case 'getting-started': + { + if ((shownTutorials || {})['getting-started'] && !isForce) return; + saveShowTutorial('getting-started'); - setOpenTutorialModal({ - multi: [ - - { - title: "1. Getting Started", - resource: { - name: "a-test", - service: "VIDEO", - identifier: "getting-started-hub", - poster: startedImg - }, - }, - { - title: "2. Overview", - resource: { - name: "a-test", - service: "VIDEO", - identifier: "overview-hub", - poster: overviewImg - }, - }, - { - title: "3. Qortal Groups", - resource: { - name: "a-test", - service: "VIDEO", - identifier: "groups-hub", - poster: groupsImg - }, - }, - { - title: "4. Obtaining Qort", - resource: { - name: "a-test", - service: "VIDEO", - identifier: "obtaining-qort", - poster: obtainingImg - }, - }, - ], - }); - } - break; - case "qapps": + setOpenTutorialModal({ + multi: [ { - if((shownTutorials || {})['qapps'] && !isForce) return - saveShowTutorial('qapps') + title: t('tutorial:1_getting_started', { + postProcess: 'capitalize', + }), + resource: { + name: 'a-test', + service: 'VIDEO', + identifier: 'getting-started-hub', + poster: startedImg, + }, + }, + { + title: t('tutorial:2_overview', { + postProcess: 'capitalize', + }), + resource: { + name: 'a-test', + service: 'VIDEO', + identifier: 'overview-hub', + poster: overviewImg, + }, + }, + { + title: t('tutorial:3_groups', { + postProcess: 'capitalize', + }), + resource: { + name: 'a-test', + service: 'VIDEO', + identifier: 'groups-hub', + poster: groupsImg, + }, + }, + { + title: t('tutorial:4_obtain_qort', { + postProcess: 'capitalize', + }), + resource: { + name: 'a-test', + service: 'VIDEO', + identifier: 'obtaining-qort', + poster: obtainingImg, + }, + }, + ], + }); + } + break; + case 'qapps': + { + if ((shownTutorials || {})['qapps'] && !isForce) return; + saveShowTutorial('qapps'); - setOpenTutorialModal({ - multi: [ - { - title: "1. Apps Dashboard", - resource: { - name: "a-test", - service: "VIDEO", - identifier: "apps-dashboard-hub", - poster: dashboardImg - }, - }, - { - title: "2. Apps Navigation", - resource: { - name: "a-test", - service: "VIDEO", - identifier: "apps-navigation-hub", - poster: navigationImg - }, - } - ], - }); - } - break; - default: - break; - } - } catch (error) { + setOpenTutorialModal({ + multi: [ + { + title: t('tutorial:apps.dashboard', { + postProcess: 'capitalize', + }), + resource: { + name: 'a-test', + service: 'VIDEO', + identifier: 'apps-dashboard-hub', + poster: dashboardImg, + }, + }, + { + title: t('tutorial:apps.navigation', { + postProcess: 'capitalize', + }), + resource: { + name: 'a-test', + service: 'VIDEO', + identifier: 'apps-navigation-hub', + poster: navigationImg, + }, + }, + ], + }); + } + break; + default: + break; + } + } catch (error) { //error - } - }, [shownTutorials]); - return { - showTutorial, - hasSeenGettingStarted: shownTutorials === null ? null : !!(shownTutorials || {})['getting-started'], - openTutorialModal, - setOpenTutorialModal, - shownTutorialsInitiated: !!shownTutorials - }; + } + }, + [shownTutorials] + ); + return useMemo( + () => ({ + showTutorial, + hasSeenGettingStarted: + shownTutorials === null + ? null + : !!(shownTutorials || {})['getting-started'], + openTutorialModal, + setOpenTutorialModal, + shownTutorialsInitiated: !!shownTutorials, + }), + [showTutorial, openTutorialModal, setOpenTutorialModal, shownTutorials] + ); }; diff --git a/src/components/UserLookup.tsx/UserLookup.tsx b/src/components/UserLookup.tsx/UserLookup.tsx index 300ca2f..ee0b8c9 100644 --- a/src/components/UserLookup.tsx/UserLookup.tsx +++ b/src/components/UserLookup.tsx/UserLookup.tsx @@ -1,5 +1,5 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { DrawerUserLookup } from "../Drawer/DrawerUserLookup"; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { DrawerUserLookup } from '../Drawer/DrawerUserLookup'; import { Avatar, Box, @@ -16,16 +16,23 @@ import { Typography, Table, CircularProgress, -} from "@mui/material"; -import { getAddressInfo, getNameOrAddress } from "../../background"; -import { getBaseApiReact } from "../../App"; -import { getNameInfo } from "../Group/Group"; -import AccountCircleIcon from "@mui/icons-material/AccountCircle"; -import { Spacer } from "../../common/Spacer"; -import { formatTimestamp } from "../../utils/time"; + useTheme, + Autocomplete, +} from '@mui/material'; +import { getAddressInfo, getNameOrAddress } from '../../background'; +import { getBaseApiReact } from '../../App'; +import { getNameInfo } from '../Group/Group'; +import AccountCircleIcon from '@mui/icons-material/AccountCircle'; +import { Spacer } from '../../common/Spacer'; +import { formatTimestamp } from '../../utils/time'; import CloseFullscreenIcon from '@mui/icons-material/CloseFullscreen'; import SearchIcon from '@mui/icons-material/Search'; -import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "../../utils/events"; +import { + executeEvent, + subscribeToEvent, + unsubscribeFromEvent, +} from '../../utils/events'; +import { useNameSearch } from '../../hooks/useNameSearch'; function formatAddress(str) { if (str.length <= 12) return str; @@ -37,469 +44,524 @@ function formatAddress(str) { } export const UserLookup = ({ isOpenDrawerLookup, setIsOpenDrawerLookup }) => { - const [nameOrAddress, setNameOrAddress] = useState(""); - const [errorMessage, setErrorMessage] = useState(""); + const theme = useTheme(); + const [nameOrAddress, setNameOrAddress] = useState(''); + const [inputValue, setInputValue] = useState(''); + const { results, isLoading } = useNameSearch(inputValue); + const options = useMemo(() => results?.map((item) => item.name), [results]); + const [errorMessage, setErrorMessage] = useState(''); const [addressInfo, setAddressInfo] = useState(null); const [isLoadingUser, setIsLoadingUser] = useState(false); const [isLoadingPayments, setIsLoadingPayments] = useState(false); const [payments, setPayments] = useState([]); - const lookupFunc = useCallback(async (messageAddressOrName) => { - try { - setErrorMessage('') - setIsLoadingUser(true) - setPayments([]) - setAddressInfo(null) - const inputAddressOrName = messageAddressOrName || nameOrAddress - if (!inputAddressOrName?.trim()) - throw new Error("Please insert a name or address"); - const owner = await getNameOrAddress(inputAddressOrName); - if (!owner) throw new Error("Name does not exist"); - const addressInfoRes = await getAddressInfo(owner); - if (!addressInfoRes?.publicKey) { - throw new Error("Address does not exist on blockchain"); + const lookupFunc = useCallback( + async (messageAddressOrName) => { + try { + setErrorMessage(''); + setIsLoadingUser(true); + setPayments([]); + setAddressInfo(null); + const inputAddressOrName = messageAddressOrName || nameOrAddress; + + if (!inputAddressOrName?.trim()) + throw new Error('Please insert a name or address'); + const owner = await getNameOrAddress(inputAddressOrName); + if (!owner) throw new Error('Name does not exist'); + + const addressInfoRes = await getAddressInfo(owner); + if (!addressInfoRes?.publicKey) { + throw new Error('Address does not exist on blockchain'); + } + + const name = await getNameInfo(owner); + const balanceRes = await fetch( + `${getBaseApiReact()}/addresses/balance/${owner}` + ); + + const balanceData = await balanceRes.json(); + setAddressInfo({ + ...addressInfoRes, + balance: balanceData, + name, + }); + setIsLoadingUser(false); + setIsLoadingPayments(true); + + const getPayments = await fetch( + `${getBaseApiReact()}/transactions/search?txType=PAYMENT&address=${owner}&confirmationStatus=CONFIRMED&limit=20&reverse=true` + ); + const paymentsData = await getPayments.json(); + setPayments(paymentsData); + } catch (error) { + setErrorMessage(error?.message); + console.error(error); + } finally { + setIsLoadingUser(false); + setIsLoadingPayments(false); } - const name = await getNameInfo(owner); - const balanceRes = await fetch( - `${getBaseApiReact()}/addresses/balance/${owner}` - ); - const balanceData = await balanceRes.json(); - setAddressInfo({ - ...addressInfoRes, - balance: balanceData, - name, - }); - setIsLoadingUser(false) - setIsLoadingPayments(true) + }, + [nameOrAddress] + ); - const getPayments = await fetch( - `${getBaseApiReact()}/transactions/search?txType=PAYMENT&address=${owner}&confirmationStatus=CONFIRMED&limit=20&reverse=true` - ); - const paymentsData = await getPayments.json(); - setPayments(paymentsData); - - } catch (error) { - setErrorMessage(error?.message) - console.error(error); - } finally { - setIsLoadingUser(false) - setIsLoadingPayments(false) - } - }, [nameOrAddress]); - - const openUserLookupDrawerFunc = useCallback((e) => { - setIsOpenDrawerLookup(true) + const openUserLookupDrawerFunc = useCallback( + (e) => { + setIsOpenDrawerLookup(true); const message = e.detail?.addressOrName; - if(message){ - lookupFunc(message) + if (message) { + lookupFunc(message); } - }, [lookupFunc, setIsOpenDrawerLookup]); - - useEffect(() => { - subscribeToEvent("openUserLookupDrawer", openUserLookupDrawerFunc); - - return () => { - unsubscribeFromEvent("openUserLookupDrawer", openUserLookupDrawerFunc); - }; - }, [openUserLookupDrawerFunc]); + }, + [lookupFunc, setIsOpenDrawerLookup] + ); - const onClose = ()=> { - setIsOpenDrawerLookup(false) - setNameOrAddress('') - setErrorMessage('') - setPayments([]) - setIsLoadingUser(false) - setIsLoadingPayments(false) - setAddressInfo(null) - } + useEffect(() => { + subscribeToEvent('openUserLookupDrawer', openUserLookupDrawerFunc); + return () => { + unsubscribeFromEvent('openUserLookupDrawer', openUserLookupDrawerFunc); + }; + }, [openUserLookupDrawerFunc]); + + const onClose = () => { + setIsOpenDrawerLookup(false); + setNameOrAddress(''); + setInputValue(''); + setErrorMessage(''); + setPayments([]); + setIsLoadingUser(false); + setIsLoadingPayments(false); + setAddressInfo(null); + }; return ( - - - - setNameOrAddress(e.target.value)} - size="small" - placeholder="Address or Name" - autoComplete="off" - onKeyDown={(e) => { - if (e.key === "Enter" && nameOrAddress) { - lookupFunc(); + onChange={(event: any, newValue: string | null) => { + if (!newValue) { + setNameOrAddress(''); + return; } + setNameOrAddress(newValue); + lookupFunc(newValue); }} + inputValue={inputValue} + onInputChange={(event, newInputValue) => { + setInputValue(newInputValue); + }} + id="controllable-states-demo" + loading={isLoading} + options={options} + sx={{ width: 300 }} + renderInput={(params) => ( + { + if (e.key === 'Enter' && nameOrAddress) { + lookupFunc(inputValue); + } + }} + /> + )} /> - { - lookupFunc(); - }} > - - - { - onClose() - }}> - + + { + onClose(); + }} + > + - {!isLoadingUser && errorMessage && ( - - {errorMessage} + marginTop: '40px', + width: '100%', + }} + > + {errorMessage} - )} - {isLoadingUser && ( - - + marginTop: '40px', + width: '100%', + }} + > + - )} - {!isLoadingUser && addressInfo && ( + )} + {!isLoadingUser && addressInfo && ( <> - - - - - - {addressInfo?.name ?? "Name not registered"} - - - - {addressInfo?.name ? ( - - - - ) : ( - - )} - - - - Level {addressInfo?.level} - - - + + + + {addressInfo?.name ?? 'Name not registered'} + + + + + + {addressInfo?.name ? ( + + + + ) : ( + + )} + + + + + + Level {addressInfo?.level} + + + + + + + Address + + + copy address + + } + placement="bottom" + arrow + sx={{ fontSize: '24' }} + slotProps={{ + tooltip: { + sx: { + color: theme.palette.text.primary, + backgroundColor: theme.palette.background.default, + }, + }, + arrow: { + sx: { + color: theme.palette.text.primary, + }, + }, + }} + > + { + navigator.clipboard.writeText(addressInfo?.address); + }} + > + + {addressInfo?.address} + + + + + + + Balance + {addressInfo?.balance} + + + + + + + + + )} + + + + {isLoadingPayments && ( + + + + )} + {!isLoadingPayments && addressInfo && ( + + 20 most recent payments + + + + {!isLoadingPayments && payments?.length === 0 && ( - Address + No payments - - copy address - - } - placement="bottom" - arrow - sx={{ fontSize: "24" }} - slotProps={{ - tooltip: { - sx: { - color: "#ffffff", - backgroundColor: "#444444", - }, - }, - arrow: { - sx: { - color: "#444444", - }, - }, - }} - > - { - navigator.clipboard.writeText(addressInfo?.address); - }} - > - - {addressInfo?.address} - - - - - - Balance - {addressInfo?.balance} - - - - - - + )} - - )} - - {isLoadingPayments && ( - - - - )} - {!isLoadingPayments && addressInfo && ( - - 20 most recent payments - - {!isLoadingPayments && payments?.length === 0 && ( - - No payments - - )} - - - - Sender - Reciver - Amount - Time - - - - {payments.map((payment, index) => ( - - - - copy address - - } - placement="bottom" - arrow - sx={{ fontSize: "24" }} - slotProps={{ - tooltip: { - sx: { - color: "#ffffff", - backgroundColor: "#444444", - }, - }, - arrow: { - sx: { - color: "#444444", - }, - }, - }} - > - { - navigator.clipboard.writeText( - payment?.creatorAddress - ); - }} - > - {formatAddress(payment?.creatorAddress)} - - - - - - copy address - - } - placement="bottom" - arrow - sx={{ fontSize: "24" }} - slotProps={{ - tooltip: { - sx: { - color: "#ffffff", - backgroundColor: "#444444", - }, - }, - arrow: { - sx: { - color: "#444444", - }, - }, - }} - > - { - navigator.clipboard.writeText(payment?.recipient); - }} - > - {formatAddress(payment?.recipient)} - - - - - - {payment?.amount} - - {formatTimestamp(payment?.timestamp)} - - ))} - -
-
+ + + + Sender + Reciver + Amount + Time + + + + + {payments.map((payment, index) => ( + + + + copy address + + } + placement="bottom" + arrow + sx={{ fontSize: '24' }} + slotProps={{ + tooltip: { + sx: { + color: theme.palette.text.primary, + backgroundColor: + theme.palette.background.default, + }, + }, + arrow: { + sx: { + color: theme.palette.text.primary, + }, + }, + }} + > + { + navigator.clipboard.writeText( + payment?.creatorAddress + ); + }} + > + {formatAddress(payment?.creatorAddress)} + + + + + + + copy address + + } + placement="bottom" + arrow + sx={{ fontSize: '24' }} + slotProps={{ + tooltip: { + sx: { + color: theme.palette.text.primary, + backgroundColor: + theme.palette.background.default, + }, + }, + arrow: { + sx: { + color: theme.palette.text.primary, + }, + }, + }} + > + { + navigator.clipboard.writeText(payment?.recipient); + }} + > + {formatAddress(payment?.recipient)} + + + + {payment?.amount} + + {formatTimestamp(payment?.timestamp)} + + + ))} + +
+ )} -
diff --git a/src/components/WrapperUserAction.tsx b/src/components/WrapperUserAction.tsx index c074c16..07b5d45 100644 --- a/src/components/WrapperUserAction.tsx +++ b/src/components/WrapperUserAction.tsx @@ -1,10 +1,20 @@ -import React, { useCallback, useContext, useEffect, useState } from 'react'; -import { Popover, Button, Box, CircularProgress } from '@mui/material'; +import { useCallback, useContext, useEffect, useState } from 'react'; +import { + Popover, + Button, + Box, + CircularProgress, + useTheme, +} from '@mui/material'; import { executeEvent } from '../utils/events'; import { MyContext } from '../App'; +import { useAtom } from 'jotai'; +import { isRunningPublicNodeAtom } from '../atoms/global'; export const WrapperUserAction = ({ children, address, name, disabled }) => { - const {isRunningPublicNode} = useContext(MyContext) + const theme = useTheme(); + const [isRunningPublicNode] = useAtom(isRunningPublicNodeAtom); + const [anchorEl, setAnchorEl] = useState(null); // Handle child element click to open Popover @@ -22,8 +32,8 @@ export const WrapperUserAction = ({ children, address, name, disabled }) => { const open = Boolean(anchorEl); const id = open ? address || name : undefined; - if(disabled){ - return children + if (disabled) { + return children; } return ( @@ -31,16 +41,16 @@ export const WrapperUserAction = ({ children, address, name, disabled }) => { {/* Render the child without altering dimensions */} @@ -49,159 +59,151 @@ export const WrapperUserAction = ({ children, address, name, disabled }) => { {/* Popover */} {open && ( - event.stopPropagation(), // Stop propagation inside popover - }, - }} - > - - {/* Option 1: Message */} - - - {/* Option 2: Send QORT */} - - - + + {/* Option 2: Send QORT */} + + + + - {!isRunningPublicNode && ( - - - )} - - + {!isRunningPublicNode && ( + + )} + + )} ); }; +const BlockUser = ({ address, name, handleClose }) => { + const [isAlreadyBlocked, setIsAlreadyBlocked] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const { isUserBlocked, addToBlockList, removeBlockFromList } = + useContext(MyContext); + const theme = useTheme(); -const BlockUser = ({address, name, handleClose})=> { - const [isAlreadyBlocked, setIsAlreadyBlocked] = useState(null) - const [isLoading, setIsLoading] = useState(false) - const {isUserBlocked, - addToBlockList, - removeBlockFromList} = useContext(MyContext) - -useEffect(()=> { - if(!address) return - setIsAlreadyBlocked(isUserBlocked(address, name)) -}, [address, setIsAlreadyBlocked, isUserBlocked, name]) + useEffect(() => { + if (!address) return; + setIsAlreadyBlocked(isUserBlocked(address, name)); + }, [address, setIsAlreadyBlocked, isUserBlocked, name]); return ( - ) -} \ No newline at end of file + }} + > + {(isAlreadyBlocked === null || isLoading) && ( + + )} + {isAlreadyBlocked && 'Unblock name'} + {isAlreadyBlocked === false && 'Block name'} + + ); +}; diff --git a/src/constants/forum.ts b/src/constants/forum.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/hooks/useHandlePaymentNotification.tsx b/src/hooks/useHandlePaymentNotification.tsx index b5df816..9ca9ff5 100644 --- a/src/hooks/useHandlePaymentNotification.tsx +++ b/src/hooks/useHandlePaymentNotification.tsx @@ -1,132 +1,138 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { getBaseApiReact } from '../App'; import { getData, storeData } from '../utils/chromeStorage'; import { checkDifference, getNameInfoForOthers } from '../background'; -import { useRecoilState } from 'recoil'; import { lastPaymentSeenTimestampAtom } from '../atoms/global'; import { subscribeToEvent, unsubscribeFromEvent } from '../utils/events'; +import { useAtom } from 'jotai'; export const useHandlePaymentNotification = (address) => { - const [latestTx, setLatestTx] = useState(null); + const [latestTx, setLatestTx] = useState(null); - const nameAddressOfSender = useRef({}) - const isFetchingName = useRef({}) - - - const [lastEnteredTimestampPayment, setLastEnteredTimestampPayment] = - useRecoilState(lastPaymentSeenTimestampAtom); - - useEffect(() => { - if (lastEnteredTimestampPayment && address) { - storeData(`last-seen-payment-${address}`, Date.now()).catch((error) => { - console.error(error); - }); - } - }, [lastEnteredTimestampPayment, address]); - - const getNameOrAddressOfSender = useCallback(async(senderAddress)=> { - if(isFetchingName.current[senderAddress]) return senderAddress - try { - isFetchingName.current[senderAddress] = true - const res = await getNameInfoForOthers(senderAddress) - nameAddressOfSender.current[senderAddress] = res || senderAddress - } catch (error) { - console.error(error) - } finally { - isFetchingName.current[senderAddress] = false - } - - }, []) + const nameAddressOfSender = useRef({}); + const isFetchingName = useRef({}); - const getNameOrAddressOfSenderMiddle = useCallback(async(senderAddress)=> { - getNameOrAddressOfSender(senderAddress) - return senderAddress - - }, [getNameOrAddressOfSender]) - - const hasNewPayment = useMemo(() => { - if (!latestTx) return false; - if (!checkDifference(latestTx?.timestamp)) return false; - if ( - !lastEnteredTimestampPayment || - lastEnteredTimestampPayment < latestTx?.timestamp - ) - return true; - - return false; - }, [lastEnteredTimestampPayment, latestTx]); - - const getLastSeenData = useCallback(async () => { - try { - if (!address) return; - const key = `last-seen-payment-${address}`; - - const res = await getData(key).catch(() => null); - if (res) { - setLastEnteredTimestampPayment(res); - } - - const response = await fetch( - `${getBaseApiReact()}/transactions/search?txType=PAYMENT&address=${address}&confirmationStatus=CONFIRMED&limit=5&reverse=true` - ); - - const responseData = await response.json(); - - const latestTx = responseData.filter( - (tx) => tx?.creatorAddress !== address && tx?.recipient === address - )[0]; - if (!latestTx) { - return; // continue to the next group - } - - setLatestTx(latestTx); - } catch (error) { + const [lastEnteredTimestampPayment, setLastEnteredTimestampPayment] = useAtom( + lastPaymentSeenTimestampAtom + ); + + useEffect(() => { + if (lastEnteredTimestampPayment && address) { + storeData(`last-seen-payment-${address}`, Date.now()).catch((error) => { console.error(error); - } - }, [address, setLastEnteredTimestampPayment]); - - - useEffect(() => { - getLastSeenData(); - // Handler function for incoming messages - const messageHandler = (event) => { - if (event.origin !== window.location.origin) { - return; - } - const message = event.data; - if (message?.action === "SET_PAYMENT_ANNOUNCEMENT" && message?.payload) { - setLatestTx(message.payload); - } - }; - - // Attach the event listener - window.addEventListener("message", messageHandler); - - // Clean up the event listener on component unmount - return () => { - window.removeEventListener("message", messageHandler); - }; - }, [getLastSeenData]); + }); + } + }, [lastEnteredTimestampPayment, address]); - const setLastEnteredTimestampPaymentEventFunc = useCallback( - (e) => { - setLastEnteredTimestampPayment(Date.now) - }, - [setLastEnteredTimestampPayment] + const getNameOrAddressOfSender = useCallback(async (senderAddress) => { + if (isFetchingName.current[senderAddress]) return senderAddress; + try { + isFetchingName.current[senderAddress] = true; + const res = await getNameInfoForOthers(senderAddress); + nameAddressOfSender.current[senderAddress] = res || senderAddress; + } catch (error) { + console.error(error); + } finally { + isFetchingName.current[senderAddress] = false; + } + }, []); + + const getNameOrAddressOfSenderMiddle = useCallback( + async (senderAddress) => { + getNameOrAddressOfSender(senderAddress); + return senderAddress; + }, + [getNameOrAddressOfSender] + ); + + const hasNewPayment = useMemo(() => { + if (!latestTx) return false; + if (!checkDifference(latestTx?.timestamp)) return false; + if ( + !lastEnteredTimestampPayment || + lastEnteredTimestampPayment < latestTx?.timestamp + ) + return true; + + return false; + }, [lastEnteredTimestampPayment, latestTx]); + + const getLastSeenData = useCallback(async () => { + try { + if (!address) return; + const key = `last-seen-payment-${address}`; + + const res = await getData(key).catch(() => null); + if (res) { + setLastEnteredTimestampPayment(res); + } + + const response = await fetch( + `${getBaseApiReact()}/transactions/search?txType=PAYMENT&address=${address}&confirmationStatus=CONFIRMED&limit=5&reverse=true` ); - - useEffect(() => { - subscribeToEvent("setLastEnteredTimestampPaymentEvent", setLastEnteredTimestampPaymentEventFunc); - - return () => { - unsubscribeFromEvent("setLastEnteredTimestampPaymentEvent", setLastEnteredTimestampPaymentEventFunc); - }; - }, [setLastEnteredTimestampPaymentEventFunc]); + + const responseData = await response.json(); + + const latestTx = responseData.filter( + (tx) => tx?.creatorAddress !== address && tx?.recipient === address + )[0]; + if (!latestTx) { + return; // continue to the next group + } + + setLatestTx(latestTx); + } catch (error) { + console.error(error); + } + }, [address, setLastEnteredTimestampPayment]); + + useEffect(() => { + getLastSeenData(); + // Handler function for incoming messages + const messageHandler = (event) => { + if (event.origin !== window.location.origin) { + return; + } + const message = event.data; + if (message?.action === 'SET_PAYMENT_ANNOUNCEMENT' && message?.payload) { + setLatestTx(message.payload); + } + }; + + // Attach the event listener + window.addEventListener('message', messageHandler); + + // Clean up the event listener on component unmount + return () => { + window.removeEventListener('message', messageHandler); + }; + }, [getLastSeenData]); + + const setLastEnteredTimestampPaymentEventFunc = useCallback( + (e) => { + setLastEnteredTimestampPayment(Date.now); + }, + [setLastEnteredTimestampPayment] + ); + + useEffect(() => { + subscribeToEvent( + 'setLastEnteredTimestampPaymentEvent', + setLastEnteredTimestampPaymentEventFunc + ); + + return () => { + unsubscribeFromEvent( + 'setLastEnteredTimestampPaymentEvent', + setLastEnteredTimestampPaymentEventFunc + ); + }; + }, [setLastEnteredTimestampPaymentEventFunc]); return { latestTx, getNameOrAddressOfSenderMiddle, hasNewPayment, setLastEnteredTimestampPayment, - nameAddressOfSender - } -} + nameAddressOfSender, + }; +}; diff --git a/src/hooks/useNameSearch.tsx b/src/hooks/useNameSearch.tsx new file mode 100644 index 0000000..6bc86ec --- /dev/null +++ b/src/hooks/useNameSearch.tsx @@ -0,0 +1,55 @@ +import { useCallback, useEffect, useState } from 'react'; +import { getBaseApiReact } from '../App'; + +interface NameListItem { + name: string; + address: string; +} +export const useNameSearch = (value: string, limit = 20) => { + const [nameList, setNameList] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const checkIfNameExisits = useCallback( + async (name: string, listLimit: number) => { + try { + if (!name) { + setNameList([]); + return; + } + + const res = await fetch( + `${getBaseApiReact()}/names/search?query=${name}&prefix=true&limit=${listLimit}` + ); + const data = await res.json(); + setNameList( + data?.map((item: any) => { + return { + name: item.name, + address: item.owner, + }; + }) + ); + } catch (error) { + console.error(error); + } finally { + setIsLoading(false); + } + }, + [] + ); + // Debounce logic + useEffect(() => { + setIsLoading(true); + const handler = setTimeout(() => { + checkIfNameExisits(value, limit); + }, 500); + + // Cleanup timeout if searchValue changes before the timeout completes + return () => { + clearTimeout(handler); + }; + }, [value, limit, checkIfNameExisits]); + return { + isLoading, + results: nameList, + }; +}; diff --git a/src/main.tsx b/src/main.tsx index 22bba47..0a35316 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,81 +1,19 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import App from './App.tsx' -import './index.css' -import "./messaging/messagesToBackground"; -import { ThemeProvider, createTheme } from '@mui/material/styles'; -import { CssBaseline } from '@mui/material'; +import { createRoot } from 'react-dom/client'; +import App from './App.tsx'; +import '../src/styles/index.css'; +import './messaging/messagesToBackground'; import { MessageQueueProvider } from './MessageQueueContext.tsx'; -import { RecoilRoot } from 'recoil'; -const theme = createTheme({ - palette: { - primary: { - main: '#232428', // Primary color (e.g., used for buttons, headers, etc.) - }, - secondary: { - main: '#232428', // Secondary color - }, - background: { - default: '#27282c', // Default background color - paper: '#1d1d1d', // Paper component background (for dropdowns, dialogs, etc.) - }, - text: { - primary: '#ffffff', // White as the primary text color - secondary: '#b0b0b0', // Light gray for secondary text - disabled: '#808080', // Gray for disabled text - }, - action: { - // disabledBackground: 'set color of background here', - disabled: 'rgb(255 255 255 / 70%)' - } - }, - typography: { - fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif', // Font family - h1: { - color: '#ffffff', // White color for h1 elements - }, - h2: { - color: '#ffffff', // White color for h2 elements - }, - body1: { - color: '#ffffff', // Default body text color - }, - body2: { - color: '#b0b0b0', // Lighter text for body2, often used for secondary text - }, - }, - components: { - MuiOutlinedInput: { - styleOverrides: { - root: { - ".MuiOutlinedInput-notchedOutline": { - borderColor: "white", // ⚪ Default outline color - }, - }, - }, - }, - MuiSelect: { - styleOverrides: { - icon: { - color: "white", // ✅ Caret (dropdown arrow) color - }, - }, - }, - }, -}); +import { ThemeProvider } from './components/Theme/ThemeContext.tsx'; +import { CssBaseline } from '@mui/material'; +import '../i18n'; -export default theme; - - -ReactDOM.createRoot(document.getElementById('root')!).render( +createRoot(document.getElementById('root')!).render( <> - + - - - - - + + + - , -) + +); diff --git a/src/qortalRequests copy.ts b/src/qortalRequests copy.ts deleted file mode 100644 index 6807fe9..0000000 --- a/src/qortalRequests copy.ts +++ /dev/null @@ -1,427 +0,0 @@ -import { addForeignServer, addListItems, createPoll, decryptData, deleteListItems, deployAt, encryptData, getCrossChainServerInfo, getDaySummary, getForeignFee, getListItems, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getWalletBalance, joinGroup, publishMultipleQDNResources, publishQDNResource, removeForeignServer, saveFile, sendChatMessage, sendCoin, setCurrentForeignServer, updateForeignFee, voteOnPoll } from "./qortalRequests/get"; - - - -// Promisify chrome.storage.local.get -function getLocalStorage(key) { - return new Promise((resolve, reject) => { - chrome.storage.local.get([key], function (result) { - if (chrome.runtime.lastError) { - return reject(chrome.runtime.lastError); - } - resolve(result[key]); - }); - }); - } - - // Promisify chrome.storage.local.set - function setLocalStorage(data) { - return new Promise((resolve, reject) => { - chrome.storage.local.set(data, function () { - if (chrome.runtime.lastError) { - return reject(chrome.runtime.lastError); - } - resolve(); - }); - }); - } - - - export async function setPermission(key, value) { - try { - // Get the existing qortalRequestPermissions object - const qortalRequestPermissions = (await getLocalStorage('qortalRequestPermissions')) || {}; - - // Update the permission - qortalRequestPermissions[key] = value; - - // Save the updated object back to storage - await setLocalStorage({ qortalRequestPermissions }); - - } catch (error) { - console.error('Error setting permission:', error); - } - } - - export async function getPermission(key) { - try { - // Get the qortalRequestPermissions object from storage - const qortalRequestPermissions = (await getLocalStorage('qortalRequestPermissions')) || {}; - - // Return the value for the given key, or null if it doesn't exist - return qortalRequestPermissions[key] || null; - } catch (error) { - console.error('Error getting permission:', error); - return null; - } - } - - - // TODO: GET_FRIENDS_LIST - // NOT SURE IF TO IMPLEMENT: LINK_TO_QDN_RESOURCE, QDN_RESOURCE_DISPLAYED, SET_TAB_NOTIFICATIONS - -chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { - if (request) { - const isFromExtension = request?.isExtension - switch (request.action) { - case "GET_USER_ACCOUNT": { - getUserAccount() - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: "Unable to get user account" }); - }); - - break; - } - case "ENCRYPT_DATA": { - const data = request.payload; - - encryptData(data, sender) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - }); - - break; - } - case "DECRYPT_DATA": { - const data = request.payload; - - decryptData(data) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - }); - - break; - } - case "GET_LIST_ITEMS": { - const data = request.payload; - - getListItems(data, isFromExtension) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - }); - - break; - } - case "ADD_LIST_ITEMS": { - const data = request.payload; - - addListItems(data, isFromExtension) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - }); - - break; - } - case "DELETE_LIST_ITEM": { - const data = request.payload; - - deleteListItems(data, isFromExtension) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - }); - - break; - } - case "PUBLISH_QDN_RESOURCE": { - const data = request.payload; - - publishQDNResource(data, sender, isFromExtension) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - }); - - break; - } - case "PUBLISH_MULTIPLE_QDN_RESOURCES": { - const data = request.payload; - - publishMultipleQDNResources(data, sender, isFromExtension) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - }); - - break; - } - case "VOTE_ON_POLL": { - const data = request.payload; - - voteOnPoll(data, isFromExtension) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - }); - - break; - } - case "CREATE_POLL": { - const data = request.payload; - - createPoll(data, isFromExtension) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - }); - - break; - } - case "SEND_CHAT_MESSAGE": { - const data = request.payload; - sendChatMessage(data, isFromExtension) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - }); - - break; - } - case "JOIN_GROUP": { - const data = request.payload; - - joinGroup(data, isFromExtension) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - }); - - break; - } - case "SAVE_FILE": { - const data = request.payload; - - saveFile(data, sender, isFromExtension) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - }); - - break; - } - case "DEPLOY_AT": { - const data = request.payload; - - deployAt(data, isFromExtension) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - }); - - break; - } - case "GET_USER_WALLET": { - const data = request.payload; - - getUserWallet(data, isFromExtension) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - }); - - break; - } - case "GET_WALLET_BALANCE": { - const data = request.payload; - - getWalletBalance(data, false, isFromExtension) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - }); - - break; - } - - case "GET_USER_WALLET_INFO": { - const data = request.payload; - - getUserWalletInfo(data, isFromExtension) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - }); - - break; - } - case "GET_CROSSCHAIN_SERVER_INFO": { - const data = request.payload; - - getCrossChainServerInfo(data) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - }); - - break; - } - case "GET_TX_ACTIVITY_SUMMARY": { - const data = request.payload; - - getTxActivitySummary(data) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - }); - - break; - } - - case "GET_FOREIGN_FEE": { - const data = request.payload; - - getForeignFee(data) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - }); - - break; - } - - case "UPDATE_FOREIGN_FEE": { - const data = request.payload; - - updateForeignFee(data) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - }); - - break; - } - - case "GET_SERVER_CONNECTION_HISTORY": { - const data = request.payload; - - getServerConnectionHistory(data) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - }); - - break; - } - - case "SET_CURRENT_FOREIGN_SERVER": { - const data = request.payload; - - setCurrentForeignServer(data) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - }); - - break; - } - - case "ADD_FOREIGN_SERVER": { - const data = request.payload; - - addForeignServer(data) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - }); - - break; - } - - case "REMOVE_FOREIGN_SERVER": { - const data = request.payload; - - removeForeignServer(data) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - }); - - break; - } - - case "GET_DAY_SUMMARY": { - const data = request.payload; - - getDaySummary(data) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - }); - - break; - } - - case "SEND_COIN": { - const data = request.payload; - - sendCoin(data, isFromExtension) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - }); - - break; - } - } - } - return true; -}); diff --git a/src/qortalRequests.ts b/src/qortalRequests.ts index 37e0126..a3bfd8b 100644 --- a/src/qortalRequests.ts +++ b/src/qortalRequests.ts @@ -1,12 +1,75 @@ -import { gateways, getApiKeyFromStorage } from "./background"; -import { listOfAllQortalRequests } from "./components/Apps/useQortalMessageListener"; -import { addForeignServer, addGroupAdminRequest, addListItems, adminAction, banFromGroupRequest, cancelGroupBanRequest, cancelGroupInviteRequest, cancelSellOrder, createAndCopyEmbedLink, createBuyOrder, createGroupRequest, createPoll, createSellOrder, decryptAESGCMRequest, decryptData, decryptDataWithSharingKey, decryptQortalGroupData, deleteHostedData, deleteListItems, deployAt, encryptData, encryptDataWithSharingKey, encryptQortalGroupData, getCrossChainServerInfo, getDaySummary, getNodeInfo, getNodeStatus, getForeignFee, getHostedData, getListItems, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getUserWalletTransactions, getWalletBalance, inviteToGroupRequest, joinGroup, kickFromGroupRequest, leaveGroupRequest, openNewTab, publishMultipleQDNResources, publishQDNResource, registerNameRequest, removeForeignServer, removeGroupAdminRequest, saveFile, sendChatMessage, sendCoin, setCurrentForeignServer, signTransaction, updateForeignFee, updateNameRequest, voteOnPoll, getArrrSyncStatus, updateGroupRequest, buyNameRequest, sellNameRequest, cancelSellNameRequest, multiPaymentWithPrivateData, transferAssetRequest } from "./qortalRequests/get"; -import { getData, storeData } from "./utils/chromeStorage"; -import { executeEvent } from "./utils/events"; +import { gateways, getApiKeyFromStorage } from './background'; +import { listOfAllQortalRequests } from './components/Apps/useQortalMessageListener'; +import { + addForeignServer, + addGroupAdminRequest, + addListItems, + adminAction, + banFromGroupRequest, + cancelGroupBanRequest, + cancelGroupInviteRequest, + cancelSellOrder, + createAndCopyEmbedLink, + createBuyOrder, + createGroupRequest, + createPoll, + createSellOrder, + decryptAESGCMRequest, + decryptData, + decryptDataWithSharingKey, + decryptQortalGroupData, + deleteHostedData, + deleteListItems, + deployAt, + encryptData, + encryptDataWithSharingKey, + encryptQortalGroupData, + getCrossChainServerInfo, + getDaySummary, + getNodeInfo, + getNodeStatus, + getForeignFee, + getHostedData, + getListItems, + getServerConnectionHistory, + getTxActivitySummary, + getUserAccount, + getUserWallet, + getUserWalletInfo, + getUserWalletTransactions, + getWalletBalance, + inviteToGroupRequest, + joinGroup, + kickFromGroupRequest, + leaveGroupRequest, + openNewTab, + publishMultipleQDNResources, + publishQDNResource, + registerNameRequest, + removeForeignServer, + removeGroupAdminRequest, + saveFile, + sendChatMessage, + sendCoin, + setCurrentForeignServer, + signTransaction, + updateForeignFee, + updateNameRequest, + voteOnPoll, + getArrrSyncStatus, + updateGroupRequest, + buyNameRequest, + sellNameRequest, + cancelSellNameRequest, + multiPaymentWithPrivateData, + transferAssetRequest, +} from './qortalRequests/get'; +import { getData, storeData } from './utils/chromeStorage'; +import { executeEvent } from './utils/events'; function getLocalStorage(key) { return getData(key).catch((error) => { - console.error("Error retrieving data:", error); + console.error('Error retrieving data:', error); throw error; }); } @@ -14,1350 +77,1826 @@ function getLocalStorage(key) { // Promisify setting data in localStorage function setLocalStorage(key, data) { return storeData(key, data).catch((error) => { - console.error("Error saving data:", error); + console.error('Error saving data:', error); throw error; }); } -export const isRunningGateway = async ()=> { - let isGateway = true; - const apiKey = await getApiKeyFromStorage(); - if (apiKey && (apiKey?.url && !gateways.some(gateway => apiKey?.url?.includes(gateway)))) { - isGateway = false; - } +export const isRunningGateway = async () => { + let isGateway = true; + const apiKey = await getApiKeyFromStorage(); + if ( + apiKey && + apiKey?.url && + !gateways.some((gateway) => apiKey?.url?.includes(gateway)) + ) { + isGateway = false; + } - return isGateway + return isGateway; +}; + +export async function setPermission(key, value) { + try { + // Get the existing qortalRequestPermissions object + const qortalRequestPermissions = + (await getLocalStorage('qortalRequestPermissions')) || {}; + + // Update the permission + qortalRequestPermissions[key] = value; + + // Save the updated object back to storage + await setLocalStorage('qortalRequestPermissions', qortalRequestPermissions); + } catch (error) { + console.error('Error setting permission:', error); + } } - - export async function setPermission(key, value) { - try { - // Get the existing qortalRequestPermissions object - const qortalRequestPermissions = (await getLocalStorage('qortalRequestPermissions')) || {}; - - // Update the permission - qortalRequestPermissions[key] = value; - - // Save the updated object back to storage - await setLocalStorage('qortalRequestPermissions', qortalRequestPermissions ); - - } catch (error) { - console.error('Error setting permission:', error); - } +export async function getPermission(key) { + try { + // Get the qortalRequestPermissions object from storage + const qortalRequestPermissions = + (await getLocalStorage('qortalRequestPermissions')) || {}; + + // Return the value for the given key, or null if it doesn't exist + return qortalRequestPermissions[key] || null; + } catch (error) { + console.error('Error getting permission:', error); + return null; } +} - export async function getPermission(key) { - try { - // Get the qortalRequestPermissions object from storage - const qortalRequestPermissions = (await getLocalStorage('qortalRequestPermissions')) || {}; - - // Return the value for the given key, or null if it doesn't exist - return qortalRequestPermissions[key] || null; - } catch (error) { - console.error('Error getting permission:', error); - return null; - } - } +// TODO: GET_FRIENDS_LIST +// NOT SURE IF TO IMPLEMENT: LINK_TO_QDN_RESOURCE, QDN_RESOURCE_DISPLAYED, SET_TAB_NOTIFICATIONS +function setupMessageListenerQortalRequest() { + window.addEventListener('message', async (event) => { + const request = event.data; - // TODO: GET_FRIENDS_LIST - // NOT SURE IF TO IMPLEMENT: LINK_TO_QDN_RESOURCE, QDN_RESOURCE_DISPLAYED, SET_TAB_NOTIFICATIONS + // Ensure the message is from a trusted source + const isFromExtension = request?.isExtension; + const appInfo = request?.appInfo; + const skipAuth = request?.skipAuth || false; + if (request?.type !== 'backgroundMessage') return; // Only process messages of type 'backgroundMessage' - function setupMessageListenerQortalRequest() { - window.addEventListener("message", async (event) => { - const request = event.data; - - // Ensure the message is from a trusted source - const isFromExtension = request?.isExtension; - const appInfo = request?.appInfo; - const skipAuth = request?.skipAuth || false - if (request?.type !== "backgroundMessage") return; // Only process messages of type 'backgroundMessage' - - - // Handle actions based on the `request.action` value - switch (request.action) { - case "GET_USER_ACCOUNT": { - try { - const res = await getUserAccount({isFromExtension, appInfo, skipAuth}); - event.source.postMessage({ + // Handle actions based on the `request.action` value + switch (request.action) { + case 'GET_USER_ACCOUNT': { + try { + const res = await getUserAccount({ + isFromExtension, + appInfo, + skipAuth, + }); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, - error: "Unable to get user account", - type: "backgroundMessageResponse", - }, event.origin); - } - break; + error: 'Unable to get user account', + type: 'backgroundMessageResponse', + }, + event.origin + ); } - - case "ENCRYPT_DATA": { - try { - const res = await encryptData(request.payload, event.source); - event.source.postMessage({ + break; + } + + case 'ENCRYPT_DATA': { + try { + const res = await encryptData(request.payload, event.source); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error?.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "ENCRYPT_QORTAL_GROUP_DATA": { - try { - const res = await encryptQortalGroupData(request.payload, event.source); - event.source.postMessage({ + case 'ENCRYPT_QORTAL_GROUP_DATA': { + try { + const res = await encryptQortalGroupData( + request.payload, + event.source + ); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error?.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "DECRYPT_QORTAL_GROUP_DATA": { - try { - const res = await decryptQortalGroupData(request.payload, event.source); - event.source.postMessage({ + case 'DECRYPT_QORTAL_GROUP_DATA': { + try { + const res = await decryptQortalGroupData( + request.payload, + event.source + ); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error?.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } - - case "DECRYPT_DATA": { - try { - const res = await decryptData(request.payload); - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } - - case "GET_LIST_ITEMS": { - try { - const res = await getListItems(request.payload, isFromExtension); - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } - - case "ADD_LIST_ITEMS": { - try { - const res = await addListItems(request.payload, isFromExtension); - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } - - case "DELETE_LIST_ITEM": { - try { - const res = await deleteListItems(request.payload, isFromExtension); - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } - - case "PUBLISH_QDN_RESOURCE": { - try { - const res = await publishQDNResource(request.payload, event.source, isFromExtension); - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } - - case "PUBLISH_MULTIPLE_QDN_RESOURCES": { - try { - const res = await publishMultipleQDNResources(request.payload, event.source, isFromExtension); - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } - - case "VOTE_ON_POLL": { - try { - const res = await voteOnPoll(request.payload, isFromExtension); - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } - - case "CREATE_POLL": { - try { - const res = await createPoll(request.payload, isFromExtension); - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } - - case "SEND_CHAT_MESSAGE": { - try { - const res = await sendChatMessage(request.payload, isFromExtension, appInfo); - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } - - case "JOIN_GROUP": { - try { - const res = await joinGroup(request.payload, isFromExtension); - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "DEPLOY_AT": { - try { - const res = await deployAt(request.payload, isFromExtension); - event.source.postMessage({ + case 'DECRYPT_DATA': { + try { + const res = await decryptData(request.payload); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } - - case "GET_USER_WALLET": { - try { - const res = await getUserWallet(request.payload, isFromExtension, appInfo); - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } - - case "GET_WALLET_BALANCE": { - try { - const res = await getWalletBalance(request.payload, false, isFromExtension, appInfo); - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } - - case "GET_USER_WALLET_TRANSACTIONS": { - try { - const res = await getUserWalletTransactions(request.payload, isFromExtension, appInfo); - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } - - case "GET_USER_WALLET_INFO": { - try { - const res = await getUserWalletInfo(request.payload, isFromExtension, appInfo); - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } - - case "GET_CROSSCHAIN_SERVER_INFO": { - try { - const res = await getCrossChainServerInfo(request.payload); - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } - - case "GET_TX_ACTIVITY_SUMMARY": { - try { - const res = await getTxActivitySummary(request.payload); - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } - - case "GET_FOREIGN_FEE": { - try { - const res = await getForeignFee(request.payload); - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } - - case "UPDATE_FOREIGN_FEE": { - try { - const res = await updateForeignFee(request.payload); - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } - - case "GET_SERVER_CONNECTION_HISTORY": { - try { - const res = await getServerConnectionHistory(request.payload); - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } - - case "SET_CURRENT_FOREIGN_SERVER": { - try { - const res = await setCurrentForeignServer(request.payload); - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } - - case "ADD_FOREIGN_SERVER": { - try { - const res = await addForeignServer(request.payload); - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } - - case "REMOVE_FOREIGN_SERVER": { - try { - const res = await removeForeignServer(request.payload); - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } - - case "GET_DAY_SUMMARY": { - try { - const res = await getDaySummary(request.payload); - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } - - case "GET_NODE_INFO": { - try { - const res = await getNodeInfo(request.payload); - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "GET_NODE_STATUS": { - try { - const res = await getNodeStatus(request.payload); - event.source.postMessage({ + case 'GET_LIST_ITEMS': { + try { + const res = await getListItems(request.payload, isFromExtension); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "SEND_COIN": { - try { - const res = await sendCoin(request.payload, isFromExtension); - event.source.postMessage({ + case 'ADD_LIST_ITEMS': { + try { + const res = await addListItems(request.payload, isFromExtension); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "CREATE_TRADE_BUY_ORDER": { - try { - const res = await createBuyOrder(request.payload, isFromExtension); - event.source.postMessage({ + case 'DELETE_LIST_ITEM': { + try { + const res = await deleteListItems(request.payload, isFromExtension); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "CREATE_TRADE_SELL_ORDER": { - try { - const res = await createSellOrder(request.payload, isFromExtension); - event.source.postMessage({ + case 'PUBLISH_QDN_RESOURCE': { + try { + const res = await publishQDNResource( + request.payload, + event.source, + isFromExtension + ); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "CANCEL_TRADE_SELL_ORDER": { - try { - const res = await cancelSellOrder(request.payload, isFromExtension); - event.source.postMessage({ + case 'PUBLISH_MULTIPLE_QDN_RESOURCES': { + try { + const res = await publishMultipleQDNResources( + request.payload, + event.source, + isFromExtension + ); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "IS_USING_PUBLIC_NODE": { - try { - let isGateway = await isRunningGateway() - event.source.postMessage({ + case 'VOTE_ON_POLL': { + try { + const res = await voteOnPoll(request.payload, isFromExtension); + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + payload: res, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + error: error.message, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } + break; + } + + case 'CREATE_POLL': { + try { + const res = await createPoll(request.payload, isFromExtension); + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + payload: res, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + error: error.message, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } + break; + } + + case 'SEND_CHAT_MESSAGE': { + try { + const res = await sendChatMessage( + request.payload, + isFromExtension, + appInfo + ); + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + payload: res, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + error: error.message, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } + break; + } + + case 'JOIN_GROUP': { + try { + const res = await joinGroup(request.payload, isFromExtension); + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + payload: res, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + error: error.message, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } + break; + } + + case 'DEPLOY_AT': { + try { + const res = await deployAt(request.payload, isFromExtension); + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + payload: res, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + error: error.message, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } + break; + } + + case 'GET_USER_WALLET': { + try { + const res = await getUserWallet( + request.payload, + isFromExtension, + appInfo + ); + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + payload: res, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + error: error.message, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } + break; + } + + case 'GET_WALLET_BALANCE': { + try { + const res = await getWalletBalance( + request.payload, + false, + isFromExtension, + appInfo + ); + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + payload: res, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + error: error.message, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } + break; + } + + case 'GET_USER_WALLET_TRANSACTIONS': { + try { + const res = await getUserWalletTransactions( + request.payload, + isFromExtension, + appInfo + ); + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + payload: res, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + error: error.message, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } + break; + } + + case 'GET_USER_WALLET_INFO': { + try { + const res = await getUserWalletInfo( + request.payload, + isFromExtension, + appInfo + ); + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + payload: res, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + error: error.message, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } + break; + } + + case 'GET_CROSSCHAIN_SERVER_INFO': { + try { + const res = await getCrossChainServerInfo(request.payload); + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + payload: res, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + error: error.message, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } + break; + } + + case 'GET_TX_ACTIVITY_SUMMARY': { + try { + const res = await getTxActivitySummary(request.payload); + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + payload: res, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + error: error.message, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } + break; + } + + case 'GET_FOREIGN_FEE': { + try { + const res = await getForeignFee(request.payload); + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + payload: res, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + error: error.message, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } + break; + } + + case 'UPDATE_FOREIGN_FEE': { + try { + const res = await updateForeignFee(request.payload); + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + payload: res, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + error: error.message, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } + break; + } + + case 'GET_SERVER_CONNECTION_HISTORY': { + try { + const res = await getServerConnectionHistory(request.payload); + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + payload: res, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + error: error.message, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } + break; + } + + case 'SET_CURRENT_FOREIGN_SERVER': { + try { + const res = await setCurrentForeignServer(request.payload); + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + payload: res, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + error: error.message, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } + break; + } + + case 'ADD_FOREIGN_SERVER': { + try { + const res = await addForeignServer(request.payload); + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + payload: res, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + error: error.message, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } + break; + } + + case 'REMOVE_FOREIGN_SERVER': { + try { + const res = await removeForeignServer(request.payload); + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + payload: res, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + error: error.message, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } + break; + } + + case 'GET_DAY_SUMMARY': { + try { + const res = await getDaySummary(request.payload); + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + payload: res, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + error: error.message, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } + break; + } + + case 'GET_NODE_INFO': { + try { + const res = await getNodeInfo(request.payload); + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + payload: res, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + error: error.message, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } + break; + } + + case 'GET_NODE_STATUS': { + try { + const res = await getNodeStatus(request.payload); + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + payload: res, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + error: error.message, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } + break; + } + + case 'SEND_COIN': { + try { + const res = await sendCoin(request.payload, isFromExtension); + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + payload: res, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + error: error.message, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } + break; + } + + case 'CREATE_TRADE_BUY_ORDER': { + try { + const res = await createBuyOrder(request.payload, isFromExtension); + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + payload: res, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + error: error.message, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } + break; + } + + case 'CREATE_TRADE_SELL_ORDER': { + try { + const res = await createSellOrder(request.payload, isFromExtension); + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + payload: res, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + error: error.message, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } + break; + } + + case 'CANCEL_TRADE_SELL_ORDER': { + try { + const res = await cancelSellOrder(request.payload, isFromExtension); + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + payload: res, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + error: error.message, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } + break; + } + + case 'IS_USING_PUBLIC_NODE': { + try { + let isGateway = await isRunningGateway(); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: isGateway, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error?.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "ADMIN_ACTION": { - try { - const res = await adminAction(request.payload, isFromExtension) - event.source.postMessage({ + case 'ADMIN_ACTION': { + try { + const res = await adminAction(request.payload, isFromExtension); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error?.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "SIGN_TRANSACTION": { - try { - const res = await signTransaction(request.payload, isFromExtension) - event.source.postMessage({ + case 'SIGN_TRANSACTION': { + try { + const res = await signTransaction(request.payload, isFromExtension); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error?.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "OPEN_NEW_TAB": { - try { - const res = await openNewTab(request.payload, isFromExtension) - event.source.postMessage({ + case 'OPEN_NEW_TAB': { + try { + const res = await openNewTab(request.payload, isFromExtension); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error?.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "CREATE_AND_COPY_EMBED_LINK": { - try { - const res = await createAndCopyEmbedLink(request.payload, isFromExtension) - event.source.postMessage({ + case 'CREATE_AND_COPY_EMBED_LINK': { + try { + const res = await createAndCopyEmbedLink( + request.payload, + isFromExtension + ); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error?.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "ENCRYPT_DATA_WITH_SHARING_KEY": { - try { - const res = await encryptDataWithSharingKey(request.payload, isFromExtension) - event.source.postMessage({ + case 'ENCRYPT_DATA_WITH_SHARING_KEY': { + try { + const res = await encryptDataWithSharingKey( + request.payload, + isFromExtension + ); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error?.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "DECRYPT_DATA_WITH_SHARING_KEY": { - try { - const res = await decryptDataWithSharingKey(request.payload, isFromExtension) - event.source.postMessage({ + case 'DECRYPT_DATA_WITH_SHARING_KEY': { + try { + const res = await decryptDataWithSharingKey( + request.payload, + isFromExtension + ); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error?.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "DELETE_HOSTED_DATA" : { - try { - const res = await deleteHostedData(request.payload, isFromExtension) - event.source.postMessage({ + case 'DELETE_HOSTED_DATA': { + try { + const res = await deleteHostedData(request.payload, isFromExtension); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error?.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } - case "GET_HOSTED_DATA" : { - try { - const res = await getHostedData(request.payload, isFromExtension) - event.source.postMessage({ + break; + } + case 'GET_HOSTED_DATA': { + try { + const res = await getHostedData(request.payload, isFromExtension); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error?.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "SHOW_ACTIONS" : { - try { - - event.source.postMessage({ + case 'SHOW_ACTIONS': { + try { + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: listOfAllQortalRequests, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error?.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "REGISTER_NAME" : { - try { - const res = await registerNameRequest(request.payload, isFromExtension) - event.source.postMessage({ + case 'REGISTER_NAME': { + try { + const res = await registerNameRequest( + request.payload, + isFromExtension + ); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error?.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "UPDATE_NAME" : { - try { - const res = await updateNameRequest(request.payload, isFromExtension) - event.source.postMessage({ + case 'UPDATE_NAME': { + try { + const res = await updateNameRequest(request.payload, isFromExtension); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error?.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "LEAVE_GROUP" : { - try { - const res = await leaveGroupRequest(request.payload, isFromExtension) - event.source.postMessage({ + case 'LEAVE_GROUP': { + try { + const res = await leaveGroupRequest(request.payload, isFromExtension); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error?.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "INVITE_TO_GROUP" : { - try { - const res = await inviteToGroupRequest(request.payload, isFromExtension) - event.source.postMessage({ + case 'INVITE_TO_GROUP': { + try { + const res = await inviteToGroupRequest( + request.payload, + isFromExtension + ); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error?.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "KICK_FROM_GROUP" : { - try { - const res = await kickFromGroupRequest(request.payload, isFromExtension) - event.source.postMessage({ + case 'KICK_FROM_GROUP': { + try { + const res = await kickFromGroupRequest( + request.payload, + isFromExtension + ); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error?.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "BAN_FROM_GROUP" : { - try { - const res = await banFromGroupRequest(request.payload, isFromExtension) - event.source.postMessage({ + case 'BAN_FROM_GROUP': { + try { + const res = await banFromGroupRequest( + request.payload, + isFromExtension + ); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error?.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "CANCEL_GROUP_BAN" : { - try { - const res = await cancelGroupBanRequest(request.payload, isFromExtension) - event.source.postMessage({ + case 'CANCEL_GROUP_BAN': { + try { + const res = await cancelGroupBanRequest( + request.payload, + isFromExtension + ); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error?.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "ADD_GROUP_ADMIN" : { - try { - const res = await addGroupAdminRequest(request.payload, isFromExtension) + case 'ADD_GROUP_ADMIN': { + try { + const res = await addGroupAdminRequest( + request.payload, + isFromExtension + ); - event.source.postMessage({ + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error?.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "DECRYPT_AESGCM" : { - try { - const res = await decryptAESGCMRequest(request.payload, isFromExtension) - event.source.postMessage({ + case 'DECRYPT_AESGCM': { + try { + const res = await decryptAESGCMRequest( + request.payload, + isFromExtension + ); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error?.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "REMOVE_GROUP_ADMIN" : { - try { - const res = await removeGroupAdminRequest(request.payload, isFromExtension) - event.source.postMessage({ + case 'REMOVE_GROUP_ADMIN': { + try { + const res = await removeGroupAdminRequest( + request.payload, + isFromExtension + ); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error?.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "CANCEL_GROUP_INVITE" : { - try { - const res = await cancelGroupInviteRequest(request.payload, isFromExtension) - event.source.postMessage({ + case 'CANCEL_GROUP_INVITE': { + try { + const res = await cancelGroupInviteRequest( + request.payload, + isFromExtension + ); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error?.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "CREATE_GROUP" : { - try { - const res = await createGroupRequest(request.payload, isFromExtension) - event.source.postMessage({ + case 'CREATE_GROUP': { + try { + const res = await createGroupRequest( + request.payload, + isFromExtension + ); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error?.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } - case "UPDATE_GROUP" : { - try { - const res = await updateGroupRequest(request.payload, isFromExtension) - event.source.postMessage({ + break; + } + case 'UPDATE_GROUP': { + try { + const res = await updateGroupRequest( + request.payload, + isFromExtension + ); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error?.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } + break; + } - case "GET_ARRR_SYNC_STATUS": { - try { - const res = await getArrrSyncStatus(request.payload); - event.source.postMessage({ + case 'GET_ARRR_SYNC_STATUS': { + try { + const res = await getArrrSyncStatus(request.payload); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } - case "SHOW_PDF_READER" : { - try { - if(!request.payload?.blob){ - throw new Error('Missing blob') - } - if(request.payload?.blob?.type !== "application/pdf") throw new Error('blob type must be application/pdf') - executeEvent("openPdf", { blob: request.payload?.blob}); - event.source.postMessage({ + break; + } + case 'SHOW_PDF_READER': { + try { + if (!request.payload?.blob) { + throw new Error('Missing blob'); + } + if (request.payload?.blob?.type !== 'application/pdf') + throw new Error('blob type must be application/pdf'); + executeEvent('openPdf', { blob: request.payload?.blob }); + event.source.postMessage( + { requestId: request.requestId, action: request.action, payload: true, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { requestId: request.requestId, action: request.action, error: error?.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; + type: 'backgroundMessageResponse', + }, + event.origin + ); } - case "BUY_NAME": { - try { - const res = await buyNameRequest(request.payload, isFromExtension); - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } - case "SELL_NAME": { - try { - const res = await sellNameRequest(request.payload, isFromExtension); - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } - case "CANCEL_SELL_NAME": { - try { - const res = await cancelSellNameRequest(request.payload, isFromExtension); - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } - - case "MULTI_ASSET_PAYMENT_WITH_PRIVATE_DATA" : { - try { - const res = await multiPaymentWithPrivateData(request.payload, isFromExtension) - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error?.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } - case "TRANSFER_ASSET" : { - try { - const res = await transferAssetRequest(request.payload, isFromExtension) - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error?.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } - default: - break; + break; } - }); - } - - // Initialize the message listener - setupMessageListenerQortalRequest(); + case 'MULTI_ASSET_PAYMENT_WITH_PRIVATE_DATA': { + try { + const res = await multiPaymentWithPrivateData( + request.payload, + isFromExtension + ); + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + payload: res, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + error: error?.message, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } + break; + } + case 'TRANSFER_ASSET': { + try { + const res = await transferAssetRequest( + request.payload, + isFromExtension + ); + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + payload: res, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + error: error?.message, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } + break; + } + + case 'BUY_NAME': { + try { + const res = await buyNameRequest(request.payload, isFromExtension); + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + payload: res, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + error: error.message, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } + break; + } + case 'SELL_NAME': { + try { + const res = await sellNameRequest(request.payload, isFromExtension); + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + payload: res, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + error: error.message, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } + break; + } + case 'CANCEL_SELL_NAME': { + try { + const res = await cancelSellNameRequest( + request.payload, + isFromExtension + ); + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + payload: res, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: request.action, + error: error.message, + type: 'backgroundMessageResponse', + }, + event.origin + ); + } + break; + } + default: + break; + } + }); +} + +// Initialize the message listener +setupMessageListenerQortalRequest(); diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index dcc391e..7b971f6 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -1,6 +1,5 @@ -import { Sha256 } from "asmcrypto.js"; +import { Sha256 } from 'asmcrypto.js'; import { - computePow, createEndpoint, getBalanceInfo, getFee, @@ -8,12 +7,10 @@ import { getLastRef, getSaveWallet, processTransactionVersion2, - removeDuplicateWindow, signChatFunc, joinGroup as joinGroupFunc, sendQortFee, sendCoin as sendCoinFunc, - isUsingLocal, createBuyOrderTx, performPowTask, parseErrorResponse, @@ -40,17 +37,24 @@ import { getAssetInfo, getPublicKey, transferAsset, -} from "../background"; -import { getNameInfo, uint8ArrayToObject } from "../backgroundFunctions/encryption"; -import { showSaveFilePicker } from "../components/Apps/useQortalMessageListener"; -import { getPublishesFromAdminsAdminSpace } from "../components/Chat/AdminSpaceInner"; -import { extractComponents } from "../components/Chat/MessageDisplay"; -import { decryptResource, getGroupAdmins, getPublishesFromAdmins, validateSecretKey } from "../components/Group/Group"; -import { QORT_DECIMALS } from "../constants/constants"; -import Base58 from "../deps/Base58"; -import ed2curve from "../deps/ed2curve"; -import nacl from "../deps/nacl-fast"; - +} from '../background'; +import { + getNameInfo, + uint8ArrayToObject, +} from '../backgroundFunctions/encryption'; +import { showSaveFilePicker } from '../components/Apps/useQortalMessageListener'; +import { getPublishesFromAdminsAdminSpace } from '../components/Chat/AdminSpaceInner'; +import { extractComponents } from '../components/Chat/MessageDisplay'; +import { + decryptResource, + getGroupAdmins, + getPublishesFromAdmins, + validateSecretKey, +} from '../components/Group/Group'; +import { QORT_DECIMALS } from '../constants/constants'; +import Base58 from '../deps/Base58'; +import ed2curve from '../deps/ed2curve'; +import nacl from '../deps/nacl-fast'; import { base64ToUint8Array, @@ -64,24 +68,24 @@ import { objectToBase64, uint8ArrayStartsWith, uint8ArrayToBase64, -} from "../qdn/encryption/group-encryption"; -import { publishData } from "../qdn/publish/pubish"; +} from '../qdn/encryption/group-encryption'; +import { publishData } from '../qdn/publish/pubish'; import { getPermission, isRunningGateway, setPermission, -} from "../qortalRequests"; -import TradeBotCreateRequest from "../transactions/TradeBotCreateRequest"; -import DeleteTradeOffer from "../transactions/TradeBotDeleteRequest"; -import signTradeBotTransaction from "../transactions/signTradeBotTransaction"; -import { createTransaction } from "../transactions/transactions"; -import { executeEvent } from "../utils/events"; -import { fileToBase64 } from "../utils/fileReading"; -import { mimeToExtensionMap } from "../utils/memeTypes"; -import { RequestQueueWithPromise } from "../utils/queue/queue"; -import utils from "../utils/utils"; -import ShortUniqueId from "short-unique-id"; -import { isValidBase64WithDecode } from "../utils/decode"; +} from '../qortalRequests'; +import TradeBotCreateRequest from '../transactions/TradeBotCreateRequest'; +import DeleteTradeOffer from '../transactions/TradeBotDeleteRequest'; +import signTradeBotTransaction from '../transactions/signTradeBotTransaction'; +import { createTransaction } from '../transactions/transactions'; +import { executeEvent } from '../utils/events'; +import { fileToBase64 } from '../utils/fileReading'; +import { mimeToExtensionMap } from '../utils/memeTypes'; +import { RequestQueueWithPromise } from '../utils/queue/queue'; +import utils from '../utils/utils'; +import ShortUniqueId from 'short-unique-id'; +import { isValidBase64WithDecode } from '../utils/decode'; const uid = new ShortUniqueId({ length: 6 }); @@ -89,28 +93,28 @@ export const requestQueueGetAtAddresses = new RequestQueueWithPromise(10); const sellerForeignFee = { LITECOIN: { - value: "~0.00005", - ticker: "LTC", + value: '~0.00005', + ticker: 'LTC', }, DOGECOIN: { - value: "~0.005", - ticker: "DOGE", + value: '~0.005', + ticker: 'DOGE', }, BITCOIN: { - value: "~0.0001", - ticker: "BTC", + value: '~0.0001', + ticker: 'BTC', }, DIGIBYTE: { - value: "~0.0005", - ticker: "DGB", + value: '~0.0005', + ticker: 'DGB', }, RAVENCOIN: { - value: "~0.006", - ticker: "RVN", + value: '~0.006', + ticker: 'RVN', }, PIRATECHAIN: { - value: "~0.0002", - ticker: "ARRR", + value: '~0.0002', + ticker: 'ARRR', }, }; @@ -122,24 +126,28 @@ const rvnFeePerByte = 0.00001125; const MAX_RETRIES = 3; // Set max number of retries - -export async function retryTransaction(fn, args, throwError, retries = MAX_RETRIES) { +export async function retryTransaction( + fn, + args, + throwError, + retries = MAX_RETRIES +) { let attempt = 0; while (attempt < retries) { try { - return await fn(...args); + return await fn(...args); } catch (error) { console.error(`Attempt ${attempt + 1} failed: ${error.message}`); attempt++; if (attempt === retries) { - console.error("Max retries reached. Skipping transaction."); - if(throwError){ - throw new Error(error?.message || "Unable to process transaction") + console.error('Max retries reached. Skipping transaction.'); + if (throwError) { + throw new Error(error?.message || 'Unable to process transaction'); } else { - throw new Error(error?.message || "Unable to process transaction") + throw new Error(error?.message || 'Unable to process transaction'); } } - await new Promise(res => setTimeout(res, 10000)); + await new Promise((res) => setTimeout(res, 10000)); } } } @@ -151,23 +159,24 @@ function roundUpToDecimals(number, decimals = 8) { export const _createPoll = async ( { pollName, pollDescription, options }, - isFromExtension, skipPermission + isFromExtension, + skipPermission ) => { - const fee = await getFee("CREATE_POLL"); - let resPermission = {} - if(!skipPermission){ - resPermission = await getUserPermission( + const fee = await getFee('CREATE_POLL'); + let resPermission = {}; + if (!skipPermission) { + resPermission = await getUserPermission( { - text1: "You are requesting to create the poll below:", + text1: 'You are requesting to create the poll below:', text2: `Poll: ${pollName}`, text3: `Description: ${pollDescription}`, - text4: `Options: ${options?.join(", ")}`, + text4: `Options: ${options?.join(', ')}`, fee: fee.fee, }, isFromExtension ); } - + const { accepted = false } = resPermission; if (accepted || skipPermission) { @@ -195,11 +204,11 @@ export const _createPoll = async ( const res = await processTransactionVersion2(signedBytes); if (!res?.signature) throw new Error( - res?.message || "Transaction was not able to be processed" + res?.message || 'Transaction was not able to be processed' ); return res; } else { - throw new Error("User declined request"); + throw new Error('User declined request'); } }; @@ -207,11 +216,11 @@ const _deployAt = async ( { name, description, tags, creationBytes, amount, assetId, atType }, isFromExtension ) => { - const fee = await getFee("DEPLOY_AT"); + const fee = await getFee('DEPLOY_AT'); const resPermission = await getUserPermission( { - text1: "Would you like to deploy this AT?", + text1: 'Would you like to deploy this AT?', text2: `Name: ${name}`, text3: `Description: ${description}`, fee: fee.fee, @@ -251,24 +260,25 @@ const _deployAt = async ( const res = await processTransactionVersion2(signedBytes); if (!res?.signature) throw new Error( - res?.message || "Transaction was not able to be processed" + res?.message || 'Transaction was not able to be processed' ); return res; } else { - throw new Error("User declined transaction"); + throw new Error('User declined transaction'); } }; export const _voteOnPoll = async ( { pollName, optionIndex, optionName }, - isFromExtension, skipPermission + isFromExtension, + skipPermission ) => { - const fee = await getFee("VOTE_ON_POLL"); - let resPermission = {} - if(!skipPermission){ + const fee = await getFee('VOTE_ON_POLL'); + let resPermission = {}; + if (!skipPermission) { resPermission = await getUserPermission( { - text1: "You are being requested to vote on the poll below:", + text1: 'You are being requested to vote on the poll below:', text2: `Poll: ${pollName}`, text3: `Option: ${optionName}`, fee: fee.fee, @@ -276,7 +286,7 @@ export const _voteOnPoll = async ( isFromExtension ); } - + const { accepted = false } = resPermission; if (accepted || skipPermission) { @@ -303,11 +313,11 @@ export const _voteOnPoll = async ( const res = await processTransactionVersion2(signedBytes); if (!res?.signature) throw new Error( - res?.message || "Transaction was not able to be processed" + res?.message || 'Transaction was not able to be processed' ); return res; } else { - throw new Error("User declined request"); + throw new Error('User declined request'); } }; @@ -318,7 +328,7 @@ const handleFileMessage = (event) => { const { action, requestId, result, error } = event.data; if ( - action === "getFileFromIndexedDBResponse" && + action === 'getFileFromIndexedDBResponse' && fileRequestResolvers.has(requestId) ) { const { resolve, reject } = fileRequestResolvers.get(requestId); @@ -327,12 +337,12 @@ const handleFileMessage = (event) => { if (result) { resolve(result); } else { - reject(error || "Failed to retrieve file"); + reject(error || 'Failed to retrieve file'); } } }; -window.addEventListener("message", handleFileMessage); +window.addEventListener('message', handleFileMessage); function getFileFromContentScript(fileId) { return new Promise((resolve, reject) => { @@ -343,14 +353,14 @@ function getFileFromContentScript(fileId) { // Send the request message window.postMessage( - { action: "getFileFromIndexedDB", fileId, requestId }, + { action: 'getFileFromIndexedDB', fileId, requestId }, targetOrigin ); // Timeout to handle no response scenario setTimeout(() => { if (fileRequestResolvers.has(requestId)) { - fileRequestResolvers.get(requestId).reject("Request timed out"); + fileRequestResolvers.get(requestId).reject('Request timed out'); fileRequestResolvers.delete(requestId); // Clean up on timeout } }, 10000); // 10-second timeout @@ -371,7 +381,7 @@ const handleMessage = (event) => { // Check if this is the expected response action and if we have a stored resolver if ( - action === "QORTAL_REQUEST_PERMISSION_RESPONSE" && + action === 'QORTAL_REQUEST_PERMISSION_RESPONSE' && responseResolvers.has(requestId) ) { // Resolve the stored promise with the result @@ -380,7 +390,7 @@ const handleMessage = (event) => { } }; -window.addEventListener("message", handleMessage); +window.addEventListener('message', handleMessage); async function getUserPermission(payload, isFromExtension) { return new Promise((resolve) => { @@ -391,7 +401,7 @@ async function getUserPermission(payload, isFromExtension) { // Send the request message window.postMessage( { - action: "QORTAL_REQUEST_PERMISSION", + action: 'QORTAL_REQUEST_PERMISSION', payload, requestId, isFromExtension, @@ -409,7 +419,11 @@ async function getUserPermission(payload, isFromExtension) { }); } -export const getUserAccount = async ({ isFromExtension, appInfo, skipAuth }) => { +export const getUserAccount = async ({ + isFromExtension, + appInfo, + skipAuth, +}) => { try { const value = (await getPermission(`qAPPAutoAuth-${appInfo?.name}`)) || false; @@ -417,17 +431,17 @@ export const getUserAccount = async ({ isFromExtension, appInfo, skipAuth }) => if (value) { skip = true; } - if(skipAuth){ - skip = true + if (skipAuth) { + skip = true; } let resPermission; if (!skip) { resPermission = await getUserPermission( { - text1: "Do you give this application permission to authenticate?", + text1: 'Do you give this application permission to authenticate?', checkbox1: { value: false, - label: "Always authenticate automatically", + label: 'Always authenticate automatically', }, }, isFromExtension @@ -447,10 +461,10 @@ export const getUserAccount = async ({ isFromExtension, appInfo, skipAuth }) => publicKey, }; } else { - throw new Error("User declined request"); + throw new Error('User declined request'); } } catch (error) { - throw new Error("Unable to fetch user account"); + throw new Error('Unable to fetch user account'); } }; @@ -461,7 +475,7 @@ export const encryptData = async (data, sender) => { data64 = await fileToBase64(data?.file || data?.blob); } if (!data64) { - throw new Error("Please include data to encrypt"); + throw new Error('Please include data to encrypt'); } const resKeyPair = await getKeyPair(); const parsedData = resKeyPair; @@ -477,195 +491,203 @@ export const encryptData = async (data, sender) => { if (encryptDataResponse) { return encryptDataResponse; } else { - throw new Error("Unable to encrypt"); + throw new Error('Unable to encrypt'); } }; export const encryptQortalGroupData = async (data, sender) => { let data64 = data?.data64 || data?.base64; - let groupId = data?.groupId - let isAdmins = data?.isAdmins - if(!groupId){ - throw new Error('Please provide a groupId') + let groupId = data?.groupId; + let isAdmins = data?.isAdmins; + if (!groupId) { + throw new Error('Please provide a groupId'); } if (data?.file || data?.blob) { data64 = await fileToBase64(data?.file || data?.blob); } if (!data64) { - throw new Error("Please include data to encrypt"); + throw new Error('Please include data to encrypt'); } - - let secretKeyObject - if(!isAdmins){ - if(groupSecretkeys[groupId] && groupSecretkeys[groupId].secretKeyObject && groupSecretkeys[groupId]?.timestamp && (Date.now() - groupSecretkeys[groupId]?.timestamp) < 1200000){ - secretKeyObject = groupSecretkeys[groupId].secretKeyObject - } - - if(!secretKeyObject){ - const { names } = - await getGroupAdmins(groupId) - - const publish = - await getPublishesFromAdmins(names, groupId); - if(publish === false) throw new Error('No group key found.') - const url = await createEndpoint(`/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${ - publish.identifier - }?encoding=base64&rebuild=true`); - - const res = await fetch( -url - ); - const resData = await res.text(); - - const decryptedKey: any = await decryptResource(resData, true); - - const dataint8Array = base64ToUint8Array(decryptedKey.data); - const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); - - if (!validateSecretKey(decryptedKeyToObject)) - throw new Error("SecretKey is not valid"); - secretKeyObject = decryptedKeyToObject - groupSecretkeys[groupId] = { - secretKeyObject, - timestamp: Date.now() + let secretKeyObject; + if (!isAdmins) { + if ( + groupSecretkeys[groupId] && + groupSecretkeys[groupId].secretKeyObject && + groupSecretkeys[groupId]?.timestamp && + Date.now() - groupSecretkeys[groupId]?.timestamp < 1200000 + ) { + secretKeyObject = groupSecretkeys[groupId].secretKeyObject; } - } -} else { - if(groupSecretkeys[`admins-${groupId}`] && groupSecretkeys[`admins-${groupId}`].secretKeyObject && groupSecretkeys[`admins-${groupId}`]?.timestamp && (Date.now() - groupSecretkeys[`admins-${groupId}`]?.timestamp) < 1200000){ - secretKeyObject = groupSecretkeys[`admins-${groupId}`].secretKeyObject - } + if (!secretKeyObject) { + const { names } = await getGroupAdmins(groupId); - if(!secretKeyObject){ - const { names } = - await getGroupAdmins(groupId) + const publish = await getPublishesFromAdmins(names, groupId); + if (publish === false) throw new Error('No group key found.'); + const url = await createEndpoint( + `/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${ + publish.identifier + }?encoding=base64&rebuild=true` + ); - const publish = - await getPublishesFromAdminsAdminSpace(names, groupId); - if(publish === false) throw new Error('No group key found.') - const url = await createEndpoint(`/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${ - publish.identifier - }?encoding=base64&rebuild=true`); + const res = await fetch(url); + const resData = await res.text(); - const res = await fetch( -url - ); - const resData = await res.text(); - const decryptedKey: any = await decryptResource(resData, true); - const dataint8Array = base64ToUint8Array(decryptedKey.data); - const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); + const decryptedKey: any = await decryptResource(resData, true); - if (!validateSecretKey(decryptedKeyToObject)) - throw new Error("SecretKey is not valid"); - secretKeyObject = decryptedKeyToObject - groupSecretkeys[`admins-${groupId}`] = { - secretKeyObject, - timestamp: Date.now() + const dataint8Array = base64ToUint8Array(decryptedKey.data); + const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); + + if (!validateSecretKey(decryptedKeyToObject)) + throw new Error('SecretKey is not valid'); + secretKeyObject = decryptedKeyToObject; + groupSecretkeys[groupId] = { + secretKeyObject, + timestamp: Date.now(), + }; + } + } else { + if ( + groupSecretkeys[`admins-${groupId}`] && + groupSecretkeys[`admins-${groupId}`].secretKeyObject && + groupSecretkeys[`admins-${groupId}`]?.timestamp && + Date.now() - groupSecretkeys[`admins-${groupId}`]?.timestamp < 1200000 + ) { + secretKeyObject = groupSecretkeys[`admins-${groupId}`].secretKeyObject; + } + + if (!secretKeyObject) { + const { names } = await getGroupAdmins(groupId); + + const publish = await getPublishesFromAdminsAdminSpace(names, groupId); + if (publish === false) throw new Error('No group key found.'); + const url = await createEndpoint( + `/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${ + publish.identifier + }?encoding=base64&rebuild=true` + ); + + const res = await fetch(url); + const resData = await res.text(); + const decryptedKey: any = await decryptResource(resData, true); + const dataint8Array = base64ToUint8Array(decryptedKey.data); + const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); + + if (!validateSecretKey(decryptedKeyToObject)) + throw new Error('SecretKey is not valid'); + secretKeyObject = decryptedKeyToObject; + groupSecretkeys[`admins-${groupId}`] = { + secretKeyObject, + timestamp: Date.now(), + }; } } + const resGroupEncryptedResource = encryptSingle({ + data64, + secretKeyObject: secretKeyObject, + }); - -} - - const resGroupEncryptedResource = encryptSingle({ - data64, secretKeyObject: secretKeyObject, - }) - if (resGroupEncryptedResource) { return resGroupEncryptedResource; } else { - throw new Error("Unable to encrypt"); + throw new Error('Unable to encrypt'); } }; export const decryptQortalGroupData = async (data, sender) => { let data64 = data?.data64 || data?.base64; - let groupId = data?.groupId - let isAdmins = data?.isAdmins - if(!groupId){ - throw new Error('Please provide a groupId') + let groupId = data?.groupId; + let isAdmins = data?.isAdmins; + if (!groupId) { + throw new Error('Please provide a groupId'); } if (!data64) { - throw new Error("Please include data to encrypt"); + throw new Error('Please include data to encrypt'); } - let secretKeyObject - if(!isAdmins){ - if(groupSecretkeys[groupId] && groupSecretkeys[groupId].secretKeyObject && groupSecretkeys[groupId]?.timestamp && (Date.now() - groupSecretkeys[groupId]?.timestamp) < 1200000){ - secretKeyObject = groupSecretkeys[groupId].secretKeyObject - } - if(!secretKeyObject){ - const { names } = - await getGroupAdmins(groupId) - - const publish = - await getPublishesFromAdmins(names, groupId); - if(publish === false) throw new Error('No group key found.') - const url = await createEndpoint(`/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${ - publish.identifier - }?encoding=base64&rebuild=true`); - - const res = await fetch( -url - ); - const resData = await res.text(); - const decryptedKey: any = await decryptResource(resData, true); - - const dataint8Array = base64ToUint8Array(decryptedKey.data); - const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); - if (!validateSecretKey(decryptedKeyToObject)) - throw new Error("SecretKey is not valid"); - secretKeyObject = decryptedKeyToObject - groupSecretkeys[groupId] = { - secretKeyObject, - timestamp: Date.now() + let secretKeyObject; + if (!isAdmins) { + if ( + groupSecretkeys[groupId] && + groupSecretkeys[groupId].secretKeyObject && + groupSecretkeys[groupId]?.timestamp && + Date.now() - groupSecretkeys[groupId]?.timestamp < 1200000 + ) { + secretKeyObject = groupSecretkeys[groupId].secretKeyObject; } - } -} else { - if(groupSecretkeys[`admins-${groupId}`] && groupSecretkeys[`admins-${groupId}`].secretKeyObject && groupSecretkeys[`admins-${groupId}`]?.timestamp && (Date.now() - groupSecretkeys[`admins-${groupId}`]?.timestamp) < 1200000){ - secretKeyObject = groupSecretkeys[`admins-${groupId}`].secretKeyObject - } - if(!secretKeyObject){ - const { names } = - await getGroupAdmins(groupId) + if (!secretKeyObject) { + const { names } = await getGroupAdmins(groupId); - const publish = - await getPublishesFromAdminsAdminSpace(names, groupId); - if(publish === false) throw new Error('No group key found.') - const url = await createEndpoint(`/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${ - publish.identifier - }?encoding=base64&rebuild=true`); + const publish = await getPublishesFromAdmins(names, groupId); + if (publish === false) throw new Error('No group key found.'); + const url = await createEndpoint( + `/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${ + publish.identifier + }?encoding=base64&rebuild=true` + ); - const res = await fetch( -url - ); - const resData = await res.text(); - const decryptedKey: any = await decryptResource(resData, true); + const res = await fetch(url); + const resData = await res.text(); + const decryptedKey: any = await decryptResource(resData, true); - const dataint8Array = base64ToUint8Array(decryptedKey.data); - const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); - if (!validateSecretKey(decryptedKeyToObject)) - throw new Error("SecretKey is not valid"); - secretKeyObject = decryptedKeyToObject - groupSecretkeys[`admins-${groupId}`] = { - secretKeyObject, - timestamp: Date.now() + const dataint8Array = base64ToUint8Array(decryptedKey.data); + const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); + if (!validateSecretKey(decryptedKeyToObject)) + throw new Error('SecretKey is not valid'); + secretKeyObject = decryptedKeyToObject; + groupSecretkeys[groupId] = { + secretKeyObject, + timestamp: Date.now(), + }; + } + } else { + if ( + groupSecretkeys[`admins-${groupId}`] && + groupSecretkeys[`admins-${groupId}`].secretKeyObject && + groupSecretkeys[`admins-${groupId}`]?.timestamp && + Date.now() - groupSecretkeys[`admins-${groupId}`]?.timestamp < 1200000 + ) { + secretKeyObject = groupSecretkeys[`admins-${groupId}`].secretKeyObject; + } + if (!secretKeyObject) { + const { names } = await getGroupAdmins(groupId); + + const publish = await getPublishesFromAdminsAdminSpace(names, groupId); + if (publish === false) throw new Error('No group key found.'); + const url = await createEndpoint( + `/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${ + publish.identifier + }?encoding=base64&rebuild=true` + ); + + const res = await fetch(url); + const resData = await res.text(); + const decryptedKey: any = await decryptResource(resData, true); + + const dataint8Array = base64ToUint8Array(decryptedKey.data); + const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); + if (!validateSecretKey(decryptedKeyToObject)) + throw new Error('SecretKey is not valid'); + secretKeyObject = decryptedKeyToObject; + groupSecretkeys[`admins-${groupId}`] = { + secretKeyObject, + timestamp: Date.now(), + }; } } - -} - - const resGroupDecryptResource = decryptSingle({ - data64, secretKeyObject: secretKeyObject, skipDecodeBase64: true - }) + const resGroupDecryptResource = decryptSingle({ + data64, + secretKeyObject: secretKeyObject, + skipDecodeBase64: true, + }); if (resGroupDecryptResource) { return resGroupDecryptResource; } else { - throw new Error("Unable to decrypt"); + throw new Error('Unable to decrypt'); } }; @@ -676,14 +698,14 @@ export const encryptDataWithSharingKey = async (data, sender) => { data64 = await fileToBase64(data?.file || data?.blob); } if (!data64) { - throw new Error("Please include data to encrypt"); + throw new Error('Please include data to encrypt'); } - const symmetricKey = createSymmetricKeyAndNonce() + const symmetricKey = createSymmetricKeyAndNonce(); const dataObject = { data: data64, - key:symmetricKey.messageKey - } - const dataObjectBase64 = await objectToBase64(dataObject) + key: symmetricKey.messageKey, + }; + const dataObjectBase64 = await objectToBase64(dataObject); const resKeyPair = await getKeyPair(); const parsedData = resKeyPair; @@ -695,70 +717,70 @@ export const encryptDataWithSharingKey = async (data, sender) => { publicKeys: publicKeys, privateKey, userPublicKey, - customSymmetricKey: symmetricKey.messageKey + customSymmetricKey: symmetricKey.messageKey, }); if (encryptDataResponse) { return encryptDataResponse; } else { - throw new Error("Unable to encrypt"); + throw new Error('Unable to encrypt'); } }; export const decryptDataWithSharingKey = async (data, sender) => { const { encryptedData, key } = data; - if (!encryptedData) { - throw new Error("Please include data to decrypt"); + throw new Error('Please include data to decrypt'); } - const decryptedData = await decryptGroupEncryptionWithSharingKey({data64EncryptedData: encryptedData, key}) - const base64ToObject = JSON.parse(atob(decryptedData)) - if(!base64ToObject.data) throw new Error('No data in the encrypted resource') - return base64ToObject.data + const decryptedData = await decryptGroupEncryptionWithSharingKey({ + data64EncryptedData: encryptedData, + key, + }); + const base64ToObject = JSON.parse(atob(decryptedData)); + if (!base64ToObject.data) + throw new Error('No data in the encrypted resource'); + return base64ToObject.data; }; export const getHostedData = async (data, isFromExtension) => { const isGateway = await isRunningGateway(); if (isGateway) { - throw new Error("This action cannot be done through a public node"); + throw new Error('This action cannot be done through a public node'); } const resPermission = await getUserPermission( { - text1: "Do you give this application permission to", + text1: 'Do you give this application permission to', text2: `Get a list of your hosted data?`, }, isFromExtension ); const { accepted } = resPermission; - if(accepted){ + if (accepted) { const limit = data?.limit ? data?.limit : 20; - const query = data?.query ? data?.query : "" - const offset = data?.offset ? data?.offset : 0 + const query = data?.query ? data?.query : ''; + const offset = data?.offset ? data?.offset : 0; - let urlPath = `/arbitrary/hosted/resources/?limit=${limit}&offset=${offset}` - if(query){ - urlPath = urlPath + `&query=${query}` + let urlPath = `/arbitrary/hosted/resources/?limit=${limit}&offset=${offset}`; + if (query) { + urlPath = urlPath + `&query=${query}`; } - - const url = await createEndpoint(urlPath); - const response = await fetch(url); - const dataResponse = await response.json(); - return dataResponse - - } else { - throw new Error("User declined to get list of hosted resources"); + const url = await createEndpoint(urlPath); + const response = await fetch(url); + const dataResponse = await response.json(); + return dataResponse; + } else { + throw new Error('User declined to get list of hosted resources'); } - }; export const deleteHostedData = async (data, isFromExtension) => { const isGateway = await isRunningGateway(); if (isGateway) { - throw new Error("This action cannot be done through a public node"); + throw new Error('This action cannot be done through a public node'); } - const requiredFields = ["hostedData"]; + const requiredFields = ['hostedData']; const missingFields: string[] = []; requiredFields.forEach((field) => { if (!data[field]) { @@ -767,35 +789,36 @@ export const deleteHostedData = async (data, isFromExtension) => { }); const resPermission = await getUserPermission( { - text1: "Do you give this application permission to", + text1: 'Do you give this application permission to', text2: `Delete ${data?.hostedData?.length} hosted resources?`, }, isFromExtension ); const { accepted } = resPermission; - if(accepted){ + if (accepted) { const { hostedData } = data; - for (const hostedDataItem of hostedData){ - try { - const url = await createEndpoint(`/arbitrary/resource/${hostedDataItem.service}/${hostedDataItem.name}/${hostedDataItem.identifier}`); - await fetch(url, { - method: "DELETE", - headers: { - "Content-Type": "application/json", - } - }); - } catch (error) { - //error + for (const hostedDataItem of hostedData) { + try { + const url = await createEndpoint( + `/arbitrary/resource/${hostedDataItem.service}/${hostedDataItem.name}/${hostedDataItem.identifier}` + ); + await fetch(url, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (error) { + //error + } } - } - return true + return true; } else { - throw new Error("User declined delete hosted resources"); + throw new Error('User declined delete hosted resources'); } - }; export const decryptData = async (data) => { const { encryptedData, publicKey } = data; @@ -809,7 +832,7 @@ export const decryptData = async (data) => { const uint8Array = base64ToUint8Array(encryptedData); const startsWithQortalEncryptedData = uint8ArrayStartsWith( uint8Array, - "qortalEncryptedData" + 'qortalEncryptedData' ); if (startsWithQortalEncryptedData) { if (!publicKey) { @@ -825,7 +848,7 @@ export const decryptData = async (data) => { } const startsWithQortalGroupEncryptedData = uint8ArrayStartsWith( uint8Array, - "qortalGroupEncryptedData" + 'qortalGroupEncryptedData' ); if (startsWithQortalGroupEncryptedData) { const decryptedData = decryptGroupDataQortalRequest( @@ -835,15 +858,15 @@ export const decryptData = async (data) => { const decryptedDataToBase64 = uint8ArrayToBase64(decryptedData); return decryptedDataToBase64; } - throw new Error("Unable to decrypt"); + throw new Error('Unable to decrypt'); }; export const getListItems = async (data, isFromExtension) => { const isGateway = await isRunningGateway(); if (isGateway) { - throw new Error("This action cannot be done through a public node"); + throw new Error('This action cannot be done through a public node'); } - const requiredFields = ["list_name"]; + const requiredFields = ['list_name']; const missingFields: string[] = []; requiredFields.forEach((field) => { if (!data[field]) { @@ -851,11 +874,11 @@ export const getListItems = async (data, isFromExtension) => { } }); if (missingFields.length > 0) { - const missingFieldsString = missingFields.join(", "); + const missingFieldsString = missingFields.join(', '); const errorMsg = `Missing fields: ${missingFieldsString}`; throw new Error(errorMsg); } - const value = (await getPermission("qAPPAutoLists")) || false; + const value = (await getPermission('qAPPAutoLists')) || false; let skip = false; if (value) { @@ -867,12 +890,12 @@ export const getListItems = async (data, isFromExtension) => { if (!skip) { resPermission = await getUserPermission( { - text1: "Do you give this application permission to", - text2: "Access the list", + text1: 'Do you give this application permission to', + text2: 'Access the list', highlightedText: data.list_name, checkbox1: { value: value, - label: "Always allow lists to be retrieved automatically", + label: 'Always allow lists to be retrieved automatically', }, }, isFromExtension @@ -880,27 +903,27 @@ export const getListItems = async (data, isFromExtension) => { const { accepted, checkbox1 } = resPermission; acceptedVar = accepted; checkbox1Var = checkbox1; - setPermission("qAPPAutoLists", checkbox1); + setPermission('qAPPAutoLists', checkbox1); } if (acceptedVar || skip) { const url = await createEndpoint(`/lists/${data.list_name}`); const response = await fetch(url); - if (!response.ok) throw new Error("Failed to fetch"); + if (!response.ok) throw new Error('Failed to fetch'); const list = await response.json(); return list; } else { - throw new Error("User declined to share list"); + throw new Error('User declined to share list'); } }; export const addListItems = async (data, isFromExtension) => { const isGateway = await isRunningGateway(); if (isGateway) { - throw new Error("This action cannot be done through a public node"); + throw new Error('This action cannot be done through a public node'); } - const requiredFields = ["list_name", "items"]; + const requiredFields = ['list_name', 'items']; const missingFields: string[] = []; requiredFields.forEach((field) => { if (!data[field]) { @@ -908,7 +931,7 @@ export const addListItems = async (data, isFromExtension) => { } }); if (missingFields.length > 0) { - const missingFieldsString = missingFields.join(", "); + const missingFieldsString = missingFields.join(', '); const errorMsg = `Missing fields: ${missingFieldsString}`; throw new Error(errorMsg); } @@ -918,9 +941,9 @@ export const addListItems = async (data, isFromExtension) => { const resPermission = await getUserPermission( { - text1: "Do you give this application permission to", + text1: 'Do you give this application permission to', text2: `Add the following to the list ${list_name}:`, - highlightedText: items.join(", "), + highlightedText: items.join(', '), }, isFromExtension ); @@ -933,14 +956,14 @@ export const addListItems = async (data, isFromExtension) => { }; const bodyToString = JSON.stringify(body); const response = await fetch(url, { - method: "POST", + method: 'POST', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, body: bodyToString, }); - if (!response.ok) throw new Error("Failed to add to list"); + if (!response.ok) throw new Error('Failed to add to list'); let res; try { res = await response.clone().json(); @@ -949,16 +972,16 @@ export const addListItems = async (data, isFromExtension) => { } return res; } else { - throw new Error("User declined add to list"); + throw new Error('User declined add to list'); } }; export const deleteListItems = async (data, isFromExtension) => { const isGateway = await isRunningGateway(); if (isGateway) { - throw new Error("This action cannot be done through a public node"); + throw new Error('This action cannot be done through a public node'); } - const requiredFields = ["list_name"]; + const requiredFields = ['list_name']; const missingFields: string[] = []; requiredFields.forEach((field) => { if (!data[field]) { @@ -966,20 +989,20 @@ export const deleteListItems = async (data, isFromExtension) => { } }); if (missingFields.length > 0) { - const missingFieldsString = missingFields.join(", "); + const missingFieldsString = missingFields.join(', '); const errorMsg = `Missing fields: ${missingFieldsString}`; throw new Error(errorMsg); } - if(!data?.item && !data?.items){ - throw new Error('Missing fields: items') + if (!data?.item && !data?.items) { + throw new Error('Missing fields: items'); } const item = data?.item; - const items = data?.items + const items = data?.items; const list_name = data.list_name; const resPermission = await getUserPermission( { - text1: "Do you give this application permission to", + text1: 'Do you give this application permission to', text2: `Remove the following from the list ${list_name}:`, highlightedText: items ? JSON.stringify(items) : item, }, @@ -994,14 +1017,14 @@ export const deleteListItems = async (data, isFromExtension) => { }; const bodyToString = JSON.stringify(body); const response = await fetch(url, { - method: "DELETE", + method: 'DELETE', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, body: bodyToString, }); - if (!response.ok) throw new Error("Failed to add to list"); + if (!response.ok) throw new Error('Failed to add to list'); let res; try { res = await response.clone().json(); @@ -1010,7 +1033,7 @@ export const deleteListItems = async (data, isFromExtension) => { } return res; } else { - throw new Error("User declined delete from list"); + throw new Error('User declined delete from list'); } }; @@ -1019,7 +1042,7 @@ export const publishQDNResource = async ( sender, isFromExtension ) => { - const requiredFields = ["service"]; + const requiredFields = ['service']; const missingFields: string[] = []; requiredFields.forEach((field) => { if (!data[field]) { @@ -1027,25 +1050,25 @@ export const publishQDNResource = async ( } }); if (missingFields.length > 0) { - const missingFieldsString = missingFields.join(", "); + const missingFieldsString = missingFields.join(', '); const errorMsg = `Missing fields: ${missingFieldsString}`; throw new Error(errorMsg); } if (!data.file && !data.data64 && !data.base64) { - throw new Error("No data or file was submitted"); + throw new Error('No data or file was submitted'); } // Use "default" if user hasn't specified an identifier const service = data.service; - const appFee = data?.appFee ? +data.appFee : undefined - const appFeeRecipient = data?.appFeeRecipient - let hasAppFee = false - if(appFee && appFee > 0 && appFeeRecipient){ - hasAppFee = true + const appFee = data?.appFee ? +data.appFee : undefined; + const appFeeRecipient = data?.appFeeRecipient; + let hasAppFee = false; + if (appFee && appFee > 0 && appFeeRecipient) { + hasAppFee = true; } const registeredName = await getNameInfo(); const name = registeredName; - if(!name){ - throw new Error('User has no Qortal name') + if (!name) { + throw new Error('User has no Qortal name'); } let identifier = data.identifier; let data64 = data.data64 || data.base64; @@ -1055,18 +1078,18 @@ export const publishQDNResource = async ( const category = data.category; const tags = data?.tags || []; -const result = {}; + const result = {}; -// Fill tags dynamically while maintaining backward compatibility -for (let i = 0; i < 5; i++) { - result[`tag${i + 1}`] = tags[i] || data[`tag${i + 1}`] || undefined; -} + // Fill tags dynamically while maintaining backward compatibility + for (let i = 0; i < 5; i++) { + result[`tag${i + 1}`] = tags[i] || data[`tag${i + 1}`] || undefined; + } -// Access tag1 to tag5 from result -const { tag1, tag2, tag3, tag4, tag5 } = result; + // Access tag1 to tag5 from result + const { tag1, tag2, tag3, tag4, tag5 } = result; if (data.identifier == null) { - identifier = "default"; + identifier = 'default'; } if (data?.file || data?.blob) { data64 = await fileToBase64(data?.file || data?.blob); @@ -1076,10 +1099,9 @@ const { tag1, tag2, tag3, tag4, tag5 } = result; (!data.publicKeys || (Array.isArray(data.publicKeys) && data.publicKeys.length === 0)) ) { - throw new Error("Encrypting data requires public keys"); + throw new Error('Encrypting data requires public keys'); } - if (data.encrypt) { try { const resKeyPair = await getKeyPair(); @@ -1097,46 +1119,45 @@ const { tag1, tag2, tag3, tag4, tag5 } = result; } } catch (error) { throw new Error( - error.message || "Upload failed due to failed encryption" + error.message || 'Upload failed due to failed encryption' ); } } - const fee = await getFee("ARBITRARY"); + const fee = await getFee('ARBITRARY'); - const handleDynamicValues = {} - if(hasAppFee){ - const feePayment = await getFee("PAYMENT"); + const handleDynamicValues = {}; + if (hasAppFee) { + const feePayment = await getFee('PAYMENT'); - handleDynamicValues['appFee'] = +appFee + +feePayment.fee, - handleDynamicValues['checkbox1'] = { - value: true, - label: "accept app fee", - } + (handleDynamicValues['appFee'] = +appFee + +feePayment.fee), + (handleDynamicValues['checkbox1'] = { + value: true, + label: 'accept app fee', + }); } - if(!!data?.encrypt){ - handleDynamicValues['highlightedText'] = `isEncrypted: ${!!data.encrypt}` + if (!!data?.encrypt) { + handleDynamicValues['highlightedText'] = `isEncrypted: ${!!data.encrypt}`; } const resPermission = await getUserPermission( { - text1: "Do you give this application permission to publish to QDN?", + text1: 'Do you give this application permission to publish to QDN?', text2: `service: ${service}`, text3: `identifier: ${identifier || null}`, fee: fee.fee, - ...handleDynamicValues + ...handleDynamicValues, }, isFromExtension ); const { accepted, checkbox1 = false } = resPermission; if (accepted) { - try { const resPublish = await publishData({ registeredName: encodeURIComponent(name), file: data64, service: service, identifier: encodeURIComponent(identifier), - uploadType: "file", + uploadType: 'file', isBase64: true, filename: filename, title, @@ -1150,18 +1171,21 @@ const { tag1, tag2, tag3, tag4, tag5 } = result; apiVersion: 2, withFee: true, }); - if(resPublish?.signature && hasAppFee && checkbox1){ - sendCoinFunc({ - amount: appFee, - receiver: appFeeRecipient - }, true) + if (resPublish?.signature && hasAppFee && checkbox1) { + sendCoinFunc( + { + amount: appFee, + receiver: appFeeRecipient, + }, + true + ); } return resPublish; } catch (error) { - throw new Error(error?.message || "Upload failed"); + throw new Error(error?.message || 'Upload failed'); } } else { - throw new Error("User declined request"); + throw new Error('User declined request'); } }; @@ -1171,9 +1195,9 @@ export const checkArrrSyncStatus = async (seed) => { while (tries < 36) { const response = await fetch(_url, { - method: "POST", + method: 'POST', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, body: seed, }); @@ -1185,7 +1209,7 @@ export const checkArrrSyncStatus = async (seed) => { res = await response.text(); } - if (res.indexOf('<') > -1 || res !== "Synchronized") { + if (res.indexOf('<') > -1 || res !== 'Synchronized') { // Wait 2 seconds before trying again await new Promise((resolve) => setTimeout(resolve, 2000)); tries += 1; @@ -1196,16 +1220,15 @@ export const checkArrrSyncStatus = async (seed) => { } // If we exceed 6 tries, throw an error - throw new Error("Failed to synchronize after 36 attempts"); + throw new Error('Failed to synchronize after 36 attempts'); }; - export const publishMultipleQDNResources = async ( data: any, sender, isFromExtension ) => { - const requiredFields = ["resources"]; + const requiredFields = ['resources']; const missingFields: string[] = []; let feeAmount = null; requiredFields.forEach((field) => { @@ -1214,32 +1237,32 @@ export const publishMultipleQDNResources = async ( } }); if (missingFields.length > 0) { - const missingFieldsString = missingFields.join(", "); + const missingFieldsString = missingFields.join(', '); const errorMsg = `Missing fields: ${missingFieldsString}`; throw new Error(errorMsg); } const resources = data.resources; if (!Array.isArray(resources)) { - throw new Error("Invalid data"); + throw new Error('Invalid data'); } if (resources.length === 0) { - throw new Error("No resources to publish"); + throw new Error('No resources to publish'); } - const encrypt = data?.encrypt + const encrypt = data?.encrypt; for (const resource of resources) { - const resourceEncrypt = encrypt && resource?.disableEncrypt !== true - if (!resourceEncrypt && resource?.service.endsWith("_PRIVATE")) { - const errorMsg = "Only encrypted data can go into private services"; - throw new Error(errorMsg) - } else if(resourceEncrypt && !resource?.service.endsWith("_PRIVATE")){ - const errorMsg = "For an encrypted publish please use a service that ends with _PRIVATE"; - throw new Error(errorMsg) + const resourceEncrypt = encrypt && resource?.disableEncrypt !== true; + if (!resourceEncrypt && resource?.service.endsWith('_PRIVATE')) { + const errorMsg = 'Only encrypted data can go into private services'; + throw new Error(errorMsg); + } else if (resourceEncrypt && !resource?.service.endsWith('_PRIVATE')) { + const errorMsg = + 'For an encrypted publish please use a service that ends with _PRIVATE'; + throw new Error(errorMsg); } } - - + // if ( // data.encrypt && // (!data.publicKeys || @@ -1247,35 +1270,35 @@ export const publishMultipleQDNResources = async ( // ) { // throw new Error("Encrypting data requires public keys"); // } - const fee = await getFee("ARBITRARY"); + const fee = await getFee('ARBITRARY'); const registeredName = await getNameInfo(); const name = registeredName; - if(!name){ - throw new Error('You need a Qortal name to publish.') + if (!name) { + throw new Error('You need a Qortal name to publish.'); } - const appFee = data?.appFee ? +data.appFee : undefined - const appFeeRecipient = data?.appFeeRecipient - let hasAppFee = false - if(appFee && appFee > 0 && appFeeRecipient){ - hasAppFee = true + const appFee = data?.appFee ? +data.appFee : undefined; + const appFeeRecipient = data?.appFeeRecipient; + let hasAppFee = false; + if (appFee && appFee > 0 && appFeeRecipient) { + hasAppFee = true; } - const handleDynamicValues = {} - if(hasAppFee){ - const feePayment = await getFee("PAYMENT"); + const handleDynamicValues = {}; + if (hasAppFee) { + const feePayment = await getFee('PAYMENT'); - handleDynamicValues['appFee'] = +appFee + +feePayment.fee, - handleDynamicValues['checkbox1'] = { - value: true, - label: "accept app fee", - } + (handleDynamicValues['appFee'] = +appFee + +feePayment.fee), + (handleDynamicValues['checkbox1'] = { + value: true, + label: 'accept app fee', + }); } - if(data?.encrypt){ - handleDynamicValues['highlightedText'] = `isEncrypted: ${!!data.encrypt}` + if (data?.encrypt) { + handleDynamicValues['highlightedText'] = `isEncrypted: ${!!data.encrypt}`; } const resPermission = await getUserPermission( { - text1: "Do you give this application permission to publish to QDN?", + text1: 'Do you give this application permission to publish to QDN?', html: `
- ${pendingTransactions. - filter((item)=> item.type === 'PAYMENT').map( + ${pendingTransactions + .filter((item) => item.type === 'PAYMENT') + .map( (payment) => `
Recipient: ${ @@ -5079,10 +5123,11 @@ const assetBalance = await getAssetBalanceInfo(assetId)
Amount: ${payment.amount}
` ) - .join("")} - ${[...pendingTransactions, ...pendingAdditionalArbitraryTxs]. - filter((item)=> item.type === 'ARBITRARY').map( - (arbitraryTx) => ` + .join('')} + ${[...pendingTransactions, ...pendingAdditionalArbitraryTxs] + .filter((item) => item.type === 'ARBITRARY') + .map( + (arbitraryTx) => `
Service: ${ arbitraryTx.service @@ -5092,170 +5137,195 @@ const assetBalance = await getAssetBalanceInfo(assetId) arbitraryTx.identifier }
` - ) - .join("")} + ) + .join('')}
`, - highlightedText: `Total Amount: ${totalAmount}`, - fee: fee - }, - isFromExtension - ); - const { accepted, checkbox1 = false } = resPermission; - if (!accepted) { - throw new Error("User declined request"); - } + highlightedText: `Total Amount: ${totalAmount}`, + fee: fee, + }, + isFromExtension + ); + const { accepted, checkbox1 = false } = resPermission; + if (!accepted) { + throw new Error('User declined request'); + } + // const failedTxs = [] + const paymentsDone = {}; + const transactionsDone = []; + for (const transaction of pendingTransactions) { + const type = transaction.type; - // const failedTxs = [] - const paymentsDone = { - - } - - const transactionsDone = [] - - - for (const transaction of pendingTransactions) { - const type = transaction.type; - - if (type === "PAYMENT") { - const makePayment = await retryTransaction( - transferAsset, - [{ amount: transaction.amount, assetId, recipient: transaction.recipientAddress }], true - ); - if (makePayment) { - transactionsDone.push(makePayment?.signature); - if (transaction.paymentRefId) { - paymentsDone[transaction.paymentRefId] = makePayment - } - } - } - else if (type === "ARBITRARY" && paymentsDone[transaction.paymentRefId]) { - const objectToEncrypt = { - data: transaction.base64, - payment: paymentsDone[transaction.paymentRefId], - }; - - const toBase64 = await retryTransaction(objectToBase64, [objectToEncrypt], true); - - if (!toBase64) continue; // Skip if encryption fails - - const encryptDataResponse = await retryTransaction(encryptDataGroup, [ + if (type === 'PAYMENT') { + const makePayment = await retryTransaction( + transferAsset, + [ { - data64: toBase64, - publicKeys: transaction.publicKeys, - privateKey, - userPublicKey, + amount: transaction.amount, + assetId, + recipient: transaction.recipientAddress, }, - ], true); - - if (!encryptDataResponse) continue; // Skip if encryption fails - - const resPublish = await retryTransaction(publishData, [ - { - registeredName: encodeURIComponent(name), - file: encryptDataResponse, - service: transaction.service, - identifier: encodeURIComponent(transaction.identifier), - uploadType: "file", - description: transaction?.description, - isBase64: true, - apiVersion: 2, - withFee: true, - }, - ], true); - - if (resPublish?.signature) { - transactionsDone.push(resPublish?.signature); + ], + true + ); + if (makePayment) { + transactionsDone.push(makePayment?.signature); + if (transaction.paymentRefId) { + paymentsDone[transaction.paymentRefId] = makePayment; } } - } + } else if (type === 'ARBITRARY' && paymentsDone[transaction.paymentRefId]) { + const objectToEncrypt = { + data: transaction.base64, + payment: paymentsDone[transaction.paymentRefId], + }; - for (const transaction of pendingAdditionalArbitraryTxs) { + const toBase64 = await retryTransaction( + objectToBase64, + [objectToEncrypt], + true + ); - const objectToEncrypt = { - data: transaction.base64, - }; - - const toBase64 = await retryTransaction(objectToBase64, [objectToEncrypt], true); - - if (!toBase64) continue; // Skip if encryption fails - - const encryptDataResponse = await retryTransaction(encryptDataGroup, [ + if (!toBase64) continue; // Skip if encryption fails + + const encryptDataResponse = await retryTransaction( + encryptDataGroup, + [ { data64: toBase64, publicKeys: transaction.publicKeys, privateKey, userPublicKey, }, - ], true); - - if (!encryptDataResponse) continue; // Skip if encryption fails - - const resPublish = await retryTransaction(publishData, [ + ], + true + ); + + if (!encryptDataResponse) continue; // Skip if encryption fails + + const resPublish = await retryTransaction( + publishData, + [ { registeredName: encodeURIComponent(name), file: encryptDataResponse, service: transaction.service, identifier: encodeURIComponent(transaction.identifier), - uploadType: "file", + uploadType: 'file', description: transaction?.description, isBase64: true, apiVersion: 2, withFee: true, }, - ], true); - - if (resPublish?.signature) { - transactionsDone.push(resPublish?.signature); - } - + ], + true + ); + + if (resPublish?.signature) { + transactionsDone.push(resPublish?.signature); + } } - - return transactionsDone + } + + for (const transaction of pendingAdditionalArbitraryTxs) { + const objectToEncrypt = { + data: transaction.base64, + }; + + const toBase64 = await retryTransaction( + objectToBase64, + [objectToEncrypt], + true + ); + + if (!toBase64) continue; // Skip if encryption fails + + const encryptDataResponse = await retryTransaction( + encryptDataGroup, + [ + { + data64: toBase64, + publicKeys: transaction.publicKeys, + privateKey, + userPublicKey, + }, + ], + true + ); + + if (!encryptDataResponse) continue; // Skip if encryption fails + + const resPublish = await retryTransaction( + publishData, + [ + { + registeredName: encodeURIComponent(name), + file: encryptDataResponse, + service: transaction.service, + identifier: encodeURIComponent(transaction.identifier), + uploadType: 'file', + description: transaction?.description, + isBase64: true, + apiVersion: 2, + withFee: true, + }, + ], + true + ); + + if (resPublish?.signature) { + transactionsDone.push(resPublish?.signature); + } + } + + return transactionsDone; }; - export const transferAssetRequest = async (data, isFromExtension) => { - const requiredFields = ["amount", "assetId", "recipient"]; + const requiredFields = ['amount', 'assetId', 'recipient']; requiredFields.forEach((field) => { - if (data[field] === undefined || data[field] === null) { - throw new Error(`Missing required field: ${field}`); - } + if (data[field] === undefined || data[field] === null) { + throw new Error(`Missing required field: ${field}`); + } }); - const amount = data.amount - const assetId = data.assetId - const recipient = data.recipient + const amount = data.amount; + const assetId = data.assetId; + const recipient = data.recipient; - - const {fee} = await getFee("TRANSFER_ASSET"); + const { fee } = await getFee('TRANSFER_ASSET'); const balance = await getBalanceInfo(); - if(+balance < +fee) throw new Error('Your QORT balance is insufficient') - const assetBalance = await getAssetBalanceInfo(assetId) - if(assetBalance < amount) throw new Error('Your asset balance is insufficient') + if (+balance < +fee) throw new Error('Your QORT balance is insufficient'); + const assetBalance = await getAssetBalanceInfo(assetId); + if (assetBalance < amount) + throw new Error('Your asset balance is insufficient'); const confirmReceiver = await getNameOrAddress(recipient); if (confirmReceiver.error) { - throw new Error("Invalid receiver address or name"); + throw new Error('Invalid receiver address or name'); } - const assetInfo = await getAssetInfo(assetId) + const assetInfo = await getAssetInfo(assetId); const resPermission = await getUserPermission( { text1: `Do you give this application permission to transfer the following asset?`, text2: `Asset: ${assetInfo?.name}`, highlightedText: `Amount: ${amount}`, - fee: fee + fee: fee, }, isFromExtension ); const { accepted } = resPermission; if (!accepted) { - throw new Error("User declined request"); + throw new Error('User declined request'); } - const res = await transferAsset({amount, recipient: confirmReceiver, assetId}) - return res -} \ No newline at end of file + const res = await transferAsset({ + amount, + recipient: confirmReceiver, + assetId, + }); + return res; +}; diff --git a/src/styles/App-styles.ts b/src/styles/App-styles.ts new file mode 100644 index 0000000..af273e5 --- /dev/null +++ b/src/styles/App-styles.ts @@ -0,0 +1,232 @@ +import { Typography, Box, TextField, InputLabel } from '@mui/material'; +import { styled } from '@mui/system'; + +export const AppContainer = styled(Box)(({ theme }) => ({ + alignItems: 'center', + backgroundColor: theme.palette.background.default, + color: theme.palette.text.primary, + display: 'flex', + flexDirection: 'column', + height: '100vh', + overflow: 'hidden', + radius: '15px', + width: '100vw', +})); + +export const AuthenticatedContainer = styled(Box)(({ theme }) => ({ + backgroundColor: theme.palette.background.default, + color: theme.palette.text.primary, + display: 'flex', + height: '100%', + justifyContent: 'space-between', + width: '100%', +})); + +export const AuthenticatedContainerInnerLeft = styled(Box)(({ theme }) => ({ + alignItems: 'center', + backgroundColor: theme.palette.background.default, + color: theme.palette.text.primary, + display: 'flex', + flexDirection: 'column', + height: '100%', + width: '100%', +})); + +export const AuthenticatedContainerInnerRight = styled(Box)(({ theme }) => ({ + alignItems: 'center', + backgroundColor: theme.palette.background.default, + color: theme.palette.text.primary, + display: 'flex', + flexDirection: 'column', + height: '100%', + width: '60px', +})); + +export const AuthenticatedContainerInnerTop = styled(Box)(({ theme }) => ({ + alignItems: 'center', + backgroundColor: theme.palette.background.default, + color: theme.palette.text.primary, + display: 'flex', + height: '60px', + justifyContent: 'flex-start', + padding: '20px', + width: '100%px', +})); + +export const TextP = styled(Typography)(({ theme }) => ({ + color: theme.palette.text.primary, + fontFamily: 'Inter', + fontSize: '13px', + fontWeight: 600, +})); + +export const TextItalic = styled('span')(({ theme }) => ({ + color: theme.palette.text.primary, + fontFamily: 'Inter', + fontSize: '13px', + fontStyle: 'italic', + fontWeight: 600, +})); + +export const TextSpan = styled('span')(({ theme }) => ({ + color: theme.palette.text.primary, + fontFamily: 'Inter', + fontSize: '13px', + fontWeight: 800, +})); + +export const AddressBox = styled(Box)(({ theme }) => ({ + alignItems: 'center', + backgroundColor: theme.palette.background.default, + borderColor: theme.palette.background.paper, + borderRadius: '100px', + borderStyle: 'solid', + borderWidth: '1px', + color: theme.palette.text.primary, + cursor: 'pointer', + display: 'flex', + fontFamily: 'Inter', + fontSize: '12px', + fontWeight: 600, + gap: '5px', + height: '25px', + justifyContent: 'space-between', + lineHeight: '14.52px', + padding: '5px 15px', + textAlign: 'left', + transition: 'all 0.2s', + width: 'auto', + '&:hover': { + backgroundColor: theme.palette.background.paper, + color: theme.palette.text.secondary, + 'svg path': { + fill: theme.palette.mode === 'dark' ? '#fff' : '#000', + }, + }, +})); + +export const CustomButton = styled(Box)(({ theme }) => ({ + alignItems: 'center', + backgroundColor: theme.palette.background.default, + borderColor: theme.palette.background.paper, + borderRadius: '5px', + borderStyle: 'solid', + borderWidth: '0.5px', + boxSizing: 'border-box', + color: theme.palette.text.primary, + cursor: 'pointer', + display: 'inline-flex', + filter: 'drop-shadow(1px 4px 10.5px rgba(0, 0, 0, 0.3))', + fontFamily: 'Inter', + fontWeight: 600, + gap: '10px', + justifyContent: 'center', + minWidth: '160px', + padding: '15px 20px', + textAlign: 'center', + transition: 'all 0.2s', + width: 'fit-content', + '&:hover': { + backgroundColor: theme.palette.background.paper, + 'svg path': { + fill: theme.palette.background.secondary, + }, + }, +})); + +interface CustomButtonProps { + bgColor?: string; + color?: string; +} + +export const CustomButtonAccept = styled(Box)( + ({ bgColor, color, theme }) => ({ + alignItems: 'center', + backgroundColor: bgColor || theme.palette.background.default, + borderColor: theme.palette.background.paper, + borderRadius: 5, + borderStyle: 'solid', + borderWidth: '0.5px', + boxSizing: 'border-box', + color: color || theme.palette.background.default, + cursor: 'pointer', + display: 'inline-flex', + filter: 'drop-shadow(1px 4px 10.5px rgba(0,0,0,0.3))', + fontFamily: 'Inter', + fontWeight: 600, + gap: '10px', + justifyContent: 'center', + minWidth: 160, + opacity: 0.7, + padding: '15px 20px', + textAlign: 'center', + transition: 'all 0.2s', + width: 'fit-content', + '&:hover': { + opacity: 1, + backgroundColor: bgColor || theme.palette.background.default, + color: color || '#fff', + svg: { + path: { + fill: color || '#fff', + }, + }, + }, + }) +); + +export const CustomInput = styled(TextField)(({ theme }) => ({ + backgroundColor: theme.palette.background.default, + borderColor: theme.palette.background.paper, + borderRadius: '5px', + color: theme.palette.text.primary, + outline: 'none', + width: '183px', // Adjust the width as needed + input: { + fontSize: '12px', + fontFamily: 'Inter', + fontWeight: 400, + color: theme.palette.text.primary, + '&::placeholder': { + fontSize: 16, + color: theme.palette.text.secondary, + }, + outline: 'none', + padding: '10px', + }, + '& .MuiOutlinedInput-root': { + '& fieldset': { + borderColor: theme.palette.background.paper, + borderRadius: '0.5px', + borderStyle: 'solid', + }, + '&:hover fieldset': { + borderColor: theme.palette.background.paper, + borderRadius: '0.5px', + borderStyle: 'solid', + }, + '&.Mui-focused fieldset': { + borderColor: theme.palette.background.paper, + borderRadius: '0.5px', + borderStyle: 'solid', + }, + }, + '& .MuiInput-underline:before': { + borderBottom: 'none', + }, + '& .MuiInput-underline:hover:not(.Mui-disabled):before': { + borderBottom: 'none', + }, + '& .MuiInput-underline:after': { + borderBottom: 'none', + }, +})); + +export const CustomLabel = styled(InputLabel)(({ theme }) => ({ + backgroundColor: theme.palette.background.default, + color: theme.palette.text.primary, + fontFamily: 'Inter', + fontSize: '15px', + fontWeight: 400, + lineHeight: '12px', +})); diff --git a/src/styles/CoreSyncStatus.css b/src/styles/CoreSyncStatus.css new file mode 100644 index 0000000..0cacfec --- /dev/null +++ b/src/styles/CoreSyncStatus.css @@ -0,0 +1,54 @@ +.lineHeight { + line-height: 33%; +} + +.tooltip { + display: inline-block; + position: relative; + text-align: left; +} + +.tooltip .bottom { + border-radius: 8px; + border: 1px solid var(--black); + box-shadow: 0 1px 8px rgba(0, 0, 0, 0.5); + box-sizing: border-box; + font-size: 13px; + font-weight: normal; + max-width: 250px; + min-width: 225px; + opacity: 0; + padding: 10px 10px; + position: absolute; + right: 0px; + top: 35px; + transition: opacity 0.2s; + visibility: hidden; + z-index: 99999999; +} + +.tooltip[data-theme='light'] .bottom { + background-color: #f1f1f1; + color: #000000; +} + +.tooltip[data-theme='dark'] .bottom { + background-color: var(--bg-2); + color: var(--black); +} + +.tooltip:hover .bottom { + visibility: visible; + opacity: 1; + z-index: 100; +} + +.tooltip .bottom i { + bottom: 100%; + height: 12px; + left: 50%; + margin-left: -12px; + overflow: hidden; + position: absolute; + width: 24px; +} diff --git a/src/index.css b/src/styles/index.css similarity index 52% rename from src/index.css rename to src/styles/index.css index 867382c..ce338ba 100644 --- a/src/index.css +++ b/src/styles/index.css @@ -1,50 +1,24 @@ @font-face { font-family: 'Inter'; - src: url('./styles/fonts/Inter-SemiBold.ttf') format('truetype'); + src: url('./fonts/Inter-SemiBold.ttf') format('truetype'); font-weight: 600; } - @font-face { font-family: 'Inter'; - src: url('./styles/fonts/Inter-ExtraBold.ttf') format('truetype'); + src: url('./fonts/Inter-ExtraBold.ttf') format('truetype'); font-weight: 800; } @font-face { font-family: 'Inter'; - src: url('./styles/fonts/Inter-Bold.ttf') format('truetype'); + src: url('./fonts/Inter-Bold.ttf') format('truetype'); font-weight: 700; } @font-face { font-family: 'Inter'; - src: url('./styles/fonts/Inter-Regular.ttf') format('truetype'); + src: url('./fonts/Inter-Regular.ttf') format('truetype'); font-weight: 400; } - -:root { - padding: 0px; - margin: 0px; - box-sizing: border-box !important; - word-break: break-word; - --color-instance : #1E1E20; - --color-instance-popover-bg: #222222; - --Mail-Background: rgba(49, 51, 56, 1); - --new-message-text: black; - - --bg-primary : rgba(31, 32, 35, 1); - --bg-2: #27282c; - --bg-3: rgba(0, 0, 0, 0.1); - --unread: #4297e2; - --danger: #B14646; - --apps-circle: #1F2023; - --green: #5EB049; -} - -body { - margin: 0px; - overflow: hidden; -} - .image-container { position: relative; } @@ -68,43 +42,18 @@ body { opacity: 0.6; } -::-webkit-scrollbar-track { - background-color: transparent; -} -::-webkit-scrollbar-track:hover { - background-color: transparent; -} - -::-webkit-scrollbar { - width: 16px; - height: 10px; -} - -::-webkit-scrollbar-thumb { - background-color: #444444; - border-radius: 8px; - background-clip: content-box; - border: 4px solid transparent; - transition: 0.3s background-color; -} -::-webkit-scrollbar-thumb:hover { - background-color: #363636; -} - @property --var1 { - syntax: ""; + syntax: ''; inherits: true; initial-value: transparent; } - .scrollable-container { transition: --var1 0.4s; - } .scrollable-container:hover { - --var1: #444444; + --var1: var(--primary-main); } .scrollable-container::-webkit-scrollbar-thumb { @@ -115,11 +64,6 @@ body { opacity: 0; } - - - - - /* Mobile-specific scrollbar styles */ @media only screen and (max-width: 600px) { ::-webkit-scrollbar { @@ -137,11 +81,7 @@ body { background-color: whitesmoke; } -html, body { - overscroll-behavior:none !important; +html, +body { + overscroll-behavior: none !important; } - -.swiper { - width: 100%; -} - diff --git a/src/styles/theme-common.ts b/src/styles/theme-common.ts new file mode 100644 index 0000000..600b6da --- /dev/null +++ b/src/styles/theme-common.ts @@ -0,0 +1,85 @@ +// Extend the Theme interface +const commonThemeOptions = { + typography: { + fontFamily: ['Inter'].join(','), + h1: { + fontSize: '2rem', + fontWeight: 600, + }, + h2: { + fontSize: '1.75rem', + fontWeight: 500, + }, + h3: { + fontSize: '1.5rem', + fontWeight: 500, + }, + h4: { + fontSize: '1.25rem', + fontWeight: 500, + }, + h5: { + fontSize: '1rem', + fontWeight: 500, + }, + h6: { + fontSize: '0.875rem', + fontWeight: 500, + }, + body: { + margin: '0px', + overflow: 'hidden', + }, + body1: { + fontSize: '16px', + fontWeight: 400, + lineHeight: 1.5, + letterSpacing: 'normal', + }, + body2: { + fontSize: '18px', + fontWeight: 400, + lineHeight: 1.4, + letterSpacing: '0.2px', + }, + }, + spacing: 8, + shape: { + borderRadius: 4, + }, + breakpoints: { + values: { + xs: 0, + sm: 600, + md: 900, + lg: 1200, + xl: 1536, + }, + }, + components: { + MuiButton: { + styleOverrides: { + root: { + backgroundColor: 'inherit', + transition: 'filter 0.3s ease-in-out', + '&:hover': { + filter: 'brightness(1.1)', + }, + }, + }, + defaultProps: { + disableElevation: true, + disableRipple: true, + }, + }, + MuiModal: { + styleOverrides: { + root: { + zIndex: 50000, + }, + }, + }, + }, +}; + +export { commonThemeOptions }; diff --git a/src/styles/theme-dark.ts b/src/styles/theme-dark.ts new file mode 100644 index 0000000..142ee54 --- /dev/null +++ b/src/styles/theme-dark.ts @@ -0,0 +1,125 @@ +import { createTheme, ThemeOptions } from '@mui/material/styles'; +import { commonThemeOptions } from './theme-common'; + +export const darkThemeOptions: ThemeOptions = { + ...commonThemeOptions, + palette: { + mode: 'dark', + primary: { + main: 'rgb(100, 155, 240)', + dark: 'rgb(45, 92, 201)', + light: 'rgb(130, 185, 255)', + }, + secondary: { + main: 'rgb(69, 173, 255)', + }, + background: { + default: 'rgb(49, 51, 56)', + paper: 'rgb(62, 64, 68)', + surface: 'rgb(58, 60, 65)', + }, + text: { + primary: 'rgb(255, 255, 255)', + secondary: 'rgb(179, 179, 179)', + }, + border: { + main: 'rgba(255, 255, 255, 0.12)', + subtle: 'rgba(255, 255, 255, 0.08)', + }, + other: { + positive: 'rgb(94, 176, 73)', + danger: 'rgb(177, 70, 70)', + unread: 'rgb(66, 151, 226)', + }, + }, + + components: { + MuiCard: { + styleOverrides: { + root: { + boxShadow: 'none', + borderRadius: '8px', + transition: 'all 0.3s ease-in-out', + '&:hover': { + cursor: 'pointer', + boxShadow: + ' 0px 3px 4px 0px hsla(0,0%,0%,0.14), 0px 3px 3px -2px hsla(0,0%,0%,0.12), 0px 1px 8px 0px hsla(0,0%,0%,0.2);', + }, + }, + }, + }, + MuiCssBaseline: { + styleOverrides: (theme) => ({ + ':root': { + '--Mail-Background': 'rgb(43, 43, 43)', + '--bg-primary': 'rgba(31, 32, 35, 1)', + '--bg-2': 'rgb(39, 40, 44)', + '--primary-main': theme.palette.primary.main, + }, + '*, *::before, *::after': { + boxSizing: 'border-box', + }, + html: { + padding: 0, + margin: 0, + }, + body: { + padding: 0, + margin: 0, + wordBreak: 'break-word', + backgroundColor: 'var(--bg-primary)', + }, + '::-webkit-scrollbar-track': { + backgroundColor: 'transparent', + }, + + '::-webkit-scrollbar-track:hover': { + backgroundColor: 'transparent', + }, + + '::-webkit-scrollbar': { + width: '16px', + height: '10px', + }, + + '::-webkit-scrollbar-thumb': { + backgroundColor: theme.palette.primary.main, + borderRadius: '8px', + backgroundClip: 'content-box', + border: '4px solid transparent', + transition: '0.3s background-color', + }, + + '::-webkit-scrollbar-thumb:hover': { + backgroundColor: theme.palette.primary.light, + }, + }), + }, + MuiIcon: { + defaultProps: { + style: { + color: 'rgb(255, 255, 255)', + opacity: 0.5, + }, + }, + }, + MuiDialog: { + styleOverrides: { + paper: { + backgroundImage: 'none', + }, + }, + }, + MuiPopover: { + styleOverrides: { + paper: { + backgroundImage: 'none', + }, + }, + }, + }, +}; + +const darkTheme = createTheme(darkThemeOptions); + +export { darkTheme }; diff --git a/src/styles/theme-light.ts b/src/styles/theme-light.ts new file mode 100644 index 0000000..26ca782 --- /dev/null +++ b/src/styles/theme-light.ts @@ -0,0 +1,130 @@ +import { createTheme, ThemeOptions } from '@mui/material/styles'; +import { commonThemeOptions } from './theme-common'; + +export const lightThemeOptions: ThemeOptions = { + ...commonThemeOptions, + palette: { + mode: 'light', + primary: { + main: 'rgb(162, 162, 221)', + dark: 'rgb(113, 198, 212)', + light: 'rgb(180, 200, 235)', + }, + secondary: { + main: 'rgba(194, 222, 236, 1)', + }, + background: { + default: 'rgba(250, 250, 250, 1)', + paper: 'rgb(220, 220, 220)', // darker card background + surface: 'rgb(240, 240, 240)', // optional middle gray for replies, side panels + }, + text: { + primary: 'rgba(0, 0, 0, 0.87)', // 87% black (slightly softened) + secondary: 'rgba(0, 0, 0, 0.6)', // 60% black + }, + border: { + main: 'rgba(0, 0, 0, 0.12)', + subtle: 'rgba(0, 0, 0, 0.08)', + }, + other: { + positive: 'rgb(94, 176, 73)', + danger: 'rgb(177, 70, 70)', + unread: 'rgb(66, 151, 226)', + }, + }, + components: { + MuiCard: { + styleOverrides: { + root: { + boxShadow: + 'rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(230, 200, 200, 0.06) 0px 1px 2px 0px;', + borderRadius: '8px', + transition: 'all 0.3s ease-in-out', + '&:hover': { + cursor: 'pointer', + boxShadow: + 'rgba(0, 0, 0, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px;', + }, + }, + }, + }, + MuiCssBaseline: { + styleOverrides: (theme) => ({ + ':root': { + '--Mail-Background': 'rgba(49, 51, 56, 1)', + '--bg-primary': 'rgba(31, 32, 35, 1)', + '--bg-2': 'rgba(39, 40, 44, 1)', + '--primary-main': theme.palette.primary.main, + }, + + '*, *::before, *::after': { + boxSizing: 'border-box', + }, + + html: { + padding: 0, + margin: 0, + }, + + body: { + padding: 0, + margin: 0, + wordBreak: 'break-word', + backgroundColor: 'var(--bg-primary)', + }, + + '::-webkit-scrollbar-track': { + backgroundColor: 'transparent', + }, + + '::-webkit-scrollbar-track:hover': { + backgroundColor: 'transparent', + }, + + '::-webkit-scrollbar': { + width: '16px', + height: '10px', + }, + + '::-webkit-scrollbar-thumb': { + backgroundColor: theme.palette.primary.main, + borderRadius: '8px', + backgroundClip: 'content-box', + border: '4px solid transparent', + transition: '0.3s background-color', + }, + + '::-webkit-scrollbar-thumb:hover': { + backgroundColor: theme.palette.primary.light, + }, + }), + }, + + MuiIcon: { + defaultProps: { + style: { + color: 'rgba(0, 0, 0, 1)', + opacity: 0.5, + }, + }, + }, + MuiDialog: { + styleOverrides: { + paper: { + backgroundImage: 'none', + }, + }, + }, + MuiPopover: { + styleOverrides: { + paper: { + backgroundImage: 'none', + }, + }, + }, + }, +}; + +const lightTheme = createTheme(lightThemeOptions); + +export { lightTheme }; diff --git a/src/styles/theme.d.ts b/src/styles/theme.d.ts new file mode 100644 index 0000000..e10c5c1 --- /dev/null +++ b/src/styles/theme.d.ts @@ -0,0 +1,29 @@ +import '@mui/material/styles'; + +declare module '@mui/material/styles' { + interface TypeBackground { + surface: string; + } + interface Palette { + border: { + main: string; + subtle: string; + }; + other: { + positive: string; + danger: string; + unread: string; + }; + } + interface PaletteOptions { + border?: { + main?: string; + subtle?: string; + }; + other?: { + positive?: string; + danger?: string; + unread?: string; + }; + } +} diff --git a/src/styles/theme.ts b/src/styles/theme.ts deleted file mode 100644 index feb0367..0000000 --- a/src/styles/theme.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { createTheme } from '@mui/material/styles' - - -// Extend the Theme interface - -const commonThemeOptions = { - typography: { - fontFamily: [ - 'Roboto' - ].join(','), - h1: { - fontSize: '2rem', - fontWeight: 600 - }, - h2: { - fontSize: '1.75rem', - fontWeight: 500 - }, - h3: { - fontSize: '1.5rem', - fontWeight: 500 - }, - h4: { - fontSize: '1.25rem', - fontWeight: 500 - }, - h5: { - fontSize: '1rem', - fontWeight: 500 - }, - h6: { - fontSize: '0.875rem', - fontWeight: 500 - }, - body1: { - fontSize: '23px', - fontWeight: 400, - lineHeight: 1.5, - letterSpacing: '0.5px' - }, - - body2: { - fontSize: '18px', - fontWeight: 400, - lineHeight: 1.4, - letterSpacing: '0.2px' - } - }, - spacing: 8, - shape: { - borderRadius: 4 - }, - breakpoints: { - values: { - xs: 0, - sm: 600, - md: 900, - lg: 1200, - xl: 1536 - } - }, - components: { - MuiButton: { - styleOverrides: { - root: { - backgroundColor: 'inherit', - transition: 'filter 0.3s ease-in-out', - '&:hover': { - filter: 'brightness(1.1)' - } - } - }, - defaultProps: { - disableElevation: true, - disableRipple: true - } - }, - MuiModal: { - styleOverrides: { - root: { - zIndex: 50000, - }, - } - - } - } -} - -const lightTheme = createTheme({ - ...commonThemeOptions, - palette: { - mode: 'light', - primary: { - main: '#f4f4fb', - dark: '#eaecf4', - light: '#f9f9fd' - }, - secondary: { - main: '#1EAAF1' - }, - background: { - default: '#fafafa', - paper: '#f0f0f0' - }, - text: { - primary: '#000000', - secondary: '#525252' - } - }, - - components: { - MuiCard: { - styleOverrides: { - root: { - boxShadow: - 'rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.06) 0px 1px 2px 0px;', - borderRadius: '8px', - transition: 'all 0.3s ease-in-out', - '&:hover': { - cursor: 'pointer', - boxShadow: - 'rgba(0, 0, 0, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px;' - } - } - } - }, - MuiIcon: { - defaultProps: { - style: { - color: '#000000' - } - } - } - }, -}) - -const darkTheme = createTheme({ - ...commonThemeOptions, - palette: { - mode: 'dark', - primary: { - main: '#2e3d60', - dark: "#1a2744", - light: "#3f4b66", - }, - secondary: { - main: '#45adff' - }, - - background: { - default: '#313338', - paper: "#1e1e20" - }, - text: { - primary: '#ffffff', - secondary: '#b3b3b3' - } - }, - components: { - MuiCard: { - styleOverrides: { - root: { - boxShadow: "none", - borderRadius: '8px', - transition: 'all 0.3s ease-in-out', - '&:hover': { - cursor: 'pointer', - boxShadow: - ' 0px 3px 4px 0px hsla(0,0%,0%,0.14), 0px 3px 3px -2px hsla(0,0%,0%,0.12), 0px 1px 8px 0px hsla(0,0%,0%,0.2);' - } - } - } - }, - MuiIcon: { - defaultProps: { - style: { - color: '#ffffff' - } - } - } - }, -}) - -export { lightTheme, darkTheme } diff --git a/src/useAppFullscreen.tsx b/src/useAppFullscreen.tsx index 3dbe4a1..1ce6842 100644 --- a/src/useAppFullscreen.tsx +++ b/src/useAppFullscreen.tsx @@ -1,67 +1,85 @@ import { useCallback, useEffect } from 'react'; -import { isMobile } from './App'; export const useAppFullScreen = (setFullScreen) => { - const enterFullScreen = useCallback(() => { - const element = document.documentElement; // Target the entire HTML document - if (element.requestFullscreen) { - element.requestFullscreen(); - } else if (element.mozRequestFullScreen) { // Firefox - element.mozRequestFullScreen(); - } else if (element.webkitRequestFullscreen) { // Chrome, Safari and Opera - element.webkitRequestFullscreen(); - } else if (element.msRequestFullscreen) { // IE/Edge - element.msRequestFullscreen(); - } - }, []); + const enterFullScreen = useCallback(() => { + const element = document.documentElement; // Target the entire HTML document + if (element.requestFullscreen) { + element.requestFullscreen(); + } else if (element.mozRequestFullScreen) { + // Firefox + element.mozRequestFullScreen(); + } else if (element.webkitRequestFullscreen) { + // Chrome, Safari and Opera + element.webkitRequestFullscreen(); + } else if (element.msRequestFullscreen) { + // IE/Edge + element.msRequestFullscreen(); + } + }, []); - const exitFullScreen = useCallback(() => { - if (document.fullscreenElement) { - document.exitFullscreen(); - } else if (document.mozFullScreenElement) { - document.mozCancelFullScreen(); - } else if (document.webkitFullscreenElement) { - document.webkitExitFullscreen(); - } else if (document.msFullscreenElement) { - document.msExitFullscreen(); - } - }, []); + const exitFullScreen = useCallback(() => { + if (document.fullscreenElement) { + document.exitFullscreen(); + } else if (document.mozFullScreenElement) { + document.mozCancelFullScreen(); + } else if (document.webkitFullscreenElement) { + document.webkitExitFullscreen(); + } else if (document.msFullscreenElement) { + document.msExitFullscreen(); + } + }, []); - const toggleFullScreen = useCallback(() => { - if(!isMobile || isMobile) return - if (document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement) { - exitFullScreen(); - setFullScreen(false) - } else { - enterFullScreen(); - setFullScreen(true) - } - }, [enterFullScreen, exitFullScreen]); + const toggleFullScreen = useCallback(() => { + if ( + document.fullscreenElement || + document.mozFullScreenElement || + document.webkitFullscreenElement || + document.msFullscreenElement + ) { + exitFullScreen(); + setFullScreen(false); + } else { + enterFullScreen(); + setFullScreen(true); + } + }, [enterFullScreen, exitFullScreen]); - // Listen for changes to fullscreen state - useEffect(() => { - const handleFullScreenChange = () => { - if (document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement) { - - } else { - setFullScreen(false); - } - }; + // Listen for changes to fullscreen state + useEffect(() => { + const handleFullScreenChange = () => { + if ( + document.fullscreenElement || + document.mozFullScreenElement || + document.webkitFullscreenElement || + document.msFullscreenElement + ) { + // TODO check empty block + } else { + setFullScreen(false); + } + }; - document.addEventListener('fullscreenchange', handleFullScreenChange); - document.addEventListener('webkitfullscreenchange', handleFullScreenChange); // Safari - document.addEventListener('mozfullscreenchange', handleFullScreenChange); // Firefox - document.addEventListener('MSFullscreenChange', handleFullScreenChange); // IE/Edge + document.addEventListener('fullscreenchange', handleFullScreenChange); + document.addEventListener('webkitfullscreenchange', handleFullScreenChange); // Safari + document.addEventListener('mozfullscreenchange', handleFullScreenChange); // Firefox + document.addEventListener('MSFullscreenChange', handleFullScreenChange); // IE/Edge - return () => { - document.removeEventListener('fullscreenchange', handleFullScreenChange); - document.removeEventListener('webkitfullscreenchange', handleFullScreenChange); - document.removeEventListener('mozfullscreenchange', handleFullScreenChange); - document.removeEventListener('MSFullscreenChange', handleFullScreenChange); - }; - }, []); + return () => { + document.removeEventListener('fullscreenchange', handleFullScreenChange); + document.removeEventListener( + 'webkitfullscreenchange', + handleFullScreenChange + ); + document.removeEventListener( + 'mozfullscreenchange', + handleFullScreenChange + ); + document.removeEventListener( + 'MSFullscreenChange', + handleFullScreenChange + ); + }; + }, []); - return { enterFullScreen, exitFullScreen, toggleFullScreen }; + return { enterFullScreen, exitFullScreen, toggleFullScreen }; }; - - diff --git a/src/useQortalGetSaveSettings.tsx b/src/useQortalGetSaveSettings.tsx index 532e049..66691b7 100644 --- a/src/useQortalGetSaveSettings.tsx +++ b/src/useQortalGetSaveSettings.tsx @@ -1,97 +1,129 @@ -import React, { useCallback, useEffect } from 'react' -import { useRecoilState, useSetRecoilState } from 'recoil'; -import { canSaveSettingToQdnAtom, isUsingImportExportSettingsAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from './atoms/global'; +import { useCallback, useEffect } from 'react'; +import { + canSaveSettingToQdnAtom, + isUsingImportExportSettingsAtom, + oldPinnedAppsAtom, + settingsLocalLastUpdatedAtom, + settingsQDNLastUpdatedAtom, + sortablePinnedAppsAtom, +} from './atoms/global'; import { getArbitraryEndpointReact, getBaseApiReact } from './App'; import { decryptResource } from './components/Group/Group'; -import { base64ToUint8Array, uint8ArrayToObject } from './backgroundFunctions/encryption'; +import { + base64ToUint8Array, + uint8ArrayToObject, +} from './backgroundFunctions/encryption'; +import { useAtom, useSetAtom } from 'jotai'; function fetchFromLocalStorage(key) { - try { - const serializedValue = localStorage.getItem(key); - if (serializedValue === null) { - console.log(`No data found for key: ${key}`); - return null; - } - return JSON.parse(serializedValue); - } catch (error) { - console.error('Error fetching from localStorage:', error); - return null; + try { + const serializedValue = localStorage.getItem(key); + if (serializedValue === null) { + console.log(`No data found for key: ${key}`); + return null; } + return JSON.parse(serializedValue); + } catch (error) { + console.error('Error fetching from localStorage:', error); + return null; + } } const getPublishRecord = async (myName) => { - // const validApi = await findUsableApi(); - const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT_PRIVATE&identifier=ext_saved_settings&exactmatchnames=true&limit=1&prefix=true&name=${myName}`; - const response = await fetch(url); - if (!response.ok) { - throw new Error("network error"); - } - const publishData = await response.json(); + // const validApi = await findUsableApi(); + const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT_PRIVATE&identifier=ext_saved_settings&exactmatchnames=true&limit=1&prefix=true&name=${myName}`; + const response = await fetch(url); + if (!response.ok) { + throw new Error('network error'); + } + const publishData = await response.json(); - if(publishData?.length > 0) return {hasPublishRecord: true, timestamp: publishData[0]?.updated || publishData[0].created} + if (publishData?.length > 0) + return { + hasPublishRecord: true, + timestamp: publishData[0]?.updated || publishData[0].created, + }; - return {hasPublishRecord: false} - }; - const getPublish = async (myName) => { - try { - let data + return { hasPublishRecord: false }; +}; + +const getPublish = async (myName) => { + try { + let data; const res = await fetch( - `${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${myName}/ext_saved_settings?encoding=base64` - ); - data = await res.text(); - - if(!data) throw new Error('Unable to fetch publish') + `${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${myName}/ext_saved_settings?encoding=base64` + ); + data = await res.text(); + + if (!data) throw new Error('Unable to fetch publish'); const decryptedKey: any = await decryptResource(data); - const dataint8Array = base64ToUint8Array(decryptedKey.data); const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); - return decryptedKeyToObject - } catch (error) { - return null - } - }; - + return decryptedKeyToObject; + } catch (error) { + return null; + } +}; export const useQortalGetSaveSettings = (myName, isAuthenticated) => { - const setSortablePinnedApps = useSetRecoilState(sortablePinnedAppsAtom); - const setCanSave = useSetRecoilState(canSaveSettingToQdnAtom); - const setSettingsQDNLastUpdated = useSetRecoilState(settingsQDNLastUpdatedAtom); - const [settingsLocalLastUpdated] = useRecoilState(settingsLocalLastUpdatedAtom); - const [isUsingImportExportSettings] = useRecoilState(isUsingImportExportSettingsAtom); + const setSortablePinnedApps = useSetAtom(sortablePinnedAppsAtom); + const setCanSave = useSetAtom(canSaveSettingToQdnAtom); + const setSettingsQDNLastUpdated = useSetAtom(settingsQDNLastUpdatedAtom); - const [oldPinnedApps, setOldPinnedApps] = useRecoilState(oldPinnedAppsAtom) + const [settingsLocalLastUpdated] = useAtom(settingsLocalLastUpdatedAtom); + const [isUsingImportExportSettings] = useAtom( + isUsingImportExportSettingsAtom + ); + const setOldPinnedApps = useSetAtom(oldPinnedAppsAtom); - const getSavedSettings = useCallback(async (myName, settingsLocalLastUpdated)=> { - try { - const {hasPublishRecord, timestamp} = await getPublishRecord(myName) - if(hasPublishRecord){ - const settings = await getPublish(myName) - if(settings?.sortablePinnedApps && timestamp > settingsLocalLastUpdated){ - setSortablePinnedApps(settings.sortablePinnedApps) - - setSettingsQDNLastUpdated(timestamp || 0) - } else if(settings?.sortablePinnedApps){ - setSettingsQDNLastUpdated(timestamp || 0) - setOldPinnedApps(settings.sortablePinnedApps) - } - if(!settings){ - // set -100 to indicate that it couldn't fetch the publish - setSettingsQDNLastUpdated(-100) + const getSavedSettings = useCallback( + async (myName, settingsLocalLastUpdated) => { + try { + const { hasPublishRecord, timestamp } = await getPublishRecord(myName); + if (hasPublishRecord) { + const settings = await getPublish(myName); + if ( + settings?.sortablePinnedApps && + timestamp > settingsLocalLastUpdated + ) { + setSortablePinnedApps(settings.sortablePinnedApps); - } - } else { - setSettingsQDNLastUpdated( 0) - } - setCanSave(true) - } catch (error) { - + setSettingsQDNLastUpdated(timestamp || 0); + } else if (settings?.sortablePinnedApps) { + setSettingsQDNLastUpdated(timestamp || 0); + setOldPinnedApps(settings.sortablePinnedApps); + } + if (!settings) { + // set -100 to indicate that it couldn't fetch the publish + setSettingsQDNLastUpdated(-100); + } + } else { + setSettingsQDNLastUpdated(0); } - }, []) - useEffect(()=> { - if(!myName || !settingsLocalLastUpdated || !isAuthenticated || isUsingImportExportSettings === null) return - if(isUsingImportExportSettings) return - getSavedSettings(myName, settingsLocalLastUpdated) - }, [getSavedSettings, myName, settingsLocalLastUpdated, isAuthenticated, isUsingImportExportSettings]) - -} + setCanSave(true); + } catch (error) { + console.log(error); + } + }, + [] + ); + + useEffect(() => { + if ( + !myName || + !settingsLocalLastUpdated || + !isAuthenticated || + isUsingImportExportSettings === null + ) + return; + if (isUsingImportExportSettings) return; + getSavedSettings(myName, settingsLocalLastUpdated); + }, [ + getSavedSettings, + myName, + settingsLocalLastUpdated, + isAuthenticated, + isUsingImportExportSettings, + ]); +}; diff --git a/src/useRetrieveDataLocalStorage.tsx b/src/useRetrieveDataLocalStorage.tsx index fafcd5c..6ddc673 100644 --- a/src/useRetrieveDataLocalStorage.tsx +++ b/src/useRetrieveDataLocalStorage.tsx @@ -1,55 +1,61 @@ -import React, { useCallback, useEffect } from 'react' -import { useSetRecoilState } from 'recoil'; -import { isUsingImportExportSettingsAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from './atoms/global'; +import { useCallback, useEffect } from 'react'; +import { + isUsingImportExportSettingsAtom, + oldPinnedAppsAtom, + settingsLocalLastUpdatedAtom, + settingsQDNLastUpdatedAtom, + sortablePinnedAppsAtom, +} from './atoms/global'; +import { useSetAtom } from 'jotai'; function fetchFromLocalStorage(key) { - try { - const serializedValue = localStorage.getItem(key); - if (serializedValue === null) { - return null; - } - return JSON.parse(serializedValue); - } catch (error) { - console.error('Error fetching from localStorage:', error); - return null; + try { + const serializedValue = localStorage.getItem(key); + if (serializedValue === null) { + return null; } + return JSON.parse(serializedValue); + } catch (error) { + console.error('Error fetching from localStorage:', error); + return null; + } } export const useRetrieveDataLocalStorage = (address) => { - const setSortablePinnedApps = useSetRecoilState(sortablePinnedAppsAtom); - const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom); - const setIsUsingImportExportSettings = useSetRecoilState(isUsingImportExportSettingsAtom) - const setSettingsQDNLastUpdated = useSetRecoilState(settingsQDNLastUpdatedAtom); - const setOldPinnedApps = useSetRecoilState(oldPinnedAppsAtom) + const setSortablePinnedApps = useSetAtom(sortablePinnedAppsAtom); + const setSettingsLocalLastUpdated = useSetAtom(settingsLocalLastUpdatedAtom); + const setIsUsingImportExportSettings = useSetAtom( + isUsingImportExportSettingsAtom + ); + const setSettingsQDNLastUpdated = useSetAtom(settingsQDNLastUpdatedAtom); + const setOldPinnedApps = useSetAtom(oldPinnedAppsAtom); - const getSortablePinnedApps = useCallback(()=> { - const pinnedAppsLocal = fetchFromLocalStorage('ext_saved_settings') - if(pinnedAppsLocal?.sortablePinnedApps){ - setSortablePinnedApps(pinnedAppsLocal?.sortablePinnedApps) - setSettingsLocalLastUpdated(pinnedAppsLocal?.timestamp || -1) - } else { - setSettingsLocalLastUpdated(-1) - } - - }, []) - const getSortablePinnedAppsImportExport = useCallback(()=> { - const pinnedAppsLocal = fetchFromLocalStorage('ext_saved_settings_import_export') - if(pinnedAppsLocal?.sortablePinnedApps){ - setOldPinnedApps(pinnedAppsLocal?.sortablePinnedApps) - - - setIsUsingImportExportSettings(true) - setSettingsQDNLastUpdated(pinnedAppsLocal?.timestamp || 0) - - } else { - setIsUsingImportExportSettings(false) - } - - }, []) - useEffect(()=> { - - getSortablePinnedApps() - getSortablePinnedAppsImportExport() - }, [getSortablePinnedApps, address]) - -} + const getSortablePinnedApps = useCallback(() => { + const pinnedAppsLocal = fetchFromLocalStorage('ext_saved_settings'); + + if (pinnedAppsLocal?.sortablePinnedApps) { + setSortablePinnedApps(pinnedAppsLocal?.sortablePinnedApps); + setSettingsLocalLastUpdated(pinnedAppsLocal?.timestamp || -1); + } else { + setSettingsLocalLastUpdated(-1); + } + }, []); + + const getSortablePinnedAppsImportExport = useCallback(() => { + const pinnedAppsLocal = fetchFromLocalStorage( + 'ext_saved_settings_import_export' + ); + if (pinnedAppsLocal?.sortablePinnedApps) { + setOldPinnedApps(pinnedAppsLocal?.sortablePinnedApps); + setIsUsingImportExportSettings(true); + setSettingsQDNLastUpdated(pinnedAppsLocal?.timestamp || 0); + } else { + setIsUsingImportExportSettings(false); + } + }, []); + + useEffect(() => { + getSortablePinnedApps(); + getSortablePinnedAppsImportExport(); + }, [getSortablePinnedApps, address]); +}; diff --git a/src/utils/fileReading/index.ts b/src/utils/fileReading/index.ts index a6295c8..a04435c 100644 --- a/src/utils/fileReading/index.ts +++ b/src/utils/fileReading/index.ts @@ -1,63 +1,93 @@ // @ts-nocheck class Semaphore { - constructor(count) { - this.count = count - this.waiting = [] - } - acquire() { - return new Promise(resolve => { - if (this.count > 0) { - this.count-- - resolve() - } else { - this.waiting.push(resolve) - } - }) - } - release() { - if (this.waiting.length > 0) { - const resolve = this.waiting.shift() - resolve() - } else { - this.count++ - } - } + constructor(count) { + this.count = count; + this.waiting = []; + } + acquire() { + return new Promise((resolve) => { + if (this.count > 0) { + this.count--; + resolve(); + } else { + this.waiting.push(resolve); + } + }); + } + release() { + if (this.waiting.length > 0) { + const resolve = this.waiting.shift(); + resolve(); + } else { + this.count++; + } + } } -let semaphore = new Semaphore(1) -let reader = new FileReader() +let semaphore = new Semaphore(1); +let reader = new FileReader(); -export const fileToBase64 = (file) => new Promise(async (resolve, reject) => { - const reader = new FileReader(); // Create a new instance - await semaphore.acquire(); - reader.readAsDataURL(file); - reader.onload = () => { - const dataUrl = reader.result; - semaphore.release(); - if (typeof dataUrl === 'string') { - resolve(dataUrl.split(',')[1]); - } else { - reject(new Error('Invalid data URL')); - } - reader.onload = null; // Clear the handler - reader.onerror = null; // Clear the handle - }; - reader.onerror = (error) => { - semaphore.release(); - reject(error); - reader.onload = null; // Clear the handler - reader.onerror = null; // Clear the handle - }; +export const fileToBase64 = (file) => + new Promise(async (resolve, reject) => { + const reader = new FileReader(); // Create a new instance + await semaphore.acquire(); + reader.readAsDataURL(file); + reader.onload = () => { + const dataUrl = reader.result; + semaphore.release(); + if (typeof dataUrl === 'string') { + resolve(dataUrl.split(',')[1]); + } else { + reject(new Error('Invalid data URL')); + } + reader.onload = null; // Clear the handler + reader.onerror = null; // Clear the handle + }; + reader.onerror = (error) => { + semaphore.release(); + reject(error); + reader.onload = null; // Clear the handler + reader.onerror = null; // Clear the handle + }; }); - -export const base64ToBlobUrl = (base64, mimeType = "image/png") => { - const binary = atob(base64); - const array = []; - for (let i = 0; i < binary.length; i++) { - array.push(binary.charCodeAt(i)); - } - const blob = new Blob([new Uint8Array(array)], { type: mimeType }); - return URL.createObjectURL(blob); - }; \ No newline at end of file +export const base64ToBlobUrl = (base64, mimeType = 'image/png') => { + const binary = atob(base64); + const array = []; + for (let i = 0; i < binary.length; i++) { + array.push(binary.charCodeAt(i)); + } + const blob = new Blob([new Uint8Array(array)], { type: mimeType }); + return URL.createObjectURL(blob); +}; + +export const handleImportClick = async (fileTypes) => { + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.accept = fileTypes; + + // Create a promise to handle file selection and reading synchronously + return await new Promise((resolve, reject) => { + fileInput.onchange = () => { + const file = fileInput.files[0]; + if (!file) { + reject(new Error('No file selected')); + return; + } + + const reader = new FileReader(); + reader.onload = (e) => { + resolve(e.target.result); // Resolve with the file content + }; + reader.onerror = () => { + reject(new Error('Error reading file')); + }; + + reader.readAsText(file); // Read the file as text (Base64 string) + }; + + // Trigger the file input dialog + fileInput.click(); + }); +}; diff --git a/src/utils/mobile/mobileUtils.ts b/src/utils/mobile/mobileUtils.ts deleted file mode 100644 index 144c8c1..0000000 --- a/src/utils/mobile/mobileUtils.ts +++ /dev/null @@ -1,5 +0,0 @@ - -export const getRootHeight = ()=> { - - return document?.getElementById('root')?.style?.height || '100%' -} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index e49f745..24c5d39 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", + "noImplicitAny": false, "skipLibCheck": true, /* Bundler mode */ diff --git a/vite.config.ts b/vite.config.ts index e515bea..d902a5e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,51 +1,55 @@ /// -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; // Import path module for resolving file paths -import fixReactVirtualized from 'esbuild-plugin-react-virtualized' -import wasm from 'vite-plugin-wasm'; -import topLevelAwait from 'vite-plugin-top-level-await'; -import { VitePWA } from 'vite-plugin-pwa'; +import fixReactVirtualized from "esbuild-plugin-react-virtualized"; +import wasm from "vite-plugin-wasm"; +import topLevelAwait from "vite-plugin-top-level-await"; +import { VitePWA } from "vite-plugin-pwa"; export default defineConfig({ - test: { - environment: 'jsdom', + environment: "jsdom", globals: true, - setupFiles: ['./src/test/setup.ts'] + setupFiles: ["./src/test/setup.ts"], }, - assetsInclude: ['**/*.wasm'], - plugins: [react(), wasm(), topLevelAwait(), VitePWA({ - registerType: 'prompt', - manifest: { - name: 'Qortal Hub', - short_name: 'Hub', - description: 'Your easy access to the Qortal blockchain', - start_url: '/', - display: 'standalone', - theme_color: '#ffffff', - background_color: '#ffffff', - icons: [ - { - src: '/qortal192.png', - sizes: '192x192', - type: 'image/png', - }, - { - src: '/qortal.png', - sizes: '512x512', - type: 'image/png', - }, - ], - }, - workbox: { - maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, // 5MB limit - disableDevLogs: true, // Suppresses logs in development - }, - })], + assetsInclude: ["**/*.wasm"], + plugins: [ + react(), + wasm(), + topLevelAwait(), + VitePWA({ + registerType: "prompt", + manifest: { + name: "Qortal Hub", + short_name: "Hub", + description: "Your easy access to the Qortal blockchain", + start_url: "/", + display: "standalone", + theme_color: "#ffffff", + background_color: "#ffffff", + icons: [ + { + src: "/qortal192.png", + sizes: "192x192", + type: "image/png", + }, + { + src: "/qortal.png", + sizes: "512x512", + type: "image/png", + }, + ], + }, + workbox: { + maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, // 5MB limit + disableDevLogs: true, // Suppresses logs in development + }, + }), + ], optimizeDeps: { esbuildOptions: { plugins: [fixReactVirtualized], - } - } + }, + }, });