feat/ng-app: design for device name input #31

Closed
laurin wants to merge 56 commits from feat/ng-app/ask-device-name into master
  1. 1
      .gitignore
  2. 14
      .prettierrc.json
  3. 75
      Cargo.lock
  4. 8
      README.md
  5. 1
      nextgraph/.gitignore
  6. 4
      nextgraph/Cargo.toml
  7. 34
      nextgraph/src/lib.rs
  8. 293
      nextgraph/src/local_broker.rs
  9. 1
      nextgraph/src/local_broker_dev_env.rs
  10. 2
      ng-app/package.json
  11. 3
      ng-app/src-tauri/Cargo.toml
  12. 1
      ng-app/src-tauri/gen/android/app/src/main/AndroidManifest.xml
  13. 3
      ng-app/src-tauri/gen/android/buildSrc/src/main/java/org/nextgraph/ng_app_native/kotlin/BuildTask.kt
  14. 98
      ng-app/src-tauri/src/lib.rs
  15. 24
      ng-app/src/App.svelte
  16. 16
      ng-app/src/api.ts
  17. 2
      ng-app/src/assets/nextgraph-nofill.svg
  18. 8
      ng-app/src/classes.ts
  19. 8
      ng-app/src/lib/CenteredLayout.svelte
  20. 74
      ng-app/src/lib/FullLayout.svelte
  21. 16
      ng-app/src/lib/Home.svelte
  22. 33
      ng-app/src/lib/Install.svelte
  23. 589
      ng-app/src/lib/Login.svelte
  24. 23
      ng-app/src/lib/NoWallet.svelte
  25. 114
      ng-app/src/lib/Test.svelte
  26. 89
      ng-app/src/lib/components/CopyToClipboard.svelte
  27. 57
      ng-app/src/lib/components/DeviceIcon.svelte
  28. 40
      ng-app/src/lib/components/Logo.svelte
  29. 87
      ng-app/src/lib/components/PasswordInput.svelte
  30. 38
      ng-app/src/lib/components/Spinner.svelte
  31. 231
      ng-app/src/locales/de.json
  32. 580
      ng-app/src/locales/en.json
  33. 231
      ng-app/src/locales/es.json
  34. 231
      ng-app/src/locales/fr.json
  35. 231
      ng-app/src/locales/it.json
  36. 231
      ng-app/src/locales/pt.json
  37. 231
      ng-app/src/locales/ru.json
  38. 231
      ng-app/src/locales/zh.json
  39. 367
      ng-app/src/routes/AccountInfo.svelte
  40. 16
      ng-app/src/routes/Home.svelte
  41. 4
      ng-app/src/routes/Install.svelte
  42. 2
      ng-app/src/routes/Invitation.svelte
  43. 3
      ng-app/src/routes/NURI.svelte
  44. 5
      ng-app/src/routes/NotFound.svelte
  45. 51
      ng-app/src/routes/ScanQR.svelte
  46. 207
      ng-app/src/routes/User.svelte
  47. 32
      ng-app/src/routes/UserRegistered.svelte
  48. 797
      ng-app/src/routes/WalletCreate.svelte
  49. 379
      ng-app/src/routes/WalletInfo.svelte
  50. 99
      ng-app/src/routes/WalletLogin.svelte
  51. 207
      ng-app/src/store.ts
  52. 28
      ng-app/src/styles.css
  53. 324
      ng-app/src/wallet_emojis.ts
  54. 11
      ng-app/src/worker.js
  55. 8
      ng-app/tailwind.config.cjs
  56. 101
      ng-broker/src/server_broker.rs
  57. 5
      ng-net/src/actor.rs
  58. 2
      ng-net/src/actors/client/mod.rs
  59. 89
      ng-net/src/actors/client/wallet_put_export.rs
  60. 2
      ng-net/src/actors/ext/mod.rs
  61. 109
      ng-net/src/actors/ext/wallet_get_export.rs
  62. 2
      ng-net/src/actors/mod.rs
  63. 72
      ng-net/src/actors/start.rs
  64. 28
      ng-net/src/broker.rs
  65. 103
      ng-net/src/connection.rs
  66. 13
      ng-net/src/server_broker.rs
  67. 121
      ng-net/src/types.rs
  68. 45
      ng-repo/src/errors.rs
  69. 127
      ng-sdk-js/src/lib.rs
  70. 9
      ng-storage-rocksdb/src/block_storage.rs
  71. 9
      ng-storage-rocksdb/src/kcv_storage.rs
  72. 20
      ng-verifier/src/verifier.rs
  73. 1
      ng-wallet/Cargo.toml
  74. 26
      ng-wallet/src/bip39.rs
  75. 2
      ng-wallet/src/emojis.rs
  76. 7
      ng-wallet/src/lib.rs
  77. 27
      ng-wallet/src/types.rs
  78. 5
      ngaccount/.env
  79. 15
      ngaccount/README.md
  80. 7
      ngaccount/src/main.rs
  81. 2
      ngaccount/web/src/routes/Create.svelte
  82. 2
      ngaccount/web/src/routes/Delete.svelte
  83. 36
      ngd/docker/Dockerfile.alpine
  84. 46
      ngd/docker/Dockerfile.fedora
  85. 47
      ngd/docker/Dockerfile.ubuntu
  86. 4
      ngone/web/src/routes/WalletCreate.svelte
  87. 7
      package.json
  88. 471
      pnpm-lock.yaml

1
.gitignore vendored

@ -14,3 +14,4 @@ node_modules
*/tests/*.mnemonic
*/ng-example/*
.vscode/settings.json
.env.local

@ -0,0 +1,14 @@
{
"plugins": ["prettier-plugin-svelte"],
"overrides": [
{
"files": "*",
"excludeFiles": ["*.svelte", "*.html", "*.json"],
"options": {
"tabWidth": 4
}
},
{ "files": "*.svelte", "options": { "parser": "svelte" } }
],
"trailingComma": "es5"
}

75
Cargo.lock generated

@ -2797,14 +2797,13 @@ checksum = "f850fafca79ebacd70eab9d80cb75a33aeda38bde8f3dd784c1837cdf0bde631"
[[package]]
name = "json-patch"
version = "1.0.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f54898088ccb91df1b492cc80029a6fdf1c48ca0db7c6822a8babad69c94658"
checksum = "ec9ad60d674508f3ca8f380a928cfe7b096bc729c4e2dbfe3852bc45da3ab30b"
dependencies = [
"serde",
"serde_json",
"thiserror",
"treediff",
]
[[package]]
@ -2942,9 +2941,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.19"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
dependencies = [
"value-bag",
]
@ -3264,6 +3263,7 @@ dependencies = [
"async-trait",
"base64-url",
"futures",
"lazy_static",
"ng-client-ws",
"ng-net",
"ng-repo",
@ -3271,9 +3271,12 @@ dependencies = [
"ng-verifier",
"ng-wallet",
"once_cell",
"qrcode",
"serde_bare",
"serde_bytes",
"serde_json",
"web-time",
"whoami",
"zeroize",
]
@ -3293,7 +3296,9 @@ dependencies = [
"sys-locale",
"tauri",
"tauri-build",
"tauri-plugin-barcode-scanner",
"tauri-plugin-window",
"tauri-utils",
]
[[package]]
@ -3547,6 +3552,7 @@ dependencies = [
"aes-gcm-siv",
"argon2",
"async-std",
"base64-url",
"blake3",
"chacha20poly1305",
"crypto_box",
@ -4436,6 +4442,12 @@ dependencies = [
"bytemuck",
]
[[package]]
name = "qrcode"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec"
[[package]]
name = "quick-xml"
version = "0.28.2"
@ -4608,6 +4620,15 @@ dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_syscall"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_users"
version = "0.4.3"
@ -5575,6 +5596,20 @@ dependencies = [
"tauri-utils",
]
[[package]]
name = "tauri-plugin-barcode-scanner"
version = "2.0.0-alpha.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "058922dd9cafc89a865593c99507177c4cdbdad9d22a911ac41872ed7dbf0348"
dependencies = [
"log",
"serde",
"serde_json",
"tauri",
"tauri-build",
"thiserror",
]
[[package]]
name = "tauri-plugin-window"
version = "2.0.0-alpha.1"
@ -6043,15 +6078,6 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "treediff"
version = "4.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52984d277bdf2a751072b5df30ec0377febdb02f7696d64c2d7d54630bac4303"
dependencies = [
"serde_json",
]
[[package]]
name = "try-lock"
version = "0.2.4"
@ -6198,9 +6224,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "value-bag"
version = "1.4.0"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4d330786735ea358f3bc09eea4caa098569c1c93f342d9aca0514915022fe7e"
checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101"
[[package]]
name = "vcpkg"
@ -6325,6 +6351,12 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]]
name = "wasm-bindgen"
version = "0.2.87"
@ -6540,6 +6572,17 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"
[[package]]
name = "whoami"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9"
dependencies = [
"redox_syscall 0.4.1",
"wasite",
"web-sys",
]
[[package]]
name = "winapi"
version = "0.3.9"

@ -57,6 +57,14 @@ cargo install wasm-pack --git https://github.com/rustwasm/wasm-pack.git --rev c2
then :
create a file called `nextgraph/src/local_broker_dev_env.rs` with the content :
```
pub const PEER_ID: &str = "FtdzuDYGewfXWdoPuXIPb0wnd0SAg1WoA2B14S7jW3MA";
```
once your ngd server will run in your dev env, replace the above string with the actual PEER ID of your ngd server.
```
cargo install cargo-watch
// optionally, if you want a Rust REPL: cargo install evcxr_repl

@ -1 +1,2 @@
tests
local_broker_dev_env_peer_id.rs

@ -18,6 +18,7 @@ maintenance = { status = "actively-developed" }
[dependencies]
serde_bare = "0.5.0"
serde_json = "1.0"
serde_bytes = "0.11.7"
base64-url = "2.0.0"
once_cell = "1.17.1"
zeroize = { version = "1.7.0", features = ["zeroize_derive"] }
@ -25,7 +26,10 @@ futures = "0.3.24"
async-std = { version = "1.12.0", features = [ "attributes", "unstable" ] }
async-trait = "0.1.64"
async-once-cell = "0.5.3"
lazy_static = "1.4.0"
web-time = "0.2.0"
whoami = "1.5.1"
qrcode = { version = "0.14.1", default-features = false, features = ["svg"] }
ng-repo = { path = "../ng-repo", version = "0.1.0-preview.1" }
ng-net = { path = "../ng-net", version = "0.1.0-preview.1" }
ng-wallet = { path = "../ng-wallet", version = "0.1.0-preview.5" }

@ -98,3 +98,37 @@ pub mod verifier {
pub mod wallet {
pub use ng_wallet::*;
}
pub fn get_device_name() -> String {
let mut list: Vec<String> = Vec::with_capacity(3);
#[cfg(not(target_arch = "wasm32"))]
if let Ok(realname) = whoami::fallible::realname() {
list.push(realname);
} else {
#[cfg(not(target_arch = "wasm32"))]
if let Ok(username) = whoami::fallible::username() {
list.push(username);
}
}
if let Ok(devicename) = whoami::fallible::devicename() {
list.push(devicename);
} else {
#[cfg(not(target_arch = "wasm32"))]
if let Ok(hostname) = whoami::fallible::hostname() {
list.push(hostname);
} else {
if let Ok(distro) = whoami::fallible::distro() {
list.push(distro);
}
}
}
#[cfg(target_arch = "wasm32")]
if let Ok(distro) = whoami::fallible::distro() {
list.push(distro);
}
list.join(" ")
}
#[cfg(debug_assertions)]
mod local_broker_dev_env;

@ -20,6 +20,8 @@ use once_cell::sync::Lazy;
use serde_bare::to_vec;
use serde_json::json;
use zeroize::Zeroize;
use qrcode::{render::svg, QrCode};
use lazy_static::lazy_static;
use ng_repo::block_storage::BlockStorage;
use ng_repo::block_storage::HashMapBlockStorage;
@ -27,7 +29,7 @@ use ng_repo::errors::{NgError, ProtocolError};
use ng_repo::log::*;
use ng_repo::os_info::get_os_info;
use ng_repo::types::*;
use ng_repo::utils::{derive_key, generate_keypair};
use ng_repo::utils::{derive_key, generate_keypair, encrypt_in_place};
use ng_net::{actor::*,actors::admin::*};
use ng_net::broker::*;
@ -40,6 +42,7 @@ use ng_verifier::types::*;
use ng_verifier::verifier::Verifier;
use ng_wallet::emojis::encode_pazzle;
use ng_wallet::bip39::encode_mnemonic;
use ng_wallet::{create_wallet_first_step_v0, create_wallet_second_step_v0, types::*};
#[cfg(not(target_family = "wasm"))]
@ -1491,6 +1494,235 @@ pub async fn wallet_add(lws: LocalWalletStorageV0) -> Result<(), NgError> {
}
Ok(())
}
#[cfg(debug_assertions)]
lazy_static! {
static ref NEXTGRAPH_EU: BrokerServerV0 = BrokerServerV0 {
server_type: BrokerServerTypeV0::Localhost(14400),
can_verify: false,
can_forward: false,
peer_id: ng_repo::utils::decode_key({use crate::local_broker_dev_env::PEER_ID; PEER_ID}).unwrap(),
};
}
#[cfg(not(debug_assertions))]
lazy_static! {
static ref NEXTGRAPH_EU: BrokerServerV0 = BrokerServerV0 {
server_type: BrokerServerTypeV0::Domain("nextgraph.eu".to_string()),
can_verify: false,
can_forward: false,
peer_id: ng_repo::utils::decode_key("FtdzuDYGewfXWdoPuXIPb0wnd0SAg1WoA2B14S7jW3MA").unwrap(),
};
}
/// Obtains a Wallet object from a QRCode or a TextCode.
///
/// The returned object can be used to import the wallet into a new Device
/// with the help of the function [wallet_open_with_pazzle_words]
/// followed by [wallet_import]
pub async fn wallet_import_from_code(code: String) -> Result<Wallet, NgError> {
let qr = NgQRCode::from_code(code)?;
match qr {
NgQRCode::V0(NgQRCodeV0{broker, rendezvous, secret_key, is_rendezvous}) => {
let wallet: ExportedWallet = do_ext_call(
&broker,
ExtWalletGetExportV0 {
id:rendezvous,
is_rendezvous
}).await?;
let mut buf = wallet.0.into_vec();
encrypt_in_place(&mut buf,*secret_key.slice(), [0;12]);
let wallet: Wallet = serde_bare::from_slice(&buf)?;
let broker = match LOCAL_BROKER.get() {
None | Some(Err(_)) => return Err(NgError::LocalBrokerNotInitialized),
Some(Ok(broker)) => broker.read().await,
};
// check that the wallet is not already present in local_broker
let wallet_name = wallet.name();
if broker.wallets.get(&wallet_name).is_none() {
Ok(wallet)
} else {
Err(NgError::WalletAlreadyAdded)
}
}
}
}
/// Starts a rendez-vous to obtain a wallet from other device.
///
/// A rendezvous is used when the device that is importing, doesn't have a camera.
/// The QRCode is displayed on that device, and another device (with camera, and with the wallet) will scan it.
///
/// Returns the QRcode in SVG format, and the code (a string) to be used with [wallet_import_from_code]
pub async fn wallet_import_rendezvous(size: u32) -> Result<(String,String), NgError> {
let code = NgQRCode::V0(NgQRCodeV0 {
broker: NEXTGRAPH_EU.clone(),
rendezvous: SymKey::random(),
secret_key: SymKey::random(),
is_rendezvous: true
});
let code_string = code.to_code();
let code_svg = match QrCode::with_error_correction_level(&code_string, qrcode::EcLevel::M) {
Ok(qr) => {
let svg = qr
.render()
.max_dimensions(size, size)
.dark_color(svg::Color("#000000"))
.light_color(svg::Color("#ffffff"))
.build();
svg
},
Err(_e) => {return Err(NgError::BrokerError)}
};
Ok((code_svg,code_string))
}
/// Gets the TextCode to display in order to export the wallet of the current session ID
///
/// The ExportedWallet is valid for 5 min.
///
/// Returns the TextCode
pub async fn wallet_export_get_textcode(session_id: u64) -> Result<String, NgError> {
let broker = match LOCAL_BROKER.get() {
None | Some(Err(_)) => return Err(NgError::LocalBrokerNotInitialized),
Some(Ok(broker)) => broker.read().await,
};
match &broker.config {
LocalBrokerConfig::Headless(_) => {
return Err(NgError::LocalBrokerIsHeadless)
},
_ => {
let (real_session_id, is_remote) = broker.get_real_session_id_for_mut(session_id)?;
if is_remote {
return Err(NgError::NotImplemented);
} else {
let session = broker.opened_sessions_list[real_session_id].as_ref()
.ok_or(NgError::SessionNotFound)?;
let wallet_name = session.config.wallet_name();
match broker.wallets.get(&wallet_name) {
None => Err(NgError::WalletNotFound),
Some(lws) => {
//let broker = lws.bootstrap.servers().first().unwrap();
let wallet = &lws.wallet;
let secret_key = SymKey::random();
let rendezvous = SymKey::random();
let code = NgQRCode::V0(NgQRCodeV0 {
broker: NEXTGRAPH_EU.clone(),
rendezvous: rendezvous.clone(),
secret_key: secret_key.clone(),
is_rendezvous: false
});
let code_string = code.to_code();
let mut wallet_ser = serde_bare::to_vec(wallet)?;
encrypt_in_place(&mut wallet_ser,*secret_key.slice(), [0;12]);
let exported_wallet = ExportedWallet(serde_bytes::ByteBuf::from(wallet_ser));
match session.verifier.client_request::<WalletPutExport, ()>(WalletPutExport::V0(WalletPutExportV0{wallet:exported_wallet, rendezvous_id:rendezvous, is_rendezvous:false})).await {
Err(e) => Err(e),
Ok(SoS::Stream(_)) => Err(NgError::InvalidResponse),
Ok(SoS::Single(_)) => Ok(code_string)
}
}
}
}
}
}
}
/// Gets the QRcode to display in order to export a wallet of the current session ID
///
/// The ExportedWallet is valid for 5 min.
///
/// Returns the QRcode in SVG format
pub async fn wallet_export_get_qrcode(session_id: u64, size: u32) -> Result<String, NgError> {
let code_string = wallet_export_get_textcode(session_id).await?;
let code_svg = match QrCode::with_error_correction_level(&code_string, qrcode::EcLevel::M) {
Ok(qr) => {
let svg = qr
.render()
.max_dimensions(size, size)
.dark_color(svg::Color("#000000"))
.light_color(svg::Color("#ffffff"))
.build();
svg
},
Err(_e) => {return Err(NgError::BrokerError)}
};
Ok(code_svg)
}
/// Puts the Wallet to the rendezvous ID that was scanned
///
/// The rendezvous ID is valid for 5 min.
pub async fn wallet_export_rendezvous(session_id: u64, code: String) -> Result<(), NgError> {
let qr = NgQRCode::from_code(code)?;
match qr {
NgQRCode::V0(NgQRCodeV0{broker: _, rendezvous, secret_key, is_rendezvous}) => {
if !is_rendezvous {
return Err(NgError::NotARendezVous);
}
let broker = match LOCAL_BROKER.get() {
None | Some(Err(_)) => return Err(NgError::LocalBrokerNotInitialized),
Some(Ok(broker)) => broker.read().await,
};
match &broker.config {
LocalBrokerConfig::Headless(_) => {
return Err(NgError::LocalBrokerIsHeadless)
},
_ => {
let (real_session_id, is_remote) = broker.get_real_session_id_for_mut(session_id)?;
if is_remote {
return Err(NgError::NotImplemented);
} else {
let session = broker.opened_sessions_list[real_session_id].as_ref()
.ok_or(NgError::SessionNotFound)?;
let wallet_name = session.config.wallet_name();
match broker.wallets.get(&wallet_name) {
None => Err(NgError::WalletNotFound),
Some(lws) => {
//let broker = lws.bootstrap.servers().first().unwrap();
let wallet = &lws.wallet;
let mut wallet_ser = serde_bare::to_vec(wallet)?;
encrypt_in_place(&mut wallet_ser,*secret_key.slice(), [0;12]);
let exported_wallet = ExportedWallet(serde_bytes::ByteBuf::from(wallet_ser));
// TODO: send the WalletPutExport client request to the broker received from QRcode. for now it is cheer luck that all clients are connected to nextgraph.eu.
// if the user doesn't have an account with nextgraph.eu, their broker should relay the request (core protocol ?)
match session.verifier.client_request::<WalletPutExport, ()>(WalletPutExport::V0(WalletPutExportV0{wallet:exported_wallet, rendezvous_id:rendezvous, is_rendezvous:true})).await {
Err(e) => Err(e),
Ok(SoS::Stream(_)) => Err(NgError::InvalidResponse),
Ok(SoS::Single(_)) => Ok(())
}
}
}
}
}
}
}
}
}
/// Reads a binary Wallet File and decodes it to a Wallet object.
///
@ -1555,6 +1787,27 @@ pub fn wallet_open_with_pazzle(
Ok(opened_wallet)
}
#[doc(hidden)]
/// This is a bit hard to use as the mnemonic words are encoded in u16.
/// prefer the function wallet_open_with_mnemonic_words
pub fn wallet_open_with_mnemonic(
wallet: &Wallet,
mnemonic: Vec<u16>,
pin: [u8; 4],
) -> Result<SensitiveWallet, NgError> {
if mnemonic.len() != 12 {
return Err(NgError::InvalidMnemonic);
}
// Convert from vec to array.
let mut mnemonic_arr = [0u16; 12];
for (place, element) in mnemonic_arr.iter_mut().zip(mnemonic.iter()) {
*place = *element;
}
let opened_wallet = ng_wallet::open_wallet_with_mnemonic(wallet, mnemonic_arr, pin)?;
Ok(opened_wallet)
}
/// Opens a wallet by providing an ordered list of words, and the pin.
///
/// If you are opening a wallet that is already known to the LocalBroker, you must then call [wallet_was_opened].
@ -1569,6 +1822,21 @@ pub fn wallet_open_with_pazzle_words(
wallet_open_with_pazzle(wallet, encode_pazzle(pazzle_words)?, pin)
}
/// Opens a wallet by providing an ordered list of mnemonic words, and the pin.
///
/// If you are opening a wallet that is already known to the LocalBroker, you must then call [wallet_was_opened].
/// Otherwise, if you are importing, then you must call [wallet_import].
pub fn wallet_open_with_mnemonic_words(
wallet: &Wallet,
mnemonic: &Vec<String>,
pin: [u8; 4],
) -> Result<SensitiveWallet, NgError> {
let encoded: Vec<u16> = encode_mnemonic(mnemonic)?;
wallet_open_with_mnemonic(wallet, encoded, pin)
}
/// Imports a wallet into the LocalBroker so the user can then access its content.
///
/// the wallet should have been previous opened with [wallet_open_with_pazzle_words].
@ -2177,6 +2445,29 @@ async fn do_admin_call<
.await
}
async fn do_ext_call<
A: Into<ProtocolMessage> + Into<ExtRequestContentV0> + std::fmt::Debug + Sync + Send + 'static,
B: TryFrom<ProtocolMessage, Error = ProtocolError>
+ std::fmt::Debug
+ Sync
+ Send
+ 'static,
>(
broker_server: &BrokerServerV0,
cmd: A,
) -> Result<B, NgError> {
let (peer_privk, peer_pubk) = generate_keypair();
Broker::ext(
Box::new(ConnectionWebSocket {}),
peer_privk,
peer_pubk,
broker_server.peer_id,
broker_server.get_ws_url(&None).await.unwrap().0, // for now we are only connecting to NextGraph SaaS cloud (nextgraph.eu) so it is safe.
cmd,
)
.await
}
#[doc(hidden)]
pub async fn admin_create_user(server_peer_id: DirectPeerId, admin_user_key: PrivKey, server_addr: BindAddress) -> Result<UserId, ProtocolError> {

@ -0,0 +1 @@
pub const PEER_ID: &str = "FtdzuDYGewfXWdoPuXIPb0wnd0SAg1WoA2B14S7jW3MA";

@ -18,12 +18,14 @@
"dependencies": {
"@popperjs/core": "^2.11.8",
"@tauri-apps/api": "2.0.0-alpha.8",
"@tauri-apps/plugin-barcode-scanner": "2.0.0-alpha.0",
"@tauri-apps/plugin-window": "2.0.0-alpha.1",
"async-proxy": "^0.4.1",
"classnames": "^2.3.2",
"flowbite": "^1.6.5",
"flowbite-svelte": "^0.43.3",
"ng-sdk-js": "workspace:^0.1.0-preview.1",
"svelte-i18n": "^4.0.0",
"svelte-spa-router": "^3.3.0",
"vite-plugin-top-level-await": "^1.3.1"
},

@ -21,7 +21,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2.0.0-alpha.8", features = [] }
# tauri-macros = { version = "=2.0.0-alpha.6" }
# tauri-codegen = { version = "=2.0.0-alpha.6" }
# tauri-utils = { version = "=2.0.0-alpha.6" }
tauri-utils = { version = "=2.0.0-alpha.7" }
[dependencies]
serde = { version = "1.0", features = ["derive"] }
@ -32,6 +32,7 @@ sys-locale = { version = "0.3.1" }
ng-async-tungstenite = { git = "https://git.nextgraph.org/NextGraph/async-tungstenite.git", branch = "nextgraph", features = ["async-std-runtime", "async-native-tls"] }
tauri = { version = "2.0.0-alpha.14", features = [] }
tauri-plugin-window = "2.0.0-alpha.1"
tauri-plugin-barcode-scanner = "=2.0.0-alpha.0"
# tauri-plugin-window = { git = "https://git.nextgraph.org/NextGraph/plugins-workspace.git", branch="window-alpha.1-nextgraph" }
# tauri = { git = "https://git.nextgraph.org/NextGraph/tauri.git", branch="alpha.11-nextgraph", features = ["no-ipc-custom-protocol"] }
# tauri = { git = "https://github.com/simonhyll/tauri.git", branch="fix/ipc-mixup", features = [] }

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<application
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"

@ -16,7 +16,8 @@ open class BuildTask : DefaultTask() {
@TaskAction
fun assemble() {
val executable = """/Users/nl/.cargo/bin/cargo-tauri""";
val homePath = System.getProperty("user.home");
val executable = "$homePath/.cargo/bin/cargo-tauri";
try {
runTauriCli(executable)
} catch (e: Exception) {

@ -108,6 +108,31 @@ async fn wallet_open_with_pazzle(
Ok(wallet)
}
#[tauri::command(rename_all = "snake_case")]
async fn wallet_open_with_mnemonic(
wallet: Wallet,
mnemonic: Vec<u16>,
pin: [u8; 4],
_app: tauri::AppHandle,
) -> Result<SensitiveWallet, String> {
let wallet = nextgraph::local_broker::wallet_open_with_mnemonic(&wallet, mnemonic, pin)
.map_err(|e| e.to_string())?;
Ok(wallet)
}
#[tauri::command(rename_all = "snake_case")]
async fn wallet_open_with_mnemonic_words(
wallet: Wallet,
mnemonic_words: Vec<String>,
pin: [u8; 4],
_app: tauri::AppHandle,
) -> Result<SensitiveWallet, String> {
let wallet =
nextgraph::local_broker::wallet_open_with_mnemonic_words(&wallet, &mnemonic_words, pin)
.map_err(|e| e.to_string())?;
Ok(wallet)
}
#[tauri::command(rename_all = "snake_case")]
async fn wallet_get_file(wallet_name: String, app: tauri::AppHandle) -> Result<(), String> {
let ser = nextgraph::local_broker::wallet_get_file(&wallet_name)
@ -181,6 +206,55 @@ async fn wallet_import(
.map_err(|e: NgError| e.to_string())
}
#[tauri::command(rename_all = "snake_case")]
async fn wallet_export_rendezvous(
session_id: u64,
code: String,
_app: tauri::AppHandle,
) -> Result<(), String> {
nextgraph::local_broker::wallet_export_rendezvous(session_id, code)
.await
.map_err(|e: NgError| e.to_string())
}
#[tauri::command(rename_all = "snake_case")]
async fn wallet_export_get_qrcode(
session_id: u64,
size: u32,
_app: tauri::AppHandle,
) -> Result<String, String> {
nextgraph::local_broker::wallet_export_get_qrcode(session_id, size)
.await
.map_err(|e: NgError| e.to_string())
}
#[tauri::command(rename_all = "snake_case")]
async fn wallet_export_get_textcode(
session_id: u64,
_app: tauri::AppHandle,
) -> Result<String, String> {
nextgraph::local_broker::wallet_export_get_textcode(session_id)
.await
.map_err(|e: NgError| e.to_string())
}
#[tauri::command(rename_all = "snake_case")]
async fn wallet_import_rendezvous(
size: u32,
_app: tauri::AppHandle,
) -> Result<(String, String), String> {
nextgraph::local_broker::wallet_import_rendezvous(size)
.await
.map_err(|e: NgError| e.to_string())
}
#[tauri::command(rename_all = "snake_case")]
async fn wallet_import_from_code(code: String, _app: tauri::AppHandle) -> Result<Wallet, String> {
nextgraph::local_broker::wallet_import_from_code(code)
.await
.map_err(|e: NgError| e.to_string())
}
#[tauri::command(rename_all = "snake_case")]
async fn get_wallets(
app: tauri::AppHandle,
@ -467,6 +541,11 @@ fn client_info_rust() -> Result<Value, String> {
Ok(ng_repo::os_info::get_os_info())
}
#[tauri::command(rename_all = "snake_case")]
fn get_device_name() -> Result<String, String> {
Ok(nextgraph::get_device_name())
}
#[derive(Default)]
pub struct AppBuilder {
setup: Option<SetupHook>,
@ -493,7 +572,7 @@ impl AppBuilder {
pub fn run(self) {
let setup = self.setup;
tauri::Builder::default()
let builder = tauri::Builder::default()
.setup(move |app| {
if let Some(setup) = setup {
(setup)(app)?;
@ -509,18 +588,32 @@ impl AppBuilder {
}
Ok(())
})
.plugin(tauri_plugin_window::init())
.plugin(tauri_plugin_window::init());
#[cfg(mobile)]
{
let builder = builder.plugin(tauri_plugin_barcode_scanner::init());
}
builder
.invoke_handler(tauri::generate_handler![
test,
locales,
wallet_gen_shuffle_for_pazzle_opening,
wallet_gen_shuffle_for_pin,
wallet_open_with_pazzle,
wallet_open_with_mnemonic,
wallet_open_with_mnemonic_words,
wallet_was_opened,
wallet_create,
wallet_read_file,
wallet_get_file,
wallet_import,
wallet_export_rendezvous,
wallet_export_get_qrcode,
wallet_export_get_textcode,
wallet_import_rendezvous,
wallet_import_from_code,
wallet_close,
encode_create_account,
session_start,
@ -539,6 +632,7 @@ impl AppBuilder {
app_request_stream,
app_request,
upload_chunk,
get_device_name,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

@ -11,12 +11,13 @@
<script lang="ts">
import { push, default as Router } from "svelte-spa-router";
import { isLoading } from "svelte-i18n";
import { onMount, tick, onDestroy } from "svelte";
import {
wallets,
active_wallet,
opened_wallets,
active_session,
close_active_session,
disconnections_subscribe,
select_default_lang,
@ -30,11 +31,14 @@
import WalletCreate from "./routes/WalletCreate.svelte";
import Invitation from "./routes/Invitation.svelte";
import WalletLogin from "./routes/WalletLogin.svelte";
import WalletInfo from "./routes/WalletInfo.svelte";
import User from "./routes/User.svelte";
import UserRegistered from "./routes/UserRegistered.svelte";
import Install from "./routes/Install.svelte";
import ScanQR from "./routes/ScanQR.svelte";
import ng from "./api";
import AccountInfo from "./routes/AccountInfo.svelte";
const routes = new Map();
routes.set("/", Home);
@ -44,6 +48,9 @@
routes.set("/i/:invitation", Invitation);
routes.set("/user", User);
routes.set("/user/registered", UserRegistered);
routes.set("/wallet", WalletInfo);
routes.set("/user/accounts", AccountInfo);
routes.set("/wallet/scanqr", ScanQR);
if (import.meta.env.NG_APP_WEB) routes.set("/install", Install);
routes.set(/^\/did:ng(.*)/i, NURI);
routes.set("*", NotFound);
@ -60,9 +67,10 @@
onMount(async () => {
try {
await select_default_lang();
await disconnections_subscribe();
await select_default_lang();
} catch (e) {
console.error(e);
//console.log("called disconnections_subscribe twice");
}
let tauri_platform = import.meta.env.TAURI_PLATFORM;
@ -262,6 +270,9 @@
unsubscribe();
if (unsub_main_close) unsub_main_close();
});
// import { to_debug } from "./wallet_emojis";
// to_debug();
</script>
<!-- <p>
@ -271,4 +282,11 @@
{JSON.stringify(Object.keys($opened_wallets))}
{JSON.stringify($active_session)}
</p> -->
<Router {routes} />
{#if $isLoading}
<p class="text-center">Loading translations...</p>
{:else}
<Router {routes} />
{/if}

@ -16,11 +16,17 @@ const mapping = {
"wallet_gen_shuffle_for_pazzle_opening": ["pazzle_length"],
"wallet_gen_shuffle_for_pin": [],
"wallet_open_with_pazzle": ["wallet","pazzle","pin"],
"wallet_open_with_mnemonic_words": ["wallet","mnemonic_words","pin"],
"wallet_was_opened": ["opened_wallet"],
"wallet_create": ["params"],
"wallet_read_file": ["file"],
"wallet_get_file": ["wallet_name"],
"wallet_import": ["encrypted_wallet","opened_wallet","in_memory"],
"wallet_export_rendezvous": ["session_id", "code"],
"wallet_export_get_qrcode": ["session_id", "size"],
"wallet_export_get_textcode": ["session_id"],
"wallet_import_rendezvous": ["size"],
"wallet_import_from_code": ["code"],
"wallet_close": ["wallet_name"],
"encode_create_account": ["payload"],
"session_start": ["wallet_name","user"],
@ -33,6 +39,7 @@ const mapping = {
"user_disconnect": ["user_id"],
"app_request": ["request"],
"test": [ ],
"get_device_name": [],
"doc_fetch_private_subscribe": [],
"doc_fetch_repo_subscribe": ["repo_id"],
}
@ -72,7 +79,7 @@ const handler = {
switch (tauri_platform) {
case 'macos': client_type = "NativeMacOS";break;
case 'linux': client_type = "NativeLinux";break;
case 'windows': client_type = "NativeWindows";break;
case 'windows': client_type = "NativeWin";break;
case 'android': client_type = "NativeAndroid";break;
case 'ios': client_type = "NativeIos";break;
}
@ -182,7 +189,7 @@ const handler = {
return false;
} else if (path[0] === "get_local_url") {
return false;
} else if (path[0] === "wallet_open_with_pazzle") {
} else if (path[0] === "wallet_open_with_pazzle" || path[0] === "wallet_open_with_mnemonic_words") {
let arg:any = {};
args.map((el,ix) => arg[mapping[path[0]][ix]]=el)
let img = Array.from(new Uint8Array(arg.wallet.V0.content.security_img));
@ -190,9 +197,8 @@ const handler = {
arg.wallet = {V0:{id:arg.wallet.V0.id, sig:arg.wallet.V0.sig, content:{}}};
Object.assign(arg.wallet.V0.content,old_content);
arg.wallet.V0.content.security_img = img;
return tauri.invoke(path[0],arg)
}
else {
return tauri.invoke(path[0],arg);
} else {
let arg = {};
args.map((el,ix) => arg[mapping[path[0]][ix]]=el)
return tauri.invoke(path[0],arg)

@ -11,6 +11,6 @@
style="fill:#ffffff;stroke:none;stroke-width:0.268375" />
<path
d="M 98.343352,190.26108 C 80.403778,187.53354 65.011938,179.57839 52.608228,166.62327 38.602093,151.99448 31.178059,133.41381 31.178059,112.98841 c 0,-10.21889 1.700058,-19.44396 5.221234,-28.332119 4.28678,-10.820699 10.037295,-19.39063 18.535095,-27.62263 4.72982,-4.58187 6.60687,-6.10643 11.28099,-9.16256 11.89869,-7.779841 24.173884,-11.879991 38.095802,-12.724761 19.80437,-1.2017 39.11165,5.11306 54.60284,17.858751 1.50718,1.24006 2.72951,2.35934 2.71628,2.48729 -0.0132,0.12795 -3.85821,3.63443 -8.54442,7.79217 -4.6862,4.157729 -10.04724,8.96276 -11.91342,10.677819 -1.86617,1.715071 -3.54094,3.11831 -3.7217,3.11831 -0.18075,0 -1.39985,-0.745188 -2.70911,-1.655969 -7.53011,-5.23834 -15.99428,-7.82188 -25.62597,-7.82188 -12.731628,0 -23.249192,4.3379 -32.143882,13.257541 -6.39594,6.413868 -10.70387,14.555268 -12.50018,23.623578 -0.69099,3.48832 -0.68968,13.53072 0.002,17.00893 3.70508,18.62577 18.31886,33.10194 36.642322,36.29729 4.16439,0.72621 11.98099,0.71223 15.98975,-0.0286 14.03187,-2.59311 25.86047,-11.36806 32.26533,-23.93578 0.77379,-1.51834 1.26018,-2.88461 1.08086,-3.03616 -0.17934,-0.15156 -6.87448,-1.1779 -14.87813,-2.28078 -9.7795,-1.34758 -14.92353,-2.21379 -15.68471,-2.64117 -1.52067,-0.85379 -2.83611,-2.88806 -2.83611,-4.3859 0,-1.1732 2.02687,-15.86876 2.49085,-18.05962 0.29676,-1.40127 2.42559,-3.4934 3.84317,-3.77691 0.62227,-0.12445 8.82712,0.85555 18.28065,2.18348 9.43343,1.32511 17.26269,2.29453 17.39833,2.15427 0.13566,-0.14026 1.11808,-6.54833 2.18313,-14.24014 1.10778,-8.000208 2.20407,-14.60184 2.56177,-15.426229 0.34392,-0.792599 1.11019,-1.849131 1.70287,-2.34782 2.06321,-1.736079 3.1433,-1.785011 12.20439,-0.55291 9.63637,1.310309 10.70873,1.56224 12.28077,2.88503 1.64359,1.382979 2.2732,2.810909 2.25906,5.123309 -0.007,1.10173 -0.92172,8.29645 -2.03332,15.98826 -1.11158,7.69182 -1.97159,14.04091 -1.91113,14.1091 0.0605,0.0682 7.16644,1.11143 15.79109,2.31832 11.10566,1.55407 16.00827,2.38757 16.80223,2.85657 1.53015,0.90389 2.48023,2.64785 2.45017,4.49756 -0.0462,2.84349 -2.41252,18.12279 -2.97521,19.21089 -0.66164,1.27949 -2.60244,2.54696 -3.92109,2.56074 -0.51973,0.005 -7.87449,-0.95937 -16.34391,-2.144 -8.46944,-1.18464 -15.47588,-2.077 -15.56986,-1.98301 -0.094,0.094 -1.18792,7.34163 -2.43097,16.10589 -1.44004,10.15311 -2.49792,16.43621 -2.91556,17.31631 -0.72531,1.52848 -2.76261,3.06291 -4.53817,3.41802 -0.95688,0.19138 -10.90014,-0.92798 -13.59859,-1.53084 -0.5471,-0.12223 -1.89146,0.67252 -4.50941,2.66588 -11.2627,8.57562 -24.34195,13.90917 -38.35741,15.64164 -4.40038,0.54395 -15.72658,0.43298 -19.853658,-0.19451 z"
style="fill:#888;fill-opacity:1;stroke:#888;stroke-width:0.377976;stroke-opacity:1" />
/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

@ -90,7 +90,8 @@ export const official_classes = {
"ng:a": "A Source Code file. many languages supported",
"ng:o": "n:g:z:pre",
"ng:w": "n:g:z:code_editor",
"ng:compat": ["code/*","file/iana/text/javascript","file/iana/text/css","file/iana/text/html","file/iana/text/markdown", "file/iana/application/xml", "file/iana/application/yaml", "file/iana/text/xml", "file/iana/application/xhtml+xml"],
"ng:compat": ["code/*","file/iana/text/javascript","file/iana/text/css","file/iana/text/html","file/iana/text/markdown", "file/iana/application/xml",
"file/iana/application/yaml", "file/iana/text/xml", "file/iana/application/xhtml+xml"],
},
"app": {
"ng:n": "Official App",
@ -235,7 +236,8 @@ export const official_classes = {
"rdf":true,
"xsd":true,
},
"ng:compat": [ "rdf:*", "xsd:*", "file/iana/text/n3", "file/iana/text/rdf+n3", "file/iana/text/turtle", "file/iana/application/n-quads", "file/iana/application/trig", "file/iana/application/n-triples", "file/iana/application/rdf+xml", "file/iana/application/ld+json"],
"ng:compat": [ "rdf:*", "xsd:*", "file/iana/text/n3", "file/iana/text/rdf+n3", "file/iana/text/turtle", "file/iana/application/n-quads", "file/iana/application/trig", "file/iana/application/n-triples",
"file/iana/application/rdf+xml", "file/iana/application/ld+json"],
},
"data/json": {
"ng:crdt": "Automerge",
@ -863,7 +865,7 @@ export const official_classes = {
"ng:a": "Text file",
"ng:o": "n:g:z:file_viewer",
"ng:w": "n:g:z:file_viewer",
"ng:compat": ["file/iana/text/*", "image/svg+xml", "file/iana/application/n-quads", "file/iana/application/trig", "file/iana/application/n-triples", "file/iana/application/rdf+xml", "file/iana/application/ld+json",
"ng:compat": ["file/iana/text/*", "file/iana/image/svg+xml", "file/iana/application/n-quads", "file/iana/application/trig", "file/iana/application/n-triples", "file/iana/application/rdf+xml", "file/iana/application/ld+json",
"file/iana/application/xml", "file/iana/application/yaml", "file/iana/application/xhtml+xml", "file/iana/application/node","file/iana/application/sparql-results+json","file/iana/application/sparql-results+xml",
"file/iana/message/rfc822","file/iana/multipart/related", "file/iana/text/vnd.graphviz", "file/iana/application/vnd.excalidraw+json", "file/iana/application/x-tex","file/iana/text/x-tex",
"file/iana/application/vcard+json", "file/iana/application/vcard+xml", "file/iana/text/calendar", "file/iana/application/calendar+xml", "file/iana/application/calendar+json",

@ -12,7 +12,8 @@
<script lang="ts">
import ng from "../api";
import { onMount, tick } from "svelte";
import { current_lang, available_languages } from "../store";
import { locale, t } from "svelte-i18n";
import { available_languages } from "../store";
import { Language } from "svelte-heros-v2";
export let displayFooter = false;
@ -30,7 +31,7 @@
}
const selectLang = async (lang) => {
current_lang.set(lang);
locale.set(lang);
changingLang = false;
await tick();
scrollToTop();
@ -72,7 +73,8 @@
<button
on:click={displayNextgraphOrg}
class="text-primary-700 bg-[#f6f6f6] bg-none ring-0 hover:bg-primary-100/90 focus:ring-4 focus:ring-primary-100/50 font-medium rounded-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-100/55 mb-2"
>About NextGraph
>
{$t("common.about_nextgraph")}
</button>
</div>
</div>

@ -22,9 +22,7 @@
// @ts-ignore
import Logo from "../assets/nextgraph.svg?component";
// @ts-ignore
import LogoGray from "../assets/nextgraph-gray.svg?component";
import { online } from "../store";
import { t } from "svelte-i18n";
import { onMount, tick } from "svelte";
import {
@ -91,76 +89,96 @@
<div class="full-layout">
<Sidebar {activeUrl} {asideClass} {nonActiveClass} class="fixed">
<SidebarWrapper
divClass="bg-gray-60 overflow-y-auto tall:py-4 px-3 rounded dark:bg-gray-800"
divClass="bg-gray-60 overflow-y-auto tall-xs:py-4 px-3 rounded dark:bg-gray-800"
>
<SidebarGroup ulClass="space-y-1 tall:space-y-2">
<SidebarGroup ulClass="space-y-1 tall-xs:space-y-2">
<SidebarItem label="NextGraph" href="#/user" class="mt-1">
<svelte:fragment slot="icon">
{#if $online}
<Logo class="w-7 h-7 tall:w-10 tall:h-10" />
{:else}
<LogoGray class="w-7 h-7 tall:w-10 tall:h-10" />
{/if}
<Logo className="w-7 h-7 tall:w-10 tall:h-10" />
</svelte:fragment>
</SidebarItem>
<SidebarItem
label="Home"
label={$t("pages.full_layout.home")}
href="#/"
on:click={scrollToTop}
class="py-1 tall:p-2"
class="py-1 tall-xs:p-2"
>
<svelte:fragment slot="icon">
<Home
tabindex="-1"
class="w-7 h-7 text-black transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
class="w-7 h-7 text-black transition duration-75 focus:outline-none dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
</svelte:fragment>
</SidebarItem>
<SidebarItem label="Stream" href="#/stream" class="py-1 tall:p-2">
<SidebarItem
label={$t("pages.full_layout.stream")}
href="#/stream"
class="py-1 tall-xs:p-2"
>
<svelte:fragment slot="icon">
<Bolt
tabindex="-1"
class="w-7 h-7 text-black transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
class="w-7 h-7 text-black transition duration-75 focus:outline-none dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
</svelte:fragment>
</SidebarItem>
<SidebarItem label="Search" href="#/search" class="py-1 tall:p-2">
<SidebarItem
label={$t("pages.full_layout.search")}
href="#/search"
class="py-1 tall-xs:p-2"
>
<svelte:fragment slot="icon">
<MagnifyingGlass
tabindex="-1"
class="w-7 h-7 text-black transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
class="w-7 h-7 text-black transition duration-75 focus:outline-none dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
</svelte:fragment>
</SidebarItem>
<SidebarItem label="Create" href="#/create" class="py-1 tall:p-2">
<SidebarItem
label={$t("pages.full_layout.create")}
href="#/create"
class="py-1 tall-xs:p-2"
>
<svelte:fragment slot="icon">
<PlusCircle
tabindex="-1"
class="w-7 h-7 text-black transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
class="w-7 h-7 text-black transition duration-75 focus:outline-none dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
</svelte:fragment>
</SidebarItem>
<SidebarItem label="Shared" href="#/shared" class="py-1 tall:p-2">
<SidebarItem
label={$t("pages.full_layout.shared")}
href="#/shared"
class="py-1 tall-xs:p-2"
>
<svelte:fragment slot="icon">
<Users
tabindex="-1"
class="w-7 h-7 text-black transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
class="w-7 h-7 text-black transition duration-75 focus:outline-none dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
</svelte:fragment>
</SidebarItem>
<SidebarItem label="Site" href="#/site" class="py-1 tall:p-2">
<SidebarItem
label={$t("pages.full_layout.site")}
href="#/site"
class="py-1 tall-xs:p-2"
>
<svelte:fragment slot="icon">
<User
tabindex="-1"
class="w-7 h-7 text-black transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
class="w-7 h-7 text-black transition duration-75 focus:outline-none dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
</svelte:fragment>
</SidebarItem>
<SidebarItem label="Messages" href="#/messages" class="py-1 tall:p-2">
<SidebarItem
label={$t("pages.full_layout.messages")}
href="#/messages"
class="py-1 tall-xs:p-2"
>
<svelte:fragment slot="icon">
<PaperAirplane
tabindex="-1"
class="-rotate-45 w-7 h-7 text-black transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
class="-rotate-45 w-7 h-7 text-black transition duration-75 focus:outline-none dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
<span
class="inline-flex justify-center items-center p-3 mt-1 -ml-3 w-3 h-3 text-sm font-medium text-primary-600 bg-primary-200 rounded-full dark:bg-primary-900 dark:text-primary-200"
@ -170,14 +188,14 @@
</svelte:fragment>
</SidebarItem>
<SidebarItem
label="Notifications"
label={$t("pages.full_layout.notifications")}
href="#/notifications"
class="mt-1 py-1 tall:p-2"
class="mt-1 py-1 tall-xs:p-2"
>
<svelte:fragment slot="icon">
<Bell
tabindex="-1"
class="w-7 h-7 text-black transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
class="w-7 h-7 text-black transition duration-75 focus:outline-none dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
<span
class="inline-flex justify-center items-center p-3 mt-1 -ml-3 w-3 h-3 text-sm font-medium text-primary-600 bg-primary-200 rounded-full dark:bg-primary-900 dark:text-primary-200"

@ -10,7 +10,6 @@
-->
<script lang="ts">
import { online } from "../store";
import FullLayout from "./FullLayout.svelte";
import Test from "./Test.svelte";
import {
@ -19,10 +18,7 @@
ArrowRightOnRectangle,
Users,
} from "svelte-heros-v2";
// @ts-ignore
import Logo from "../assets/nextgraph.svg?component";
// @ts-ignore
import LogoGray from "../assets/nextgraph-gray.svg?component";
import Logo from "./components/Logo.svelte";
let width: number;
let breakPoint: number = 662;
@ -43,13 +39,9 @@
class="mx-auto flex flex-wrap justify-between items-center w-full px-2 xxs:px-8 xs:px-10"
>
<a href="#/user" class="flex items-center" on:click>
{#if $online}
<Logo class="w-7 h-7 tall:w-10 tall:h-10" />
{:else}
<LogoGray class="w-7 h-7 tall:w-10 tall:h-10" />
{/if}
<Logo className="w-7 h-7 tall:w-10 tall:h-10" />
<span
class="ml-4 self-center text-lg font-normal text-gray-900 rounded-lg dark:text-white whitespace-nowrap"
class="ml-2 self-center text-lg font-normal text-gray-900 rounded-lg dark:text-white whitespace-nowrap"
>NextGraph</span
>
</a>
@ -60,7 +52,7 @@
class="w-7 h-7 text-black transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white focus:outline-none"
/>
</a>
<a href="#/messages" class="ml-6 row items-center" on:click>
<a href="#/messages" class="ml-4 row items-center" on:click>
<PaperAirplane
tabindex="-1"
class="w-7 h-7 text-black transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white focus:outline-none"

File diff suppressed because one or more lines are too long

@ -9,26 +9,52 @@
// according to those terms.
-->
<!--
The Login Procedure.
Has multiple states (steps) through the user flow.
-->
<script lang="ts">
import { Alert, Toggle } from "flowbite-svelte";
import { onMount, createEventDispatcher, tick } from "svelte";
import { Alert, Toggle, Button } from "flowbite-svelte";
import { onMount, createEventDispatcher } from "svelte";
import { t } from "svelte-i18n";
import ng from "../api";
import { emoji_cat, emojis, load_svg } from "../wallet_emojis";
import { PuzzlePiece } from "svelte-heros-v2";
import { emoji_cat, emojis, load_svg, type Emoji } from "../wallet_emojis";
import {
PuzzlePiece,
XCircle,
Backspace,
ArrowPath,
LockOpen,
CheckCircle,
ArrowLeft,
} from "svelte-heros-v2";
import PasswordInput from "./components/PasswordInput.svelte";
import Spinner from "./components/Spinner.svelte";
import { display_error } from "../store";
//import Worker from "../worker.js?worker&inline";
export let wallet;
export let for_import = false;
let top;
function scrollToTop() {
top.scrollIntoView();
}
let tauri_platform = import.meta.env.TAURI_PLATFORM;
let mobile = tauri_platform == "android" || tauri_platform == "ios";
const dispatch = createEventDispatcher();
onMount(async () => {
loaded = false;
await load_svg();
if (for_import) {
device_name = await ng.get_device_name();
}
load_svg();
//console.log(wallet);
await init();
});
async function init() {
@ -46,19 +72,31 @@
}
emojis2 = emojis2;
display = 0;
pazzlePage = 0;
selection = [];
error = undefined;
scrollToTop();
// This is only for awaiting that SVGs are loaded.
await load_svg();
loaded = true;
}
function letsgo() {
function start_with_pazzle() {
loaded = false;
step = "pazzle";
unlockWith = "pazzle";
scrollToTop();
}
function start_with_mnemonic() {
loaded = false;
step = "mnemonic";
unlockWith = "mnemonic";
scrollToTop();
}
let emojis2 = [];
let emojis2: Emoji[][] = [];
let shuffle;
@ -68,29 +106,35 @@
let pazzle_length = 9;
let display = 0;
let pazzlePage = 0;
let selection = [];
/** The selected emojis by category (one for each pazzle page). First will be the selected of first pazzle page. */
let selection = [].fill(null, 0, pazzle_length);
let pin_code = [];
/** The selected order from the order page. */
let ordered = [];
let last_one = {};
let shuffle_pin;
let error;
let trusted = false;
let trusted = true;
let mnemonic = "";
let unlockWith: "pazzle" | "mnemonic" | undefined;
let device_name;
function order() {
step = "order";
ordered = [];
last_one = {};
for (let i = 0; i < pazzle_length; i++) {
last_one[i] = true;
}
// In case, this is called by the cancel button, we need to reset the selection.
selection.forEach((emoji) => (emoji.sel = undefined));
selection = selection;
scrollToTop();
}
async function start_pin() {
@ -101,20 +145,19 @@
//console.log(shuffle_pin);
}
/** Called on selecting emoji in a category. */
function select(val) {
//console.log(emojis2[display][val]);
let cat_idx = shuffle.category_indices[display];
let cat_idx = shuffle.category_indices[pazzlePage];
let cat = emojis[emoji_cat[cat_idx]];
let idx = shuffle.emoji_indices[display][val];
//console.log(cat_idx, emoji_cat[cat_idx], idx, cat[idx].code);
let idx = shuffle.emoji_indices[pazzlePage][val];
selection.push({ cat: cat_idx, index: idx });
//console.log(selection);
selection[pazzlePage] = { cat: cat_idx, index: idx };
if (display == pazzle_length - 1) {
if (pazzlePage == pazzle_length - 1) {
order();
} else {
display = display + 1;
pazzlePage = pazzlePage + 1;
}
}
@ -122,20 +165,22 @@
step = "opening";
let pazzle = [];
for (const emoji of ordered) {
pazzle.push((emoji.cat << 4) + emoji.index);
}
//console.log(pazzle);
//console.log(wallet);
const mnemonic_words = mnemonic.split(" ");
// open the wallet
try {
if (tauri_platform) {
let opened_wallet = await ng.wallet_open_with_pazzle(
// TODO @niko: Add device_name as param to open_with_* APIs
let opened_wallet =
unlockWith === "pazzle"
? await ng.wallet_open_with_pazzle(wallet, pazzle, pin_code)
: await ng.wallet_open_with_mnemonic_words(
wallet,
pazzle,
mnemonic_words,
pin_code
);
// try {
@ -153,6 +198,7 @@
wallet: opened_wallet,
id: opened_wallet.V0.wallet_id,
trusted,
device_name,
});
} else {
let worker_import = await import("../worker.js?worker&inline");
@ -172,7 +218,16 @@
myWorker.onmessage = async (msg) => {
//console.log("Message received from worker", msg.data);
if (msg.data.loaded) {
myWorker.postMessage({ wallet, pazzle, pin_code });
if (unlockWith === "pazzle") {
myWorker.postMessage({ wallet, pazzle, pin_code, device_name });
} else {
myWorker.postMessage({
wallet,
mnemonic_words,
pin_code,
device_name,
});
}
//console.log("postMessage");
} else if (msg.data.success) {
//console.log(msg.data);
@ -191,6 +246,7 @@
wallet: msg.data.success,
id: msg.data.success.V0.wallet_id,
trusted,
device_name,
});
} else {
console.error(msg.data.error);
@ -214,23 +270,17 @@
dispatch("cancel");
}
async function pin(val) {
//console.log(val);
pin_code.push(val);
if (pin_code.length == 4) {
await finish();
}
async function on_pin_key(val) {
pin_code = [...pin_code, val];
}
async function select_order(val, pos) {
delete last_one[pos];
//console.log(last_one);
//console.log(val);
async function select_order(val) {
ordered.push(val);
val.sel = ordered.length;
selection = selection;
if (ordered.length == pazzle_length - 1) {
let last = selection[Object.keys(last_one)[0]];
let last = selection.find((emoji) => !emoji.sel);
ordered.push(last);
last.sel = ordered.length;
selection = selection;
@ -238,117 +288,235 @@
await start_pin();
}
}
function go_back() {
if (step === "mnemonic") {
init();
} else if (step === "pazzle") {
// Go to previous pazzle or init page, if on first pazzle.
if (pazzlePage === 0) {
init();
} else {
pazzlePage -= 1;
}
} else if (step === "order") {
if (ordered.length === 0) {
step = "pazzle";
} else {
const last_selected = ordered.pop();
last_selected.sel = null;
ordered = ordered;
selection = selection;
}
} else if (step === "pin") {
if (pin_code.length === 0) {
if (unlockWith === "mnemonic") {
start_with_mnemonic();
} else {
// Unselect the last two elements.
const to_unselect = ordered.slice(-2);
to_unselect.forEach((val) => {
val.sel = null;
});
ordered = ordered.slice(0, -2);
selection = selection;
step = "order";
}
} else {
pin_code = pin_code.slice(0, pin_code.length - 1);
}
}
}
let width: number;
let height: number;
const breakPointWidth: number = 535;
const breakPointHeight: number = 1005;
let mobile = false;
$: if (width >= breakPointWidth && height >= breakPointHeight) {
mobile = false;
} else {
mobile = true;
}
</script>
{#if step == "load"}
<div class=" max-w-xl lg:px-8 mx-auto px-4 mt-10">
<h2 class="pb-5 text-xl">How to open your wallet, step by step :</h2>
<ul class="mb-8 ml-3 space-y-4 text-left list-decimal">
<div
class="flex-col justify-center md:max-w-2xl py-4 sm:px-8"
class:h-screen={step !== "load" && height > 660}
class:flex={height > 660}
bind:this={top}
>
{#if step == "load"}
<div class="flex flex-col justify-center p-4 pt-6">
<h2 class="pb-5 text-xl self-start">
{$t("pages.login.heading")}
</h2>
<h3 class="pb-2 text-lg self-start">{$t("pages.login.with_pazzle")}</h3>
<ul class="mb-8 ml-3 space-y-4 text-justify text-sm list-decimal">
<li>
For each one of the 9 categories of images, you will be presented with
the 15 possible image choices. The categories are shuffled at every
login. They will not always appear in the same order.
{$t("pages.login.pazzle_steps.1")}
</li>
<li>
At each category, only one of the 15 displayed choices is the correct
image that belongs to your pazzle. Find it and tap or click on that one.
The 15 images are shuffled too, they will not appear at the same
position at each login. On a computer, you can also use the tab key on
your keyboard to move to the desired item on the screen, then press the
space bar to select each one.
{$t("pages.login.pazzle_steps.2")}
</li>
<li>
Once you completed the last category, you will be presented with all the
images you have previously selected. Their order is displayed as it was
when you picked them. But this is not the correct order of the images in
your pazzle. You now have to order them correctly.
{$t("pages.login.pazzle_steps.3")}
</li>
<li>
You must remember which image should be the first one in your pazzle.
Find it on the screen and click or tap on it. It will be greyed out and
the number 1 will appear on top of it.
{$t("pages.login.pazzle_steps.4")}
</li>
<li>
Move on to the second image of your pazzle (that you memorized). Find it
on the screen and tap on it. Repeat this step until you reached the last
image.
{$t("pages.login.pazzle_steps.5")}
</li>
<li>
Finally, your PIN code will be asked. enter it by clicking or tapping on
the digits.
{$t("pages.login.pazzle_steps.6")}
</li>
</ul>
</div>
<div class=" max-w-xl lg:px-8 mx-auto px-4 text-primary-700">
{#if !loaded}
Loading pazzle...
<svg
class="animate-spin my-4 h-14 w-14 mx-auto"
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
<h3 class="pb-2 text-lg self-start">
{$t("pages.login.with_mnemonic")}
</h3>
<ul class="mb-8 ml-3 space-y-4 text-justify text-sm list-decimal">
<li>
{$t("pages.login.mnemonic_steps.1")}
</li>
<li>
{$t("pages.login.mnemonic_steps.2")}
</li>
</ul>
<!-- Save wallet? -->
{#if for_import}
<div class="max-w-xl lg:px-8 mx-auto px-4 mb-2">
<span class="text-xl"
>{$t("pages.wallet_create.save_wallet_options.trust")}
</span> <br />
<p class="text-sm">
{$t("pages.wallet_create.save_wallet_options.trust_description")}
{#if !tauri_platform}
{$t("pages.login.trust_device_allow_cookies")}{/if}<br />
</p>
<div class="flex justify-center items-center my-4">
<Toggle class="" bind:checked={trusted}
>{$t("pages.login.trust_device_yes")}</Toggle
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
/>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
</div>
</div>
{/if}
<div class="max-w-xl lg:px-8 mx-auto px-4 text-primary-700">
<div class="flex flex-col justify-centerspace-x-12 mt-4 mb-4">
<!-- Device Name, if trusted-->
{#if for_import}
<label for="device-name-input" class="text-sm text-black">
{$t("pages.login.device_name_label")}
</label>
<input
id="device-name-input"
bind:value={device_name}
placeholder={$t("pages.login.device_name_placeholder")}
type="text"
class="w-full mb-10 lg:px-8 mx-auto px-4 bg-gray-50 border border-gray-300 text-xs rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
/>
</svg>
{/if}
{#if !loaded}
{$t("pages.login.loading_pazzle")}...
<Spinner className="my-4 h-14 w-14 mx-auto" />
{:else}
<button
on:click={letsgo}
class="mt-1 text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-100/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55 mb-2"
on:click={start_with_pazzle}
class="mt-1 text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-100/50 rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55 mb-2"
>
<PuzzlePiece
tabindex="-1"
class="w-8 h-8 mr-2 -ml-1 transition duration-75 group-hover:text-gray-900 dark:group-hover:text-white"
class="w-8 h-8 mr-2 -ml-1 transition duration-75 focus:outline-none group-hover:text-gray-900 dark:group-hover:text-white"
/>
Open my wallet now!
{$t("pages.login.open_with_pazzle")}
</button>
{/if}
</div>
{#if for_import}
<div class=" max-w-xl lg:px-8 mx-auto px-4 mb-8">
<span class="text-xl">Do you trust this device? </span> <br />
<div class="flex justify-center items-center my-4">
<Toggle class="" bind:checked={trusted}
>Yes, save my wallet on this device</Toggle
<button
on:click={cancel}
class="mt-3 mb-2 text-gray-500 dark:text-gray-400 focus:ring-4 focus:ring-primary-100/50 rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55"
><ArrowLeft
tabindex="-1"
class="w-8 h-8 mr-2 -ml-1 transition duration-75 focus:outline-none group-hover:text-gray-900 dark:group-hover:text-white"
/>{$t("pages.login.login_cancel")}</button
>
<span
on:click={start_with_mnemonic}
on:keypress={start_with_mnemonic}
role="link"
tabindex="0"
class="mt-1 text-lg px-5 py-2.5 text-center inline-flex items-center mb-10 underline cursor-pointer"
>
{$t("pages.login.open_with_mnemonic")}
</span>
</div>
<p class="text-sm">
If you do, if this device is yours or is used by few trusted persons of
your family or workplace, and you would like to login again from this
device in the future, then you can save your wallet on this device. To
the contrary, if this device is public and shared by strangers, do not
save your wallet here. {#if !tauri_platform}By selecting this option,
you agree to save some cookies on your browser.{/if}<br />
</p>
</div>
{/if}
{:else if step == "pazzle"}
</div>
<!-- The following steps have navigation buttons and fixed layout -->
{:else if step == "pazzle" || step == "order" || step == "pin" || step == "mnemonic"}
<div
class="h-screen aspect-[3/5] pazzleline"
class="flex-col justify-center h-screen"
class:flex={height > 660}
class:min-w-[310px]={mobile}
class:min-w-[500px]={!mobile}
class:max-w-[360px]={mobile}
class:max-w-[370px]={mobile}
class:max-w-[600px]={!mobile}
>
<div class="mt-auto flex flex-col justify-center">
<!-- Unlock Screens -->
{#if step == "mnemonic"}
<form on:submit|preventDefault={start_pin}>
<label
for="mnemonic-input"
class="block mb-2 text-xl text-gray-900 dark:text-white"
>{$t("pages.login.enter_mnemonic")}</label
>
<PasswordInput
id="mnemonic-input"
placeholder={$t("pages.login.mnemonic_placeholder")}
bind:value={mnemonic}
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
auto_complete="mnemonic"
/>
<div class="flex">
<Button
type="submit"
class="mt-3 mb-2 ml-auto text-white bg-primary-700 disabled:opacity-65 focus:ring-4 focus:ring-blue-500 focus:border-blue-500 rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-blue-500 dark:focus:border-blue-500"
on:click={start_pin}
disabled={mnemonic.split(" ").length !== 12}
><CheckCircle
tabindex="-1"
class="w-8 h-8 mr-2 -ml-1 transition duration-75 group-hover:text-gray-900 dark:group-hover:text-white"
/>{$t("buttons.confirm")}</Button
>
</div>
</form>
{:else if step == "pazzle"}
<p class="max-w-xl mx-auto lg:max-w-2xl">
<span class="text-xl">
{@html $t("pages.login.select_emoji", {
values: {
category: $t(
"emojis.category." +
emoji_cat[shuffle.category_indices[pazzlePage]]
),
},
})}</span
>
</p>
{#each [0, 1, 2, 3, 4] as row}
<div class="columns-3 gap-0">
{#each emojis2[display]?.slice(0 + row * 3, 3 + row * 3) || [] as emoji, i}
{#each emojis2[pazzlePage]?.slice(0 + row * 3, 3 + row * 3) || [] as emoji, i (pazzlePage + "-" + row + "-" + i)}
<div
role="button"
tabindex="0"
class="w-full aspect-square emoji focus:outline-none focus:bg-gray-300"
title={$t("emojis.codes." + emoji.code)}
on:click={() => select(row * 3 + i)}
on:keypress={() => select(row * 3 + i)}
>
@ -357,16 +525,10 @@
{/each}
</div>
{/each}
</div>
{:else if step == "order"}
<!-- console.log(cat_idx, emoji_cat[cat_idx], idx, cat[idx].code); -->
<div
class="h-screen aspect-[3/3] pazzleline"
class:min-w-[320px]={mobile}
class:min-w-[500px]={!mobile}
class:max-w-[360px]={mobile}
class:max-w-[600px]={!mobile}
>
{:else if step == "order"}
<p class="max-w-xl mx-auto lg:max-w-2xl mb-2">
<span class="text-xl">{$t("pages.login.order_emojis")}</span>
</p>
{#each [0, 1, 2] as row}
<div class="columns-3 gap-0">
{#each selection.slice(0 + row * 3, 3 + row * 3) || [] as emoji, i}
@ -375,85 +537,129 @@
role="button"
tabindex="0"
class="w-full aspect-square emoji focus:outline-none focus:bg-gray-300"
on:click={() => select_order(emoji, row * 3 + i)}
on:keypress={() => select_order(emoji, row * 3 + i)}
on:click={() => select_order(emoji)}
on:keypress={() => select_order(emoji)}
title={$t(
"emojis.codes." +
emojis[emoji_cat[emoji.cat]][emoji.index].code
)}
>
<svelte:component
this={emojis[emoji_cat[emoji.cat]][emoji.index].svg?.default}
this={emojis[emoji_cat[emoji.cat]][emoji.index].svg
?.default}
/>
</div>
{:else}
<div class="w-full aspect-square opacity-25 select-none sel-emoji">
<div
class="w-full aspect-square opacity-25 select-none sel-emoji"
title={$t(
"emojis.codes." +
emojis[emoji_cat[emoji.cat]][emoji.index].code
)}
>
<svelte:component
this={emojis[emoji_cat[emoji.cat]][emoji.index].svg?.default}
this={emojis[emoji_cat[emoji.cat]][emoji.index].svg
?.default}
/>
<span class="sel drop-shadow-[2px_2px_2px_rgba(255,255,255,1)]"
>{emoji.sel}</span
<span
class="sel drop-shadow-[2px_2px_2px_rgba(255,255,255,1)]"
class:text-[8em]={!mobile}
class:text-[6em]={mobile}>{emoji.sel}</span
>
</div>
{/if}
{/each}
</div>
{/each}
</div>
{:else if step == "pin"}
<div class=" max-w-6xl lg:px-8 mx-auto px-3">
<p class="max-w-xl md:mx-auto lg:max-w-2xl">
<span class="text-xl">Enter your PIN code</span>
{:else if step == "pin"}
<p class="items-center">
<span class="text-xl">{$t("pages.login.enter_pin")}</span>
</p>
<div class="w-[295px] mx-auto">
<!-- Chrome requires the columns-3 __flex__ to be set, or else it wraps the lines incorrectly.
However, this leads to the width decreasing and the buttons come together in mobile view.
So we need a way to fix the width across all screens. -->
{#each [0, 1, 2] as row}
<div class="">
<div class="columns-3 flex">
{#each shuffle_pin.slice(0 + row * 3, 3 + row * 3) as num}
<button
tabindex="0"
class="m-1 select-none align-bottom text-7xl w-[90px] h-[90px] p-0"
on:click={async () => await pin(num)}
class="pindigit m-1 disabled:opacity-15 disabled:text-gray-200 select-none align-bottom text-7xl p-0 w-full aspect-square border-0"
class:h-[160px]={!mobile}
class:h-[100px]={mobile}
class:text-8xl={!mobile}
on:click={async () => await on_pin_key(num)}
disabled={pin_code.length >= 4}
>
<span>{num}</span>
</button>
{/each}
</div>
{/each}
<div class="columns-3 flex">
<div class="m-1 w-full aspect-square" />
<button
tabindex="0"
class="m-1 select-none mx-auto align-bottom text-7xl w-[90px] h-[90px] p-0"
on:click={async () => await pin(shuffle_pin[9])}
class="pindigit disabled:opacity-15 m-1 disabled:text-gray-200 select-none align-bottom text-7xl p-0 w-full aspect-square border-0"
class:h-[160px]={!mobile}
class:h-[100px]={mobile}
class:text-8xl={!mobile}
on:click={async () => await on_pin_key(shuffle_pin[9])}
disabled={pin_code.length >= 4}
>
<span>{shuffle_pin[9]}</span>
</button>
<Button
tabindex="0"
class="w-full bg-green-300 hover:bg-green-300/90 enabled:animate-bounce disabled:bg-gray-200 disabled:opacity-15 m-1 select-none align-bottom text-7xl p-0 aspect-square border-0"
on:click={async () => await finish()}
disabled={pin_code.length < 4}
>
<LockOpen
tabindex="-1"
class="w-[50%] h-[50%] transition duration-75 focus:outline-none select-none group-hover:text-gray-900 dark:group-hover:text-white"
/>
</Button>
</div>
<span class="mt-3 text-9xl min-h-[8rem] text-center"
>{#each pin_code as pin_key}*{/each}</span
>
{/if}
</div>
{:else if step == "opening"}
<div class=" max-w-6xl lg:px-8 mx-auto px-4 text-primary-700">
Opening your wallet...<br />
Please wait
<svg
class="animate-spin mt-10 h-14 w-14 mx-auto"
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
<!-- Navigation Buttons for pazzle, order pin, mnemonic -->
<div class="flex justify-between mb-6 mt-auto">
<button
on:click={cancel}
class="mt-1 bg-red-100 hover:bg-red-100/90 focus:ring-4 focus:ring-primary-100/50 rounded-lg sm:text-lg px-5 py-2.5 text-center select-none inline-flex items-center dark:focus:ring-primary-700/55"
><XCircle
tabindex="-1"
class="w-8 h-8 mr-2 -ml-1 transition focus:outline-none duration-75 group-hover:text-gray-900 dark:group-hover:text-white"
/>{$t("buttons.cancel")}</button
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
/>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
<button
class="mt-1 ml-2 min-w-[141px] focus:ring-4 focus:ring-primary-100/50 rounded-lg sm:text-lg px-5 py-2.5 text-center select-none inline-flex items-center dark:focus:ring-primary-700/55"
on:click={go_back}
><Backspace
tabindex="-1"
class="w-8 h-8 mr-2 -ml-1 transition focus:outline-none duration-75 group-hover:text-gray-900 dark:group-hover:text-white"
/>
</svg>
{#if step === "mnemonic" || (step === "pazzle" && pazzlePage === 0)}
{$t("buttons.go_back")}
{:else}
{$t("buttons.correct")}
{/if}
</button>
</div>
{:else if step == "end"}
</div>
{:else if step == "opening"}
<div class=" max-w-6xl lg:px-8 mx-auto px-4 text-primary-700">
{@html $t("pages.login.opening_wallet")}
<Spinner className="mt-10 h-14 w-14 mx-auto" />
</div>
{:else if step == "end"}
{#if error}
<div class=" max-w-6xl lg:px-8 mx-auto px-4 text-red-800">
An error occurred !
<div class=" max-w-6xl lg:px-8 mx-auto text-red-800">
<div class="mt-auto max-w-6xl lg:px-8">
{$t("errors.an_error_occurred")}
<svg
fill="none"
class="animate-bounce mt-10 h-10 w-10 mx-auto"
@ -470,14 +676,33 @@
/>
</svg>
<Alert color="red" class="mt-5">
{error}
{display_error(error)}
</Alert>
<button class="mt-10 select-none" on:click={init}> Try again </button>
<button class="mt-10 ml-5 select-none" on:click={cancel}> Cancel </button>
</div>
<div class="flex justify-between mt-auto gap-4">
<button
on:click={cancel}
class="mt-10 bg-red-100 hover:bg-red-100/90 focus:ring-4 focus:ring-primary-100/50 rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55"
><XCircle
tabindex="-1"
class="w-8 h-8 mr-2 -ml-1 transition duration-75 focus:outline-none group-hover:text-gray-900 dark:group-hover:text-white"
/>{$t("buttons.cancel")}</button
>
<button
class="mt-10 ml-2 select-none text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-100/50 rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55"
on:click={init}
>
<ArrowPath
tabindex="-1"
class="w-8 h-8 mr-2 -ml-1 transition duration-75 focus:outline-none group-hover:text-gray-900 dark:group-hover:text-white"
/>
{$t("buttons.try_again")}
</button>
</div>
</div>
{:else}
<div class=" max-w-6xl lg:px-8 mx-auto px-4 text-green-800">
Your wallet is opened! <br />Please wait while the app is loading...
{@html $t("pages.login.wallet_opened")}
<svg
class="my-10 h-16 w-16 mx-auto"
fill="none"
@ -495,21 +720,31 @@
</svg>
</div>
{/if}
{/if}
{/if}
</div>
<svelte:window bind:innerWidth={width} bind:innerHeight={height} />
<style>
.pazzleline {
.pindigit {
min-height: 99px;
}
/* .pazzleline {
margin-right: auto;
margin-left: auto;
}
} */
.sel {
position: absolute;
display: flex;
width: 100%;
top: 45%;
height: 100%;
top: 0;
left: 0;
font-size: 100px;
font-weight: 700;
justify-content: center;
align-items: center;
}
.sel-emoji {

@ -9,32 +9,37 @@
// according to those terms.
-->
<!--
Component to inform the user, that no wallet is registered on this device.
Offers login or create wallet buttons.
-->
<script>
// @ts-ignore
import Logo from "../assets/nextgraph.svg?component";
import { link } from "svelte-spa-router";
import CenteredLayout from "./CenteredLayout.svelte";
import { t } from "svelte-i18n";
</script>
<CenteredLayout displayFooter={true}>
<div class="container3">
<div class="row">
<Logo class="logo block h-40" alt="NextGraph Logo" />
<Logo class="logo block h-40" alt={$t("common.logo")} />
</div>
<h1 class="text-2xl mb-10">Welcome to NextGraph</h1>
<h1 class="text-2xl mb-10">{$t("pages.no_wallet.welcome")}</h1>
<p class="max-w-sm">
We could not find a wallet saved on this device.<br /> If you already have
a wallet, select "Log in", otherwise, select "Create Wallet" here below
{@html $t("pages.no_wallet.description")}
</p>
<div class="row mt-5">
<a href="/wallet/create" use:link>
<button
tabindex="-1"
class="text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-700/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55 mr-2 mb-2"
class="text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-700/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55 mb-2"
>
<svg
class="w-8 h-8 mr-2 -ml-1"
class="w-8 h-8 -ml-1"
fill="none"
stroke="currentColor"
stroke-width="1.5"
@ -48,7 +53,7 @@
d="M19 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zM4 19.235v-.11a6.375 6.375 0 0112.75 0v.109A12.318 12.318 0 0110.374 21c-2.331 0-4.512-.645-6.374-1.766z"
/>
</svg>
Create wallet
{$t("pages.no_wallet.create_wallet")}
</button>
</a>
</div>
@ -56,7 +61,7 @@
<a href="/wallet/login" use:link>
<button
tabindex="-1"
class="text-primary-700 bg-primary-100 hover:bg-primary-100/90 focus:ring-4 focus:ring-primary-100/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-100/55 mr-2 mb-2"
class="text-primary-700 bg-primary-100 hover:bg-primary-100/90 focus:ring-4 focus:ring-primary-100/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-100/55 mb-2"
>
<svg
class="w-8 h-8 mr-2 -ml-1"
@ -73,7 +78,7 @@
d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z"
/>
</svg>
Log in
{$t("buttons.login")}
</button>
</a>
</div>

@ -21,17 +21,19 @@
active_session,
cannot_load_offline,
online,
get_blob,
} from "../store";
import { link } from "svelte-spa-router";
import { onMount, onDestroy, tick } from "svelte";
import { Button } from "flowbite-svelte";
import { Button, Progressbar, Spinner } from "flowbite-svelte";
import DataClassIcon from "./DataClassIcon.svelte";
import { t } from "svelte-i18n";
let is_tauri = import.meta.env.TAURI_PLATFORM;
let files = $active_session && branch_subs($active_session.private_store_id);
let upload_progress: null | { total: number; current: number; error?: any } =
null;
let img_map = {};
let files = $active_session && branch_subs($active_session.private_store_id);
let gitgraph;
@ -526,76 +528,28 @@
// ]);
});
async function get_img(ref) {
if (!ref) return false;
let cache = img_map[ref.nuri];
if (cache) {
return cache;
}
let prom = new Promise(async (resolve) => {
try {
let nuri = {
target: "PrivateStore",
entire_store: false,
access: [{ Key: ref.reference.key }],
locator: [],
object: ref.reference.id,
};
let file_request = {
V0: {
command: "FileGet",
nuri,
session_id: $active_session.session_id,
},
};
let final_blob;
let content_type;
let unsub = await ng.app_request_stream(file_request, async (blob) => {
//console.log("GOT APP RESPONSE", blob);
if (blob.V0.FileMeta) {
content_type = blob.V0.FileMeta.content_type;
final_blob = new Blob([], { type: content_type });
} else if (blob.V0.FileBinary) {
if (blob.V0.FileBinary.byteLength > 0) {
final_blob = new Blob([final_blob, blob.V0.FileBinary], {
type: content_type,
});
}
} else if (blob.V0 == "EndOfStream") {
var imageUrl = URL.createObjectURL(final_blob);
resolve(imageUrl);
}
});
} catch (e) {
console.error(e);
resolve(false);
}
});
img_map[ref.nuri] = prom;
return prom;
}
let fileinput;
function uploadFile(upload_id, nuri, file, success) {
let chunkSize = 1048564;
let chunkSize = 1_048_564;
let fileSize = file.size;
let offset = 0;
let readBlock = null;
upload_progress = { total: fileSize, current: offset };
let onLoadHandler = async function (event) {
let result = event.target.result;
if (event.target.error == null) {
offset += event.target.result.byteLength;
//console.log("chunk", event.target.result);
offset += result.byteLength;
upload_progress = { total: fileSize, current: offset };
// console.log("chunk", result);
let res = await ng.upload_chunk(
$active_session.session_id,
upload_id,
event.target.result,
result,
nuri
);
//console.log("chunk upload res", res);
@ -609,6 +563,7 @@
return;
}
// If finished:
if (offset >= fileSize) {
//console.log("file uploaded");
let res = await ng.upload_chunk(
@ -619,8 +574,16 @@
);
//console.log("end upload res", res);
if (success) {
upload_progress = { total: fileSize, current: fileSize };
success(res);
} else {
upload_progress = { total: fileSize, current: fileSize, error: true };
}
// Make progress bar disappear
setTimeout(() => {
upload_progress = null;
}, 2_500);
return;
}
@ -696,16 +659,8 @@
{#if $cannot_load_offline}
<div class="row p-4">
<p>
You are offline and using the web app. You need to connect to the broker
at least once before you can start using the app locally because the web
app does not keep a local copy of your documents.<br /><br />
Once connected, if you lose connectivity again, you will be able to have
limited access to some functionalities. Sending binary files won't be possible,
because the limit of local storage in your browser is around 5MB.<br
/><br />
All those limitations will be lifted once the "UserStorage for Web" feature
will be released. Stay tuned! <br /><br />
Check your connection status in the <a href="#/user">User panel</a>.
{@html $t("pages.test.cannot_load_offline")}
<a href="#/user">{$t("pages.user_panel.title")}</a>.
</p>
</div>
{:else}
@ -745,7 +700,7 @@
d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
/>
</svg>
Add image
{$t("pages.test.add_image")}
</Button>
<input
style="display:none"
@ -755,15 +710,30 @@
bind:this={fileinput}
/>
</div>
{#if upload_progress !== null}
<div class="mx-6 mt-2">
<Progressbar
progress={(
(100 * upload_progress.current) /
upload_progress.total
).toFixed(0)}
labelOutside={$t("pages.test.upload_progress")}
/>
</div>
{/if}
{#if files}
{#await files.load()}
<p>Currently loading...</p>
<p>{$t("connectivity.loading")}...</p>
{:then}
{#each $files as file}
<p>
{file.name}
{#await get_img(file) then url}
{#await get_blob(file)}
<div class="ml-2">
<Spinner />
</div>
{:then url}
{#if url}
<img src={url} title={file.nuri} alt={file.name} />
{/if}

@ -0,0 +1,89 @@
<!--
// Copyright (c) 2022-2024 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
-->
<script lang="ts">
export let value: string = "";
export let id: string;
let has_success: boolean = false;
const tauri_platform = import.meta.env.TAURI_PLATFORM;
const setClipboard = async (text: string) => {
if (tauri_platform) {
// TODO: this won't work for tauri platform.
// const { writeText } = await import("@tauri-apps/api/clipboard");
// await writeText(text);
} else {
navigator.clipboard.writeText(text);
}
};
const on_click = (e) => {
has_success = true;
setTimeout(() => (has_success = false), 2_000);
setClipboard(value);
};
</script>
<div class="w-full mt-2">
<div class="relative">
<textarea
{id}
rows="3"
style="resize: none;"
{value}
class="col-span-6 pr-11 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500"
disabled
readonly
/>
{#if !tauri_platform}
<button
on:click={on_click}
class="absolute inset-y-0 right-0 p-3 flex items-center text-sm leading-5 bg-transparent shadow-none"
>
<span id="default-icon" class:hidden={has_success}>
<svg
class="w-3.5 h-3.5"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 18 20"
>
<path
d="M16 1h-3.278A1.992 1.992 0 0 0 11 0H7a1.993 1.993 0 0 0-1.722 1H2a2 2 0 0 0-2 2v15a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2Zm-3 14H5a1 1 0 0 1 0-2h8a1 1 0 0 1 0 2Zm0-4H5a1 1 0 0 1 0-2h8a1 1 0 1 1 0 2Zm0-5H5a1 1 0 0 1 0-2h2V2h4v2h2a1 1 0 1 1 0 2Z"
/>
</svg>
</span>
<span
id="success-icon"
class="inline-flex items-center"
class:hidden={!has_success}
>
<svg
class="w-3.5 h-3.5 text-blue-700 dark:text-blue-500"
aria-hidden={!has_success}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 16 12"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M1 5.917 5.724 10.5 15 1.5"
/>
</svg>
</span>
</button>
{/if}
</div>
</div>

@ -0,0 +1,57 @@
<!--
// Copyright (c) 2022-2024 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
-->
<!--
@component DeviceIcon
Display an icon for a device class provided by the `device` attribute.
Pass `config` for custom attributes.
-->
<script lang="ts">
import {
Icon,
Cube,
GlobeAlt,
QuestionMarkCircle,
DevicePhoneMobile,
ComputerDesktop,
ServerStack,
Key,
CommandLine,
} from "svelte-heros-v2";
export let config = {};
export let device: string;
const mapping = {
Web: GlobeAlt,
NativeIos: DevicePhoneMobile,
NativeAndroid: DevicePhoneMobile,
NativeMacOS: ComputerDesktop,
NativeLinux: ComputerDesktop,
NativeWin: ComputerDesktop,
NativeService: ServerStack,
NodeService: ServerStack,
Verifier: ServerStack,
VerifierLocal: ServerStack,
ClientBroker: ServerStack,
WalletMaster: ServerStack,
Box: Cube,
Stick: Key, // VerifierStick
Cli: CommandLine,
};
const find = (dataClass: string) => {
return mapping[dataClass] || QuestionMarkCircle;
};
</script>
<Icon {...config} variation="outline" color="black" icon={find(device)} />

@ -0,0 +1,40 @@
<!--
// Copyright (c) 2022-2024 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
-->
<!--
@component Logo
The NextGraph Logo svg with color changing between blue and gray,
depending on connection status:
- connected: blue
- connecting: pulse between blue and gray
- disconnected: gray
Provide classes using the `className` prop.
-->
<script lang="ts">
import { connection_status } from "../../store";
// @ts-ignore
import Logo from "../../assets/nextgraph-nofill.svg?component";
export let className: string = "";
let connection_status_class = "logo-blue";
// Color is adjusted to connection status.
$: if ($connection_status === "connecting") {
connection_status_class = "logo-pulse";
} else if ($connection_status === "disconnected") {
connection_status_class = "logo-gray";
} else {
connection_status_class = "logo-blue";
}
</script>
<Logo class={`${className} ${connection_status_class}`} />

@ -0,0 +1,87 @@
<!--
// Copyright (c) 2022-2024 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
-->
<script lang="ts">
export let value: string | undefined = undefined;
export let placeholder: string | undefined = undefined;
export let className: string | undefined = undefined;
export let id: string | undefined = undefined;
export let auto_complete: string | undefined = undefined;
export let show: boolean = false;
let input;
let type: "password" | "text" = "password";
$: type = show ? "text" : "password";
function handleInput(event: Event) {
const target = event.target as HTMLInputElement;
value = target.value;
}
async function toggle() {
let { selectionStart, selectionEnd } = input;
show = !show;
input.focus();
setTimeout(function () {
input.selectionStart = selectionStart;
input.selectionEnd = selectionEnd;
}, 0);
}
</script>
<div class="relative">
<input
bind:this={input}
{value}
{placeholder}
{id}
{type}
on:input={handleInput}
class={`${className} pr-12 text-md block`}
autocomplete={auto_complete}
/>
<div
class="absolute inset-y-0 right-0 pr-3 flex items-center text-sm leading-5"
>
<svg
fill="none"
on:click={toggle}
on:keypress={toggle}
class={`${show ? "block" : "hidden"} h-6 text-gray-700`}
xmlns="http://www.w3.org/2000/svg"
viewbox="0 0 576 512"
>
<path
fill="currentColor"
d="M572.52 241.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400a144 144 0 1 1 144-144 143.93 143.93 0 0 1-144 144zm0-240a95.31 95.31 0 0 0-25.31 3.79 47.85 47.85 0 0 1-66.9 66.9A95.78 95.78 0 1 0 288 160z"
>
</path>
</svg>
<svg
fill="none"
class={`${!show ? "block" : "hidden"} h-6 text-gray-700`}
on:click={toggle}
on:keypress={toggle}
xmlns="http://www.w3.org/2000/svg"
viewbox="0 0 640 512"
>
<path
fill="currentColor"
d="M320 400c-75.85 0-137.25-58.71-142.9-133.11L72.2 185.82c-13.79 17.3-26.48 35.59-36.72 55.59a32.35 32.35 0 0 0 0 29.19C89.71 376.41 197.07 448 320 448c26.91 0 52.87-4 77.89-10.46L346 397.39a144.13 144.13 0 0 1-26 2.61zm313.82 58.1l-110.55-85.44a331.25 331.25 0 0 0 81.25-102.07 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64a308.15 308.15 0 0 0-147.32 37.7L45.46 3.37A16 16 0 0 0 23 6.18L3.37 31.45A16 16 0 0 0 6.18 53.9l588.36 454.73a16 16 0 0 0 22.46-2.81l19.64-25.27a16 16 0 0 0-2.82-22.45zm-183.72-142l-39.3-30.38A94.75 94.75 0 0 0 416 256a94.76 94.76 0 0 0-121.31-92.21A47.65 47.65 0 0 1 304 192a46.64 46.64 0 0 1-1.54 10l-73.61-56.89A142.31 142.31 0 0 1 320 112a143.92 143.92 0 0 1 144 144c0 21.63-5.29 41.79-13.9 60.11z"
>
</path>
</svg>
</div>
</div>

@ -0,0 +1,38 @@
<!--
// Copyright (c) 2022-2024 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
-->
<script lang="ts">
export let className: string = "";
export let color: string = "currentColor";
export let width: number = 24;
</script>
<svg
class={"animate-spin " + className}
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke={color}
viewBox={`0 0 ${width} ${width}`}
>
<circle
class="opacity-25"
cx={width / 2}
cy={width / 2}
r={width / 2.4}
stroke={color}
stroke-width={width / 6}
/>
<path
class="opacity-75"
fill={color}
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>

@ -0,0 +1,231 @@
{
"emojis": {
"codes": {
"happy": "grinsendes Gesicht",
"happy_tears": "Gesicht mit Freudentränen",
"halo": "lächelndes Gesicht mit Heiligenschein",
"three_hearts": "lächelndes Gesicht mit Herzen",
"with_two_hearts": "lächelndes Gesicht mit herzförmigen Augen",
"one_heart": "Kuss zuwerfendes Gesicht",
"with_tongue": "Gesicht mit herausgestreckter Zunge und zusammengekniffenen Augen",
"with_two_hands": "Gesicht mit umarmenden Händen",
"one_hand": "verlegen kicherndes Gesicht",
"silenced": "Gesicht mit Reißverschlussmund",
"celebrating": "Partygesicht",
"sunglasses": "lächelndes Gesicht mit Sonnenbrille",
"eyes_up": "Augen verdrehendes Gesicht",
"monocle": "Gesicht mit Monokel",
"sleeping": "schlafendes Gesicht",
"mask": "Gesicht mit Atemschutzmaske",
"fever": "Gesicht mit Fieberthermometer",
"bandage": "Gesicht mit Kopfverband",
"vomit": "kotzendes Gesicht",
"tissue": "niesendes Gesicht",
"hot": "Gesicht mit Kopfverband",
"cold": "frierendes Gesicht",
"crossed_eyes": "benommenes Gesicht",
"exploding": "explodierender Kopf",
"sad": "düsteres Gesicht",
"long_nose": "lügendes Gesicht",
"many_tears": "heulendes Gesicht",
"fear": "vor Angst schreiendes Gesicht",
"tired": "gähnendes Gesicht",
"annoyed": "schnaubendes Gesicht",
"clown": "Clown-Gesicht",
"ghost": "Gespenst",
"dog": "Hundegesicht",
"happy_cat": "grinsende Katze mit lachenden Augen",
"scared_cat": "erschöpfte Katze",
"sad_cat": "weinende Katze",
"monkey_no_see": "sich die Augen zuhaltendes Affengesicht",
"monkey_no_hear": "sich die Ohren zuhaltendes Affengesicht",
"monkey_no_talk": "sich den Mund zuhaltendes Affengesicht",
"builder": "Bauarbeiter(in)",
"princess": "Prinzessin",
"firefighter": "Person",
"mage": "Magier(in)",
"mermaid": "Wassermensch",
"fairy": "Märchenfee",
"letter_heart": "Liebesbrief",
"red_heart": "rotes Herz",
"two_hearts": "zwei Herzen",
"kiss": "Kussabdruck",
"hundred": "100 Punkte",
"explosion": "Zusammenstoß",
"drops": "Schweißtropfen",
"handshake": "Handschlag",
"hand_five_fingers": "Hand mit gespreizten Fingern",
"hand_two_fingers": "Victory-Geste",
"thumbs_up": "Daumen hoch",
"fist": "erhobene Faust",
"two_hands": "offene Hände",
"writing": "schreibende Hand",
"praying": "zusammengelegte Handflächen",
"arm": "angespannter Bizeps",
"leg": "Bein",
"foot": "Fuß",
"ear": "Ohr",
"nose": "Nase",
"brain": "Gehirn",
"tooth": "Zahn",
"bone": "Knochen",
"eye": "Auge",
"tongue": "Zunge",
"mouth": "Mund",
"shirt": "T-Shirt",
"pants": "Jeans",
"dress": "Kleid",
"shoe": "Sportschuh",
"fencing": "Fechter(in)",
"horse_riding": "Pferderennen",
"ski": "Skifahrer(in)",
"rowing_boat": "Person im Ruderboot",
"swim": "Schwimmer(in)",
"surf": "Surfer(in)",
"gym": "Gewichtheber(in)",
"wrestling": "Ringer(in)",
"bike": "Radfahrer(in)",
"parachute": "Fallschirm",
"football": "Fußball",
"basketball": "Basketball",
"tennis": "Tennisball",
"ping_pong": "Tischtennis",
"martial": "Kampfsportanzug",
"lion": "Löwe",
"leopard": "Leopard",
"horse": "Pferdegesicht",
"zebra": "Zebra",
"pig": "Schwein",
"goat": "Ziege",
"sheep": "Schaf",
"camel": "Dromedar",
"giraffe": "Giraffe",
"elephant": "Elefant",
"rhinoceros": "Nashorn",
"flamingo": "Flamingo",
"whale": "blasender Wal",
"dolphin": "Delfin",
"bear": "Bär",
"rooster": "Hahn",
"chick": "schlüpfendes Küken",
"eagle": "Adler",
"duck": "Ente",
"owl": "Eule",
"rabbit": "Hase",
"penguin": "Pinguin",
"lizard": "Eidechse",
"turtle": "Schildkröte",
"snake": "Schlange",
"hedgehog": "Igel",
"bat": "Fledermaus",
"fish": "Fisch",
"shell": "Schneckenhaus",
"octopus": "Oktopus",
"snail": "Schnecke",
"butterfly": "Schmetterling",
"ant": "Ameise",
"bee": "Biene",
"beetle": "Marienkäfer",
"rose": "Rose",
"sunflower": "Sonnenblume",
"fir": "Nadelbaum",
"palm_tree": "Palme",
"cactus": "Kaktus",
"clover": "Glücksklee",
"potted_plant": "Topfpflanze",
"bouquet": "Blumenstrauß",
"three_leaves": "Laub",
"mushroom": "Fliegenpilz",
"grapes": "Trauben",
"watermelon": "Wassermelone",
"lemon": "Zitrone",
"banana": "Banane",
"pineapple": "Ananas",
"apple": "roter Apfel",
"cherries": "Kirschen",
"strawberry": "Erdbeere",
"three_blueberries": "Blaubeeren",
"kiwi": "Kiwi",
"avocado": "Avocado",
"eggplant": "Aubergine",
"carrot": "Karotte",
"corn": "Maiskolben",
"pepper": "Peperoni",
"croissant": "Croissant",
"bread": "Baguette",
"pretzel": "Brezel",
"cheese": "Käsestück",
"pizza": "Pizza",
"egg": "Spiegelei in Bratpfanne",
"ice_cream": "Softeis",
"cookie": "Keks",
"cake": "Torte",
"chocolate": "Schokoladentafel",
"sweet": "Bonbon",
"coffee": "Heißgetränk",
"champagne_bottle": "Flasche mit knallendem Korken",
"glass_wine": "Weinglas",
"two_glasses": "Sektgläser",
"mountain": "schneebedeckter Berg",
"camping": "Camping",
"beach": "Strand mit Sonnenschirm",
"compass": "Kompass",
"museum": "antikes Gebäude",
"house": "Haus mit Garten",
"fountain": "Springbrunnen",
"circus": "Zirkuszelt",
"train": "Dampflokomotive",
"taxi": "Taxi",
"motorcycle": "Motorrad",
"sailboat": "Segelboot",
"airplane": "Flugzeug",
"helicopter": "Hubschrauber",
"rocket": "Rakete",
"sun": "Sonne",
"moon": "Mondsichel",
"planet": "Ringplanet",
"star": "weißer mittelgroßer Stern",
"night_sky": "Milchstraße",
"cloud": "Wolke mit Regen",
"umbrella": "Regenschirm im Regen",
"lightning": "Hochspannung",
"snowflake": "Schneeflocke",
"snowman": "Schneemann ohne Schneeflocken",
"thermometer": "Thermometer",
"fire": "Feuer",
"balloon": "Luftballon",
"kite": "Drachen",
"rainbow": "Regenbogen",
"guitar": "Gitarre",
"saxophone": "Saxofon",
"music": "Musiknote",
"painting": "Mischpalette",
"chess": "Bauer Schach",
"gift": "Geschenk",
"die": "Spielwürfel",
"puzzle": "Puzzleteil",
"teddy_bear": "Teddybär",
"firecracker": "Feuerwerkskörper",
"bullseye": "Darts",
"roller_skate": "Rollschuh",
"kick_scooter": "Tretroller",
"anchor": "Anker",
"scuba_diving": "Tauchmaske",
"broom": "Besen",
"magnifying_glass": "Lupe nach links",
"bulb": "Glühbirne",
"three_books": "Bücherstapel",
"package": "Paket",
"pencil": "Bleistift",
"pin": "Reißzwecke",
"paperclip": "Büroklammer",
"scissors": "Schere",
"key": "Schlüssel",
"lock": "offenes Schloss",
"chair": "Stuhl",
"bathtub": "Badewanne",
"sponge": "Schwamm",
"shopping_cart": "Einkaufswagen"
}
}
}

@ -0,0 +1,580 @@
{
"pages": {
"not_found": {
"title": "Page Not Found",
"message": "The page you are looking for does not exist."
},
"nextgraph_uri": {
"message": "Nextgraph URI {uri}"
},
"user_panel": {
"title": "User Panel",
"personal": "Personal"
},
"user_registered": {
"back_to_homepage": "Go Back to Homepage",
"success": "You have been successfully registered.",
"success_with_invitation": "You have been successfully registered to {invitation_name}."
},
"wallet_info": {
"title": "Wallet",
"download": "Download Wallet File",
"download_failed": "Download Failed:<br/>{error}",
"download_in_progress": "Download in progress...",
"download_successful": "You will find the file named \"{wallet_file}\" in your Downloads folder",
"download_file_button": "Click here to download the wallet file",
"remove_wallet": "Remove wallet from Device",
"remove_wallet_confirm": "Are you sure you want to remove this wallet from your device?"
},
"settings": {
"title": "Settings"
},
"admin": {
"title": "Admin"
},
"accounts": {
"title": "Accounts"
},
"full_layout": {
"home": "Home",
"stream": "Stream",
"search": "Search",
"create": "Create",
"shared": "Shared",
"site": "Site",
"messages": "Messages",
"notifications": "Notifications"
},
"install": {
"app_availability": "<b>NextGraph App</b> is available for download as a native app for your mobile, tablet, laptop and desktop.<br /> The app supports iOS, Android, Linux, macOS, Windows, or any other platform with a modern browser.",
"has_wallet_warning": "A wallet is saved in this browser. If it is yours,<br /> once the installation of the app will be finished,<br /> choose the option \"Login\" on the app.<br /> (do not create another wallet from the app).",
"android_play_store": "Android Play Store",
"download_apk": "Download APK",
"ios_app_store": "iOS App Store",
"download_mac_os": "Download for MacOS",
"download_linux": "Download Linux Package",
"download_windows": "Download for Windows",
"other_platforms": "Other platforms"
},
"no_wallet": {
"welcome": "Welcome to NextGraph",
"description": "We could not find a wallet saved on this device.<br /> If you already have a wallet, select \"Log in\", otherwise, select \"Create Wallet\" here below.",
"create_wallet": "Create Wallet"
},
"test": {
"cannot_load_offline": "You are offline and using the web app. You need to connect to the broker at least once before you can start using the app locally because the web app does not keep a local copy of your documents.<br /><br /> Once connected, if you lose connectivity again, you will be able to have limited access to some functionalities. Sending binary files won't be possible, because the limit of local storage in your browser is around 5MB.<br /><br /> All those limitations will be lifted once the \"UserStorage for Web\" feature will be released. Stay tuned! <br /><br /> Check your connection status in the ",
"add_image": "Add Image",
"upload_progress": "Uploading..."
},
"login": {
"heading": "How to open your wallet? You have 2 options:",
"with_pazzle": "With your Pazzle",
"pazzle_steps": {
"1": "For each one of the 9 categories of images, you will be presented with the 15 possible image choices. The categories are shuffled at every login. They will not always appear in the same order.",
"2": "At each category, only one of the 15 displayed choices is the correct image that belongs to your pazzle. Find it and tap or click on that one. The 15 images are shuffled too, they will not appear at the same position at each login. On a computer, you can also use the tab key on your keyboard to move to the desired item on the screen, then press the space bar to select each one.",
"3": "Once you completed the last category, you will be presented with all the images you have previously selected. Their order is displayed as it was when you picked them. But this is not the correct order of the images in your pazzle. You now have to order them correctly.",
"4": "You must remember which image should be the first one in your pazzle. Find it on the screen and click or tap on it. It will be greyed out and the number 1 will appear on top of it.",
"5": "Move on to the second image of your pazzle (that you memorized). Find it on the screen and tap on it. Repeat this step until you reached the last image.",
"6": "Finally, you are asked for your PIN code. Enter it by clicking or tapping on the digits."
},
"with_mnemonic": "With your 12 words Mnemonic (passphrase)",
"mnemonic_steps": {
"1": "Enter your twelve words mnemonic in the input field. The words must be separated by spaces.",
"2": "Enter the PIN code that you chose when you created your wallet."
},
"trust_device_allow_cookies": "By selecting this option, you agree to saving some cookies on your browser.",
"trust_device_yes": "Yes, save my wallet on this device",
"device_name_label": "Name of this device. You can edit it now",
"device_name_placeholder": "Enter name of this device",
"loading_pazzle": "Loading Pazzle",
"open_with_pazzle": "Open With Pazzle",
"login_cancel": "Cancel Login",
"open_with_mnemonic": "Open with Mnemonic instead",
"enter_mnemonic": "Enter your 12 words mnemonic.",
"mnemonic_placeholder": "12 words separated by spaces",
"select_emoji": "Select your image for category:<br />{category}",
"order_emojis": "Select each image in the correct order",
"enter_pin": "Enter your PIN code",
"opening_wallet": "Opening your wallet...<br /> Please wait",
"wallet_opened": "Your wallet is opened! <br />Please wait while the app is loading...",
"qr_code": "Wallet QRCode",
"qr_modal_title": "My Wallet QRCode",
"qr_modal_description": "Use this QRCode to log in with your wallet on new devices.",
"copy_wallet_link": "Copy Wallet Link",
"keep_wallet": "Save to Device for Future Logins"
},
"account_info": {
"title": "Accounts Info",
"site": "{name} account",
"devices": "Devices",
"brokers": "Brokers",
"no_brokers_connected": "No brokers connected"
},
"wallet_create": {
"pease_wait": "Please wait",
"redirecting_to_registration_page": "Redirecting to the Broker Service Provider registration page",
"complete_in_popup": "Complete the registration in the popup window",
"own_your_ngbox": "Own your NG-Box",
"self-host_broker": "Self-host a broker",
"tos_ng_one": "Terms of Service NextGraph.one",
"wallet_description": "A <b>NextGraph Wallet</b> is unique to each person. It stores your credentials and authorizations to access documents. You need one in order to start using NextGraph.<br /><br />If you already have a wallet, you should not create a new one. Instead, <a href=\"#/wallet/login\" >login here with your existing wallet.</a > If you never created a NextGraph Wallet before, please follow the instructions below in order to create your unique personal wallet.",
"has_wallet": "Some wallets are saved on this device,<br /> to log in with one of them, <a href=\"#/wallet/login\" use:link>click here.</a>",
"wallet_about": {
"title": "What is a wallet?",
"please_read": "Please read",
"1": "Your wallet is a secure and encrypted small file that contains some important information that only you should have access to.",
"2": "In your wallet, we store all the permissions to access documents you have been granted with, or that you have created yourself.",
"3": "In order to open it, you will need to enter your <b >pazzle</b > and a <b>PIN code</b> of 4 digits. Your personal pazzle (contraction of puzzle and password) is composed of 9 images you should remember. The order of the images is important too.",
"4": "Don't worry, it is easier to remember 9 images than a password like \"69$g&ms%C*%\", and it has the same strength as a complex password. The entropy of your pazzle is <b >66bits</b >, which is considered very high by all standards.",
"5": "You should only create <b>one unique wallet for yourself</b >. All your accounts, identities and permissions will be added to this unique wallet later on. Do not create another wallet if you already have one. Instead, you will <b>import</b> your existing wallet in all the apps and websites where you need it.",
"6": "Your wallet can be imported with the help of a small file that you download, or with a QRcode. In any case, you should never share this file or QRcode with anybody else.",
"7": "We at NextGraph will never see the content of your wallet. It is encrypted and we do not know your pazzle, so we cannot see what is inside.",
"8": "For the same reason, we won't be able to help you if you forget your pazzle or PIN code, or if you loose the wallet file. <span class=\"text-bold\"> There is no \"password recovery\" option</span > in this case. You can note your pazzle down on a piece of paper until you remember it, but don't forget to destroy this note after a while."
},
"create_wallet_now": "I create my wallet now!",
"select_server": "NextGraph is based on an efficient decentralized P2P network, and in order to join this network and start using the app, you need to first select a <b>broker&nbsp;server</b>.",
"broker_about": {
"title": "What is a broker?",
"please_read": "Please read",
"1": "The broker helps you keep all your data in <b>sync</b>, as it is connected to the internet 24/7 and keeps a copy of the updates for you. This way, even if the devices of the other participants are offline, you can still see their changes",
"2": "All your data is secure and <b>end-to-end encrypted</b>, and the broker cannot see the content of the documents as it does not have the keys to decrypt them.",
"3": "The broker helps you enforce your <b>privacy</b> as it hides your internet address (IP) from other users you share documents with.",
"4": "It will be possible in the future to use NextGraph without any broker and to have direct connections between peers, but this will imply a less smooth experience.",
"5": "At anytime you can decide to switch to another broker service provider or host it yourself. Your data is totally <b >portable</b > and can freely move to another broker.",
"6": "Soon we will offer you the opportunity to host your own broker at <b>home</b> or <b>office</b>. Instead of using a \"broker service provider\", you will own a small device that you connect behind your internet router. It is called <b>NG Box</b> and will be available soon.",
"7": "Organizations and companies have the opportunity to host a broker <b>on-premise</b> or in the <b>cloud</b>, as the software is open source. Individuals can also <b>self-host</b> a broker on any VPS server or at home, on their dedicated hardware."
},
"choose_broker": "Please choose one broker among the list",
"register_with_broker": "Register with {broker}",
"for_eu_citizens": "European Union Citizens",
"for_rest": "For the rest of the world",
"enter_invite_link": "Enter an invitation link",
"scan_invite_qr": "Scan an invitation QR-code",
"self_hosted_broker": "Self-hosted broker",
"ng_box": "NG Box (owned or invited)",
"registration_success": "You have been successfully registered to {broker}",
"choose_pin": {
"title": "Let's start creating your wallet by choosing a PIN code",
"description": "We recommend you to choose a PIN code that you already know very well. <br /> Your credit card PIN, by example, is a good choice.<br />We at NextGraph will never see your PIN.",
"rules": "Here are the rules for the PIN:",
"1": "It cannot be a series like 1234 or 8765.",
"2": "The same digit cannot repeat more than once. By example 4484 is invalid.",
"3": "Try to avoid birth dates, last digits of phone numbers, or zip codes"
},
"chosen_pin": "You have chosen:",
"confirm_pin": "Please confirm your PIN code.",
"confirm_pin_description": "Enter the same PIN again.",
"pin_confirmed_as": "You PIN is confirmed as: ",
"choose_security_phrase_and_image": {
"title": "Now let's enter a security phrase and a security image",
"description": "As a verification step, this phrase and image will be presented to you every time you are about to enter your pazzle and PIN in order to unlock your wallet.<br /> This security measure will prevent you from entering your pazzle and PIN on malicious sites and apps.",
"warning": "Every time you will use your wallet, if you do not see and recognize your own security phrase and image before entering your pazzle, please stop and DO NOT enter your pazzle, as you would be the victim of a phishing attempt.",
"rules_title": "Here are the rules for the security phrase and image:",
"1": "The phrase should be at least 10 characters long",
"2": "It should be something you will remember, but not something too personal.",
"3": "Do not enter your full name, nor address, nor phone number.",
"4": "Instead, you can enter a quote, a small sentence that you like, or something meaningless to others, but unique to you.",
"5": "The image should be minimum 150x150px. There is no need to provide more than 400x400px as it will be scaled down anyway.",
"6": "We accept several formats like JPEG, PNG, GIF, WEBP and more.",
"7": "The image should be unique to you. But it should not be too personal neither.",
"8": "Do not upload your face picture, this is not a profile pic.",
"9": "The best would be a landscape you like or any other picture that you will recognize as unique.",
"10": "Please be aware that other people who are sharing this device with you, will be able to see this image and phrase."
},
"type_security_phrase_placeholder": "Type a security phrase...",
"save_security_phrase_and_image": "Save security phrase & image",
"tap_to_upload": "Tap to upload an image",
"click_to_upload": "Click to select an image",
"or_drag_drop": "or drag and drop",
"pins_no_match": "You didn't enter the same PIN twice",
"almost_done": "We are almost done!",
"save_wallet_options": {
"description": "There are 4 options to choose before we can create your wallet. Those options can help you to use and keep your wallet. But we also want to be careful with your security and privacy.<br /><br /> Remember that in any case, once your wallet will be created, you will download a file that you should keep privately somewhere on your device, USB key or hard-disk. This is the default way you can use and keep your wallet. Now let's look at some options that can make your life a bit easier.",
"trust": "Do you trust this device?",
"trust_description": "If you do, if this device is yours, or it is used by a few trusted persons of your family or workplace, and you would like to login again from this device in the future, then you can save your wallet on this device. To the contrary, if this device is public and shared by strangers, do not save your wallet here.",
"trust_toggle": "Save my wallet on this device?",
"device_name_description": "To see which devices you are connected with, every device should have a name (e.g. Bob's laptop). Please enter it here.",
"cloud": "Keep a copy in the cloud?",
"cloud_description": "Are you afraid that you will loose the file containing your wallet? If this would happen, your wallet would be lost forever, together with all your documents. We can keep an encrypted copy of your wallet in our cloud. Only you will be able to download it with a special link. You would have to keep this link safely though. By selecting this option, you agree to the",
"cloud_toggle": "Save my wallet in the cloud?",
"cloud_tos": "Terms of Service of our cloud",
"pdf": "Create a PDF file of your wallet?",
"pdf_description": "We can prepare for you a PDF file containing all the information of your wallet, unencrypted. You should print this file and then delete the PDF (and empty the trash). Keep this printed document in a safe place. It contains all the information to regenerate your wallet in case you lost access to it.",
"pdf_toggle": "Create a PDF of my wallet?",
"link": "Create a link to access your wallet easily?",
"link_description": "When you want to use your wallet on the web or from other devices, we can help you find your wallet by creating a simple link accessible from anywhere. Only you will have access to this link. In order to do so, we will keep your wallet ID and some information about your broker on our cloud servers. If you prefer to opt out, just uncheck this option. By selecting this option, you agree to the",
"link_toggle": "Create a link to my wallet?"
},
"lets_create": "Let's create this wallet",
"creating": "Your wallet is being created",
"ready": "Your wallet is ready!",
"download_wallet_description": "Please download your wallet and keep it in a safe location",
"download_wallet": "Download my wallet",
"download_wallet_done": "Your wallet file has been downloaded into your \"Downloads\" folder, with the name<br /><span class=\"text-black\"> {download_name}</span ><br /> <span class=\"font-bold\" >Please move it to a safe and durable place.</span ><br />",
"your_pazzle": "Here below is your Pazzle. <br /> The <span class=\"font-bold\">order</span> of each image is <span class=\"font-bold\">important</span>!",
"your_mnemonic": "And here is your mnemonic (your alternative passphrase):",
"unlock_tips_1": "You can use both the pazzle or the mnemonic to unlock your wallet. The pazzle is easier to remember. The mnemonic is useful in some special cases. We recommend that you use the pazzle. <span class=\"font-bold text-xl\" >Copy both on a piece of paper.</span > You should try to memorize the pazzle. Once you did, you won't need the paper anymore.",
"unlock_tips_2": "Now click on \"Continue to Login\" and select your new wallet.",
"unlock_tips_3": "It is important that you <span class=\"font-bold\">login</span> with this wallet <span class=\"font-bold\">at least once</span> from this {platform, select, browser {browser tab} other {device}}, while connected to the internet, so your personal site can be created on your broker.",
"continue_to_login": "Continue to Login",
"continue_confirm": "Did you write down your login credentials?",
"continue_confirm_description": "The pazzle and the mnemonic <span class=\"font-bold\"> will not be shown to you again</span >. Please make sure, you have written them down.",
"continue_confirm_go_back": "Take me back",
"continue_confirm_yes": "Yes, I did"
},
"wallet_login": {
"select_wallet": "Select a wallet to login with",
"with_another_wallet": "Log in with another wallet",
"import_wallet": "Import your wallet",
"import_file": "Import a Wallet File",
"import_qr": "Import with QRCode",
"import_link": "Enter a Wallet Link",
"new_wallet": "Create a new Wallet",
"logged_in": "You are logged in.<br /> please wait while the app is loading"
}
},
"buttons": {
"ok": "Ok",
"go_back": "Go back",
"back": "Back",
"confirm": "Confirm",
"cancel": "Cancel",
"remove": "Remove",
"correct": "Correct",
"try_again": "Try again",
"back_to_homepage": "Go back to Homepage",
"logout": "Logout",
"login": "Login",
"start_over": "Start over"
},
"errors": {
"an_error_occurred": "An error occurred",
"error_occurred": "An error occurred:<br />{message}",
"AlreadyExists": "The user is already registered with the selected broker.<br />Try logging in instead.",
"InvalidSignature": "The signature is invalid.",
"IncompleteSignature": "The signature is incomplete.",
"SerializationError": "The object could not be serialized.",
"EncryptionError": "Your wallet could not be opened. You probably did a mistake.",
"DecryptionError": "Error with decryption.",
"InvalidValue": "The value is invalid.",
"ConnectionNotFound": "The connection was not found.",
"InvalidKey": "The key is invalid.",
"InvalidInvitation": "The invitation is invalid.",
"InvalidCreateAccount": "An error occurred creating the account.",
"InvalidFileFormat": "The file format is invalid.",
"InvalidArgument": "The parameter is invalid.",
"PermissionDenied": "Permission denied.",
"InvalidPazzle": "The pazzle is not correct.",
"InvalidMnemonic": "The mnemonic is not correct.",
"CommitLoadError": "Error loading commit.",
"ObjectParseError": "Error parsing object.",
"StorageError": "Storage error.",
"NotFound": "Not Found",
"JsStorageKeyNotFound": "Could not find JavaScript storage key.",
"IoError": "Input/Output error.",
"CommitVerifyError": "Verifying commit failed.",
"LocalBrokerNotInitialized": "The local broker is not initialized.",
"JsStorageReadError": "Could not read from JavaScript Storage.",
"JsStorageWriteError": "Could not write to JavaScript Storage.",
"CannotSaveWhenInMemoryConfig": "Saving while configured for In-Memory is not possible.",
"WalletNotFound": "The wallet was not found.",
"WalletAlreadyAdded": "The wallet is already added.",
"WalletAlreadyOpened": "The wallet is already open.",
"WalletError": "Error with wallet.",
"BrokerError": "Error with broker.",
"SessionNotFound": "The session cannot be found.",
"SessionAlreadyStarted": "The session is already started.",
"RepoNotFound": "The repository cannot be found.",
"BranchNotFound": "The branch cannot be found.",
"StoreNotFound": "The store cannot be found.",
"UserNotFound": "The user cannot be found.",
"TopicNotFound": "The topic cannot be found.",
"NotConnected": "You are not connected.",
"ActorError": "Actor error.",
"ProtocolError": "Protocol error.",
"ServerError": "Server error.",
"InvalidResponse": "Received an invalid response.",
"BootstrapError": "Error while bootstrapping",
"NotAServerError": "Not a server.",
"VerifierError": "Error during verification.",
"SiteNotFoundOnBroker": "The site cannot be found on the broker",
"BrokerConfigErrorStr": "{error}",
"BrokerConfigError": "Error in the broker configuration.",
"MalformedEvent": "The event has an invalid format.",
"InvalidPayload": "The payload is invalid.",
"WrongUploadId": "The upload ID is incorrect.",
"FileError": "Error with file.",
"InternalError": "Internal Error",
"OxiGraphError": "Error in OxiGraph database.",
"ConfigError": "Error in configuration.",
"LocalBrokerIsHeadless": "The local broker is headless.",
"LocalBrokerIsNotHeadless": "The local broker is not headless.",
"InvalidNuri": "Invalid NextGraph URI.",
"InvalidTarget": "Cannot resolve target.",
"ExportWalletTimeOut": "Export of wallet has expired."
},
"connectivity": {
"stopped": "Stopped",
"personal": "Personal",
"connecting": "Connecting",
"connected": "Connected",
"loading": "Loading",
"connection_error_short": "{error}",
"online": "Online",
"offline": "Offline"
},
"common": {
"version": "Version: {version}",
"logo": "NextGraph Logo",
"support_nextgraph": "Support NextGraph",
"about_nextgraph": "About NextGraph",
"donate_nextgraph": "Donate to NextGraph"
},
"emojis": {
"category": {
"face": "Face",
"face_unwell": "Bad Face",
"face_costume": "Costumed",
"emotion": "Emotion",
"body": "Body",
"sport": "Sport",
"bigger_animal": "Big Animal",
"smaller_animal": "Small Animal",
"plants": "Plants and Insects",
"fruits": "Fruits and Veggies",
"food": "Food and Drinks",
"travel": "Travel",
"sky": "Sky and Weather",
"play": "Leisure",
"house": "Household"
},
"codes": {
"COMMENT": "YOU CAN FIND I18N UNICODE DESCRIPTIONS AT https://github.com/unicode-org/cldr-json",
"happy": "happy face",
"happy_tears": "face with tears of joy",
"halo": "smiling face with halo ring",
"three_hearts": "face with 3 hearts",
"with_two_hearts": "face with 2 heart-eyes",
"one_heart": "face blowing one heart kiss",
"with_tongue": "face with tongue",
"with_two_hands": "face with two hands",
"one_hand": "face with hand over mouth",
"silenced": "silenced zipper-mouth face",
"celebrating": "partying face",
"sunglasses": "face with sunglasses",
"eyes_up": "face with rolling eyes up",
"monocle": "face with monocle",
"sleeping": "sleeping face",
"mask": "face with medical mask",
"fever": "fever face with thermometer",
"bandage": "face with head-bandage",
"vomit": "face vomiting",
"tissue": "tissue sneezing face",
"hot": "hot face with head-bandage",
"cold": "cold face",
"crossed_eyes": "face with crossed-out eyes",
"exploding": "exploding head",
"sad": "frowning sad face",
"long_nose": "long nose lying face",
"many_tears": "loudly crying face",
"fear": "face screaming in fear",
"tired": "tired yawning face",
"annoyed": "annoyed face with steam from nose",
"clown": "clown face",
"ghost": "ghost",
"dog": "dog face",
"happy_cat": "happy cat with smiling eyes",
"scared_cat": "weary scared cat",
"sad_cat": "crying cat",
"monkey_no_see": "see-no-evil monkey",
"monkey_no_hear": "hear-no-evil monkey",
"monkey_no_talk": "speak-no-evil monkey",
"builder": "construction worker",
"princess": "princess",
"firefighter": "firefighter",
"mage": "mage",
"mermaid": "mermaid",
"fairy": "fairy",
"letter_heart": "love letter",
"red_heart": "red heart",
"two_hearts": "two hearts",
"kiss": "kiss mark",
"hundred": "hundred points",
"explosion": "explosion",
"drops": "sweat droplets",
"handshake": "handshake",
"hand_five_fingers": "hand with 5 fingers splayed",
"hand_two_fingers": "victory hand with 2 fingers",
"thumbs_up": "thumbs up",
"fist": "raised fist",
"two_hands": "two open hands",
"writing": "writing hand",
"praying": "praying folded hands",
"arm": "flexed arm",
"leg": "leg",
"foot": "foot",
"ear": "ear",
"nose": "nose",
"brain": "brain",
"tooth": "tooth",
"bone": "bone",
"eye": "eye",
"tongue": "tongue",
"mouth": "mouth",
"shirt": "t-shirt",
"pants": "pants",
"dress": "dress",
"shoe": "running shoe",
"fencing": "fencing",
"horse_riding": "horse riding",
"ski": "skier",
"rowing_boat": "rowing boat",
"swim": "swimming",
"surf": "surfing",
"gym": "lifting weights",
"wrestling": "two people wrestling",
"bike": "biking",
"parachute": "parachute",
"football": "football",
"basketball": "basketball",
"tennis": "tennis",
"ping_pong": "ping pong",
"martial": "judo uniform kimono",
"lion": "lion",
"leopard": "leopard",
"horse": "horse face",
"zebra": "zebra",
"pig": "pig",
"goat": "goat",
"sheep": "sheep",
"camel": "camel",
"giraffe": "giraffe",
"elephant": "elephant",
"rhinoceros": "rhinoceros",
"flamingo": "flamingo",
"whale": "whale",
"dolphin": "dolphin",
"bear": "bear",
"rooster": "rooster",
"chick": "hatching chick",
"eagle": "eagle",
"duck": "duck",
"owl": "owl",
"rabbit": "rabbit",
"penguin": "penguin",
"lizard": "lizard",
"turtle": "turtle",
"snake": "snake",
"hedgehog": "hedgehog",
"bat": "bat",
"fish": "fish",
"shell": "shell",
"octopus": "octopus",
"snail": "snail",
"butterfly": "butterfly",
"ant": "ant",
"bee": "honeybee",
"beetle": "beetle",
"rose": "rose",
"sunflower": "sunflower",
"fir": "evergreen fir tree",
"palm_tree": "palm tree",
"cactus": "cactus",
"clover": "four leaf clover",
"potted_plant": "potted plant",
"bouquet": "bouquet",
"three_leaves": "three fallen leaf",
"mushroom": "mushroom",
"grapes": "grapes",
"watermelon": "watermelon",
"lemon": "lemon",
"banana": "banana",
"pineapple": "pineapple",
"apple": "red apple",
"cherries": "cherries",
"strawberry": "strawberry",
"three_blueberries": "three blueberries",
"kiwi": "kiwi",
"avocado": "avocado",
"eggplant": "eggplant",
"carrot": "carrot",
"corn": "corn",
"pepper": "red hot pepper",
"croissant": "croissant",
"bread": "baguette bread",
"pretzel": "pretzel",
"cheese": "cheese",
"pizza": "slice of pizza",
"egg": "fried egg",
"ice_cream": "ice cream",
"cookie": "cookie",
"cake": "piece of cake",
"chocolate": "chocolate",
"sweet": "candy",
"coffee": "coffee",
"champagne_bottle": "champagne bottle",
"glass_wine": "wine glass",
"two_glasses": "two glasses of cava",
"mountain": "smountain",
"camping": "camping",
"beach": "beach",
"compass": "compass",
"museum": "museum",
"house": "house",
"fountain": "fountain",
"circus": "circus",
"train": "train",
"taxi": "taxi",
"motorcycle": "motorcycle",
"sailboat": "sailboat",
"airplane": "airplane",
"helicopter": "helicopter",
"rocket": "rocket",
"sun": "sun",
"moon": "moon",
"planet": "planet",
"star": "star",
"night_sky": "night sky",
"cloud": "cloud with rain",
"umbrella": "umbrella with rain drops",
"lightning": "lightning",
"snowflake": "snowflake",
"snowman": "snowman",
"thermometer": "thermometer",
"fire": "fire",
"balloon": "balloon",
"kite": "kite",
"rainbow": "rainbow",
"guitar": "guitar",
"saxophone": "saxophone",
"music": "musical note",
"painting": "painting",
"chess": "chess",
"gift": "gift",
"die": "game die",
"puzzle": "puzzle",
"teddy_bear": "teddy bear",
"firecracker": "firecracker",
"bullseye": "bullseye",
"roller_skate": "roller skate",
"kick_scooter": "kick scooter",
"anchor": "anchor",
"scuba_diving": "scuba diving",
"broom": "broom",
"magnifying_glass": "magnifying glass",
"bulb": "light bulb",
"three_books": "three books",
"package": "package",
"pencil": "pencil",
"pin": "pin",
"paperclip": "paperclip",
"scissors": "scissors",
"key": "key",
"lock": "lock",
"chair": "chair",
"bathtub": "bathtub",
"sponge": "sponge",
"shopping_cart": "shopping cart"
}
}
}

@ -0,0 +1,231 @@
{
"emojis": {
"codes": {
"happy": "cara sonriendo",
"happy_tears": "cara llorando de risa",
"halo": "cara sonriendo con aureola",
"three_hearts": "cara sonriendo con corazones",
"with_two_hearts": "cara sonriendo con ojos de corazón",
"one_heart": "cara lanzando un beso",
"with_tongue": "cara con ojos cerrados y lengua fuera",
"with_two_hands": "cara con manos abrazando",
"one_hand": "cara con mano sobre la boca",
"silenced": "cara con la boca cerrada con cremallera",
"celebrating": "cara de fiesta",
"sunglasses": "cara sonriendo con gafas de sol",
"eyes_up": "cara con ojos en blanco",
"monocle": "cara con monóculo",
"sleeping": "cara durmiendo",
"mask": "cara con mascarilla médica",
"fever": "cara con termómetro",
"bandage": "cara con la cabeza vendada",
"vomit": "cara vomitando",
"tissue": "cara estornudando",
"hot": "cara con la cabeza vendada",
"cold": "cara con frío",
"crossed_eyes": "cara mareada",
"exploding": "cabeza explotando",
"sad": "cara con el ceño fruncido",
"long_nose": "cara de mentiroso",
"many_tears": "cara llorando fuerte",
"fear": "cara gritando de miedo",
"tired": "cara de bostezo",
"annoyed": "cara resoplando",
"clown": "cara de payaso",
"ghost": "fantasma",
"dog": "cara de perro",
"happy_cat": "gato sonriendo con ojos sonrientes",
"scared_cat": "gato asustado",
"sad_cat": "gato llorando",
"monkey_no_see": "mono con los ojos tapados",
"monkey_no_hear": "mono con los oídos tapados",
"monkey_no_talk": "mono con la boca tapada",
"builder": "profesional de la construcción",
"princess": "princesa",
"firefighter": "persona adulta",
"mage": "persona maga",
"mermaid": "persona sirena",
"fairy": "hada",
"letter_heart": "carta de amor",
"red_heart": "corazón rojo",
"two_hearts": "dos corazones",
"kiss": "marca de beso",
"hundred": "cien puntos",
"explosion": "colisión",
"drops": "gotas de sudor",
"handshake": "apretón de manos",
"hand_five_fingers": "mano abierta",
"hand_two_fingers": "mano con señal de victoria",
"thumbs_up": "pulgar hacia arriba",
"fist": "puño en alto",
"two_hands": "manos abiertas",
"writing": "mano escribiendo",
"praying": "manos en oración",
"arm": "bíceps flexionado",
"leg": "pierna",
"foot": "pie",
"ear": "oreja",
"nose": "nariz",
"brain": "cerebro",
"tooth": "diente",
"bone": "hueso",
"eye": "ojo",
"tongue": "lengua",
"mouth": "boca",
"shirt": "camiseta",
"pants": "vaqueros",
"dress": "vestido",
"shoe": "zapatilla deportiva",
"fencing": "persona haciendo esgrima",
"horse_riding": "carrera de caballos",
"ski": "persona esquiando",
"rowing_boat": "persona remando en un bote",
"swim": "persona nadando",
"surf": "persona haciendo surf",
"gym": "persona levantando pesas",
"wrestling": "personas luchando",
"bike": "persona en bicicleta",
"parachute": "paracaídas",
"football": "balón de fútbol",
"basketball": "balón de baloncesto",
"tennis": "pelota de tenis",
"ping_pong": "tenis de mesa",
"martial": "uniforme de artes marciales",
"lion": "león",
"leopard": "leopardo",
"horse": "cara de caballo",
"zebra": "cebra",
"pig": "cerdo",
"goat": "cabra",
"sheep": "oveja",
"camel": "dromedario",
"giraffe": "jirafa",
"elephant": "elefante",
"rhinoceros": "rinoceronte",
"flamingo": "flamenco",
"whale": "ballena soltando un chorro",
"dolphin": "delfín",
"bear": "oso",
"rooster": "gallo",
"chick": "pollito rompiendo el cascarón",
"eagle": "águila",
"duck": "pato",
"owl": "búho",
"rabbit": "conejo",
"penguin": "pingüino",
"lizard": "lagarto",
"turtle": "tortuga",
"snake": "serpiente",
"hedgehog": "erizo",
"bat": "murciélago",
"fish": "pez",
"shell": "caracola",
"octopus": "pulpo",
"snail": "caracol",
"butterfly": "mariposa",
"ant": "hormiga",
"bee": "abeja",
"beetle": "mariquita",
"rose": "rosa",
"sunflower": "girasol",
"fir": "árbol de hoja perenne",
"palm_tree": "palmera",
"cactus": "cactus",
"clover": "trébol de cuatro hojas",
"potted_plant": "planta de maceta",
"bouquet": "ramo de flores",
"three_leaves": "hojas caídas",
"mushroom": "champiñón",
"grapes": "uvas",
"watermelon": "sandía",
"lemon": "limón",
"banana": "plátano",
"pineapple": "piña",
"apple": "manzana roja",
"cherries": "cerezas",
"strawberry": "fresa",
"three_blueberries": "arándanos",
"kiwi": "kiwi",
"avocado": "aguacate",
"eggplant": "berenjena",
"carrot": "zanahoria",
"corn": "espiga de maíz",
"pepper": "chile picante",
"croissant": "cruasán",
"bread": "baguete",
"pretzel": "bretzel",
"cheese": "cuña de queso",
"pizza": "pizza",
"egg": "cocinar",
"ice_cream": "cucurucho de helado",
"cookie": "galleta",
"cake": "trozo de tarta",
"chocolate": "tableta de chocolate",
"sweet": "caramelo",
"coffee": "bebida caliente",
"champagne_bottle": "botella descorchada",
"glass_wine": "copa de vino",
"two_glasses": "copas brindando",
"mountain": "montaña con nieve",
"camping": "camping",
"beach": "playa y sombrilla",
"compass": "brújula",
"museum": "edificio clásico",
"house": "casa con jardín",
"fountain": "fuente",
"circus": "carpa de circo",
"train": "locomotora de vapor",
"taxi": "taxi",
"motorcycle": "moto",
"sailboat": "velero",
"airplane": "avión",
"helicopter": "helicóptero",
"rocket": "cohete",
"sun": "sol",
"moon": "luna",
"planet": "planeta con anillos",
"star": "estrella",
"night_sky": "Vía Láctea",
"cloud": "nube con lluvia",
"umbrella": "paraguas con gotas de lluvia",
"lightning": "alto voltaje",
"snowflake": "copo de nieve",
"snowman": "muñeco de nieve",
"thermometer": "termómetro",
"fire": "fuego",
"balloon": "globo",
"kite": "cometa",
"rainbow": "arcoíris",
"guitar": "guitarra",
"saxophone": "saxofón",
"music": "nota musical",
"painting": "paleta de pintor",
"chess": "peón de ajedrez",
"gift": "regalo",
"die": "dado",
"puzzle": "pieza de puzle",
"teddy_bear": "osito de peluche",
"firecracker": "petardo",
"bullseye": "diana",
"roller_skate": "patines",
"kick_scooter": "patinete",
"anchor": "ancla",
"scuba_diving": "máscara de buceo",
"broom": "escoba",
"magnifying_glass": "lupa orientada hacia la izquierda",
"bulb": "bombilla",
"three_books": "libros",
"package": "paquete",
"pencil": "lápiz",
"pin": "chincheta",
"paperclip": "clip",
"scissors": "tijeras",
"key": "llave",
"lock": "candado abierto",
"chair": "silla",
"bathtub": "bañera",
"sponge": "esponja",
"shopping_cart": "carrito de la compra"
}
}
}

@ -0,0 +1,231 @@
{
"emojis": {
"codes": {
"happy": "visage rieur",
"happy_tears": "visage riant aux larmes",
"halo": "visage souriant avec auréole",
"three_hearts": "visage souriant avec cœurs",
"with_two_hearts": "visage souriant avec yeux en forme de cœur",
"one_heart": "visage envoyant un bisou",
"with_tongue": "visage qui tire la langue les yeux plissés",
"with_two_hands": "visage qui fait un câlin",
"one_hand": "visage avec une main sur la bouche",
"silenced": "visage avec bouche fermeture éclair",
"celebrating": "visage festif",
"sunglasses": "visage avec lunettes de soleil",
"eyes_up": "visage roulant des yeux",
"monocle": "visage avec un monocle",
"sleeping": "visage somnolent",
"mask": "visage avec masque",
"fever": "visage avec thermomètre",
"bandage": "visage avec bandage autour de la tête",
"vomit": "visage qui vomit",
"tissue": "visage qui éternue",
"hot": "visage avec bandage autour de la tête",
"cold": "visage bleu et froid",
"crossed_eyes": "visage étourdi",
"exploding": "tête qui explose",
"sad": "visage mécontent",
"long_nose": "visage de menteur",
"many_tears": "visage qui pleure à chaudes larmes",
"fear": "visage qui hurle de peur",
"tired": "visage bâillant",
"annoyed": "visage avec fumée sortant des narines",
"clown": "visage de clown",
"ghost": "fantôme",
"dog": "tête de chien",
"happy_cat": "chat qui sourit avec des yeux rieurs",
"scared_cat": "chat fatigué",
"sad_cat": "chat qui pleure",
"monkey_no_see": "singe ne rien voir",
"monkey_no_hear": "singe ne rien entendre",
"monkey_no_talk": "singe ne rien dire",
"builder": "personnel du bâtiment",
"princess": "princesse",
"firefighter": "adulte",
"mage": "mage",
"mermaid": "créature aquatique",
"fairy": "personnage féérique",
"letter_heart": "lettre d’amour",
"red_heart": "cœur rouge",
"two_hearts": "deux cœurs",
"kiss": "trace de rouge à lèvres",
"hundred": "cent points",
"explosion": "explosion",
"drops": "gouttes de sueur",
"handshake": "poignée de main",
"hand_five_fingers": "main levée doigts écartés",
"hand_two_fingers": "V de la victoire",
"thumbs_up": "pouce vers le haut",
"fist": "poing levé",
"two_hands": "mains ouvertes",
"writing": "main qui écrit",
"praying": "mains en prière",
"arm": "biceps contracté",
"leg": "jambe",
"foot": "pied",
"ear": "oreille",
"nose": "nez",
"brain": "cerveau",
"tooth": "dent",
"bone": "os",
"eye": "œil",
"tongue": "langue",
"mouth": "bouche",
"shirt": "T-shirt",
"pants": "jean",
"dress": "robe",
"shoe": "chaussure de sport",
"fencing": "escrimeur",
"horse_riding": "course hippique",
"ski": "skieur",
"rowing_boat": "personne ramant dans une barque",
"swim": "personne nageant",
"surf": "personne faisant du surf",
"gym": "haltérophile",
"wrestling": "personnes faisant de la lutte",
"bike": "cycliste",
"parachute": "parachute",
"football": "ballon de football",
"basketball": "basket",
"tennis": "tennis",
"ping_pong": "ping-pong",
"martial": "tenue d’arts martiaux",
"lion": "tête de lion",
"leopard": "léopard",
"horse": "tête de cheval",
"zebra": "zèbre",
"pig": "cochon",
"goat": "chèvre",
"sheep": "mouton",
"camel": "dromadaire",
"giraffe": "girafe",
"elephant": "éléphant",
"rhinoceros": "rhinocéros",
"flamingo": "flamant",
"whale": "baleine soufflant par son évent",
"dolphin": "dauphin",
"bear": "ours",
"rooster": "coq",
"chick": "poussin qui éclôt",
"eagle": "aigle",
"duck": "canard",
"owl": "chouette",
"rabbit": "lapin",
"penguin": "pingouin",
"lizard": "lézard",
"turtle": "tortue",
"snake": "serpent",
"hedgehog": "hérisson",
"bat": "chauve-souris",
"fish": "poisson",
"shell": "coquille en spirale",
"octopus": "pieuvre",
"snail": "escargot",
"butterfly": "papillon",
"ant": "fourmi",
"bee": "abeille",
"beetle": "coccinelle",
"rose": "rose",
"sunflower": "tournesol",
"fir": "conifère",
"palm_tree": "palmier",
"cactus": "cactus",
"clover": "trèfle à quatre feuilles",
"potted_plant": "plante en pot",
"bouquet": "bouquet",
"three_leaves": "feuille morte",
"mushroom": "champignon",
"grapes": "raisin",
"watermelon": "pastèque",
"lemon": "citron",
"banana": "banane",
"pineapple": "ananas",
"apple": "pomme rouge",
"cherries": "cerises",
"strawberry": "fraise",
"three_blueberries": "myrtilles",
"kiwi": "kiwi",
"avocado": "avocat",
"eggplant": "aubergine",
"carrot": "carotte",
"corn": "épi de maïs",
"pepper": "piment rouge",
"croissant": "croissant",
"bread": "baguette",
"pretzel": "bretzel",
"cheese": "part de fromage",
"pizza": "pizza",
"egg": "œuf au plat",
"ice_cream": "glace italienne",
"cookie": "cookie",
"cake": "gâteau sablé",
"chocolate": "barre chocolatée",
"sweet": "bonbon",
"coffee": "boisson chaude",
"champagne_bottle": "bouteille de champagne",
"glass_wine": "verre de vin",
"two_glasses": "trinquer",
"mountain": "montagne enneigée",
"camping": "camping",
"beach": "plage avec parasol",
"compass": "boussole",
"museum": "monument classique",
"house": "maison avec jardin",
"fountain": "fontaine",
"circus": "chapiteau",
"train": "locomotive",
"taxi": "taxi",
"motorcycle": "moto",
"sailboat": "voilier",
"airplane": "avion",
"helicopter": "hélicoptère",
"rocket": "fusée",
"sun": "soleil",
"moon": "croissant de lune",
"planet": "planète à anneaux",
"star": "étoile",
"night_sky": "voie lactée",
"cloud": "nuage avec pluie",
"umbrella": "parapluie avec gouttes de pluie",
"lightning": "haute tension",
"snowflake": "flocon",
"snowman": "bonhomme de neige sans neige",
"thermometer": "thermomètre",
"fire": "feu",
"balloon": "ballon gonflable",
"kite": "cerf-volant",
"rainbow": "arc-en-ciel",
"guitar": "guitare",
"saxophone": "saxophone",
"music": "note de musique",
"painting": "palette de peinture",
"chess": "pion d’échec",
"gift": "cadeau",
"die": "dés",
"puzzle": "pièce de puzzle",
"teddy_bear": "ours en peluche",
"firecracker": "pétard",
"bullseye": "dans le mille",
"roller_skate": "patin à roulettes",
"kick_scooter": "trottinette",
"anchor": "ancre",
"scuba_diving": "masque de plongée",
"broom": "balai",
"magnifying_glass": "loupe orientée à gauche",
"bulb": "ampoule",
"three_books": "livres",
"package": "colis",
"pencil": "crayon",
"pin": "punaise",
"paperclip": "trombone",
"scissors": "ciseaux",
"key": "clé",
"lock": "cadenas ouvert",
"chair": "chaise",
"bathtub": "baignoire",
"sponge": "éponge",
"shopping_cart": "chariot"
}
}
}

@ -0,0 +1,231 @@
{
"emojis": {
"codes": {
"happy": "faccina con un gran sorriso",
"happy_tears": "faccina con lacrime di gioia",
"halo": "faccina con sorriso e aureola",
"three_hearts": "faccina con cuoricini",
"with_two_hearts": "faccina con sorriso e occhi a cuore",
"one_heart": "faccina che manda un bacio",
"with_tongue": "faccina che strizza gli occhi e mostra la lingua",
"with_two_hands": "faccina che abbraccia",
"one_hand": "faccina con mano sulla bocca",
"silenced": "faccina con bocca con cerniera",
"celebrating": "faccina che festeggia",
"sunglasses": "faccina con sorriso e occhiali da sole",
"eyes_up": "faccina con occhi al cielo",
"monocle": "faccina con monocolo",
"sleeping": "faccina che dorme",
"mask": "faccina con mascherina",
"fever": "faccina con termometro",
"bandage": "faccina con la testa bendata",
"vomit": "faccina che vomita",
"tissue": "faccina che starnutisce",
"hot": "faccina con la testa bendata",
"cold": "faccina congelata",
"crossed_eyes": "faccina frastornata",
"exploding": "testa che esplode",
"sad": "faccina imbronciata",
"long_nose": "faccina bugiarda",
"many_tears": "faccina disperata",
"fear": "faccina terrorizzata",
"tired": "faccina che sbadiglia",
"annoyed": "faccina che sbuffa",
"clown": "faccina pagliaccio",
"ghost": "fantasma",
"dog": "muso di cane",
"happy_cat": "gatto che sogghigna",
"scared_cat": "gatto esterrefatto",
"sad_cat": "gatto che piange",
"monkey_no_see": "non vedo",
"monkey_no_hear": "non sento",
"monkey_no_talk": "non parlo",
"builder": "operaio edile",
"princess": "principessa",
"firefighter": "persona",
"mage": "mago",
"mermaid": "sirena",
"fairy": "fata",
"letter_heart": "lettera d’amore",
"red_heart": "cuore rosso",
"two_hearts": "due cuori",
"kiss": "impronta della bocca",
"hundred": "100 punti",
"explosion": "collisione",
"drops": "gocce di sudore",
"handshake": "stretta di mano",
"hand_five_fingers": "mano aperta",
"hand_two_fingers": "vittoria",
"thumbs_up": "pollice in su",
"fist": "pugno",
"two_hands": "mani aperte",
"writing": "mano che scrive",
"praying": "mani giunte",
"arm": "bicipite",
"leg": "gamba",
"foot": "piede",
"ear": "orecchio",
"nose": "naso",
"brain": "cervello",
"tooth": "dente",
"bone": "osso",
"eye": "occhio",
"tongue": "lingua",
"mouth": "bocca",
"shirt": "t-shirt",
"pants": "jeans",
"dress": "vestito",
"shoe": "scarpa sportiva",
"fencing": "schermidore",
"horse_riding": "ippica",
"ski": "sciatore",
"rowing_boat": "persona in barca a remi",
"swim": "persona che nuota",
"surf": "persona che fa surf",
"gym": "persona che solleva pesi",
"wrestling": "persone che fanno la lotta",
"bike": "ciclista",
"parachute": "paracadute",
"football": "pallone da calcio",
"basketball": "palla da pallacanestro",
"tennis": "tennis",
"ping_pong": "ping pong",
"martial": "kimono per arti marziali",
"lion": "leone",
"leopard": "leopardo",
"horse": "muso di cavallo",
"zebra": "zebra",
"pig": "maiale",
"goat": "capra",
"sheep": "pecora",
"camel": "dromedario",
"giraffe": "giraffa",
"elephant": "elefante",
"rhinoceros": "rinoceronte",
"flamingo": "fenicottero",
"whale": "balena che spruzza acqua",
"dolphin": "delfino",
"bear": "orso",
"rooster": "gallo",
"chick": "pulcino che nasce",
"eagle": "aquila",
"duck": "anatra",
"owl": "gufo",
"rabbit": "coniglio",
"penguin": "pinguino",
"lizard": "lucertola",
"turtle": "tartaruga",
"snake": "serpente",
"hedgehog": "riccio",
"bat": "pipistrello",
"fish": "pesce",
"shell": "conchiglia",
"octopus": "polpo",
"snail": "lumaca",
"butterfly": "farfalla",
"ant": "formica",
"bee": "ape",
"beetle": "coccinella",
"rose": "rosa",
"sunflower": "girasole",
"fir": "albero sempreverde",
"palm_tree": "palma",
"cactus": "cactus",
"clover": "quadrifoglio",
"potted_plant": "pianta in vaso",
"bouquet": "mazzo di fiori",
"three_leaves": "foglia caduta",
"mushroom": "fungo",
"grapes": "uva",
"watermelon": "anguria",
"lemon": "limone",
"banana": "banana",
"pineapple": "ananas",
"apple": "mela rossa",
"cherries": "ciliegie",
"strawberry": "fragola",
"three_blueberries": "mirtilli",
"kiwi": "kiwi",
"avocado": "avocado",
"eggplant": "melanzana",
"carrot": "carota",
"corn": "pannocchia",
"pepper": "peperoncino",
"croissant": "croissant",
"bread": "baguette",
"pretzel": "pretzel",
"cheese": "fetta di formaggio",
"pizza": "pizza",
"egg": "cucinare",
"ice_cream": "cono gelato",
"cookie": "biscotto",
"cake": "fetta di torta",
"chocolate": "cioccolato",
"sweet": "caramella",
"coffee": "bevanda calda",
"champagne_bottle": "bottiglia stappata",
"glass_wine": "bicchiere di vino",
"two_glasses": "brindisi",
"mountain": "montagna innevata",
"camping": "campeggio",
"beach": "spiaggia con ombrellone",
"compass": "bussola",
"museum": "edificio classico",
"house": "casa con giardino",
"fountain": "fontana",
"circus": "circo",
"train": "locomotiva",
"taxi": "taxi",
"motorcycle": "motocicletta",
"sailboat": "barca a vela",
"airplane": "aeroplano",
"helicopter": "elicottero",
"rocket": "razzo",
"sun": "sole",
"moon": "spicchio di luna",
"planet": "pianeta con satellite",
"star": "stella",
"night_sky": "Via Lattea",
"cloud": "pioggia",
"umbrella": "ombrello con gocce di pioggia",
"lightning": "alta tensione",
"snowflake": "fiocco di neve",
"snowman": "pupazzo di neve senza neve",
"thermometer": "termometro",
"fire": "fuoco",
"balloon": "palloncino",
"kite": "aquilone",
"rainbow": "arcobaleno",
"guitar": "chitarra",
"saxophone": "sassofono",
"music": "nota musicale",
"painting": "tavolozza",
"chess": "pedina degli scacchi",
"gift": "regalo",
"die": "dado",
"puzzle": "pezzo di puzzle",
"teddy_bear": "orsetto",
"firecracker": "petardo",
"bullseye": "bersaglio",
"roller_skate": "pattini a rotelle",
"kick_scooter": "monopattino",
"anchor": "ancora",
"scuba_diving": "maschera da sub",
"broom": "scopa",
"magnifying_glass": "lente di ingrandimento rivolta a sinistra",
"bulb": "lampadina",
"three_books": "libri",
"package": "pacco",
"pencil": "matita",
"pin": "puntina",
"paperclip": "graffetta",
"scissors": "forbici",
"key": "chiave",
"lock": "lucchetto aperto",
"chair": "sedia",
"bathtub": "vasca",
"sponge": "spugna",
"shopping_cart": "carrello"
}
}
}

@ -0,0 +1,231 @@
{
"emojis": {
"codes": {
"happy": "rosto risonho",
"happy_tears": "rosto chorando de rir",
"halo": "rosto sorridente com auréola",
"three_hearts": "rosto sorridente com 3 corações",
"with_two_hearts": "rosto sorridente com olhos de coração",
"one_heart": "rosto mandando um beijo",
"with_tongue": "rosto com olhos semicerrados e língua para fora",
"with_two_hands": "rosto abraçando",
"one_hand": "rosto com a mão sobre a boca",
"silenced": "rosto com boca de zíper",
"celebrating": "rosto festivo",
"sunglasses": "rosto sorridente com óculos escuros",
"eyes_up": "rosto com olhos revirados",
"monocle": "rosto com monóculo",
"sleeping": "rosto dormindo",
"mask": "rosto com máscara médica",
"fever": "rosto com termômetro",
"bandage": "rosto com atadura na cabeça",
"vomit": "rosto vomitando",
"tissue": "rosto espirrando",
"hot": "rosto com atadura na cabeça",
"cold": "rosto gelado",
"crossed_eyes": "rosto atordoado",
"exploding": "cabeça explodindo",
"sad": "rosto descontente",
"long_nose": "rosto de mentiroso",
"many_tears": "rosto chorando aos berros",
"fear": "rosto gritando de medo",
"tired": "rosto bocejando",
"annoyed": "rosto soltando vapor pelo nariz",
"clown": "rosto de palhaço",
"ghost": "fantasma",
"dog": "rosto de cachorro",
"happy_cat": "rosto de gato sorrindo com olhos sorridentes",
"scared_cat": "rosto de gato desolado",
"sad_cat": "rosto de gato chorando",
"monkey_no_see": "macaco que não vê nada",
"monkey_no_hear": "macaco que não ouve nada",
"monkey_no_talk": "macaco que não fala nada",
"builder": "trabalhador de construção civil",
"princess": "princesa",
"firefighter": "pessoa",
"mage": "mago",
"mermaid": "pessoa sereia",
"fairy": "fada",
"letter_heart": "carta de amor",
"red_heart": "coração vermelho",
"two_hearts": "dois corações",
"kiss": "marca de beijo",
"hundred": "cem pontos",
"explosion": "colisão",
"drops": "pingos de suor",
"handshake": "aperto de mãos",
"hand_five_fingers": "mão aberta com os dedos separados",
"hand_two_fingers": "mão em V de vitória",
"thumbs_up": "polegar para cima",
"fist": "punho levantado",
"two_hands": "mãos abertas",
"writing": "escrevendo à mão",
"praying": "mãos juntas",
"arm": "bíceps",
"leg": "perna",
"foot": "pé",
"ear": "orelha",
"nose": "nariz",
"brain": "cérebro",
"tooth": "dente",
"bone": "osso",
"eye": "olho",
"tongue": "língua",
"mouth": "boca",
"shirt": "camiseta",
"pants": "jeans",
"dress": "vestido",
"shoe": "tênis de corrida",
"fencing": "esgrimista",
"horse_riding": "corrida de cavalos",
"ski": "esquiador",
"rowing_boat": "pessoa remando",
"swim": "pessoa nadando",
"surf": "surfista",
"gym": "pessoa levantando peso",
"wrestling": "pessoas lutando",
"bike": "ciclista",
"parachute": "paraquedas",
"football": "bola de futebol",
"basketball": "bola de basquete",
"tennis": "tênis",
"ping_pong": "pingue-pongue",
"martial": "quimono de artes marciais",
"lion": "rosto de leão",
"leopard": "leopardo",
"horse": "rosto de cavalo",
"zebra": "zebra",
"pig": "porco",
"goat": "cabra",
"sheep": "ovelha",
"camel": "camelo",
"giraffe": "girafa",
"elephant": "elefante",
"rhinoceros": "rinoceronte",
"flamingo": "flamingo",
"whale": "baleia esguichando água",
"dolphin": "golfinho",
"bear": "rosto de urso",
"rooster": "galo",
"chick": "pintinho chocando",
"eagle": "águia",
"duck": "pato",
"owl": "coruja",
"rabbit": "coelho",
"penguin": "pinguim",
"lizard": "lagartixa",
"turtle": "tartaruga",
"snake": "cobra",
"hedgehog": "porco-espinho",
"bat": "morcego",
"fish": "peixe",
"shell": "caramujo",
"octopus": "polvo",
"snail": "caracol",
"butterfly": "borboleta",
"ant": "formiga",
"bee": "abelha",
"beetle": "joaninha",
"rose": "rosa",
"sunflower": "girassol",
"fir": "conífera",
"palm_tree": "palmeira",
"cactus": "cacto",
"clover": "trevo de quatro folhas",
"potted_plant": "vaso com planta",
"bouquet": "buquê",
"three_leaves": "folhas caídas",
"mushroom": "cogumelo",
"grapes": "uvas",
"watermelon": "melancia",
"lemon": "limão",
"banana": "banana",
"pineapple": "abacaxi",
"apple": "maçã vermelha",
"cherries": "cereja",
"strawberry": "morango",
"three_blueberries": "mirtilos",
"kiwi": "kiwi",
"avocado": "abacate",
"eggplant": "berinjela",
"carrot": "cenoura",
"corn": "milho",
"pepper": "pimenta",
"croissant": "croissant",
"bread": "baguete",
"pretzel": "pretzel",
"cheese": "queijo",
"pizza": "pizza",
"egg": "ovo frito",
"ice_cream": "sorvete italiano",
"cookie": "biscoito",
"cake": "pão de ló de morango",
"chocolate": "chocolate",
"sweet": "bala",
"coffee": "café",
"champagne_bottle": "garrafa de champanhe",
"glass_wine": "vinho",
"two_glasses": "taças brindando",
"mountain": "montanha com neve",
"camping": "acampamento",
"beach": "praia e guarda-sol",
"compass": "bússola",
"museum": "prédio grego",
"house": "casa com jardim",
"fountain": "fonte",
"circus": "circo",
"train": "locomotiva",
"taxi": "táxi",
"motorcycle": "motocicleta",
"sailboat": "barco a vela",
"airplane": "avião",
"helicopter": "helicóptero",
"rocket": "foguete",
"sun": "sol",
"moon": "lua crescente",
"planet": "planeta com anéis",
"star": "estrela branca média",
"night_sky": "via láctea",
"cloud": "nuvem com chuva",
"umbrella": "sombrinha na chuva",
"lightning": "alta tensão",
"snowflake": "floco de neve",
"snowman": "boneco de neve sem neve",
"thermometer": "termômetro",
"fire": "fogo",
"balloon": "balão",
"kite": "pipa",
"rainbow": "arco-íris",
"guitar": "guitarra",
"saxophone": "saxofone",
"music": "nota musical",
"painting": "paleta de tintas",
"chess": "peão de xadrez",
"gift": "presente",
"die": "jogo de dado",
"puzzle": "quebra-cabeça",
"teddy_bear": "ursinho de pelúcia",
"firecracker": "bombinha",
"bullseye": "no alvo",
"roller_skate": "patins de rodas",
"kick_scooter": "patinete",
"anchor": "âncora",
"scuba_diving": "máscara de mergulho",
"broom": "vassoura",
"magnifying_glass": "lupa para a esquerda",
"bulb": "lâmpada",
"three_books": "livros",
"package": "pacote",
"pencil": "lápis",
"pin": "tacha",
"paperclip": "clipe de papel",
"scissors": "tesoura",
"key": "chave",
"lock": "cadeado aberto",
"chair": "cadeira",
"bathtub": "banheira",
"sponge": "esponja",
"shopping_cart": "carrinho de compras"
}
}
}

@ -0,0 +1,231 @@
{
"emojis": {
"codes": {
"happy": "широко улыбается",
"happy_tears": "смеется до слез",
"halo": "с нимбом",
"three_hearts": "улыбающееся лицо с сердечками",
"with_two_hearts": "влюбленное лицо",
"one_heart": "воздушный поцелуй",
"with_tongue": "морщится и показывает язык",
"with_two_hands": "обнимает",
"one_hand": "прикрывает рот рукой",
"silenced": "рот на замке",
"celebrating": "на вечеринке",
"sunglasses": "лицо в темных очках",
"eyes_up": "закатывает глаза",
"monocle": "с моноклем",
"sleeping": "спит",
"mask": "в медицинской маске",
"fever": "с градусником во рту",
"bandage": "с перевязанной головой",
"vomit": "рвота",
"tissue": "чихает",
"hot": "с перевязанной головой",
"cold": "мерзнет",
"crossed_eyes": "головокружение",
"exploding": "взрыв мозга",
"sad": "грустит",
"long_nose": "лжец",
"many_tears": "слезы рекой",
"fear": "в ужасе",
"tired": "зевает",
"annoyed": "в ожидании успеха",
"clown": "клоун",
"ghost": "привидение",
"dog": "морда собаки",
"happy_cat": "смеющийся кот",
"scared_cat": "кот в шоке",
"sad_cat": "плачущий кот",
"monkey_no_see": "ничего не вижу",
"monkey_no_hear": "ничего не слышу",
"monkey_no_talk": "ничего никому не скажу",
"builder": "строитель",
"princess": "принцесса",
"firefighter": "взрослый",
"mage": "маг",
"mermaid": "русалка",
"fairy": "фея",
"letter_heart": "любовное письмо",
"red_heart": "алое сердце",
"two_hearts": "два сердца",
"kiss": "след от поцелуя",
"hundred": "сто баллов",
"explosion": "взрыв",
"drops": "капли пота",
"handshake": "рукопожатие",
"hand_five_fingers": "раскрытая ладонь",
"hand_two_fingers": "жест V",
"thumbs_up": "большой палец вверх",
"fist": "поднятый кулак",
"two_hands": "ладони в стороны",
"writing": "пишущая рука",
"praying": "сложенные руки",
"arm": "бицепс",
"leg": "нога",
"foot": "щиколотка",
"ear": "ухо",
"nose": "нос",
"brain": "мозг",
"tooth": "зуб",
"bone": "кость",
"eye": "глаз",
"tongue": "язык",
"mouth": "рот",
"shirt": "футболка",
"pants": "джинсы",
"dress": "платье",
"shoe": "кроссовки",
"fencing": "фехтовальщик",
"horse_riding": "скачки",
"ski": "горные лыжи",
"rowing_boat": "гребля",
"swim": "плавание",
"surf": "серфинг",
"gym": "тяжелоатлет",
"wrestling": "борцы",
"bike": "велосипедист",
"parachute": "парашют",
"football": "футбол",
"basketball": "баскетбол",
"tennis": "теннис",
"ping_pong": "настольный теннис",
"martial": "спортивное кимоно",
"lion": "морда льва",
"leopard": "леопард",
"horse": "морда лошади",
"zebra": "зебра",
"pig": "свинья",
"goat": "коза",
"sheep": "овца",
"camel": "одногорбый верблюд",
"giraffe": "жираф",
"elephant": "слон",
"rhinoceros": "носорог",
"flamingo": "фламинго",
"whale": "кит с фонтанчиком",
"dolphin": "дельфин",
"bear": "морда медведя",
"rooster": "петух",
"chick": "цыпленок в яйце",
"eagle": "орел",
"duck": "утка",
"owl": "сова",
"rabbit": "кролик",
"penguin": "пингвин",
"lizard": "ящерица",
"turtle": "черепаха",
"snake": "змея",
"hedgehog": "еж",
"bat": "летучая мышь",
"fish": "рыба",
"shell": "раковина",
"octopus": "осьминог",
"snail": "улитка",
"butterfly": "бабочка",
"ant": "муравей",
"bee": "пчела",
"beetle": "божья коровка",
"rose": "роза",
"sunflower": "подсолнух",
"fir": "елка",
"palm_tree": "пальма",
"cactus": "кактус",
"clover": "четырехлистный клевер",
"potted_plant": "растение в горшке",
"bouquet": "букет",
"three_leaves": "падающие листья",
"mushroom": "гриб",
"grapes": "виноград",
"watermelon": "арбуз",
"lemon": "лимон",
"banana": "банан",
"pineapple": "ананас",
"apple": "красное яблоко",
"cherries": "вишня",
"strawberry": "клубника",
"three_blueberries": "голубика",
"kiwi": "киви",
"avocado": "авокадо",
"eggplant": "баклажан",
"carrot": "морковь",
"corn": "кукурузный початок",
"pepper": "острый перец",
"croissant": "круассан",
"bread": "багет",
"pretzel": "крендель",
"cheese": "сыр",
"pizza": "пицца",
"egg": "яичница на сковороде",
"ice_cream": "мороженое в стаканчике",
"cookie": "печенье",
"cake": "кусочек торта",
"chocolate": "шоколад",
"sweet": "конфета",
"coffee": "горячий напиток",
"champagne_bottle": "шампанское",
"glass_wine": "бокал вина",
"two_glasses": "чокающиеся бокалы",
"mountain": "гора со снежной шапкой",
"camping": "кемпинг",
"beach": "пляж",
"compass": "компас",
"museum": "античное здание",
"house": "дом с садом",
"fountain": "фонтан",
"circus": "цирковой шатер",
"train": "паровоз",
"taxi": "такси",
"motorcycle": "мотоцикл",
"sailboat": "парусник",
"airplane": "самолет",
"helicopter": "вертолет",
"rocket": "ракета",
"sun": "солнце",
"moon": "полумесяц",
"planet": "планета с кольцом",
"star": "желтая звезда",
"night_sky": "Млечный Путь",
"cloud": "дождь",
"umbrella": "зонт под дождем",
"lightning": "высокое напряжение",
"snowflake": "снежинка",
"snowman": "снеговик",
"thermometer": "термометр",
"fire": "огонь",
"balloon": "воздушный шарик",
"kite": "воздушный змей",
"rainbow": "радуга",
"guitar": "гитара",
"saxophone": "саксофон",
"music": "нота",
"painting": "палитра с красками",
"chess": "пешка",
"gift": "подарок",
"die": "игральная кость",
"puzzle": "пазл",
"teddy_bear": "плюшевый мишка",
"firecracker": "динамитная шашка",
"bullseye": "мишень",
"roller_skate": "роликовые коньки",
"kick_scooter": "самокат",
"anchor": "якорь",
"scuba_diving": "маска с трубкой",
"broom": "метла",
"magnifying_glass": "лупа, наклоненная влево",
"bulb": "лампочка",
"three_books": "книги",
"package": "посылка",
"pencil": "карандаш",
"pin": "канцелярская кнопка",
"paperclip": "скрепка",
"scissors": "ножницы",
"key": "ключ",
"lock": "открытый замок",
"chair": "стул",
"bathtub": "ванна",
"sponge": "губка",
"shopping_cart": "тележка для покупок"
}
}
}

@ -0,0 +1,231 @@
{
"emojis": {
"codes": {
"happy": "嘿嘿",
"happy_tears": "笑哭了",
"halo": "微笑天使",
"three_hearts": "喜笑颜开",
"with_two_hearts": "花痴",
"one_heart": "飞吻",
"with_tongue": "眯眼吐舌",
"with_two_hands": "抱抱",
"one_hand": "不说",
"silenced": "闭嘴",
"celebrating": "聚会笑脸",
"sunglasses": "墨镜笑脸",
"eyes_up": "翻白眼",
"monocle": "带单片眼镜的脸",
"sleeping": "睡着了",
"mask": "感冒",
"fever": "发烧",
"bandage": "受伤",
"vomit": "呕吐",
"tissue": "打喷嚏",
"hot": "受伤",
"cold": "冷脸",
"crossed_eyes": "晕头转向",
"exploding": "爆炸头",
"sad": "不满",
"long_nose": "说谎",
"many_tears": "放声大哭",
"fear": "吓死了",
"tired": "打呵欠",
"annoyed": "傲慢",
"clown": "小丑脸",
"ghost": "鬼",
"dog": "狗脸",
"happy_cat": "微笑的猫",
"scared_cat": "疲倦的猫",
"sad_cat": "哭泣的猫",
"monkey_no_see": "非礼勿视",
"monkey_no_hear": "非礼勿听",
"monkey_no_talk": "非礼勿言",
"builder": "建筑工人",
"princess": "公主",
"firefighter": "成人",
"mage": "法师",
"mermaid": "人鱼",
"fairy": "精灵",
"letter_heart": "情书",
"red_heart": "红心",
"two_hearts": "两颗心",
"kiss": "唇印",
"hundred": "一百分",
"explosion": "爆炸",
"drops": "汗滴",
"handshake": "握手",
"hand_five_fingers": "手掌",
"hand_two_fingers": "胜利手势",
"thumbs_up": "拇指向上",
"fist": "举起拳头",
"two_hands": "张开双手",
"writing": "写字",
"praying": "双手合十",
"arm": "肌肉",
"leg": "腿",
"foot": "脚",
"ear": "耳朵",
"nose": "鼻子",
"brain": "脑",
"tooth": "牙齿",
"bone": "骨头",
"eye": "眼睛",
"tongue": "舌头",
"mouth": "嘴",
"shirt": "T恤",
"pants": "牛仔裤",
"dress": "连衣裙",
"shoe": "跑鞋",
"fencing": "击剑选手",
"horse_riding": "赛马",
"ski": "滑雪的人",
"rowing_boat": "划艇",
"swim": "游泳",
"surf": "冲浪",
"gym": "举重",
"wrestling": "摔跤选手",
"bike": "骑自行车",
"parachute": "降落伞",
"football": "足球",
"basketball": "篮球",
"tennis": "网球",
"ping_pong": "乒乓球",
"martial": "练武服",
"lion": "狮子",
"leopard": "豹子",
"horse": "马头",
"zebra": "斑马",
"pig": "猪",
"goat": "山羊",
"sheep": "母羊",
"camel": "骆驼",
"giraffe": "长颈鹿",
"elephant": "大象",
"rhinoceros": "犀牛",
"flamingo": "火烈鸟",
"whale": "喷水的鲸",
"dolphin": "海豚",
"bear": "熊",
"rooster": "公鸡",
"chick": "小鸡破壳",
"eagle": "鹰",
"duck": "鸭子",
"owl": "猫头鹰",
"rabbit": "兔子",
"penguin": "企鹅",
"lizard": "蜥蜴",
"turtle": "龟",
"snake": "蛇",
"hedgehog": "刺猬",
"bat": "蝙蝠",
"fish": "鱼",
"shell": "海螺",
"octopus": "章鱼",
"snail": "蜗牛",
"butterfly": "蝴蝶",
"ant": "蚂蚁",
"bee": "蜜蜂",
"beetle": "瓢虫",
"rose": "玫瑰",
"sunflower": "向日葵",
"fir": "松树",
"palm_tree": "棕榈树",
"cactus": "仙人掌",
"clover": "四叶草",
"potted_plant": "盆栽植物",
"bouquet": "花束",
"three_leaves": "落叶",
"mushroom": "蘑菇",
"grapes": "葡萄",
"watermelon": "西瓜",
"lemon": "柠檬",
"banana": "香蕉",
"pineapple": "菠萝",
"apple": "红苹果",
"cherries": "樱桃",
"strawberry": "草莓",
"three_blueberries": "蓝莓",
"kiwi": "猕猴桃",
"avocado": "鳄梨",
"eggplant": "茄子",
"carrot": "胡萝卜",
"corn": "玉米",
"pepper": "红辣椒",
"croissant": "羊角面包",
"bread": "法式长棍面包",
"pretzel": "椒盐卷饼",
"cheese": "芝士",
"pizza": "披萨",
"egg": "煎蛋",
"ice_cream": "圆筒冰激凌",
"cookie": "饼干",
"cake": "水果蛋糕",
"chocolate": "巧克力",
"sweet": "糖",
"coffee": "热饮",
"champagne_bottle": "开香槟",
"glass_wine": "葡萄酒",
"two_glasses": "碰杯",
"mountain": "雪山",
"camping": "露营",
"beach": "沙滩伞",
"compass": "指南针",
"museum": "古典建筑",
"house": "别墅",
"fountain": "喷泉",
"circus": "马戏团帐篷",
"train": "蒸汽火车",
"taxi": "出租车",
"motorcycle": "摩托车",
"sailboat": "帆船",
"airplane": "飞机",
"helicopter": "直升机",
"rocket": "火箭",
"sun": "太阳",
"moon": "弯月",
"planet": "有环行星",
"star": "星星",
"night_sky": "银河",
"cloud": "下雨",
"umbrella": "雨伞",
"lightning": "高压",
"snowflake": "雪花",
"snowman": "雪人",
"thermometer": "温度计",
"fire": "火焰",
"balloon": "气球",
"kite": "风筝",
"rainbow": "彩虹",
"guitar": "吉他",
"saxophone": "萨克斯管",
"music": "音符",
"painting": "调色盘",
"chess": "兵",
"gift": "礼物",
"die": "骰子",
"puzzle": "拼图",
"teddy_bear": "泰迪熊",
"firecracker": "爆竹",
"bullseye": "正中靶心的飞镖",
"roller_skate": "四轮滑冰鞋",
"kick_scooter": "滑板车",
"anchor": "锚",
"scuba_diving": "潜水面罩",
"broom": "扫帚",
"magnifying_glass": "左斜的放大镜",
"bulb": "灯泡",
"three_books": "书",
"package": "包裹",
"pencil": "铅笔",
"pin": "图钉",
"paperclip": "回形针",
"scissors": "剪刀",
"key": "钥匙",
"lock": "打开的锁",
"chair": "椅子",
"bathtub": "浴缸",
"sponge": "海绵",
"shopping_cart": "购物车"
}
}
}

@ -0,0 +1,367 @@
<!--
// Copyright (c) 2022-2024 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
-->
<!--
@component
"Accounts Info" user panel sub menu.
Provides info about wallet, broker, etc. and download option.
-->
<script lang="ts">
import { link, push } from "svelte-spa-router";
import CenteredLayout from "../lib/CenteredLayout.svelte";
import { ArrowLeft, ServerStack } from "svelte-heros-v2";
import { onMount, tick } from "svelte";
import { Sidebar, SidebarGroup, SidebarWrapper } from "flowbite-svelte";
import { t } from "svelte-i18n";
import { active_session, active_wallet, connections, display_error } from "../store";
import { default as ng } from "../api";
import DeviceIcon from "../lib/components/DeviceIcon.svelte";
let error;
let nonActiveClass =
"flex items-center p-2 text-base font-normal text-gray-900 rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700";
let top;
async function scrollToTop() {
await tick();
top.scrollIntoView();
}
onMount(async () => {
if (!$active_session) {
push("#/");
} else {
await scrollToTop();
}
});
$: wallet_unlocked = $active_wallet?.wallet?.V0;
$: personal_site_id = wallet_unlocked?.personal_site_id;
/**
* brokers:
ZBY5Y8DTyhMo5xfo5K4FCsJHVaN3O15vKeQBwZxr76YA: [
ServerV0:
can_forward: true
can_verify: false
peer_id: Object { Ed25519PubKey: (32) [] }
server_type: Object {
Domain
Localhost: 1440 // if domain not exist
BoxPrivate // one IPv4 and optionally one IPv6 to connect to an NGbox on private LAN rs type Vec<BindAddress>
Public // one IPv4 and optionally one IPv6 to connect to an NGbox on public (edge) internet. rs type Vec<BindAddress>
BoxPublicDyn // same but with dynamic IPs that can be retrieved with a special API. rs type Vec<BindAddress>
}
]
*/
/*
* Connections Is a record of string to those objects:
error: undefined
server_id: "ZBY5Y8DTyhMo5xfo5K4FCsJHVaN3O15vKeQBwZxr76YA"
server_ip: "ws://localhost:1440"
since: Date Fri Jul 05 2024 09:46:30 GMT+0200 (Central European Summer Time)
*/
// let connections;
/**
* bootstraps: Array [ {} ]
* cores: Array [ (2) […] ]
* id: Object { Ed25519PubKey: (32) [] }
* name: "Personal"
* private: Object { id: {}, store_type: "Private" }
* protected: Object { id: {}, store_type: "Protected" }
* public: Object { id: {}, store_type: "Public" }
* site_type: Object { Individual: (2) [] } // Some key data as well
*/
$: walletSites = wallet_unlocked?.sites;
/** Type:
* client_type: "Web"
* details: '{"browser":{"name":"Firefox","version":"127.0","appVersion":"5.0 (X11)","arch":"Linux x86_64","vendor":"","ua":"Mozilla/5.0 (X11; Linux x86_64; rv:127.0) Gecko/20100101 Firefox/127.0"},"os":{"name":"Linux"},"platform":{"type":"desktop"},"engine":{"name":"Gecko","version":"20100101","sdk":"0.1.0-preview.1"}}'
* timestamp_install: 0
* timestamp_updated: 0
* version: "0.1.0"
*/
var device_info;
$: display_sites = Object.entries(walletSites || {})
?.map(([user_id, site]) => {
// Try to extract device details (for now only of the connected device).
// TODO: API for all devices
const devices = (!device_info ? [] : [device_info.V0]).map((device) => {
const device_details = JSON.parse(device.details);
return {
name: device.name, // TODO: API device.name is not provided
peer_id: device.id, // TODO: API device id is is not provided
version: device.version,
details: device_details,
device_name:
device.client_type === "web"
? `${device_details?.browser?.name}${" - " + device_details?.browser.arch || ""}`
: `${device_details?.os?.name_uname || device_details?.os?.name_rust || device_details?.os?.name} - ${device_details?.os?.version_uname || device_details?.os?.version_rust}`,
type: device.client_type,
};
});
return {
id: user_id,
connection: $connections[user_id], // error, server_id, server_ip, since
devices,
// @ts-ignore
name: site.name,
};
})
.filter((site) => site.id === personal_site_id);
$: display_brokers = Object.entries(wallet_unlocked?.brokers || {}).map(
// @ts-ignore
([broker_id, [broker]]) => {
//TODO: there can be several broker definitions for the same broker_id (if the broker can be reached by different means)
return {
id: broker_id,
can_forward: broker.ServerV0.can_forward,
can_verify: broker.ServerV0.can_verify,
address:
broker.ServerV0.server_type.Domain ||
`localhost:${broker.ServerV0.server_type.Localhost}`,
last_connected: new Date("1970-01-01T00:00:00Z").toLocaleString(), // TODO: API
};
}
);
// $: console.info(JSON.stringify(device_info));
// $: console.debug(
// "info",
// device_info,
// "walletSites",
// walletSites,
// "wallet",
// $active_wallet,
// "connections",
// $connections,
// "display_brokers",
// display_brokers,
// "display_sites",
// display_sites
// );
onMount(async () => {
ng.client_info().then((res) => {
device_info = res;
});
});
</script>
<CenteredLayout>
<div class="container3" bind:this={top}>
<div class="row mb-20">
<Sidebar {nonActiveClass}>
<SidebarWrapper
divClass="bg-gray-60 overflow-y-auto py-4 px-3 rounded dark:bg-gray-800"
>
<!-- Go Back-->
<SidebarGroup ulClass="space-y-2" role="menu">
<li>
<h2 class="text-xl mb-6">{$t("pages.account_info.title")}</h2>
</li>
<li
tabindex="0"
role="menuitem"
class="flex items-center p-2 text-base font-normal text-gray-900 clickable rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
on:keypress={() => window.history.go(-1)}
on:click={() => window.history.go(-1)}
>
<ArrowLeft
tabindex="-1"
class="w-7 h-7 text-black transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
<span class="ml-3">{$t("buttons.back")}</span>
</li>
</SidebarGroup>
<!-- For now this will only consist of the `Personal` one-->
{#each display_sites as site}
<li
class="flex items-center p-2 text-base font-normal text-gray-900"
>
<h3 class="flex items-center mt-2 text-lg font-normal">
{$t("pages.account_info.site", { values: { name: site.name } })}
</h3>
</li>
<!-- Device Details -->
<SidebarGroup ulClass="space-y-1">
<li
class="flex items-center p-2 text-base font-normal text-gray-900"
>
<h4
class="flex items-center mt-2 text-base font-normal text-gray-600"
>
{$t("pages.account_info.devices")}
</h4>
</li>
{#each site.devices as device, index}
<li
class="flex items-center p-2 text-base font-normal text-gray-900 bg-white shadow-md rounded-lg"
class:border-b={index !== site.devices.length - 1}
>
<div>
<DeviceIcon device={device.type} />
</div>
<div
class="flex flex-col ml-3 items-start text-left overflow-auto"
>
<div>
<span class="text-gray-500">Name</span>
<span class="break-all">{device.name}</span>
</div>
<div>
<span class="text-gray-500">ID</span>
<span class="break-all">{device.peer_id}</span>
</div>
<div>
<span class="text-gray-500">Version</span>
<span>{device.version}</span>
</div>
<div>
<span class="text-gray-500">System</span>
<span> {device.device_name}</span>
</div>
</div>
</li>
{/each}
</SidebarGroup>
<!-- Broker Details -->
<SidebarGroup ulClass="space-y-1">
<li
class="flex items-center p-2 text-base font-normal text-gray-900"
>
<h4
class="flex items-center mt-2 text-base font-normal text-gray-600"
>
Brokers
</h4>
</li>
{#if display_brokers.length > 0}
{#each Object.values(display_brokers) as broker, index}
<!--
(peerId, IP/port or domain, last time connected)
-->
<li
class="flex items-center p-2 text-base font-normal text-gray-900 bg-white shadow-md rounded-lg"
class:border-b={index !== display_brokers.length - 1}
>
<div>
<ServerStack
tabindex="-1"
class="w-7 h-7 text-black transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
</div>
<div class="flex flex-col ml-3 items-start text-left">
<div>
<span class="text-gray-500">Address</span><br />
<span class="break-all">{broker.address}</span>
</div>
<div>
<span class="text-gray-500">Last Connected</span><br />
<span class="break-all">{broker.last_connected}</span>
</div>
<div>
<span class="text-gray-500">ID</span>
<span class="break-all">{broker.id}</span>
</div>
<!-- <div>
<span class="text-gray-500">Can Forward?</span>
<span>{broker.can_forward}</span>
</div>
<div>
<span class="text-gray-500">Can Verify?</span>
<span>{broker.can_verify}</span>
</div> -->
</div>
</li>
{/each}
{:else}
<li
class="flex items-center p-2 text-base font-normal text-gray-900"
>
<span class="ml-3"
>{$t("pages.account_info.no_brokers_connected")}</span
>
</li>
{/if}
</SidebarGroup>
{/each}
</SidebarWrapper>
</Sidebar>
</div>
{#if error}
<div class=" max-w-6xl lg:px-8 mx-auto px-4 text-red-800">
<svg
class="animate-bounce mt-10 h-16 w-16 mx-auto"
fill="none"
stroke="currentColor"
stroke-width="1.5"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"
/>
</svg>
{#if error == "AlreadyExists"}
<p class="max-w-xl md:mx-auto lg:max-w-2xl mb-5">
{@html $t("errors.AlreadyExists")}
</p>
<a use:link href="/">
<button
tabindex="-1"
class="text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-700/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55 mb-2"
>
Login
</button>
</a>
{:else}
<p class="max-w-xl md:mx-auto lg:max-w-2xl mb-5">
{@html $t("errors.error_occurred", {
values: { message: display_error(error) },
})}
</p>
<a use:link href="/">
<button
tabindex="-1"
class="text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-700/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55 mb-2"
>
{$t("buttons.back_to_homepage")}
</button>
</a>
{/if}
</div>
{/if}
</div>
</CenteredLayout>
<style>
li.clickable {
cursor: pointer;
}
</style>

@ -9,18 +9,28 @@
// according to those terms.
-->
<!--
Home page to display for logged in users.
Redirects to no-wallet or login page, if not logged in.
-->
<script>
import { Button } from "flowbite-svelte";
import { link } from "svelte-spa-router";
import Home from "../lib/Home.svelte";
import NoWallet from "../lib/NoWallet.svelte";
import { push } from "svelte-spa-router";
import { onMount, onDestroy } from "svelte";
import { active_wallet, has_wallets, derived } from "../store";
import {
active_wallet,
has_wallets,
derived,
cannot_load_offline,
} from "../store";
let display_login_create = !$has_wallets || !$active_wallet;
let unsubscribe;
onMount(() => {
cannot_load_offline.set(false);
//setTimeout(function () {}, 2);
const combined = derived([active_wallet, has_wallets], ([$s1, $s2]) => [
$s1,
$s2,

@ -10,16 +10,12 @@
-->
<script type="ts">
import { Button } from "flowbite-svelte";
import { link } from "svelte-spa-router";
import Install from "../lib/Install.svelte";
import { push } from "svelte-spa-router";
import { onMount, onDestroy } from "svelte";
import CenteredLayout from "../lib/CenteredLayout.svelte";
import { has_wallets } from "../store";
let display_has_wallets_warning = $has_wallets != 0;
let unsubscribe;
onMount(() => {});
onDestroy(() => {});

@ -10,7 +10,7 @@
-->
<script lang="ts">
export let params = {};
export let params: { invitation: string } = { invitation: "" };
import { onMount } from "svelte";
onMount(
() => (window.location.href = "/#/wallet/create?i=" + params.invitation)

@ -9,6 +9,7 @@
// according to those terms.
-->
<script>
import { t } from "svelte-i18n";
// The params prop contains values matched from the URL
export let params = {};
@ -19,6 +20,6 @@
<div
class="h-screen aspect-[3/5] pazzleline max-w-[720px] bg-yellow-300 inner"
>
<p>Nextgraph URI {params[1]}</p>
<p>{$t("pages.nextgraph_uri.message", { values: { uri: params[1] } })}</p>
</div>
</div>

@ -12,12 +12,15 @@
<script>
import { Alert } from "flowbite-svelte";
import CenteredLayout from "../lib/CenteredLayout.svelte";
import { t } from "svelte-i18n";
</script>
<CenteredLayout displayFooter={true}>
<div class="p-8">
<Alert color="red">
<span class="font-medium">404</span> Page not found.
<span class="font-medium">404</span> {$t("pages.not_found.title")}
<br />
<span class="text-sm">{$t("pages.not_found.message")}</span>
</Alert>
</div>
</CenteredLayout>

@ -0,0 +1,51 @@
<!--
// Copyright (c) 2022-2024 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
-->
<!--
Home page to display for logged in users.
Redirects to no-wallet or login page, if not logged in.
-->
<script>
import { onMount, onDestroy } from "svelte";
import {
wallet_import_qrcode
} from "../store";
let tauri_platform = import.meta.env.TAURI_PLATFORM;
let mobile = tauri_platform == "android" || tauri_platform == "ios";
onMount(async () => {
//TODO: here we should also take into account the case of a webapp with camera feature, and sue the lib https://www.npmjs.com/package/html5-qrcode
if (mobile) {
const scanner = await import("@tauri-apps/plugin-barcode-scanner");
let perms = await scanner.requestPermissions();
console.log(perms);
wallet_import_qrcode.set("");
let result = await scanner.scan({ windowed: false, cameraDirection: "back", formats: [scanner.Format.QRCode] })
console.log(result)
wallet_import_qrcode.set(result.content);
window.history.go(-1);
}
});
onDestroy(async () => {
if (mobile) {
const scanner = await import("@tauri-apps/plugin-barcode-scanner");
await scanner.cancel();
}
});
</script>
<div class="text-center">
<!-- please translate this too. i didnt want to do it to avoid a merge conflict-->
Scanning the QRcode
</div>

@ -8,6 +8,13 @@
// notice may not be copied, modified, or distributed except
// according to those terms.
-->
<!--
@component
"User Panel" page.
Provides wallet, logout, offline/online switch, and other user actions.
-->
<script>
// @ts-nocheck
@ -16,6 +23,7 @@
import CenteredLayout from "../lib/CenteredLayout.svelte";
import { version } from "../../package.json";
import Time from "svelte-time";
import { t } from "svelte-i18n";
// @ts-ignore
import Logo from "../assets/nextgraph.svg?component";
import {
@ -88,30 +96,6 @@
push("#/wallet/login");
}
let downloading = false;
let wallet_file_ready = false;
let download_link = false;
let download_error = false;
async function download_wallet() {
try {
downloading = true;
let file = await ng.wallet_get_file($active_wallet.id);
// @ts-ignore
wallet_file_ready = "wallet-" + $active_wallet.id + ".ngw";
if (!tauri_platform) {
const blob = new Blob([file], {
type: "application/octet-stream",
});
// @ts-ignore
download_link = URL.createObjectURL(blob);
} else {
download_link = true;
}
} catch (e) {
download_error = e;
}
}
$: personal_site = $active_wallet?.wallet?.V0.personal_site_id;
$: personal_site_id = $active_wallet?.wallet?.V0.personal_site;
@ -126,10 +110,13 @@
}
};
const donate = async () => {
await displayPopup("https://nextgraph.org/donate", "Support NextGraph");
await displayPopup(
"https://nextgraph.org/donate",
$t("common.support_nextgraph")
);
};
const about = async () => {
await displayPopup("https://nextgraph.org", "About NextGraph");
await displayPopup("https://nextgraph.org", $t("common.about_nextgraph"));
};
</script>
@ -140,22 +127,22 @@
<SidebarWrapper
divClass="bg-gray-60 overflow-y-auto py-4 px-3 rounded dark:bg-gray-800"
>
<SidebarGroup ulClass="space-y-2">
<SidebarGroup ulClass="space-y-2" role="menu">
<li>
<h2 class="text-xl mb-6">User panel</h2>
<h2 class="text-xl mb-6">{$t("pages.user_panel.title")}</h2>
</li>
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<li
tabindex="0"
role="menuitem"
class="flex items-center p-2 text-base font-normal text-gray-900 clickable rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
on:keypress={() => window.history.go(-1)}
on:click={() => window.history.go(-1)}
>
<ArrowLeft
tabindex="-1"
class="w-7 h-7 text-black transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
class="w-7 h-7 text-black transition duration-75 focus:outline-none dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
<span class="ml-3">Back</span>
<span class="ml-3">{$t("buttons.back")}</span>
</li>
<li
@ -164,32 +151,34 @@
{#if $online}
<Signal
tabindex="-1"
class="w-7 h-7 text-green-600 transition duration-75 dark:text-green-400 "
class="w-7 h-7 text-green-600 transition duration-75 focus:outline-none dark:text-green-400 "
/>
<span class="ml-3 text-green-600 dark:text-green-400"
>Online</span
>{$t("connectivity.online")}</span
>
{:else}
<SignalSlash
tabindex="-1"
class="w-7 h-7 text-red-600 transition duration-75 dark:text-red-400 "
class="w-7 h-7 text-red-600 transition duration-75 focus:outline-none dark:text-red-400 "
/>
<span class="ml-3 text-red-600 dark:text-red-400">Offline</span>
<span class="ml-3 text-red-600 dark:text-red-400"
>{$t("connectivity.offline")}</span
>
{/if}
</li>
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<li
tabindex="0"
role="menuitem"
class="flex items-center p-2 text-base font-normal text-gray-900 clickable rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
on:keypress={logout}
on:click={logout}
>
<ArrowRightOnRectangle
tabindex="-1"
class="w-7 h-7 text-black transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
class="w-7 h-7 text-black transition duration-75 focus:outline-none dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
<span class="ml-3">Logout</span>
<span class="ml-3">{$t("buttons.logout")}</span>
</li>
<!-- <li
tabindex="0"
@ -203,106 +192,52 @@
/>
<span class="ml-3">Switch wallet</span>
</li> -->
{#if !downloading}
<li
tabindex="0"
role="menuitem"
class="flex items-center p-2 text-base font-normal text-gray-900 clickable rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
on:keypress={download_wallet}
on:click={download_wallet}
>
<DocumentArrowDown
tabindex="-1"
class="w-7 h-7 text-black transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
<span class="ml-3">Download wallet file</span>
</li>
{:else if download_error}
<li
tabindex="-1"
class="flex items-center p-2 text-base font-normal text-red-700 rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
>
<NoSymbol
tabindex="-1"
class="w-7 h-7 text-red-700 transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
<span class="ml-3 text-left"
>Download failed:<br /> {download_error}</span
>
</li>
{:else if !wallet_file_ready}
<li
tabindex="-1"
class="flex items-center p-2 text-base font-normal text-blue-700 rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
>
<DocumentArrowDown
tabindex="-1"
class="w-7 h-7 text-blue-700 transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
<span class="ml-3 text-left">Download in progress...</span>
</li>
{:else if download_link === true}
<li
tabindex="-1"
class="flex p-2 text-sm text-left break-all font-normal text-blue-700 rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
>
<span
>You will find the file named "{wallet_file_ready}" <br />in
your Downloads folder</span
<SidebarItem
label="Settings"
href="#/user/settings"
class="p-2 opacity-50 pointer-events-none"
disabled
>
</li>
{:else}
<li
tabindex="-1"
class="flex items-center text-base font-normal text-gray-900 clickable rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
>
<a
href={download_link}
target="_blank"
download={wallet_file_ready}
>
<button
tabindex="-1"
class=" text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-700/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55"
>
<DocumentArrowDown
tabindex="-1"
class="w-14 h-14 transition duration-75 dark:text-white dark:group-hover:text-white"
/>
Click here to download the wallet file
</button>
</a>
</li>
{/if}
<SidebarItem label="Settings" href="#/user/settings" class="p-2">
<svelte:fragment slot="icon">
<Cog6Tooth
tabindex="-1"
class="w-7 h-7 text-black transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
class="w-7 h-7 text-black transition duration-75 focus:outline-none dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
</svelte:fragment>
</SidebarItem>
<SidebarItem label="Wallet" href="#/wallet" class="p-2">
<SidebarItem
label={$t("pages.wallet_info.title")}
href="#/wallet"
class="p-2"
>
<svelte:fragment slot="icon">
<PuzzlePiece
tabindex="-1"
class="w-7 h-7 text-black transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
class="w-7 h-7 text-black transition duration-75 focus:outline-none dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
</svelte:fragment>
</SidebarItem>
<SidebarItem label="Admin" href="#/user/admin" class="p-2">
<SidebarItem
label={$t("pages.admin.title")}
href="#/user/admin"
class="p-2 opacity-50 pointer-events-none"
>
<svelte:fragment slot="icon">
<Key
tabindex="-1"
class="w-7 h-7 text-black transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
class="w-7 h-7 text-black transition duration-75 focus:outline-none dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
</svelte:fragment>
</SidebarItem>
<SidebarItem label="Accounts" href="#/user/accounts" class="p-2">
<SidebarItem
label={$t("pages.accounts.title")}
href="#/user/accounts"
class="p-2"
>
<svelte:fragment slot="icon">
<User
tabindex="-1"
class="w-7 h-7 text-black transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
class="w-7 h-7 text-black transition duration-75 focus:outline-none dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
</svelte:fragment>
</SidebarItem>
@ -312,7 +247,7 @@
personal_site_status.server_ip +
" " +
personal_site_status.server_id) ||
"offline"}
$t("pages.user_panel.offline")}
>
<Toggle
on:change={async () => {
@ -320,14 +255,17 @@
$connections[personal_site].connecting = true;
await reconnect();
} else {
$connections[personal_site].error = "Stopped";
$connections[personal_site].error = $t(
"connectivity.stopped"
);
personal_site_status.since = new Date();
await ng.user_disconnect(personal_site);
}
}}
checked={personal_site_status &&
(personal_site_status.connecting ||
!personal_site_status.error)}>Personal</Toggle
!personal_site_status.error)}
>{$t("connectivity.personal")}</Toggle
>
</li>
{#if personal_site_status}
@ -335,12 +273,14 @@
class="site-cnx-details flex items-center px-2 text-sm text-left font-normal text-gray-600"
>
{#if personal_site_status.connecting}
Connecting...
{$t("connectivity.connecting")}...
{:else}
{#if !personal_site_status.error}
Connected
{$t("connectivity.connected")}
{:else}
{personal_site_status.error}
{$t("connectivity.connection_error_short", {
values: { error: personal_site_status.error },
})}
{/if}
<Time
style="display:contents;"
@ -363,9 +303,9 @@
>
<Gift
tabindex="-1"
class="w-7 h-7 text-black transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
class="w-7 h-7 text-black transition duration-75 focus:outline-none dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
<span class="ml-3">Donate to NextGraph</span>
<span class="ml-3">{$t("common.donate_nextgraph")}</span>
</li>
<li
@ -377,15 +317,17 @@
>
<InformationCircle
tabindex="-1"
class="w-7 h-7 text-black transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
class="w-7 h-7 text-black transition duration-75 focus:outline-none dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
<span class="ml-3">About NextGraph</span>
<span class="ml-3">
{$t("common.about_nextgraph")}
</span>
</li>
<li
class="flex items-center p-2 text-base font-normal text-gray-900"
>
Version: {version}
{$t("common.version", { values: { version } })}
</li>
</SidebarGroup>
</SidebarWrapper>
@ -410,27 +352,28 @@
</svg>
{#if error == "AlreadyExists"}
<p class="max-w-xl md:mx-auto lg:max-w-2xl mb-5">
The user is already registered with the selected broker.<br /> Try logging
in instead
{@html $t("errors.AlreadyExists")}
</p>
<a use:link href="/">
<button
tabindex="-1"
class="text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-700/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55 mb-2"
>
Login
{$t("buttons.login")}
</button>
</a>
{:else}
<p class="max-w-xl md:mx-auto lg:max-w-2xl mb-5">
An error occurred:<br />{error}
{@html $t("errors.error_occurred", {
values: { message: display_error(error) },
})}
</p>
<a use:link href="/">
<button
tabindex="-1"
class="text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-700/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55 mb-2"
>
Go back to homepage
{$t("buttons.back_to_homepage")}
</button>
</a>
{/if}

@ -10,7 +10,7 @@
-->
<script>
import { Button, Alert, Dropzone, Toggle } from "flowbite-svelte";
import { t } from "svelte-i18n";
import { link, querystring } from "svelte-spa-router";
import CenteredLayout from "../lib/CenteredLayout.svelte";
// @ts-ignore
@ -18,12 +18,8 @@
import { onMount, tick } from "svelte";
import {
NG_EU_BSP,
NG_NET_BSP,
APP_ACCOUNT_REGISTERED_SUFFIX,
default as ng,
} from "../api";
import { default as ng } from "../api";
import { display_error } from "../store";
const param = new URLSearchParams($querystring);
@ -49,7 +45,7 @@
<div class="container3">
<div class="row">
<a href="#/">
<Logo class="logo block h-40" alt="NextGraph Logo" />
<Logo class="logo block h-40" alt={$t("common.logo")} />
</a>
</div>
{#if error}
@ -71,27 +67,28 @@
</svg>
{#if error == "AlreadyExists"}
<p class="max-w-xl md:mx-auto lg:max-w-2xl mb-5">
The user is already registered with the selected broker.<br /> Try logging
in instead
{@html $t("pages.user_registered.already_exists")}
</p>
<a use:link href="/">
<button
tabindex="-1"
class="text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-700/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55 mb-2"
>
Login
{$t("buttons.login")}
</button>
</a>
{:else}
<p class="max-w-xl md:mx-auto lg:max-w-2xl mb-5">
An error occurred:<br />{error}
{@html $t("errors.error_occurred", {
values: { message: display_error(error) },
})}
</p>
<a use:link href="/">
<button
tabindex="-1"
class="text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-700/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55 mb-2"
>
Go back to homepage
{$t("buttons.back_to_homepage")}
</button>
</a>
{/if}
@ -114,8 +111,13 @@
/>
</svg>
<p class="max-w-xl md:mx-auto lg:max-w-2xl">
You have been successfully <br />registered {#if invitation?.V0?.name}
to {invitation?.V0?.name}{/if}
{#if invitation?.V0?.name}
{$t("pages.user_registered.success_with_invitation", {
values: { invitation_name: invitation?.V0?.name },
})}
{:else}
{$t("pages.user_registered.success")}
{/if}
</p>
</div>
{/if}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,379 @@
<!--
// Copyright (c) 2022-2024 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
-->
<!--
@component
"Wallet Info" user panel sub menu.
Provides info about wallet, broker, etc. and download option.
-->
<script>
import { Modal } from "flowbite-svelte";
import { link, push } from "svelte-spa-router";
import CenteredLayout from "../lib/CenteredLayout.svelte";
import {
ArrowLeft,
Trash,
DocumentArrowDown,
NoSymbol,
QrCode,
Link,
ArrowDownOnSquare,
} from "svelte-heros-v2";
import { onMount, tick } from "svelte";
import { Sidebar, SidebarGroup, SidebarWrapper } from "flowbite-svelte";
import { t } from "svelte-i18n";
import { close_active_wallet, active_session, active_wallet, display_error } from "../store";
import { default as ng } from "../api";
let tauri_platform = import.meta.env.TAURI_PLATFORM;
let error;
let nonActiveClass =
"flex items-center p-2 text-base font-normal text-gray-900 rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700";
let top;
async function scrollToTop() {
await tick();
top.scrollIntoView();
}
onMount(async () => {
if (!$active_session) {
push("#/");
} else {
await scrollToTop();
}
text_code = await ng.wallet_export_get_textcode($active_session.session_id);
qr_code = await ng.wallet_export_get_qrcode($active_session.session_id, 250);
});
let downloading = false;
let wallet_file_ready = false;
let download_link = false;
let download_error = false;
async function download_wallet() {
try {
downloading = true;
let file = await ng.wallet_get_file($active_wallet.id);
// @ts-ignore
wallet_file_ready = "wallet-" + $active_wallet.id + ".ngw";
if (!tauri_platform) {
const blob = new Blob([file], {
type: "application/octet-stream",
});
// @ts-ignore
download_link = URL.createObjectURL(blob);
} else {
download_link = true;
}
} catch (e) {
download_error = e;
}
}
let text_code;
let qr_code;
let wallet_remove_modal_open = false;
async function remove_wallet_clicked() {
//wallet_remove_modal_open = true;
try {
await ng.wallet_export_rendezvous($active_session.session_id, "AABAOAAAAHNb4y7hdWADqFWDgER3J0xvD3K5D9pZ1wd7Bja4c9cWAGpnQYDjun-jOFI8XookNLWKfgpQIkDS21VruUzizWahAH6fStLoA0kBMVR5ZMPHMv7RpQITUGZmZsjlWjnxCRZBAQ");
} catch (e) {
console.error(e);
}
}
const close_modal = () => {
wallet_remove_modal_open = false;
};
async function remove_wallet_confirmed() {
if (!active_wallet) return;
// TODO: Wait for implementation
// await ng.wallet_remove($active_wallet.id);
close_active_wallet();
}
</script>
<CenteredLayout>
<div class="container3" bind:this={top}>
<div class="row mb-20">
<Sidebar {nonActiveClass}>
<SidebarWrapper
divClass="bg-gray-60 overflow-y-auto py-4 px-3 rounded dark:bg-gray-800"
>
<SidebarGroup ulClass="space-y-2" role="menu">
<li>
<h2 class="text-xl mb-6">{$t("pages.wallet_info.title")}</h2>
</li>
<!-- Go Back -->
<li
tabindex="0"
role="menuitem"
class="text-left flex items-center p-2 text-base font-normal text-gray-900 clickable rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
on:keypress={() => window.history.go(-1)}
on:click={() => window.history.go(-1)}
>
<ArrowLeft
tabindex="-1"
class="w-7 h-7 text-black transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
<span class="ml-3">Back</span>
</li>
<!-- Download Wallet -->
{#if !downloading}
<li
tabindex="0"
role="menuitem"
class="flex items-center p-2 text-base font-normal text-gray-900 clickable rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
on:keypress={download_wallet}
on:click={download_wallet}
>
<div>
<DocumentArrowDown
tabindex="-1"
class="w-7 h-7 text-black transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
</div>
<span class="ml-3">{$t("pages.wallet_info.download")}</span>
</li>
{:else if download_error}
<li
tabindex="-1"
class="flex items-center p-2 text-base font-normal text-red-700 rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
>
<div>
<NoSymbol
tabindex="-1"
class="w-7 h-7 text-red-700 transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
</div>
<span class="ml-3 text-left"
>{$t("pages.wallet_info.download_failed", {
values: { error: download_error },
})}</span
>
</li>
{:else if !wallet_file_ready}
<li
tabindex="-1"
class="flex items-center p-2 text-base font-normal text-blue-700 rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
>
<div>
<DocumentArrowDown
tabindex="-1"
class="w-7 h-7 text-blue-700 transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
</div>
<span class="ml-3 text-left"
>{$t("pages.wallet_info.download_in_progress")}</span
>
</li>
{:else if download_link === true}
<li
tabindex="-1"
class="flex p-2 text-sm text-left break-all font-normal text-blue-700 rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
>
<span
>{@html $t("pages.wallet_info.download_successful", {
values: { wallet_file: wallet_file_ready },
})}</span
>
</li>
{:else}
<li
tabindex="-1"
class="flex items-center text-base font-normal text-gray-900 clickable rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
>
<a
href={download_link}
target="_blank"
download={wallet_file_ready}
>
<button
tabindex="-1"
class=" text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-700/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55"
>
<div>
<DocumentArrowDown
tabindex="-1"
class="w-14 h-14 transition duration-75 dark:text-white dark:group-hover:text-white"
/>
</div>
{$t("pages.wallet_info.download_file_button")}
</button>
</a>
</li>
{/if}
<li class="break-all">
{text_code}
{#if qr_code}
{@html qr_code}
{/if}
</li>
<!-- Remove Wallet -->
<li
tabindex="0"
role="menuitem"
class="text-left flex items-center p-2 text-base font-normal text-gray-900 clickable rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
on:keypress={remove_wallet_clicked}
on:click={remove_wallet_clicked}
>
<div>
<Trash
tabindex="-1"
class="w-7 h-7 text-black transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
</div>
<span class="ml-3">{$t("pages.wallet_info.remove_wallet")}</span>
</li>
<Modal
autoclose
outsideclose
bind:open={wallet_remove_modal_open}
title="Remove Wallet"
>
<p class="mt-4">
{$t("pages.wallet_info.remove_confirm")}
</p>
<div class="mt-4 flex justify-end">
<button on:click={close_modal}>{$t("buttons.cancel")}</button>
<button
class="mr-2 bg-primary-700 text-white"
on:click={remove_wallet_confirmed}
>
{$t("buttons.remove")}
</button>
</div>
</Modal>
<!-- TODO: Show QRCode -->
{#if false}
<li
tabindex="0"
role="menuitem"
class="text-left flex items-center p-2 text-base font-normal text-gray-900 clickable rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
>
<div>
<QrCode
tabindex="-1"
class="w-7 h-7 text-black transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
</div>
<span class="ml-3">{$t("qr_code")}</span>
</li>
<Modal
autoclose
outsideclose
title={$t("pages.wallet_info.qr_modal_title")}
>{@html $t("pages.wallet_info.qr_modal_description")}
</Modal>
{/if}
<!-- TODO: Copy Wallet Link -->
{#if false}
<li
tabindex="0"
role="menuitem"
class="text-left flex items-center p-2 text-base font-normal text-gray-900 clickable rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
>
<div>
<Link
tabindex="-1"
class="w-7 h-7 text-black transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
</div>
<span class="ml-3">{$t("pages.login.copy_wallet_link")}</span>
</li>
{/if}
<!-- TODO: Save to Device -->
{#if false}
<li
tabindex="0"
role="menuitem"
class="text-left flex items-center p-2 text-base font-normal text-gray-900 clickable rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
>
<!-- TODO: Same as with the trash icon, this is not same-sized as the others. -->
<ArrowDownOnSquare
tabindex="-1"
class="w-7 h-7 text-black transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
<span class="ml-3">{$t("pages.login.keep_wallet")}</span>
</li>
{/if}
</SidebarGroup>
</SidebarWrapper>
</Sidebar>
</div>
{#if error}
<div class=" max-w-6xl lg:px-8 mx-auto px-4 text-red-800">
<svg
class="animate-bounce mt-10 h-16 w-16 mx-auto"
fill="none"
stroke="currentColor"
stroke-width="1.5"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"
/>
</svg>
{#if error == "AlreadyExists"}
<p class="max-w-xl md:mx-auto lg:max-w-2xl mb-5">
{@html $t("errors.AlreadyExists")}
</p>
<a use:link href="/">
<button
tabindex="-1"
class="text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-700/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55 mb-2"
>
{$t("buttons.login")}
</button>
</a>
{:else}
<p class="max-w-xl md:mx-auto lg:max-w-2xl mb-5">
{@html $t("errors.error_occurred", {
values: { message: display_error(error) },
})}
</p>
<a use:link href="/">
<button
tabindex="-1"
class="text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-700/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55 mb-2"
>
{$t("buttons.back_to_homepage")}
</button>
</a>
{/if}
</div>
{/if}
</div>
</CenteredLayout>
<style>
li.clickable {
cursor: pointer;
}
</style>

@ -9,9 +9,16 @@
// according to those terms.
-->
<!--
"Select a wallet to login with" page.
This page is usually the first page the user sees when they visit the app.
It allows the user to select a wallet to login with, create, or import a wallet.
-->
<script lang="ts">
import { onMount, onDestroy, tick } from "svelte";
import { link, push } from "svelte-spa-router";
import { t, locale } from "svelte-i18n";
import Login from "../lib/Login.svelte";
import CenteredLayout from "../lib/CenteredLayout.svelte";
import ng from "../api";
@ -25,6 +32,8 @@
active_session,
set_active_session,
has_wallets,
wallet_import_qrcode,
display_error,
} from "../store";
let tauri_platform = import.meta.env.TAURI_PLATFORM;
@ -48,7 +57,10 @@
return imageUrl;
}
let qrcode;
onMount(async () => {
step = "open";
wallets_unsub = wallets.subscribe((value) => {
wallet = selected && $wallets[selected]?.wallet;
@ -85,6 +97,25 @@
}
}
});
if ($wallet_import_qrcode) {
let code = $wallet_import_qrcode;
wallet_import_qrcode.set("");
try {
wallet = await ng.wallet_import_from_code(code);
importing = true;
} catch(e) {
error = e;
}
}
// example of rendezvous for desktop and web without cam (please remove it)
qrcode = await ng.wallet_import_rendezvous(300);
try {
wallet = await ng.wallet_import_from_code(qrcode[1]);
importing = true;
} catch (e) {
error = e;
}
});
function loggedin() {
step = "loggedin";
@ -206,7 +237,9 @@
</svg>
<p class="max-w-xl md:mx-auto lg:max-w-2xl mb-5">
An error occurred:<br />{error}
{@html $t("errors.error_occurred", {
values: { message: display_error(error) },
})}
</p>
<button
on:click={() => {
@ -216,7 +249,7 @@
}}
class="text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-700/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55 mb-2"
>
Start over
{$t("buttons.start_over")}
</button>
</div>
{:else if wallet}
@ -231,7 +264,7 @@
<div class="row">
<Logo class="logo block h-40" alt="NextGraph Logo" />
</div>
<h2 class="pb-5 text-xl">Select a wallet to login with</h2>
<h2 class="pb-5 text-xl">{$t("pages.wallet_login.select_wallet")}</h2>
<div class="flex flex-wrap justify-center gap-5 mb-10">
{#each Object.entries($wallets) as wallet_entry}
<div
@ -258,9 +291,35 @@
/>
</div>
{/each}
<!-- remove all this-->
<div
class="wallet-box"
role="button"
tabindex="0"
>
{#if qrcode}
{@html qrcode[0]}
{/if}
</div>
<div
class="wallet-box break-all"
role="button"
tabindex="0"
>
{#if qrcode}
{qrcode[1]}
{/if}
</div>
<!-- remove until here -->
<div class="wallet-box">
{#if $has_wallets}<p class="mt-1">Log in with another wallet</p>
{:else}<p class="mt-1">Import your wallet</p>
{#if $has_wallets}
<p class="mt-1">
{$t("pages.wallet_login.with_another_wallet")}
</p>
{:else}
<p class="mt-1">
{$t("pages.wallet_login.import_wallet")}
</p>
{/if}
<Fileupload
style="display:none;"
@ -289,11 +348,11 @@
d="M9 8.25H7.5a2.25 2.25 0 00-2.25 2.25v9a2.25 2.25 0 002.25 2.25h9a2.25 2.25 0 002.25-2.25v-9a2.25 2.25 0 00-2.25-2.25H15M9 12l3 3m0 0l3-3m-3 3V2.25"
/>
</svg>
Import a Wallet File
{$t("pages.wallet_login.import_file")}
</button>
<Button
<a href="/wallet/scanqr" use:link>
<button
style="min-width: 250px;justify-content: left;"
disabled
class="disabled mt-1 text-primary-700 bg-primary-100 hover:bg-primary-100/90 focus:ring-4 focus:ring-primary-700/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center justify-center dark:focus:ring-primary-100/55 mb-2"
>
<svg
@ -316,11 +375,19 @@
d="M6.75 6.75h.75v.75h-.75v-.75zM6.75 16.5h.75v.75h-.75v-.75zM16.5 6.75h.75v.75h-.75v-.75zM13.5 13.5h.75v.75h-.75v-.75zM13.5 19.5h.75v.75h-.75v-.75zM19.5 13.5h.75v.75h-.75v-.75zM19.5 19.5h.75v.75h-.75v-.75zM16.5 16.5h.75v.75h-.75v-.75z"
/>
</svg>
Import with QRcode
</Button>
<Button
{$t("pages.wallet_login.import_qr")}
</button>
</a>
<button
on:click={async () => {
try {
wallet = await ng.wallet_import_from_code("AABAOAAAAHNb4y7hdWADqFWDgER3J0xvD3K5D9pZ1wd7Bja4c9cWAOFNpmUIZOFRro0UIpZWr5Ah8U7PlRFe1GFZSKuIextFAA8A45zZUJmUPhfdBrcho1vYPfgda0BAgIT1qjzgEkBQAA");
importing = true;
} catch (e) {
error = e;
}
}}
style="min-width: 250px;justify-content: left;"
disabled
class="mt-1 text-primary-700 bg-primary-100 hover:bg-primary-100/90 focus:ring-4 focus:ring-primary-700/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-100/55 mb-2"
>
<svg
@ -339,8 +406,8 @@
/>
</svg>
Enter a Wallet Link
</Button>
{$t("pages.wallet_login.import_link")}
</button>
<a href="/wallet/create" use:link>
<button
tabindex="-1"
@ -361,14 +428,14 @@
d="M19 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zM4 19.235v-.11a6.375 6.375 0 0112.75 0v.109A12.318 12.318 0 0110.374 21c-2.331 0-4.512-.645-6.374-1.766z"
/>
</svg>
Create a new wallet
{$t("pages.wallet_login.new_wallet")}
</button>
</a>
</div>
</div>
<!-- {:else if step == "security"}{:else if step == "qrcode"}{:else if step == "cloud"} -->
{:else if step == "loggedin"}
You are logged in.<br /> please wait while the app is loading...{/if}
{@html $t("pages.wallet_login.logged_in")}...{/if}
</CenteredLayout>
</div>

@ -7,37 +7,62 @@
// notice may not be copied, modified, or distributed except
// according to those terms.
import { writable, readable, readonly, derived, get } from "svelte/store";
import {
writable,
readable,
readonly,
derived,
get,
type Writable,
} from "svelte/store";
import { register, init, locale, format } from "svelte-i18n";
import ng from "./api";
import { official_classes } from "./classes";
import { official_apps, official_services } from "./zeras";
let all_branches = {};
// Make sure that a file named `locales/<lang>.json` exists when adding it here.
export const available_languages = {
"en": "English",
"de": "Deutsch",
"fr": "Français",
"ru": "Русский",
"es": "Español",
"it": "Italiano",
"zh": "中文",
"pt": "Português",
en: "English",
de: "Deutsch",
fr: "Français",
ru: "Русский",
es: "Español",
it: "Italiano",
zh: "中文",
pt: "Português",
};
export const current_lang = writable("en");
for (const lang of Object.keys(available_languages)) {
register(lang, () => import(`./locales/${lang}.json`))
}
init({
fallbackLocale: "en",
initialLocale: "en",
});
export const display_error = (error:string) => {
const parts = error.split(":");
let res = get(format)("errors."+parts[0]);
if (parts[1]) {
res += " "+get(format)("errors."+parts[1]);
}
return res;
}
export const select_default_lang = async() => {
export const select_default_lang = async () => {
let locales = await ng.locales();
for (let lo of locales) {
if (available_languages[lo]) {
// exact match (if locales is a 2 chars lang code, or if we support regionalized translations)
current_lang.set(lo);
locale.set(lo);
return;
}
lo = lo.substr(0,2);
lo = lo.substr(0, 2);
if (available_languages[lo]) {
current_lang.set(lo);
locale.set(lo);
return;
}
}
@ -58,7 +83,7 @@ export const load_app = async (appName: string) => {
};
export const invoke_service = async (serviceName: string, nuri:string, args: object) => {
export const invoke_service = async (serviceName: string, nuri: string, args: object) => {
if (serviceName.startsWith("n:g:z")) {
let service = official_services[serviceName];
@ -77,7 +102,7 @@ export const invoke_service = async (serviceName: string, nuri:string, args: obj
export const cur_tab = writable({
cur_store: {
has_outer : {
has_outer: {
nuri_trail: ":v:l"
},
type: "public", // "protected", "private", "group", "dialog",
@ -117,7 +142,7 @@ export const cur_tab = writable({
authors: "",
icon: "",
description: "",
stream : {
stream: {
notif: 1,
last: "",
},
@ -132,6 +157,8 @@ export const cur_tab = writable({
});
export const wallet_import_qrcode = writable("");
export const opened_wallets = writable({});
/// { wallet:, id: }
@ -139,45 +166,76 @@ export const active_wallet = writable(undefined);
export const wallets = writable({});
export const connections = writable({});
export const connections: Writable<Record<string, any>> = writable({});
export const active_session = writable(undefined);
let next_reconnect = null;
export const connection_status: Writable<"disconnected" | "connected" | "connecting"> = writable("disconnected");
export const online = derived(connections,($connections) => {
for (const cnx of Object.keys($connections)) {
if (!$connections[cnx].error) return true;
else if ($connections[cnx].error=="PeerAlreadyConnected") {
connections.update((c) => {
let next_reconnect: NodeJS.Timeout | null = null;
const updateConnectionStatus = ($connections: Record<string, any>) => {
// Reset error state for PeerAlreadyConnected errors.
Object.entries($connections).forEach(([cnx, connection]) => {
if (connection.error === "PeerAlreadyConnected") {
connections.update(c => {
c[cnx].error = undefined;
return c;
});
return true; }
else if ($connections[cnx].error=="ConnectionError" && !$connections[cnx].connecting && next_reconnect==null) {
}
});
// Check if any connection is active.
const is_connected = Object.values($connections).some(connection => !connection.error);
// Check if any connection is connecting.
const is_connecting = Object.values($connections).some(connection => connection.connecting);
// Check, if reconnect is needed.
const should_reconnect = !is_connecting && (next_reconnect === null) && Object.values($connections).some(
connection => connection.error === "ConnectionError"
);
if (should_reconnect) {
console.log("will try reconnect in 20 sec");
next_reconnect = setTimeout(async ()=> {
next_reconnect = setTimeout(async () => {
await reconnect();
},20000);
next_reconnect = null;
}, 20000);
}
if (is_connected) {
connection_status.set("connected");
} else if (is_connecting) {
connection_status.set("connecting");
} else {
connection_status.set("disconnected");
}
return false;
};
connections.subscribe(($connections) => {
updateConnectionStatus($connections);
});
export const online = derived(connection_status, ($connectionStatus) => $connectionStatus == "connected");
export const cannot_load_offline = writable(false);
if (!get(online) && !import.meta.env.TAURI_PLATFORM) {
if (get(connection_status) == "disconnected" && !import.meta.env.TAURI_PLATFORM) {
cannot_load_offline.set(true);
let unsubscribe = online.subscribe(async (value) => {
if (value) {
let unsubscribe = connection_status.subscribe(async (value) => {
if (value != "disconnected") {
cannot_load_offline.set(false);
if (value == "connected") {
unsubscribe();
}
});
} else {
cannot_load_offline.set(true);
}
});
}
export const has_wallets = derived(wallets,($wallets) => Object.keys($wallets).length);
export const has_wallets = derived(wallets, ($wallets) => Object.keys($wallets).length);
@ -223,8 +281,7 @@ export const close_active_session = async function() {
const can_connect = derived([active_wallet, active_session], ([$s1, $s2]) => [
$s1,
$s2,
]
);
]);
export const reconnect = async function() {
if (next_reconnect) {
@ -235,6 +292,7 @@ export const reconnect = async function() {
return;
}
console.log("attempting to connect...");
if (!get(online)) connection_status.set("connecting");
try {
let info = await ng.client_info()
//console.log("Connecting with",get(active_session).user);
@ -243,9 +301,7 @@ export const reconnect = async function() {
get(active_session).user,
location.href
));
}catch (e) {
} catch (e) {
console.error(e)
}
}
@ -280,7 +336,6 @@ export const disconnections_subscribe = async function() {
readable(false, function start(set) {
return function stop() {
disconnections_unsub();
};
@ -291,7 +346,7 @@ can_connect.subscribe(async (value) => {
await reconnect();
}
});
});
export const branch_subs = function(nuri) {
// console.log("branch_commits")
@ -337,12 +392,11 @@ export const branch_subs = function(nuri) {
}
},
subscribe: (run, invalid) => {
let already_subscribed = all_branches[nuri];
if (!already_subscribed) {
const { subscribe, set, update } = writable([]); // create the underlying writable store
let count = 0;
let unsub = () => {};
let unsub = () => { };
already_subscribed = {
load: async () => {
try {
@ -353,33 +407,32 @@ export const branch_subs = function(nuri) {
return;
}
unsub();
unsub = () => {};
unsub = () => { };
set([]);
let req= await ng.doc_fetch_repo_subscribe(nuri);
let req = await ng.doc_fetch_repo_subscribe(nuri);
req.V0.session_id = session.session_id;
unsub = await ng.app_request_stream(req,
async (commit) => {
//console.log("GOT APP RESPONSE", commit);
if (commit.V0.State) {
for (const file of commit.V0.State.files) {
update( (old) => {old.unshift(file); return old;} )
update((old) => { old.unshift(file); return old; })
}
} else if (commit.V0.Patch.other?.FileAdd) {
update( (old) => {old.unshift(commit.V0.Patch.other.FileAdd); return old;} )
update((old) => { old.unshift(commit.V0.Patch.other.FileAdd); return old; })
}
});
}
catch (e) {
console.error(e);
}
// this is in case decrease has been called before the load function returned.
if (count == 0) {unsub();}
if (count == 0) { unsub(); }
},
increase: () => {
count += 1;
//console.log("increase sub to",count);
return readonly({subscribe});
return readonly({ subscribe });
},
decrease: () => {
count -= 1;
@ -392,7 +445,7 @@ export const branch_subs = function(nuri) {
},
unsubscribe: () => {
unsub();
console.log("unsubscribed ",nuri);
console.log("unsubscribed ", nuri);
delete all_branches[nuri];
}
}
@ -411,4 +464,56 @@ export const branch_subs = function(nuri) {
}
};
let blob_cache = {};
export async function get_blob(ref: { nuri: string; reference: { key: any; id: any; }; }) {
if (!ref) return false;
const cached = blob_cache[ref.nuri];
if (cached) {
return cached;
}
let prom = new Promise(async (resolve) => {
try {
let nuri = {
target: "PrivateStore",
entire_store: false,
access: [{ Key: ref.reference.key }],
locator: [],
object: ref.reference.id,
};
let file_request = {
V0: {
command: "FileGet",
nuri,
session_id: get(active_session).session_id,
},
};
let final_blob;
let content_type;
let unsub = await ng.app_request_stream(file_request, async (blob) => {
//console.log("GOT APP RESPONSE", blob);
if (blob.V0.FileMeta) {
content_type = blob.V0.FileMeta.content_type;
final_blob = new Blob([], { type: content_type });
} else if (blob.V0.FileBinary) {
if (blob.V0.FileBinary.byteLength > 0) {
final_blob = new Blob([final_blob, blob.V0.FileBinary], {
type: content_type,
});
}
} else if (blob.V0 == "EndOfStream") {
var imageUrl = URL.createObjectURL(final_blob);
resolve(imageUrl);
}
});
} catch (e) {
console.error(e);
resolve(false);
}
});
blob_cache[ref.nuri] = prom;
return prom;
}
//export default branch_commits;

@ -16,6 +16,34 @@
padding-bottom: 1em;
}
@keyframes pulse-logo-color {
0%,
100% {
fill: rgb(73, 114, 165);
stroke: rgb(73, 114, 165);
}
50% {
/* Mid-transition color */
stroke: #bbb;
fill: #bbb;
}
}
.logo-pulse path {
animation: pulse-logo-color 2s infinite;
animation-timing-function: cubic-bezier(0.65, 0.01, 0.59, 0.83);
}
.logo-gray path {
fill: #bbb;
stroke: #bbb;
}
.logo-blue path {
fill: rgb(73, 114, 165);
stroke: rgb(73, 114, 165);
}
.container3 {
margin: 0;
min-width: 280px;

@ -7,6 +7,13 @@
// notice may not be copied, modified, or distributed except
// according to those terms.
export type Emoji = {
hexcode: string;
shortcode: string;
code: string;
svg?: any;
};
let face = [
{
hexcode: "1f600",
@ -81,16 +88,16 @@ let face = [
{
hexcode: "1f9d0",
shortcode: "face_with_monocle",
code: "monocole",
code: "monocle",
},
{
hexcode: "1f634",
shortcode: "sleeping_face",
code: "sleeping",
},
];
];
let face_unwell = [
let face_unwell = [
{
hexcode: "1f637",
shortcode: "face_with_medical_mask",
@ -171,9 +178,9 @@ let face = [
shortcode: "face_with_steam_from_nose",
code: "annoyed",
},
];
];
let face_costume = [
let face_costume = [
{
hexcode: "1f921",
shortcode: "clown_face",
@ -254,9 +261,9 @@ let face = [
shortcode: "fairy",
code: "fairy",
},
];
];
let emotion = [
let emotion = [
{
hexcode: "1f48c",
shortcode: "love_letter",
@ -337,9 +344,9 @@ let face = [
shortcode: "folded_hands",
code: "praying",
},
];
];
let body = [
let body = [
{
hexcode: "1f4aa",
shortcode: "flexed_biceps",
@ -420,9 +427,9 @@ let face = [
shortcode: "running_shoe",
code: "shoe",
},
];
];
let sport = [
let sport = [
{
hexcode: "1f93a",
shortcode: "person_fencing",
@ -503,9 +510,9 @@ let face = [
shortcode: "martial_arts_uniform",
code: "martial",
},
];
];
let bigger_animal = [
let bigger_animal = [
{
hexcode: "1f981",
shortcode: "lion",
@ -582,9 +589,9 @@ let face = [
shortcode: "polar_bear",
code: "bear",
},
];
];
let smaller_animal = [
let smaller_animal = [
{
hexcode: "1f413",
shortcode: "rooster",
@ -661,9 +668,9 @@ let face = [
shortcode: "octopus",
code: "octopus",
},
];
];
let plants = [
let plants = [
{
hexcode: "1f40c",
shortcode: "snail",
@ -743,9 +750,9 @@ let face = [
shortcode: "mushroom",
code: "mushroom",
},
];
];
let fruits = [
let fruits = [
{
hexcode: "1f347",
shortcode: "grapes",
@ -826,9 +833,9 @@ let face = [
shortcode: "hot_pepper",
code: "pepper",
},
];
];
let food = [
let food = [
{
hexcode: "1f950",
shortcode: "croissant",
@ -909,9 +916,9 @@ let face = [
shortcode: "clinking_glasses",
code: "two_glasses",
},
];
];
let travel = [
let travel = [
{
hexcode: "1f3d4",
shortcode: "snow_capped_mountain",
@ -992,9 +999,9 @@ let face = [
shortcode: "rocket",
code: "rocket",
},
];
];
let sky = [
let sky = [
{
hexcode: "2600",
shortcode: "sun",
@ -1075,9 +1082,9 @@ let face = [
shortcode: "rainbow",
code: "rainbow",
},
];
];
let play = [
let play = [
{
hexcode: "1f3b8",
shortcode: "guitar",
@ -1158,9 +1165,9 @@ let face = [
shortcode: "diving_mask",
code: "scuba_diving",
},
];
];
let house = [
let house = [
{
hexcode: "1f9f9",
shortcode: "broom",
@ -1241,10 +1248,16 @@ let face = [
shortcode: "shopping_cart",
code: "shopping_cart",
},
];
];
let svgs_loaded = Promise.resolve(false);
/** Loads SVGs if they are not yet. TODO: This could probably be sped up by not awaiting sequentially. */
export async function load_svg() {
if (await svgs_loaded) {
return;
}
svgs_loaded = new Promise(async (resolve) => {
face[0].svg = await import("./assets/pazzle/emoji_u1f600.svg?component");
face[1].svg = await import("./assets/pazzle/emoji_u1f602.svg?component");
face[2].svg = await import("./assets/pazzle/emoji_u1f607.svg?component");
@ -1371,48 +1384,28 @@ export async function load_svg() {
/************** EMOTION *********************/
emotion[0].svg = await import(
"./assets/pazzle/emoji_u1f48c.svg?component"
);
emotion[0].svg = await import("./assets/pazzle/emoji_u1f48c.svg?component");
emotion[1].svg = await import("./assets/pazzle/emoji_u2764.svg?component");
emotion[2].svg = await import(
"./assets/pazzle/emoji_u1f495.svg?component"
);
emotion[2].svg = await import("./assets/pazzle/emoji_u1f495.svg?component");
emotion[3].svg = await import(
"./assets/pazzle/emoji_u1f48b.svg?component"
);
emotion[4].svg = await import(
"./assets/pazzle/emoji_u1f4af.svg?component"
);
emotion[5].svg = await import(
"./assets/pazzle/emoji_u1f4a5.svg?component"
);
emotion[3].svg = await import("./assets/pazzle/emoji_u1f48b.svg?component");
emotion[4].svg = await import("./assets/pazzle/emoji_u1f4af.svg?component");
emotion[5].svg = await import("./assets/pazzle/emoji_u1f4a5.svg?component");
emotion[6].svg = await import(
"./assets/pazzle/emoji_u1f4a6.svg?component"
);
emotion[7].svg = await import(
"./assets/pazzle/emoji_u1f91d.svg?component"
);
emotion[8].svg = await import(
"./assets/pazzle/emoji_u1f590.svg?component"
);
emotion[6].svg = await import("./assets/pazzle/emoji_u1f4a6.svg?component");
emotion[7].svg = await import("./assets/pazzle/emoji_u1f91d.svg?component");
emotion[8].svg = await import("./assets/pazzle/emoji_u1f590.svg?component");
emotion[9].svg = await import("./assets/pazzle/emoji_u270c.svg?component");
emotion[10].svg = await import(
"./assets/pazzle/emoji_u1f44d.svg?component"
);
emotion[11].svg = await import(
"./assets/pazzle/emoji_u270a.svg?component"
);
emotion[11].svg = await import("./assets/pazzle/emoji_u270a.svg?component");
emotion[12].svg = await import(
"./assets/pazzle/emoji_u1f450.svg?component"
);
emotion[13].svg = await import(
"./assets/pazzle/emoji_u270d.svg?component"
);
emotion[13].svg = await import("./assets/pazzle/emoji_u270d.svg?component");
emotion[14].svg = await import(
"./assets/pazzle/emoji_u1f64f.svg?component"
);
@ -1463,47 +1456,91 @@ export async function load_svg() {
/************** BIGGER ANIMAL *********************/
bigger_animal[0].svg = await import("./assets/pazzle/emoji_u1f981.svg?component");
bigger_animal[1].svg = await import("./assets/pazzle/emoji_u1f406.svg?component");
bigger_animal[2].svg = await import("./assets/pazzle/emoji_u1f434.svg?component");
bigger_animal[0].svg = await import(
"./assets/pazzle/emoji_u1f981.svg?component"
);
bigger_animal[1].svg = await import(
"./assets/pazzle/emoji_u1f406.svg?component"
);
bigger_animal[2].svg = await import(
"./assets/pazzle/emoji_u1f434.svg?component"
);
bigger_animal[3].svg = await import("./assets/pazzle/emoji_u1f993.svg?component");
bigger_animal[4].svg = await import("./assets/pazzle/emoji_u1f416.svg?component");
bigger_animal[5].svg = await import("./assets/pazzle/emoji_u1f410.svg?component");
bigger_animal[3].svg = await import(
"./assets/pazzle/emoji_u1f993.svg?component"
);
bigger_animal[4].svg = await import(
"./assets/pazzle/emoji_u1f416.svg?component"
);
bigger_animal[5].svg = await import(
"./assets/pazzle/emoji_u1f410.svg?component"
);
bigger_animal[6].svg = await import("./assets/pazzle/emoji_u1f411.svg?component");
bigger_animal[7].svg = await import("./assets/pazzle/emoji_u1f42a.svg?component");
bigger_animal[8].svg = await import("./assets/pazzle/emoji_u1f992.svg?component");
bigger_animal[6].svg = await import(
"./assets/pazzle/emoji_u1f411.svg?component"
);
bigger_animal[7].svg = await import(
"./assets/pazzle/emoji_u1f42a.svg?component"
);
bigger_animal[8].svg = await import(
"./assets/pazzle/emoji_u1f992.svg?component"
);
bigger_animal[9].svg = await import("./assets/pazzle/emoji_u1f418.svg?component");
bigger_animal[9].svg = await import(
"./assets/pazzle/emoji_u1f418.svg?component"
);
bigger_animal[10].svg = await import(
"./assets/pazzle/emoji_u1f98f.svg?component"
);
bigger_animal[11].svg = await import("./assets/pazzle/emoji_u1f9a9.svg?component");
bigger_animal[11].svg = await import(
"./assets/pazzle/emoji_u1f9a9.svg?component"
);
bigger_animal[12].svg = await import("./assets/pazzle/emoji_u1f433.svg?component");
bigger_animal[13].svg = await import("./assets/pazzle/emoji_u1f42c.svg?component");
bigger_animal[12].svg = await import(
"./assets/pazzle/emoji_u1f433.svg?component"
);
bigger_animal[13].svg = await import(
"./assets/pazzle/emoji_u1f42c.svg?component"
);
bigger_animal[14].svg = await import(
"./assets/pazzle/emoji_u1f43b_200d_2744.svg?component"
);
/************** SMALLER ANIMAL *********************/
smaller_animal[0].svg = await import("./assets/pazzle/emoji_u1f413.svg?component");
smaller_animal[1].svg = await import("./assets/pazzle/emoji_u1f423.svg?component");
smaller_animal[2].svg = await import("./assets/pazzle/emoji_u1f985.svg?component");
smaller_animal[0].svg = await import(
"./assets/pazzle/emoji_u1f413.svg?component"
);
smaller_animal[1].svg = await import(
"./assets/pazzle/emoji_u1f423.svg?component"
);
smaller_animal[2].svg = await import(
"./assets/pazzle/emoji_u1f985.svg?component"
);
smaller_animal[3].svg = await import("./assets/pazzle/emoji_u1f986.svg?component");
smaller_animal[4].svg = await import("./assets/pazzle/emoji_u1f989.svg?component");
smaller_animal[3].svg = await import(
"./assets/pazzle/emoji_u1f986.svg?component"
);
smaller_animal[4].svg = await import(
"./assets/pazzle/emoji_u1f989.svg?component"
);
smaller_animal[5].svg = await import(
"./assets/pazzle/emoji_u1f407.svg?component"
);
smaller_animal[6].svg = await import("./assets/pazzle/emoji_u1f427.svg?component");
smaller_animal[7].svg = await import("./assets/pazzle/emoji_u1f98e.svg?component");
smaller_animal[8].svg = await import("./assets/pazzle/emoji_u1f422.svg?component");
smaller_animal[6].svg = await import(
"./assets/pazzle/emoji_u1f427.svg?component"
);
smaller_animal[7].svg = await import(
"./assets/pazzle/emoji_u1f98e.svg?component"
);
smaller_animal[8].svg = await import(
"./assets/pazzle/emoji_u1f422.svg?component"
);
smaller_animal[9].svg = await import("./assets/pazzle/emoji_u1f40d.svg?component");
smaller_animal[9].svg = await import(
"./assets/pazzle/emoji_u1f40d.svg?component"
);
smaller_animal[10].svg = await import(
"./assets/pazzle/emoji_u1f994.svg?component"
);
@ -1511,9 +1548,15 @@ export async function load_svg() {
"./assets/pazzle/emoji_u1f987.svg?component"
);
smaller_animal[12].svg = await import("./assets/pazzle/emoji_u1f41f.svg?component");
smaller_animal[13].svg = await import("./assets/pazzle/emoji_u1f41a.svg?component");
smaller_animal[14].svg = await import("./assets/pazzle/emoji_u1f419.svg?component");
smaller_animal[12].svg = await import(
"./assets/pazzle/emoji_u1f41f.svg?component"
);
smaller_animal[13].svg = await import(
"./assets/pazzle/emoji_u1f41a.svg?component"
);
smaller_animal[14].svg = await import(
"./assets/pazzle/emoji_u1f419.svg?component"
);
/************** PLANTS *********************/
@ -1552,22 +1595,12 @@ export async function load_svg() {
fruits[8].svg = await import("./assets/pazzle/emoji_u1fad0.svg?component");
fruits[9].svg = await import("./assets/pazzle/emoji_u1f95d.svg?component");
fruits[10].svg = await import(
"./assets/pazzle/emoji_u1f951.svg?component"
);
fruits[11].svg = await import(
"./assets/pazzle/emoji_u1f346.svg?component"
);
fruits[10].svg = await import("./assets/pazzle/emoji_u1f951.svg?component");
fruits[11].svg = await import("./assets/pazzle/emoji_u1f346.svg?component");
fruits[12].svg = await import(
"./assets/pazzle/emoji_u1f955.svg?component"
);
fruits[13].svg = await import(
"./assets/pazzle/emoji_u1f33d.svg?component"
);
fruits[14].svg = await import(
"./assets/pazzle/emoji_u1f336.svg?component"
);
fruits[12].svg = await import("./assets/pazzle/emoji_u1f955.svg?component");
fruits[13].svg = await import("./assets/pazzle/emoji_u1f33d.svg?component");
fruits[14].svg = await import("./assets/pazzle/emoji_u1f336.svg?component");
/************** FOOD *********************/
@ -1606,18 +1639,12 @@ export async function load_svg() {
travel[8].svg = await import("./assets/pazzle/emoji_u1f682.svg?component");
travel[9].svg = await import("./assets/pazzle/emoji_u1f695.svg?component");
travel[10].svg = await import(
"./assets/pazzle/emoji_u1f3cd.svg?component"
);
travel[10].svg = await import("./assets/pazzle/emoji_u1f3cd.svg?component");
travel[11].svg = await import("./assets/pazzle/emoji_u26f5.svg?component");
travel[12].svg = await import("./assets/pazzle/emoji_u2708.svg?component");
travel[13].svg = await import(
"./assets/pazzle/emoji_u1f681.svg?component"
);
travel[14].svg = await import(
"./assets/pazzle/emoji_u1f680.svg?component"
);
travel[13].svg = await import("./assets/pazzle/emoji_u1f681.svg?component");
travel[14].svg = await import("./assets/pazzle/emoji_u1f680.svg?component");
/************** SKY *********************/
@ -1685,9 +1712,11 @@ export async function load_svg() {
house[13].svg = await import("./assets/pazzle/emoji_u1f9fd.svg?component");
house[14].svg = await import("./assets/pazzle/emoji_u1f6d2.svg?component");
resolve(true);
});
}
export const emojis = {
export const emojis: Record<string, Emoji[]> = {
face,
face_unwell,
face_costume,
@ -1703,7 +1732,7 @@ export const emojis = {
sky,
play,
house,
};
};
export const emoji_cat = [
"face",
@ -1725,12 +1754,81 @@ export const emoji_cat = [
"emotion",
];
export function display_pazzle(pazzle) {
export function display_pazzle(pazzle: number[]) {
let res = [];
for (const emoji of pazzle) {
let cat = (emoji & 240) >> 4;
let idx = emoji & 15;
res.push(emoji_cat[cat] +": "+ emojis[emoji_cat[cat]][idx].code);
res.push(emoji_cat[cat] + ": " + emojis[emoji_cat[cat]][idx].code);
}
return res;
}
export function emojis_from_pazzle_ids(pazzle: number[]) {
return pazzle.map((emoji_id) => {
const cat_id = (emoji_id & 240) >> 4;
const cat_name = emoji_cat[cat_id];
let idx = emoji_id & 15;
return { cat: cat_name, ...emojis[cat_name][idx] };
});
}
/*
// To find more unicode (emoji) translations files (xx_annotations.json),
// visit: https://github.com/unicode-org/cldr-json/tree/main/cldr-json/cldr-annotations-full/annotations
// Some of the face-emoji names have been slightly adjusted for unambiguity.
// See the English translations.
import en_annotations from "./locales/en_annotations.json";
import de_annotations from "./locales/de_annotations.json";
import fr_annotations from "./locales/fr_annotations.json";
import ru_annotations from "./locales/ru_annotations.json";
import es_annotations from "./locales/es_annotations.json";
import it_annotations from "./locales/it_annotations.json";
import zh_annotations from "./locales/zh_annotations.json";
import pt_annotations from "./locales/pt_annotations.json";
function i18n_for_emoji(annotations, emoji: Emoji) {
const char = String.fromCodePoint(Number.parseInt(emoji.hexcode, 16));
return annotations.annotations.annotations[char]?.tts[0];
}
function match_codes(annotations, emojis: Record<string, Emoji[]>) {
const not_found = [];
const map: Record<string, string> = {};
for (const cat in emojis) {
for (const emoji of emojis[cat]) {
const name = i18n_for_emoji(annotations, emoji);
if (!name) {
not_found.push(emoji);
} else {
map[emoji.code] = name;
}
}
}
if (not_found.length > 0) {
console.log("Not found: ", not_found);
}
return map;
}
export function to_debug() {
const en = match_codes(en_annotations, emojis);
const de = match_codes(de_annotations, emojis);
const fr = match_codes(fr_annotations, emojis);
const ru = match_codes(ru_annotations, emojis);
const es = match_codes(es_annotations, emojis);
const it = match_codes(it_annotations, emojis);
const zh = match_codes(zh_annotations, emojis);
const pt = match_codes(pt_annotations, emojis);
console.debug("en", JSON.stringify(en));
console.debug("de", JSON.stringify(de));
console.debug("fr", JSON.stringify(fr));
console.debug("ru", JSON.stringify(ru));
console.debug("es", JSON.stringify(es));
console.debug("it", JSON.stringify(it));
console.debug("zh", JSON.stringify(zh));
console.debug("pt", JSON.stringify(pt));
}
*/

@ -7,11 +7,20 @@ onmessage = (e) => {
//console.log("Message received by worker", e.data);
(async function() {
try {
let secret_wallet = await ng.wallet_open_with_pazzle(
let secret_wallet;
if (e.data.pazzle) {
secret_wallet = await ng.wallet_open_with_pazzle(
e.data.wallet,
e.data.pazzle,
e.data.pin_code
);
} else if (e.data.mnemonic_words) {
secret_wallet = await ng.wallet_open_with_mnemonic_words(
e.data.wallet,
e.data.mnemonic_words,
e.data.pin_code
);
}
postMessage({success:secret_wallet});
} catch (e) {
postMessage({error:e});

@ -16,7 +16,15 @@ const config = {
'xxs': '400px',
'xs': '500px',
...defaultTheme.screens,
'tall': { 'raw': '(min-height: 450px)' },
'tall-xxs': { 'raw': '(min-height: 360px)' },
'tall-xs': { 'raw': '(min-height: 480px)' },
'tall-sm': { 'raw': '(min-height: 640px)' },
'tall-md': { 'raw': '(min-height: 800px)' },
'tall-l': { 'raw': '(min-height: 1000px)' },
'tall-xl': { 'raw': '(min-height: 1200px)' },
'tall-xxl': { 'raw': '(min-height: 1400px)' },
},
},

@ -12,14 +12,15 @@
//! Implementation of the Server Broker
use std::{
collections::{HashMap, HashSet},
collections::{BTreeMap, HashMap, HashSet},
path::PathBuf,
sync::Arc,
time::{Duration, SystemTime},
};
use async_std::sync::{Mutex, RwLock};
use either::Either;
use futures::StreamExt;
use futures::{channel::mpsc, SinkExt, StreamExt};
use serde::{Deserialize, Serialize};
use ng_repo::{
@ -35,6 +36,7 @@ use ng_net::{
connection::NoiseFSM,
server_broker::IServerBroker,
types::*,
utils::{spawn_and_log_error, Receiver, ResultSend, Sender},
};
use ng_verifier::{
@ -147,6 +149,10 @@ pub struct ServerBrokerState {
verifiers: HashMap<UserId, Arc<RwLock<DetachableVerifier>>>,
remote_apps: HashMap<(DirectPeerId, u64), UserId>,
wallet_rendezvous: HashMap<SymKey, Sender<ExportedWallet>>,
wallet_exports: HashMap<SymKey, ExportedWallet>,
wallet_exports_timestamp: BTreeMap<SystemTime, SymKey>,
}
pub struct ServerBroker {
@ -167,6 +173,9 @@ impl ServerBroker {
local_subscriptions: HashMap::new(),
verifiers: HashMap::new(),
remote_apps: HashMap::new(),
wallet_rendezvous: HashMap::new(),
wallet_exports: HashMap::new(),
wallet_exports_timestamp: BTreeMap::new(),
}),
path_users,
@ -273,10 +282,98 @@ impl ServerBroker {
}
}
use async_std::future::timeout;
async fn wait_for_wallet(
mut internal_receiver: Receiver<ExportedWallet>,
mut sender: Sender<Result<ExportedWallet, ServerError>>,
rendezvous: SymKey,
) -> ResultSend<()> {
let wallet_future = internal_receiver.next();
let _ = sender
.send(
match timeout(Duration::from_millis(5 * 60_000), wallet_future).await {
Err(_) => Err(ServerError::ExportWalletTimeOut),
Ok(Some(w)) => Ok(w),
Ok(None) => Err(ServerError::BrokerError),
},
)
.await;
BROKER
.read()
.await
.get_server_broker()?
.read()
.await
.remove_rendezvous(&rendezvous)
.await;
Ok(())
}
//TODO: the purpose of this trait is to have a level of indirection so we can keep some data in memory (cache) and avoid hitting the storage backend (rocksdb) at every call.
//for now this cache is not implemented, but the structs are ready (see above), and it would just require to change slightly the implementation of the trait functions here below.
#[async_trait::async_trait]
impl IServerBroker for ServerBroker {
async fn remove_rendezvous(&self, rendezvous: &SymKey) {
let mut lock = self.state.write().await;
let _ = lock.wallet_rendezvous.remove(&rendezvous);
}
async fn wait_for_wallet_at_rendezvous(
&self,
rendezvous: SymKey,
) -> Receiver<Result<ExportedWallet, ServerError>> {
let (internal_sender, internal_receiver) = mpsc::unbounded();
let (mut sender, receiver) = mpsc::unbounded();
{
let mut state = self.state.write().await;
if state.wallet_rendezvous.contains_key(&rendezvous) {
let _ = sender.send(Err(ServerError::BrokerError));
sender.close_channel();
return receiver;
} else {
let _ = state
.wallet_rendezvous
.insert(rendezvous.clone(), internal_sender);
}
}
spawn_and_log_error(wait_for_wallet(internal_receiver, sender, rendezvous));
receiver
}
async fn get_wallet_export(&self, rendezvous: SymKey) -> Result<ExportedWallet, ServerError> {
let mut state = self.state.write().await;
match state.wallet_exports.remove(&rendezvous) {
Some(wallet) => Ok(wallet),
None => Err(ServerError::NotFound),
}
}
async fn put_wallet_export(&self, rendezvous: SymKey, export: ExportedWallet) {
let mut state = self.state.write().await;
let _ = state.wallet_exports.insert(rendezvous.clone(), export);
let _ = state
.wallet_exports_timestamp
.insert(SystemTime::now(), rendezvous);
}
// TODO: periodically (every 5 min) remove entries in wallet_exports_timestamp and wallet_exports
async fn put_wallet_at_rendezvous(
&self,
rendezvous: SymKey,
export: ExportedWallet,
) -> Result<(), ServerError> {
let mut state = self.state.write().await;
match state.wallet_rendezvous.remove(&rendezvous) {
None => Err(ServerError::NotFound),
Some(mut sender) => {
let _ = sender.send(export).await;
Ok(())
}
}
}
fn get_block_storage(
&self,
) -> std::sync::Arc<std::sync::RwLock<dyn BlockStorage + Send + Sync>> {

@ -58,6 +58,7 @@ pub(crate) struct Actor<
//initiator: bool,
}
#[derive(Debug)]
pub enum SoS<B> {
Single(B),
Stream(Receiver<B>),
@ -185,6 +186,10 @@ impl<
}
fsm.lock().await.remove_actor(self.id).await;
let server_error: Result<ServerError, NgError> = (&msg).try_into();
//log_debug!("server_error {:?}", server_error);
if server_error.is_ok() {
return Err(NgError::ServerError(server_error.unwrap()));
}
let response: B = match msg.try_into() {
Ok(b) => b,
Err(ProtocolError::ServerError) => {

@ -15,3 +15,5 @@ pub mod blocks_put;
pub mod blocks_exist;
pub mod blocks_get;
pub mod wallet_put_export;

@ -0,0 +1,89 @@
/*
* Copyright (c) 2022-2024 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_std::sync::Mutex;
use ng_repo::errors::*;
use ng_repo::log::*;
use ng_repo::types::OverlayId;
use crate::broker::BROKER;
use crate::connection::NoiseFSM;
use crate::types::*;
use crate::{actor::*, types::ProtocolMessage};
impl WalletPutExport {
pub fn get_actor(&self, id: i64) -> Box<dyn EActor> {
Actor::<WalletPutExport, ()>::new_responder(id)
}
}
impl TryFrom<ProtocolMessage> for WalletPutExport {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
let req: ClientRequestContentV0 = msg.try_into()?;
if let ClientRequestContentV0::WalletPutExport(a) = req {
Ok(a)
} else {
log_debug!("INVALID {:?}", req);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<WalletPutExport> for ProtocolMessage {
fn from(msg: WalletPutExport) -> ProtocolMessage {
ProtocolMessage::from_client_request_v0(
ClientRequestContentV0::WalletPutExport(msg),
OverlayId::nil(),
)
}
}
impl Actor<'_, WalletPutExport, ()> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, WalletPutExport, ()> {
async fn respond(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
let req = WalletPutExport::try_from(msg)?;
let sb = { BROKER.read().await.get_server_broker()? };
let mut res: Result<(), ServerError> = Ok(());
match req {
WalletPutExport::V0(v0) => {
if v0.is_rendezvous {
res = sb
.read()
.await
.put_wallet_at_rendezvous(v0.rendezvous_id, v0.wallet)
.await;
} else {
sb.read()
.await
.put_wallet_export(v0.rendezvous_id, v0.wallet)
.await;
}
}
}
fsm.lock()
.await
.send_in_reply_to(res.into(), self.id())
.await?;
Ok(())
}
}

@ -0,0 +1,2 @@
pub mod wallet_get_export;
pub use wallet_get_export::*;

@ -0,0 +1,109 @@
/*
* Copyright (c) 2022-2024 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_std::stream::StreamExt;
use async_std::sync::Mutex;
use ng_repo::errors::*;
use ng_repo::log::*;
use super::super::StartProtocol;
use crate::broker::BROKER;
use crate::connection::NoiseFSM;
use crate::types::*;
use crate::{actor::*, types::ProtocolMessage};
impl ExtWalletGetExportV0 {
pub fn get_actor(&self) -> Box<dyn EActor> {
Actor::<ExtWalletGetExportV0, ExportedWallet>::new_responder(0)
}
}
impl TryFrom<ProtocolMessage> for ExtWalletGetExportV0 {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
if let ProtocolMessage::Start(StartProtocol::Ext(ExtRequest::V0(ExtRequestV0 {
content: ExtRequestContentV0::WalletGetExport(a),
..
}))) = msg
{
Ok(a)
} else {
log_debug!("INVALID {:?}", msg);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<ExtWalletGetExportV0> for ProtocolMessage {
fn from(_msg: ExtWalletGetExportV0) -> ProtocolMessage {
unimplemented!();
}
}
impl From<ExtWalletGetExportV0> for ExtRequestContentV0 {
fn from(msg: ExtWalletGetExportV0) -> ExtRequestContentV0 {
ExtRequestContentV0::WalletGetExport(msg)
}
}
impl TryFrom<ProtocolMessage> for ExportedWallet {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<ExportedWallet, Self::Error> {
let content: ExtResponseContentV0 = msg.try_into()?;
if let ExtResponseContentV0::Wallet(res) = content {
Ok(res)
} else {
Err(ProtocolError::InvalidValue)
}
}
}
impl Actor<'_, ExtWalletGetExportV0, ExportedWallet> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, ExtWalletGetExportV0, ExportedWallet> {
async fn respond(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
let req = ExtWalletGetExportV0::try_from(msg)?;
let result = if req.is_rendezvous {
let mut receiver = {
let broker = BROKER.read().await;
let sb = broker.get_server_broker()?;
let lock = sb.read().await;
lock.wait_for_wallet_at_rendezvous(req.id).await
};
match receiver.next().await {
None => Err(ServerError::BrokerError),
Some(Err(e)) => Err(e),
Some(Ok(w)) => Ok(ExtResponseContentV0::Wallet(w)),
}
} else {
{
let broker = BROKER.read().await;
let sb = broker.get_server_broker()?;
let lock = sb.read().await;
lock.get_wallet_export(req.id).await
}
.map(|wallet| ExtResponseContentV0::Wallet(wallet))
};
let response: ExtResponseV0 = result.into();
fsm.lock().await.send(response.into()).await?;
Ok(())
}
}

@ -21,3 +21,5 @@ pub mod client;
pub mod admin;
pub mod app;
pub mod ext;

@ -23,7 +23,7 @@ use crate::actors::noise::Noise;
use crate::connection::NoiseFSM;
use crate::types::{
AdminRequest, ClientInfo, CoreBrokerConnect, CoreBrokerConnectResponse, CoreMessage,
CoreMessageV0, CoreResponse, CoreResponseContentV0, CoreResponseV0, ExtResponse,
CoreMessageV0, CoreResponse, CoreResponseContentV0, CoreResponseV0, ExtRequest,
};
use crate::{actor::*, types::ProtocolMessage};
@ -34,7 +34,7 @@ use crate::{actor::*, types::ProtocolMessage};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum StartProtocol {
Client(ClientHello),
Ext(ExtHello),
Ext(ExtRequest),
Core(CoreHello),
Admin(AdminRequest),
App(AppHello),
@ -136,28 +136,28 @@ impl EActor for Actor<'_, CoreBrokerConnect, CoreBrokerConnectResponse> {
}
}
/// External Hello (finalizes the Noise handshake and sends first ExtRequest)
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ExtHello {
// contains the 3rd Noise handshake message "s,se"
pub noise: Noise,
/// Noise encrypted payload (an ExtRequest)
#[serde(with = "serde_bytes")]
pub payload: Vec<u8>,
}
impl ExtHello {
pub fn get_actor(&self) -> Box<dyn EActor> {
Actor::<ExtHello, ExtResponse>::new_responder(0)
}
}
impl From<ExtHello> for ProtocolMessage {
fn from(msg: ExtHello) -> ProtocolMessage {
ProtocolMessage::Start(StartProtocol::Ext(msg))
}
}
// /// External Hello (finalizes the Noise handshake and sends first ExtRequest)
// #[derive(Clone, Debug, Serialize, Deserialize)]
// pub struct ExtHello {
// // contains the 3rd Noise handshake message "s,se"
// pub noise: Noise,
// /// Noise encrypted payload (an ExtRequest)
// #[serde(with = "serde_bytes")]
// pub payload: Vec<u8>,
// }
// impl ExtHello {
// pub fn get_actor(&self) -> Box<dyn EActor> {
// Actor::<ExtHello, ExtResponse>::new_responder(0)
// }
// }
// impl From<ExtHello> for ProtocolMessage {
// fn from(msg: ExtHello) -> ProtocolMessage {
// ProtocolMessage::Start(StartProtocol::Ext(msg))
// }
// }
/// Client Hello
#[derive(Clone, Debug, Serialize, Deserialize)]
@ -251,18 +251,18 @@ impl EActor for Actor<'_, ClientHello, ServerHello> {
}
}
impl Actor<'_, ExtHello, ExtResponse> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, ExtHello, ExtResponse> {
async fn respond(
&mut self,
_msg: ProtocolMessage,
_fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
Ok(())
}
}
// impl Actor<'_, ExtHello, ExtResponse> {}
// #[async_trait::async_trait]
// impl EActor for Actor<'_, ExtHello, ExtResponse> {
// async fn respond(
// &mut self,
// _msg: ProtocolMessage,
// _fsm: Arc<Mutex<NoiseFSM>>,
// ) -> Result<(), ProtocolError> {
// Ok(())
// }
// }
// ///////////// APP HELLO ///////////////

@ -894,6 +894,34 @@ impl Broker {
connection.admin::<A>().await
}
pub async fn ext<
A: Into<ProtocolMessage> + Into<ExtRequestContentV0> + std::fmt::Debug + Sync + Send + 'static,
B: TryFrom<ProtocolMessage, Error = ProtocolError> + std::fmt::Debug + Sync + Send + 'static,
>(
cnx: Box<dyn IConnect>,
peer_privk: PrivKey,
peer_pubk: PubKey,
remote_peer_id: DirectPeerId,
url: String,
request: A,
) -> Result<B, NgError> {
let config = StartConfig::Ext(ExtConfig {
url,
request: request.into(),
});
let remote_peer_id_dh = remote_peer_id.to_dh_from_ed();
let mut connection = cnx
.open(
config.get_url(),
peer_privk.clone(),
peer_pubk,
remote_peer_id_dh,
config.clone(),
)
.await?;
connection.ext::<A, B>().await
}
#[doc(hidden)]
pub fn connect_local(&mut self, peer_pubk: PubKey, user: UserId) -> Result<(), ProtocolError> {
if self.closing {

@ -112,7 +112,7 @@ pub enum FSMstate {
Noise3,
AdminRequest,
ExtRequest,
ExtResponse,
//ExtResponse,
ClientHello,
ServerHello,
ClientAuth,
@ -177,7 +177,10 @@ pub struct ClientConfig {
}
#[derive(Debug, Clone)]
pub struct ExtConfig {}
pub struct ExtConfig {
pub url: String,
pub request: ExtRequestContentV0,
}
#[derive(Debug, Clone)]
pub struct CoreConfig {
@ -215,10 +218,14 @@ pub enum StartConfig {
impl StartConfig {
pub fn get_url(&self) -> String {
match self {
Self::Client(config) => config.url.clone(),
Self::Admin(config) => format!("ws://{}:{}", config.addr.ip, config.addr.port),
Self::Core(config) => format!("ws://{}:{}", config.addr.ip, config.addr.port),
Self::App(config) => format!("ws://{}:{}", config.addr.ip, config.addr.port),
Self::Client(ClientConfig { url, .. }) | Self::Ext(ExtConfig { url, .. }) => {
url.clone()
}
Self::Admin(AdminConfig { addr, .. })
| Self::Core(CoreConfig { addr, .. })
| Self::App(AppConfig { addr, .. }) => {
format!("ws://{}:{}", addr.ip, addr.port)
}
_ => unimplemented!(),
}
}
@ -235,9 +242,9 @@ impl StartConfig {
_ => false,
}
}
pub fn is_admin(&self) -> bool {
pub fn is_oneshot(&self) -> bool {
match self {
StartConfig::Admin(_) => true,
StartConfig::Admin(_) | StartConfig::Ext(_) => true,
_ => false,
}
}
@ -635,7 +642,10 @@ impl NoiseFSM {
self.state = FSMstate::ClientHello;
}
StartConfig::Ext(_ext_config) => {
todo!();
let noise = Noise::V0(NoiseV0 { data: payload });
self.send(noise.into()).await?;
self.state = FSMstate::Noise3;
next_step = StepReply::ReEnter;
}
StartConfig::Core(_core_config) => {
todo!();
@ -774,8 +784,18 @@ impl NoiseFSM {
StartConfig::Client(_) => {
return Err(ProtocolError::InvalidState);
}
StartConfig::Ext(_ext_config) => {
todo!();
StartConfig::Ext(ext_config) => {
let ext_req = ExtRequestV0 {
content: ext_config.request.clone(),
id: 0,
overlay: None,
};
let protocol_start = StartProtocol::Ext(ExtRequest::V0(ext_req));
self.send(protocol_start.into()).await?;
self.state = FSMstate::ExtRequest;
return Ok(StepReply::NONE);
}
StartConfig::Core(_core_config) => {
todo!();
@ -807,8 +827,9 @@ impl NoiseFSM {
StartProtocol::Client(_) => {
return Err(ProtocolError::InvalidState);
}
StartProtocol::Ext(_ext_config) => {
todo!();
StartProtocol::Ext(_ext_req) => {
self.state = FSMstate::Closing;
return Ok(StepReply::Responder(msg_opt.unwrap()));
}
// StartProtocol::Core(core_config) => {
// todo!();
@ -852,8 +873,15 @@ impl NoiseFSM {
return Ok(StepReply::Response(msg));
}
}
FSMstate::ExtRequest => {}
FSMstate::ExtResponse => {}
FSMstate::ExtRequest => {
// CLIENT side receiving ExtResponse
if let Some(msg) = msg_opt {
if self.dir.is_server() || msg.type_id() != TypeId::of::<ExtResponse>() {
return Err(ProtocolError::InvalidState);
}
return Ok(StepReply::Response(msg));
}
}
FSMstate::ClientHello => {
if let Some(msg) = msg_opt.as_ref() {
if !self.dir.is_server() {
@ -1371,6 +1399,47 @@ impl ConnectionBase {
}
}
pub async fn ext<
A: Into<ProtocolMessage> + Into<ExtRequestContentV0> + std::fmt::Debug + Sync + Send + 'static,
B: TryFrom<ProtocolMessage, Error = ProtocolError> + std::fmt::Debug + Sync + Send + 'static,
>(
&mut self,
) -> Result<B, NgError> {
if !self.dir.is_server() {
let mut actor = Box::new(Actor::<A, B>::new(0, true));
self.actors.lock().await.insert(0, actor.get_receiver_tx());
let mut receiver = actor.detach_receiver();
match receiver.next().await {
Some(ConnectionCommand::Msg(msg)) => {
self.fsm
.as_ref()
.unwrap()
.lock()
.await
.remove_actor(0)
.await;
let server_error: Result<ServerError, NgError> = (&msg).try_into();
let response: B = match msg.try_into() {
Ok(b) => b,
Err(ProtocolError::ServerError) => {
return Err(NgError::ServerError(server_error?));
}
Err(e) => return Err(NgError::ProtocolError(e)),
};
self.close().await;
Ok(response)
}
Some(ConnectionCommand::ProtocolError(e)) => Err(e.into()),
Some(ConnectionCommand::Error(e)) => Err(ProtocolError::from(e).into()),
Some(ConnectionCommand::Close) => Err(ProtocolError::Closing.into()),
_ => Err(ProtocolError::ActorError.into()),
}
} else {
panic!("cannot call ext on a server-side connection");
}
}
pub async fn probe(&mut self) -> Result<Option<PubKey>, ProtocolError> {
if !self.dir.is_server() {
let config = StartConfig::Probe;
@ -1431,7 +1500,7 @@ impl ConnectionBase {
pub async fn start(&mut self, config: StartConfig) -> Result<(), ProtocolError> {
// BOOTSTRAP the protocol from client-side
if !self.dir.is_server() {
let is_admin = config.is_admin();
let is_oneshot = config.is_oneshot();
let res;
{
let mut fsm = self.fsm.as_ref().unwrap().lock().await;
@ -1442,7 +1511,7 @@ impl ConnectionBase {
self.send(ConnectionCommand::ProtocolError(err.clone()))
.await;
Err(err)
} else if !is_admin {
} else if !is_oneshot {
let mut actor = Box::new(Actor::<Connecting, ()>::new(0, true));
self.actors.lock().await.insert(0, actor.get_receiver_tx());

@ -24,9 +24,22 @@ use crate::app_protocol::{AppRequest, AppSessionStart, AppSessionStartResponse,
use crate::broker::ClientPeerId;
use crate::connection::NoiseFSM;
use crate::types::*;
use crate::utils::Receiver;
#[async_trait::async_trait]
pub trait IServerBroker: Send + Sync {
async fn remove_rendezvous(&self, rendezvous: &SymKey);
async fn put_wallet_export(&self, rendezvous: SymKey, export: ExportedWallet);
async fn get_wallet_export(&self, rendezvous: SymKey) -> Result<ExportedWallet, ServerError>;
async fn put_wallet_at_rendezvous(
&self,
rendezvous: SymKey,
export: ExportedWallet,
) -> Result<(), ServerError>;
async fn wait_for_wallet_at_rendezvous(
&self,
rendezvous: SymKey,
) -> Receiver<Result<ExportedWallet, ServerError>>;
fn get_path_users(&self) -> PathBuf;
fn get_block_storage(&self) -> Arc<std::sync::RwLock<dyn BlockStorage + Send + Sync>>;
fn put_block(&self, overlay_id: &OverlayId, block: Block) -> Result<(), ServerError>;

@ -3249,6 +3249,20 @@ impl CommitGet {
}
}
/// Request to store one or more blocks
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct WalletPutExportV0 {
pub wallet: ExportedWallet,
pub rendezvous_id: SymKey,
pub is_rendezvous: bool,
}
/// Request to store one or more blocks
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum WalletPutExport {
V0(WalletPutExportV0),
}
/// Request to store one or more blocks
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BlocksPutV0 {
@ -3414,6 +3428,8 @@ pub enum ClientRequestContentV0 {
// For InnerOverlay's only :
BlocksPut(BlocksPut),
PublishEvent(PublishEvent),
WalletPutExport(WalletPutExport),
}
impl ClientRequestContentV0 {
@ -3428,6 +3444,7 @@ impl ClientRequestContentV0 {
ClientRequestContentV0::BlocksPut(a) => a.set_overlay(overlay),
ClientRequestContentV0::BlocksExist(a) => a.set_overlay(overlay),
ClientRequestContentV0::BlocksGet(a) => a.set_overlay(overlay),
ClientRequestContentV0::WalletPutExport(_a) => {}
_ => unimplemented!(),
}
}
@ -3479,6 +3496,7 @@ impl ClientRequest {
ClientRequestContentV0::BlocksPut(r) => r.get_actor(self.id()),
ClientRequestContentV0::BlocksExist(r) => r.get_actor(self.id()),
ClientRequestContentV0::BlocksGet(r) => r.get_actor(self.id()),
ClientRequestContentV0::WalletPutExport(r) => r.get_actor(self.id()),
_ => unimplemented!(),
},
}
@ -3946,23 +3964,41 @@ pub enum ExtObjectGet {
V0(ExtObjectGetV0),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ExtWalletGetExportV0 {
pub id: SymKey,
pub is_rendezvous: bool,
}
/// Topic synchronization request
pub type ExtTopicSyncReq = TopicSyncReq;
/// Content of ExtRequestV0
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ExtRequestContentV0 {
WalletGetExport(ExtWalletGetExportV0),
ExtObjectGet(ExtObjectGet),
ExtTopicSyncReq(ExtTopicSyncReq),
// TODO inbox requests
// TODO subreq ?
}
impl ExtRequestContentV0 {
pub fn get_actor(&self) -> Box<dyn EActor> {
match self {
Self::WalletGetExport(a) => a.get_actor(),
_ => unimplemented!()
// Self::ExtObjectGet(a) => a.get_actor(),
// Self::ExtTopicSyncReq(a) => a.get_actor(),
}
}
}
/// External request with its request ID
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ExtRequestV0 {
/// outer overlayId
pub overlay: Digest,
pub overlay: Option<Digest>,
/// Request ID
pub id: i64,
@ -3993,16 +4029,48 @@ impl ExtRequest {
}
}
}
pub fn get_actor(&self) -> Box<dyn EActor> {
match self {
Self::V0(a) => a.content.get_actor(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ExportedWallet(pub serde_bytes::ByteBuf);
/// Content of ExtResponseV0
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ExtResponseContentV0 {
EmptyResponse,
Block(Block),
Wallet(ExportedWallet),
// TODO inbox related replies
// TODO event ?
}
impl TryFrom<ProtocolMessage> for ExtResponseContentV0 {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
if let ProtocolMessage::ExtResponse(ExtResponse::V0(ExtResponseV0 {
content,
result,
..
})) = msg
{
let err = ServerError::try_from(result).unwrap();
if !err.is_err() {
Ok(content)
} else {
Err(ProtocolError::ServerError)
}
} else {
log_debug!("INVALID {:?}", msg);
Err(ProtocolError::InvalidValue)
}
}
}
/// Response to an ExtRequest
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ExtResponseV0 {
@ -4013,7 +4081,7 @@ pub struct ExtResponseV0 {
pub result: u16,
/// Response content
pub content: Option<ExtResponseContentV0>,
pub content: ExtResponseContentV0,
}
/// Response to an ExtRequest
@ -4035,6 +4103,16 @@ impl ExtResponse {
}
}
}
pub fn result(&self) -> u16 {
match self {
Self::V0(o) => o.result,
}
}
pub fn content_v0(&self) -> ExtResponseContentV0 {
match self {
Self::V0(o) => o.content.clone(),
}
}
}
impl TryFrom<ProtocolMessage> for ExtResponse {
@ -4048,6 +4126,29 @@ impl TryFrom<ProtocolMessage> for ExtResponse {
}
}
impl From<Result<ExtResponseContentV0, ServerError>> for ExtResponseV0 {
fn from(res: Result<ExtResponseContentV0, ServerError>) -> ExtResponseV0 {
match res {
Err(e) => ExtResponseV0 {
id: 0,
result: e.into(),
content: ExtResponseContentV0::EmptyResponse,
},
Ok(content) => ExtResponseV0 {
id: 0,
result: 0,
content,
},
}
}
}
impl From<ExtResponseV0> for ProtocolMessage {
fn from(msg: ExtResponseV0) -> ProtocolMessage {
ProtocolMessage::ExtResponse(ExtResponse::V0(msg))
}
}
//
// PROTOCOL MESSAGES
//
@ -4149,6 +4250,12 @@ impl TryFrom<&ProtocolMessage> for ServerError {
return Ok(ServerError::try_from(res).unwrap());
}
}
if let ProtocolMessage::ExtResponse(ref bm) = msg {
let res = bm.result();
if res != 0 {
return Ok(ServerError::try_from(res).unwrap());
}
}
if let ProtocolMessage::AppMessage(ref bm) = msg {
let res = bm.result();
if res != 0 {
@ -4205,12 +4312,6 @@ impl ProtocolMessage {
match self {
ProtocolMessage::ClientMessage(s) => Some(s as &dyn IStreamable),
ProtocolMessage::AppMessage(s) => Some(s as &dyn IStreamable),
// ProtocolMessage::ServerHello(a) => a.get_actor(),
// ProtocolMessage::ClientAuth(a) => a.get_actor(),
// ProtocolMessage::AuthResult(a) => a.get_actor(),
// ProtocolMessage::ExtRequest(a) => a.get_actor(),
// ProtocolMessage::ExtResponse(a) => a.get_actor(),
// ProtocolMessage::BrokerMessage(a) => a.get_actor(),
_ => None,
}
}
@ -4224,8 +4325,8 @@ impl ProtocolMessage {
// ProtocolMessage::ServerHello(a) => a.get_actor(),
// ProtocolMessage::ClientAuth(a) => a.get_actor(),
// ProtocolMessage::AuthResult(a) => a.get_actor(),
// ProtocolMessage::ExtRequest(a) => a.get_actor(),
// ProtocolMessage::ExtResponse(a) => a.get_actor(),
//ProtocolMessage::ExtRequest(a) => a.get_actor(),
//ProtocolMessage::ExtResponse(a) => a.get_actor(),
// ProtocolMessage::BrokerMessage(a) => a.get_actor(),
_ => unimplemented!(),
}

@ -17,6 +17,7 @@ use num_enum::TryFromPrimitive;
pub use crate::commit::{CommitLoadError, CommitVerifyError};
use crate::file::FileError;
use crate::log::*;
use crate::object::Object;
use crate::types::BlockId;
@ -37,6 +38,7 @@ pub enum NgError {
InvalidArgument,
PermissionDenied,
InvalidPazzle,
InvalidMnemonic,
CommitLoadError(CommitLoadError),
ObjectParseError(ObjectParseError),
StorageError(StorageError),
@ -82,6 +84,9 @@ pub enum NgError {
LocalBrokerIsNotHeadless,
InvalidNuri,
InvalidTarget,
InvalidQrCode,
NotImplemented,
NotARendezVous,
}
impl Error for NgError {}
@ -89,24 +94,24 @@ impl Error for NgError {}
impl fmt::Display for NgError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::WalletError(string) => write!(f, "WalletError: {}", string),
Self::JsStorageWriteError(string) => write!(f, "JsStorageWriteError: {}", string),
Self::WalletError(string) => write!(f, "WalletError:{}", string),
Self::JsStorageWriteError(string) => write!(f, "JsStorageWriteError:{}", string),
Self::CommitVerifyError(commit_verify_error) => {
write!(f, "CommitVerifyError: {:?}", commit_verify_error)
write!(f, "CommitVerifyError:{:?}", commit_verify_error)
}
Self::ProtocolError(error) => write!(f, "ProtocolError: {:?}", error),
Self::ServerError(error) => write!(f, "ServerError: {:?}", error),
Self::VerifierError(error) => write!(f, "VerifierError: {:?}", error),
Self::ProtocolError(error) => write!(f, "ProtocolError:{:?}", error),
Self::ServerError(error) => write!(f, "ServerError:{:?}", error),
Self::VerifierError(error) => write!(f, "VerifierError:{:?}", error),
Self::CommitLoadError(commit_load_error) => {
write!(f, "CommitLoadError: {:?}", commit_load_error)
write!(f, "CommitLoadError:{:?}", commit_load_error)
}
Self::BootstrapError(error) => {
write!(f, "BootstrapError: {:?}", error)
write!(f, "BootstrapError:{:?}", error)
}
Self::ObjectParseError(error) => write!(f, "ObjectParseError: {:?}", error),
Self::StorageError(storage_error) => write!(f, "StorageError: {:?}", storage_error),
Self::BrokerConfigErrorStr(s) => write!(f, "BrokerConfigError: {s}"),
Self::BrokerConfigError(s) => write!(f, "BrokerConfigError: {s}"),
Self::ObjectParseError(error) => write!(f, "ObjectParseError:{:?}", error),
Self::StorageError(storage_error) => write!(f, "StorageError:{:?}", storage_error),
Self::BrokerConfigErrorStr(s) => write!(f, "BrokerConfigError:{s}"),
Self::BrokerConfigError(s) => write!(f, "BrokerConfigError:{s}"),
_ => write!(f, "{:?}", self),
}
}
@ -217,6 +222,7 @@ pub enum StorageError {
OverlayBranchNotFound,
Abort,
NotEmpty,
ServerAlreadyRunningInOtherProcess,
}
impl core::fmt::Display for StorageError {
@ -262,6 +268,8 @@ pub enum ServerError {
OxiGraphError,
InvalidNuri,
InvalidTarget,
ExportWalletTimeOut,
NetError,
}
impl From<StorageError> for ServerError {
@ -273,12 +281,23 @@ impl From<StorageError> for ServerError {
}
}
impl From<NetError> for ServerError {
fn from(e: NetError) -> Self {
match e {
_ => ServerError::NetError,
}
}
}
impl From<ProtocolError> for ServerError {
fn from(e: ProtocolError) -> Self {
match e {
ProtocolError::NotFound => ServerError::NotFound,
ProtocolError::BrokerError => ServerError::BrokerError,
_ => ServerError::ProtocolError,
_ => {
log_err!("{:?}", e);
ServerError::ProtocolError
}
}
}
}

@ -60,6 +60,11 @@ pub async fn locales() -> Result<JsValue, JsValue> {
Ok(serde_wasm_bindgen::to_value(&get_locales().collect::<Vec<_>>()).unwrap())
}
#[wasm_bindgen]
pub async fn get_device_name() -> Result<JsValue, JsValue> {
Ok(serde_wasm_bindgen::to_value(&nextgraph::get_device_name()).unwrap())
}
#[wasm_bindgen]
pub async fn get_local_bootstrap(location: String, invite: JsValue) -> JsValue {
let res = retrieve_local_bootstrap(location, invite.as_string(), false).await;
@ -140,6 +145,53 @@ pub fn wallet_open_with_pazzle(
}
}
#[wasm_bindgen]
pub fn wallet_open_with_mnemonic(
wallet: JsValue,
mnemonic: Vec<u16>,
pin: JsValue,
) -> Result<JsValue, JsValue> {
let encrypted_wallet = serde_wasm_bindgen::from_value::<Wallet>(wallet)
.map_err(|_| "Deserialization error of wallet")?;
let pin = serde_wasm_bindgen::from_value::<[u8; 4]>(pin)
.map_err(|_| "Deserialization error of pin")?;
let res = nextgraph::local_broker::wallet_open_with_mnemonic(&encrypted_wallet, mnemonic, pin);
match res {
Ok(r) => Ok(r
.serialize(&serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true))
.unwrap()),
Err(e) => Err(e.to_string().into()),
}
}
#[wasm_bindgen]
pub fn wallet_open_with_mnemonic_words(
wallet: JsValue,
mnemonic_words: Array,
pin: JsValue,
) -> Result<JsValue, JsValue> {
let encrypted_wallet = serde_wasm_bindgen::from_value::<Wallet>(wallet)
.map_err(|_| "Deserialization error of wallet")?;
let pin = serde_wasm_bindgen::from_value::<[u8; 4]>(pin)
.map_err(|_| "Deserialization error of pin")?;
let mnemonic_vec: Vec<String> = mnemonic_words
.iter()
.map(|word| word.as_string().unwrap())
.collect();
let res = nextgraph::local_broker::wallet_open_with_mnemonic_words(
&encrypted_wallet,
&mnemonic_vec,
pin,
);
match res {
Ok(r) => Ok(r
.serialize(&serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true))
.unwrap()),
Err(e) => Err(e.to_string().into()),
}
}
#[wasm_bindgen]
pub fn wallet_update(wallet_id: JsValue, operations: JsValue) -> Result<JsValue, JsValue> {
let _wallet = serde_wasm_bindgen::from_value::<WalletId>(wallet_id)
@ -488,6 +540,81 @@ pub async fn wallet_read_file(file: JsValue) -> Result<JsValue, String> {
Ok(serde_wasm_bindgen::to_value(&wallet).unwrap())
}
#[wasm_bindgen]
pub async fn wallet_import_from_code(code: JsValue) -> Result<JsValue, String> {
init_local_broker_with_lazy(&INIT_LOCAL_BROKER).await;
let code = serde_wasm_bindgen::from_value::<String>(code)
.map_err(|_| "Deserialization error of code".to_string())?;
let wallet = nextgraph::local_broker::wallet_import_from_code(code)
.await
.map_err(|e: NgError| e.to_string())?;
Ok(serde_wasm_bindgen::to_value(&wallet).unwrap())
}
#[wasm_bindgen]
pub async fn wallet_import_rendezvous(size: JsValue) -> Result<JsValue, String> {
init_local_broker_with_lazy(&INIT_LOCAL_BROKER).await;
let size: u32 = serde_wasm_bindgen::from_value::<u32>(size)
.map_err(|_| "Deserialization error of size".to_string())?;
let res = nextgraph::local_broker::wallet_import_rendezvous(size)
.await
.map_err(|e: NgError| e.to_string())?;
Ok(serde_wasm_bindgen::to_value(&res).unwrap())
}
#[wasm_bindgen]
pub async fn wallet_export_get_qrcode(
session_id: JsValue,
size: JsValue,
) -> Result<JsValue, String> {
let session_id: u64 = serde_wasm_bindgen::from_value::<u64>(session_id)
.map_err(|_| "Deserialization error of session_id".to_string())?;
let size: u32 = serde_wasm_bindgen::from_value::<u32>(size)
.map_err(|_| "Deserialization error of size".to_string())?;
init_local_broker_with_lazy(&INIT_LOCAL_BROKER).await;
let res = nextgraph::local_broker::wallet_export_get_qrcode(session_id, size)
.await
.map_err(|e: NgError| e.to_string())?;
Ok(serde_wasm_bindgen::to_value(&res).unwrap())
}
#[wasm_bindgen]
pub async fn wallet_export_get_textcode(session_id: JsValue) -> Result<JsValue, String> {
let session_id: u64 = serde_wasm_bindgen::from_value::<u64>(session_id)
.map_err(|_| "Deserialization error of session_id".to_string())?;
init_local_broker_with_lazy(&INIT_LOCAL_BROKER).await;
let res = nextgraph::local_broker::wallet_export_get_textcode(session_id)
.await
.map_err(|e: NgError| e.to_string())?;
Ok(serde_wasm_bindgen::to_value(&res).unwrap())
}
#[wasm_bindgen]
pub async fn wallet_export_rendezvous(session_id: JsValue, code: JsValue) -> Result<(), String> {
let session_id: u64 = serde_wasm_bindgen::from_value::<u64>(session_id)
.map_err(|_| "Deserialization error of session_id".to_string())?;
let code = serde_wasm_bindgen::from_value::<String>(code)
.map_err(|_| "Deserialization error of code".to_string())?;
init_local_broker_with_lazy(&INIT_LOCAL_BROKER).await;
nextgraph::local_broker::wallet_export_rendezvous(session_id, code)
.await
.map_err(|e: NgError| e.to_string())?;
Ok(())
}
#[wasm_bindgen]
pub async fn wallet_was_opened(
opened_wallet: JsValue, //SensitiveWallet

@ -79,7 +79,14 @@ impl RocksDbBlockStorage {
let env = Env::enc_env(key).unwrap();
opts.set_env(&env);
let tx_options = TransactionDBOptions::new();
let db: TransactionDB = TransactionDB::open(&opts, &tx_options, &path).unwrap();
let db: TransactionDB = TransactionDB::open(&opts, &tx_options, &path).map_err(|e| {
log_err!("{e}");
if e.into_string().starts_with("IO error: While lock file") {
StorageError::ServerAlreadyRunningInOtherProcess
} else {
StorageError::BackendError
}
})?;
log_info!(
"created blockstorage with Rocksdb Version: {}",

@ -702,7 +702,14 @@ impl RocksDbKCVStorage {
// TODO: use open_cf and choose which column family to create/ versus using set_prefix_extractor and doing prefix seek
let tx_options = TransactionDBOptions::new();
let db: TransactionDB = TransactionDB::open(&opts, &tx_options, &path).unwrap();
let db: TransactionDB = TransactionDB::open(&opts, &tx_options, &path).map_err(|e| {
log_err!("{e}");
if e.into_string().starts_with("IO error: While lock file") {
StorageError::ServerAlreadyRunningInOtherProcess
} else {
StorageError::BackendError
}
})?;
log_info!(
"created kcv storage with Rocksdb Version: {}",

@ -813,6 +813,26 @@ impl Verifier {
}
}
pub async fn client_request<
A: Into<ProtocolMessage> + std::fmt::Debug + Sync + Send + 'static,
B: TryFrom<ProtocolMessage, Error = ProtocolError> + std::fmt::Debug + Sync + Send + 'static,
>(
&self,
msg: A,
) -> Result<SoS<B>, NgError> {
if self.connected_broker.is_some() {
let connected_broker = self.connected_broker.clone();
let broker = BROKER.read().await;
let user = self.user_id().clone();
broker
.request::<A, B>(&Some(user), &connected_broker.into(), msg)
.await
} else {
Err(NgError::NotConnected)
}
}
async fn send_or_save_event_to_outbox<'a>(
&'a mut self,
commit_ref: ObjectRef,

@ -25,6 +25,7 @@ rand = { version = "0.7", features = ["getrandom"] }
aes-gcm-siv = {version = "0.11.1", features = ["aes","heapless","getrandom","std"] }
zeroize = { version = "1.7.0", features = ["zeroize_derive"] }
crypto_box = { version = "0.8.2", features = ["seal"] }
base64-url = "2.0.0"
blake3 = "1.3.1"
argon2 = "0.5.0"
chacha20poly1305 = "0.10.1"

@ -1,3 +1,6 @@
use ng_repo::errors::NgError;
use std::collections::HashMap;
#[allow(non_upper_case_globals)]
pub const bip39_wordlist: [&str; 2048] = [
"abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd",
@ -212,3 +215,26 @@ pub const bip39_wordlist: [&str; 2048] = [
"write", "wrong", "yard", "year", "yellow", "you", "young", "youth", "zebra", "zero", "zone",
"zoo",
];
lazy_static! {
pub static ref BIP39_WORD_MAP: HashMap<String, u16> = {
let mut m = HashMap::new();
for (i, word) in bip39_wordlist.iter().enumerate() {
m.insert(word.to_string(), i as u16);
}
m
};
}
/// Taking a list of bip39 words, returns a list of u16 codes
pub fn encode_mnemonic(words: &Vec<String>) -> Result<Vec<u16>, NgError> {
let mut res = vec![];
for word in words {
res.push(
*BIP39_WORD_MAP
.get(word.as_str())
.ok_or(NgError::InvalidMnemonic)?,
);
}
Ok(res)
}

@ -86,7 +86,7 @@ const face: [EmojiDef<'static>; 15] = [
EmojiDef {
hexcode: "1f9d0",
shortcode: "face_with_monocle",
code: "monocole",
code: "monocle",
},
EmojiDef {
hexcode: "1f634",

@ -362,7 +362,7 @@ pub fn open_wallet_with_pazzle(
}
pub fn open_wallet_with_mnemonic(
wallet: Wallet,
wallet: &Wallet,
mut mnemonic: [u16; 12],
mut pin: [u8; 4],
) -> Result<SensitiveWallet, NgWalletError> {
@ -388,7 +388,7 @@ pub fn open_wallet_with_mnemonic(
mnemonic_key.zeroize();
Ok(SensitiveWallet::V0(dec_encrypted_block(
v0.content.encrypted,
v0.content.encrypted.clone(),
master_key,
v0.content.peer_id,
v0.content.nonce,
@ -786,6 +786,7 @@ pub async fn create_wallet_second_step_v0(
wallet_file,
pazzle,
mnemonic: mnemonic.clone(),
mnemonic_str: display_mnemonic(&mnemonic),
wallet_name: params.wallet_name.clone(),
client: params.client.clone(),
user,
@ -893,7 +894,7 @@ mod test {
let _opening_mnemonic = Instant::now();
let _w = open_wallet_with_mnemonic(Wallet::V0(v0.clone()), res.mnemonic, pin.clone())
let _w = open_wallet_with_mnemonic(&Wallet::V0(v0.clone()), res.mnemonic, pin.clone())
.expect("open with mnemonic");
//log_debug!("encrypted part {:?}", w);

@ -1303,6 +1303,8 @@ pub struct CreateWalletResultV0 {
/// randomly generated mnemonic. It is an alternate way to open the wallet.
/// A BIP39 list of 12 words. We argue that the Pazzle is easier to remember than this.
pub mnemonic: [u16; 12],
/// The words of the mnemonic, in a human readable form.
pub mnemonic_str: Vec<String>,
#[zeroize(skip)]
/// a string identifying uniquely the wallet
pub wallet_name: String,
@ -1368,6 +1370,7 @@ pub enum NgWalletError {
InvalidPin,
InvalidPazzle,
InvalidPazzleLength,
InvalidMnemonic,
InvalidSecurityImage,
InvalidSecurityText,
SubmissionError,
@ -1423,3 +1426,27 @@ pub struct ShuffledPazzle {
pub category_indices: Vec<u8>,
pub emoji_indices: Vec<Vec<u8>>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NgQRCodeV0 {
pub broker: BrokerServerV0,
pub rendezvous: SymKey, // Rendez-vous ID
pub secret_key: SymKey,
pub is_rendezvous: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum NgQRCode {
V0(NgQRCodeV0),
}
impl NgQRCode {
pub fn from_code(code: String) -> Result<Self, NgError> {
let decoded = base64_url::decode(&code).map_err(|_| NgError::SerializationError)?;
Ok(serde_bare::from_slice(&decoded)?)
}
pub fn to_code(&self) -> String {
let ser = serde_bare::to_vec(self).unwrap();
base64_url::encode(&ser)
}
}

@ -0,0 +1,5 @@
NG_ACCOUNT_DOMAIN=
NG_ACCOUNT_ADMIN=
NG_ACCOUNT_LOCAL_PEER_KEY=
NG_ACCOUNT_SERVER=127.0.0.1,1440,[the broker's peer ID]
RUST_LOG=

@ -12,16 +12,23 @@ pnpm --ignore-workspace install
## Dev
```
```bash
cd web
pnpm run dev --host
// in another terminal
# In another terminal...
cd ../
export NG_ACCOUNT_DOMAIN=[?]; export NG_ACCOUNT_ADMIN=[?]; export NG_ACCOUNT_LOCAL_PEER_KEY=[?]; export NG_ACCOUNT_SERVER=127.0.0.1,14400,[?]; export RUST_LOG=debug
# Please set the required environment variables in the .env and then source it it with:
source .env
cargo watch -c -w src -x run
// then open http://localhost:5173/
# Then open http://localhost:5173/#/create
```
> Currently, the ng-account server api is listening on http://127.0.0.1:3031 only which might cause you trouble (coded in `main.rs`, `Create.svelte` and `Delete.svelte`).
> If you need to test from a (virtual) android device, you can use adb to tunnel the connection like: [`adb reverse tcp:3031 tcp:3031`](https://justinchips.medium.com/proxying-adb-client-connections-2ab495f774eb).
## Prod
```

@ -258,7 +258,7 @@ async fn main() -> anyhow::Result<()> {
"Content-Security-Policy",
HeaderValue::from_static(
#[cfg(debug_assertions)]
"default-src 'self' data:; connect-src ipc: https://ipc.localhost 'self' http://192.168.192.2:3031",
"default-src 'self' data:; connect-src ipc: https://ipc.localhost 'self' http://localhost:3031",
#[cfg(not(debug_assertions))]
"default-src 'self' data:; connect-src ipc: https://ipc.localhost 'self'",
@ -304,9 +304,10 @@ async fn main() -> anyhow::Result<()> {
{
log_debug!("CORS: any origin");
cors = cors.allow_any_origin();
log::info!("Starting server on http://192.168.192.2:3031");
log::info!("Starting server on http://localhost:3031");
warp::serve(api_v1.or(static_files).with(cors).with(incoming_log))
.run(([192, 168, 192, 2], 3031))
// TODO: Change this to local network ip?
.run(([127, 0, 0, 1], 3031))
.await;
}

@ -27,7 +27,7 @@
let top;
const api_url = import.meta.env.PROD
? "api/v1/"
: "http://192.168.192.2:3031/api/v1/";
: "http://127.0.0.1:3031/api/v1/";
async function register() {
wait = true;

@ -27,7 +27,7 @@
let top;
const api_url = import.meta.env.PROD
? "api/v1/"
: "http://192.168.192.2:3031/api/v1/";
: "http://127.0.0.1:3031/api/v1/";
async function bootstrap() {}
let error;

@ -0,0 +1,36 @@
# Use rust's latest alpine image as base image.
FROM rust:alpine
ENV LD_LIBRARY_PATH=/lib:$LD_LIBRARY_PATH
RUN apk add git nodejs npm llvm-static llvm-dev clang-static clang-dev openssl openssl-dev perl gtk+3.0-dev webkit2gtk-dev librsvg-dev curl wget pkgconf eudev-dev build-base zlib-static bzip2-static build-base ncursers-static && \
# Install Rust and Node.js tools
cargo install cargo-watch && \
cargo install wasm-pack --git https://github.com/rustwasm/wasm-pack.git --rev c2b663f25abe50631a236d57a8c6d7fd806413b2 && \
cargo install tauri-cli --version "2.0.0-alpha.11" --locked && \
npm install -g pnpm
# Clone the nextgraph-rs repository
RUN git clone https://git.nextgraph.org/NextGraph/nextgraph-rs.git && \
cd /nextgraph-rs/ng-sdk-js && \
wasm-pack build --target bundler && npm install --no-save pkg &&
# Build ng-app web version
cd /nextgraph-rs/ng-app && pnpm install && pnpm webfilebuild
# From here the build fails due to llvm / clang linking issues...
#
# WORKDIR /nextgraph-rs
## Build the nextgraph-rs project and its subprojects
# RUN cd /nextgraph-rs && git pull && cargo update -p ng-rocksdb && \
# cargo build -r && \
# cargo build -r -p ngd && \
# cargo build -r -p ngcli
# TODO: Build the platform-specific ng-app versions
# cd /nextgraph-rs/ng-app && cargo tauri build --target x86_64-unknown-linux-gnu
# ...
# TODO: To remove the image size, remove ~/.cargo, ~/.rustup, and the build dependencies
# To build the image, run:
# docker build -t nextgraph-rs:alpine -f docker/Dockerfile.alpine .

@ -0,0 +1,46 @@
# Use fedora:40 as base image
FROM fedora:40
# Set the environment variable to ensure cargo is available in the PATH
ENV PATH="/root/.cargo/bin:${PATH}"
SHELL ["/bin/bash", "-c"]
# Install the required packages and Rust
RUN dnf install -y git clang-devel webkit2gtk4.1-devel openssl openssl-devel curl wget file libappindicator-gtk3-devel librsvg2-devel perl && \
dnf group install -y "C Development Tools and Libraries" && \
# Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | bash -s -- -y && \
# Node.js
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash && \
export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" && [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" && \
nvm install 22 && \
npm install -g pnpm && \
# Clear Cache
rm -rf /var/cache/dnf && \
# Install Rust and Node.js tools
cargo install cargo-watch && \
cargo install wasm-pack --git https://github.com/rustwasm/wasm-pack.git --rev c2b663f25abe50631a236d57a8c6d7fd806413b2 && \
cargo install tauri-cli --version "2.0.0-alpha.11" --locked && \
# Clone the nextgraph-rs repository (TODO: It might be better to put this into a seperate RUN command to avoid rebuilding the image if the repository changes)
git clone https://git.nextgraph.org/NextGraph/nextgraph-rs.git && \
# Build sdk and ng-app web version
cd /nextgraph-rs/ng-sdk-js && wasm-pack build --target bundler && npm install --no-save pkg && \
cd /nextgraph-rs/ng-app && pnpm install && pnpm webfilebuild
# Build the nextgraph-rs project
RUN export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" && [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" && \
cd /nextgraph-rs && git pull && cargo update -p ng-rocksdb && \
cargo build -r && \
cargo build -r -p ngd && \
cargo build -r -p ngcli
# TODO: Build the platform-specific ng-app versions
# cd /nextgraph-rs/ng-app && cargo tauri build --target x86_64-unknown-linux-gnu
# ...
# TODO: To remove the image size, remove ~/.cargo, ~/.rustup, and the build dependencies
# To build the image, run:
# docker build -t nextgraph-rs:fedora -f docker/Dockerfile.fedora .

@ -0,0 +1,47 @@
# Use ubuntu 22.04 as base image
FROM ubuntu:22.04
SHELL ["/bin/bash", "-c"]
# Set the environment variable to ensure cargo is available in the PATH
ENV PATH="/root/.cargo/bin:${PATH}"
# Install the required packages and Rust
RUN apt update && \
apt upgrade -y && \
apt install -y git llvm-dev libclang-dev clang libssl-dev perl libappindicator3-dev libwebkit2gtk-4.0-dev librsvg2-dev curl wget pkg-config libudev-dev build-essential && \
rm -rf /var/cache/apt && \
# Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && \
# Node.js
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash && \
export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" && [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" && \
nvm install 22 && \
npm install -g pnpm && \
# Install Rust and Node.js tools
cargo install cargo-watch && \
cargo install wasm-pack --git https://github.com/rustwasm/wasm-pack.git --rev c2b663f25abe50631a236d57a8c6d7fd806413b2 && \
cargo install tauri-cli --version "2.0.0-alpha.11" --locked && \
npm install -g pnpm && \
# Clone the nextgraph-rs repository (TODO: It might be better to put this into a seperate RUN command to avoid rebuilding the image if the repository changes)
git clone https://git.nextgraph.org/NextGraph/nextgraph-rs.git && \
# Build sdk and ng-app web version
cd /nextgraph-rs/ng-sdk-js && wasm-pack build --target bundler && npm install --no-save pkg && \
cd /nextgraph-rs/ng-app && \
pnpm install && pnpm webfilebuild
# Build the nextgraph-rs project and its subprojects
WORKDIR /nextgraph-rs
RUN cargo build -r && \
cargo build -r -p ngd && \
cargo build -r -p ngcli
# TODO: Build the platform-specific ng-app versions
# WORKDIR /nextgraph-rs/ng-app
# RUN cargo tauri build --target x86_64-unknown-linux-gnu
# TODO: To remove the image size, remove ~/.cargo, ~/.rustup, and the build dependencies
# To build the image, run:
# docker build -t nextgraph-rs:ubuntu -f docker/Dockerfile.ubuntu .

@ -51,7 +51,7 @@
A <b>NextGraph Wallet</b> is unique to each person.<br /> It stores your
credentials to access documents. You need one in order to start using
NextGraph.<br />If you already have a wallet, you should not create a new
one, instead,
one. Instead,
{#if display_note_on_local_wallets}
<a href="/" use:link>login here with your existing wallet.</a>
{:else}
@ -73,7 +73,7 @@
class="text-primary-700 bg-primary-100 hover:bg-primary-100/90 focus:ring-4 focus:ring-primary-100/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-100/55 mr-2 mb-2"
>
<EULogo class="mr-4 block h-10 w-10" alt="European Union flag" />
For European Union citizens
European Union citizens
</button>
</a>
</div>

@ -8,6 +8,9 @@
"rollup"
]
}
},
"devDependencies": {
"prettier": "^3.3.2",
"prettier-plugin-svelte": "^3.2.5"
}
}
}

@ -3,7 +3,12 @@ lockfileVersion: 5.4
importers:
.:
specifiers: {}
specifiers:
prettier: ^3.3.2
prettier-plugin-svelte: ^3.2.5
devDependencies:
prettier: 3.3.2
prettier-plugin-svelte: 3.2.5_prettier@3.3.2
ng-app:
specifiers:
@ -11,6 +16,7 @@ importers:
'@sveltejs/vite-plugin-svelte': ^2.0.0
'@tauri-apps/api': 2.0.0-alpha.8
'@tauri-apps/cli': 2.0.0-alpha.14
'@tauri-apps/plugin-barcode-scanner': 2.0.0-alpha.0
'@tauri-apps/plugin-window': 2.0.0-alpha.1
'@tsconfig/svelte': ^3.0.0
'@types/node': ^18.7.10
@ -30,6 +36,7 @@ importers:
svelte: ^3.54.0
svelte-check: ^3.0.0
svelte-heros-v2: ^0.10.12
svelte-i18n: ^4.0.0
svelte-preprocess: ^5.0.3
svelte-spa-router: ^3.3.0
svelte-time: ^0.8.0
@ -44,12 +51,14 @@ importers:
dependencies:
'@popperjs/core': 2.11.8
'@tauri-apps/api': 2.0.0-alpha.8
'@tauri-apps/plugin-barcode-scanner': 2.0.0-alpha.0
'@tauri-apps/plugin-window': 2.0.0-alpha.1
async-proxy: 0.4.1
classnames: 2.3.2
flowbite: 1.6.5
flowbite-svelte: 0.43.3_svelte@3.59.1
ng-sdk-js: link:../ng-sdk-js/pkg
svelte-i18n: 4.0.0_svelte@3.59.1
svelte-spa-router: 3.3.0
vite-plugin-top-level-await: 1.3.1_vite@4.3.9
devDependencies:
@ -117,6 +126,15 @@ packages:
engines: {node: '>=10'}
dev: true
/@esbuild/aix-ppc64/0.19.12:
resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==}
engines: {node: '>=12'}
cpu: [ppc64]
os: [aix]
requiresBuild: true
dev: false
optional: true
/@esbuild/android-arm/0.17.19:
resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==}
engines: {node: '>=12'}
@ -125,6 +143,15 @@ packages:
requiresBuild: true
optional: true
/@esbuild/android-arm/0.19.12:
resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==}
engines: {node: '>=12'}
cpu: [arm]
os: [android]
requiresBuild: true
dev: false
optional: true
/@esbuild/android-arm64/0.17.19:
resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==}
engines: {node: '>=12'}
@ -133,6 +160,15 @@ packages:
requiresBuild: true
optional: true
/@esbuild/android-arm64/0.19.12:
resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==}
engines: {node: '>=12'}
cpu: [arm64]
os: [android]
requiresBuild: true
dev: false
optional: true
/@esbuild/android-x64/0.17.19:
resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==}
engines: {node: '>=12'}
@ -141,6 +177,15 @@ packages:
requiresBuild: true
optional: true
/@esbuild/android-x64/0.19.12:
resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==}
engines: {node: '>=12'}
cpu: [x64]
os: [android]
requiresBuild: true
dev: false
optional: true
/@esbuild/darwin-arm64/0.17.19:
resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==}
engines: {node: '>=12'}
@ -149,6 +194,15 @@ packages:
requiresBuild: true
optional: true
/@esbuild/darwin-arm64/0.19.12:
resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==}
engines: {node: '>=12'}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/@esbuild/darwin-x64/0.17.19:
resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==}
engines: {node: '>=12'}
@ -157,6 +211,15 @@ packages:
requiresBuild: true
optional: true
/@esbuild/darwin-x64/0.19.12:
resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==}
engines: {node: '>=12'}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/@esbuild/freebsd-arm64/0.17.19:
resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==}
engines: {node: '>=12'}
@ -165,6 +228,15 @@ packages:
requiresBuild: true
optional: true
/@esbuild/freebsd-arm64/0.19.12:
resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==}
engines: {node: '>=12'}
cpu: [arm64]
os: [freebsd]
requiresBuild: true
dev: false
optional: true
/@esbuild/freebsd-x64/0.17.19:
resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==}
engines: {node: '>=12'}
@ -173,6 +245,15 @@ packages:
requiresBuild: true
optional: true
/@esbuild/freebsd-x64/0.19.12:
resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==}
engines: {node: '>=12'}
cpu: [x64]
os: [freebsd]
requiresBuild: true
dev: false
optional: true
/@esbuild/linux-arm/0.17.19:
resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==}
engines: {node: '>=12'}
@ -181,6 +262,15 @@ packages:
requiresBuild: true
optional: true
/@esbuild/linux-arm/0.19.12:
resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==}
engines: {node: '>=12'}
cpu: [arm]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@esbuild/linux-arm64/0.17.19:
resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==}
engines: {node: '>=12'}
@ -189,6 +279,15 @@ packages:
requiresBuild: true
optional: true
/@esbuild/linux-arm64/0.19.12:
resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==}
engines: {node: '>=12'}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@esbuild/linux-ia32/0.17.19:
resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==}
engines: {node: '>=12'}
@ -197,6 +296,15 @@ packages:
requiresBuild: true
optional: true
/@esbuild/linux-ia32/0.19.12:
resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==}
engines: {node: '>=12'}
cpu: [ia32]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@esbuild/linux-loong64/0.17.19:
resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==}
engines: {node: '>=12'}
@ -205,6 +313,15 @@ packages:
requiresBuild: true
optional: true
/@esbuild/linux-loong64/0.19.12:
resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==}
engines: {node: '>=12'}
cpu: [loong64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@esbuild/linux-mips64el/0.17.19:
resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==}
engines: {node: '>=12'}
@ -213,6 +330,15 @@ packages:
requiresBuild: true
optional: true
/@esbuild/linux-mips64el/0.19.12:
resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==}
engines: {node: '>=12'}
cpu: [mips64el]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@esbuild/linux-ppc64/0.17.19:
resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==}
engines: {node: '>=12'}
@ -221,6 +347,15 @@ packages:
requiresBuild: true
optional: true
/@esbuild/linux-ppc64/0.19.12:
resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==}
engines: {node: '>=12'}
cpu: [ppc64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@esbuild/linux-riscv64/0.17.19:
resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==}
engines: {node: '>=12'}
@ -229,6 +364,15 @@ packages:
requiresBuild: true
optional: true
/@esbuild/linux-riscv64/0.19.12:
resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==}
engines: {node: '>=12'}
cpu: [riscv64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@esbuild/linux-s390x/0.17.19:
resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==}
engines: {node: '>=12'}
@ -237,6 +381,15 @@ packages:
requiresBuild: true
optional: true
/@esbuild/linux-s390x/0.19.12:
resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==}
engines: {node: '>=12'}
cpu: [s390x]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@esbuild/linux-x64/0.17.19:
resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==}
engines: {node: '>=12'}
@ -245,6 +398,15 @@ packages:
requiresBuild: true
optional: true
/@esbuild/linux-x64/0.19.12:
resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==}
engines: {node: '>=12'}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@esbuild/netbsd-x64/0.17.19:
resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==}
engines: {node: '>=12'}
@ -253,6 +415,15 @@ packages:
requiresBuild: true
optional: true
/@esbuild/netbsd-x64/0.19.12:
resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==}
engines: {node: '>=12'}
cpu: [x64]
os: [netbsd]
requiresBuild: true
dev: false
optional: true
/@esbuild/openbsd-x64/0.17.19:
resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==}
engines: {node: '>=12'}
@ -261,6 +432,15 @@ packages:
requiresBuild: true
optional: true
/@esbuild/openbsd-x64/0.19.12:
resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==}
engines: {node: '>=12'}
cpu: [x64]
os: [openbsd]
requiresBuild: true
dev: false
optional: true
/@esbuild/sunos-x64/0.17.19:
resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==}
engines: {node: '>=12'}
@ -269,6 +449,15 @@ packages:
requiresBuild: true
optional: true
/@esbuild/sunos-x64/0.19.12:
resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==}
engines: {node: '>=12'}
cpu: [x64]
os: [sunos]
requiresBuild: true
dev: false
optional: true
/@esbuild/win32-arm64/0.17.19:
resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==}
engines: {node: '>=12'}
@ -277,6 +466,15 @@ packages:
requiresBuild: true
optional: true
/@esbuild/win32-arm64/0.19.12:
resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==}
engines: {node: '>=12'}
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: false
optional: true
/@esbuild/win32-ia32/0.17.19:
resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==}
engines: {node: '>=12'}
@ -285,6 +483,15 @@ packages:
requiresBuild: true
optional: true
/@esbuild/win32-ia32/0.19.12:
resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==}
engines: {node: '>=12'}
cpu: [ia32]
os: [win32]
requiresBuild: true
dev: false
optional: true
/@esbuild/win32-x64/0.17.19:
resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==}
engines: {node: '>=12'}
@ -293,6 +500,15 @@ packages:
requiresBuild: true
optional: true
/@esbuild/win32-x64/0.19.12:
resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==}
engines: {node: '>=12'}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: false
optional: true
/@floating-ui/core/1.5.0:
resolution: {integrity: sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==}
dependencies:
@ -310,6 +526,40 @@ packages:
resolution: {integrity: sha512-qprfWkn82Iw821mcKofJ5Pk9wgioHicxcQMxx+5zt5GSKoqdWvgG5AxVmpmUUjzTLPVSH5auBrhI93Deayn/DA==}
dev: false
/@formatjs/ecma402-abstract/2.0.0:
resolution: {integrity: sha512-rRqXOqdFmk7RYvj4khklyqzcfQl9vEL/usogncBHRZfZBDOwMGuSRNFl02fu5KGHXdbinju+YXyuR+Nk8xlr/g==}
dependencies:
'@formatjs/intl-localematcher': 0.5.4
tslib: 2.5.3
dev: false
/@formatjs/fast-memoize/2.2.0:
resolution: {integrity: sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==}
dependencies:
tslib: 2.5.3
dev: false
/@formatjs/icu-messageformat-parser/2.7.8:
resolution: {integrity: sha512-nBZJYmhpcSX0WeJ5SDYUkZ42AgR3xiyhNCsQweFx3cz/ULJjym8bHAzWKvG5e2+1XO98dBYC0fWeeAECAVSwLA==}
dependencies:
'@formatjs/ecma402-abstract': 2.0.0
'@formatjs/icu-skeleton-parser': 1.8.2
tslib: 2.5.3
dev: false
/@formatjs/icu-skeleton-parser/1.8.2:
resolution: {integrity: sha512-k4ERKgw7aKGWJZgTarIcNEmvyTVD9FYh0mTrrBMHZ1b8hUu6iOJ4SzsZlo3UNAvHYa+PnvntIwRPt1/vy4nA9Q==}
dependencies:
'@formatjs/ecma402-abstract': 2.0.0
tslib: 2.5.3
dev: false
/@formatjs/intl-localematcher/0.5.4:
resolution: {integrity: sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==}
dependencies:
tslib: 2.5.3
dev: false
/@jridgewell/gen-mapping/0.3.3:
resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==}
engines: {node: '>=6.0.0'}
@ -644,6 +894,12 @@ packages:
'@tauri-apps/cli-win32-x64-msvc': 2.0.0-alpha.14
dev: true
/@tauri-apps/plugin-barcode-scanner/2.0.0-alpha.0:
resolution: {integrity: sha512-qXc/HfGdWI2x2vUEfgf65kb4Bw3PEDMLz6tNizLdzQ5Q4wJLMkaPZxFsqS6ZbuRjILAqM0lfp/otgR93OGVOgA==}
dependencies:
'@tauri-apps/api': 2.0.0-alpha.8
dev: false
/@tauri-apps/plugin-window/2.0.0-alpha.1:
resolution: {integrity: sha512-dFOAgal/3Txz3SQ+LNQq0AK1EPC+acdaFlwPVB/6KXUZYmaFleIlzgxDVoJCQ+/xOhxvYrdQaFLefh0I/Kldbg==}
dependencies:
@ -779,6 +1035,17 @@ packages:
resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==}
dev: false
/cli-color/2.0.4:
resolution: {integrity: sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==}
engines: {node: '>=0.10'}
dependencies:
d: 1.0.2
es5-ext: 0.10.64
es6-iterator: 2.0.3
memoizee: 0.4.17
timers-ext: 0.1.8
dev: false
/commander/4.1.1:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
@ -854,6 +1121,14 @@ packages:
css-tree: 2.2.1
dev: true
/d/1.0.2:
resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==}
engines: {node: '>=0.12'}
dependencies:
es5-ext: 0.10.64
type: 2.7.3
dev: false
/dayjs/1.11.10:
resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==}
dev: true
@ -873,7 +1148,6 @@ packages:
/deepmerge/4.3.1:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
dev: true
/default-gateway/6.0.3:
resolution: {integrity: sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==}
@ -931,10 +1205,46 @@ packages:
engines: {node: '>=0.12'}
dev: true
/es5-ext/0.10.64:
resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==}
engines: {node: '>=0.10'}
requiresBuild: true
dependencies:
es6-iterator: 2.0.3
es6-symbol: 3.1.4
esniff: 2.0.1
next-tick: 1.1.0
dev: false
/es6-iterator/2.0.3:
resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==}
dependencies:
d: 1.0.2
es5-ext: 0.10.64
es6-symbol: 3.1.4
dev: false
/es6-promise/3.3.1:
resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==}
dev: true
/es6-symbol/3.1.4:
resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==}
engines: {node: '>=0.12'}
dependencies:
d: 1.0.2
ext: 1.7.0
dev: false
/es6-weak-map/2.0.3:
resolution: {integrity: sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==}
dependencies:
d: 1.0.2
es5-ext: 0.10.64
es6-iterator: 2.0.3
es6-symbol: 3.1.4
dev: false
/esbuild/0.17.19:
resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==}
engines: {node: '>=12'}
@ -964,11 +1274,63 @@ packages:
'@esbuild/win32-ia32': 0.17.19
'@esbuild/win32-x64': 0.17.19
/esbuild/0.19.12:
resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==}
engines: {node: '>=12'}
hasBin: true
requiresBuild: true
optionalDependencies:
'@esbuild/aix-ppc64': 0.19.12
'@esbuild/android-arm': 0.19.12
'@esbuild/android-arm64': 0.19.12
'@esbuild/android-x64': 0.19.12
'@esbuild/darwin-arm64': 0.19.12
'@esbuild/darwin-x64': 0.19.12
'@esbuild/freebsd-arm64': 0.19.12
'@esbuild/freebsd-x64': 0.19.12
'@esbuild/linux-arm': 0.19.12
'@esbuild/linux-arm64': 0.19.12
'@esbuild/linux-ia32': 0.19.12
'@esbuild/linux-loong64': 0.19.12
'@esbuild/linux-mips64el': 0.19.12
'@esbuild/linux-ppc64': 0.19.12
'@esbuild/linux-riscv64': 0.19.12
'@esbuild/linux-s390x': 0.19.12
'@esbuild/linux-x64': 0.19.12
'@esbuild/netbsd-x64': 0.19.12
'@esbuild/openbsd-x64': 0.19.12
'@esbuild/sunos-x64': 0.19.12
'@esbuild/win32-arm64': 0.19.12
'@esbuild/win32-ia32': 0.19.12
'@esbuild/win32-x64': 0.19.12
dev: false
/escalade/3.1.1:
resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
engines: {node: '>=6'}
dev: true
/esniff/2.0.1:
resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==}
engines: {node: '>=0.10'}
dependencies:
d: 1.0.2
es5-ext: 0.10.64
event-emitter: 0.3.5
type: 2.7.3
dev: false
/estree-walker/2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
dev: false
/event-emitter/0.3.5:
resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==}
dependencies:
d: 1.0.2
es5-ext: 0.10.64
dev: false
/execa/5.1.1:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'}
@ -984,6 +1346,12 @@ packages:
strip-final-newline: 2.0.0
dev: true
/ext/1.7.0:
resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==}
dependencies:
type: 2.7.3
dev: false
/fast-glob/3.2.12:
resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==}
engines: {node: '>=8.6.0'}
@ -1106,6 +1474,14 @@ packages:
path-is-absolute: 1.0.1
dev: true
/globalyzer/0.1.0:
resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==}
dev: false
/globrex/0.1.2:
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
dev: false
/graceful-fs/4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
dev: true
@ -1156,6 +1532,15 @@ packages:
engines: {node: '>= 0.10'}
dev: true
/intl-messageformat/10.5.14:
resolution: {integrity: sha512-IjC6sI0X7YRjjyVH9aUgdftcmZK7WXdHeil4KwbjDnRWjnVitKpAx3rr6t6di1joFp5188VqKcobOPA6mCLG/w==}
dependencies:
'@formatjs/ecma402-abstract': 2.0.0
'@formatjs/fast-memoize': 2.2.0
'@formatjs/icu-messageformat-parser': 2.7.8
tslib: 2.5.3
dev: false
/ip-regex/4.3.0:
resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==}
engines: {node: '>=8'}
@ -1203,6 +1588,10 @@ packages:
engines: {node: '>=0.12.0'}
dev: true
/is-promise/2.2.2:
resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==}
dev: false
/is-stream/2.0.1:
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
engines: {node: '>=8'}
@ -1231,6 +1620,12 @@ packages:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
dev: true
/lru-queue/0.1.0:
resolution: {integrity: sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==}
dependencies:
es5-ext: 0.10.64
dev: false
/magic-string/0.27.0:
resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==}
engines: {node: '>=12'}
@ -1253,6 +1648,20 @@ packages:
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
dev: true
/memoizee/0.4.17:
resolution: {integrity: sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==}
engines: {node: '>=0.12'}
dependencies:
d: 1.0.2
es5-ext: 0.10.64
es6-weak-map: 2.0.3
event-emitter: 0.3.5
is-promise: 2.2.2
lru-queue: 0.1.0
next-tick: 1.1.0
timers-ext: 0.1.8
dev: false
/merge-stream/2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
dev: true
@ -1305,7 +1714,6 @@ packages:
/mri/1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'}
dev: true
/ms/2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
@ -1324,6 +1732,10 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
/next-tick/1.1.0:
resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==}
dev: false
/node-gzip/1.1.2:
resolution: {integrity: sha512-ZB6zWpfZHGtxZnPMrJSKHVPrRjURoUzaDbLFj3VO70mpLTW5np96vXyHwft4Id0o+PYIzgDkBUjIzaNHhQ8srw==}
dev: true
@ -1509,6 +1921,21 @@ packages:
picocolors: 1.0.0
source-map-js: 1.0.2
/prettier-plugin-svelte/3.2.5_prettier@3.3.2:
resolution: {integrity: sha512-vP/M/Goc8z4iVIvrwXwbrYVjJgA0Hf8PO1G4LBh/ocSt6vUP6sLvyu9F3ABEGr+dbKyxZjEKLkeFsWy/yYl0HQ==}
peerDependencies:
prettier: ^3.0.0
svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0
dependencies:
prettier: 3.3.2
dev: true
/prettier/3.3.2:
resolution: {integrity: sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==}
engines: {node: '>=14'}
hasBin: true
dev: true
/queue-microtask/1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
dev: true
@ -1582,7 +2009,6 @@ packages:
engines: {node: '>=6'}
dependencies:
mri: 1.2.0
dev: true
/sander/0.5.1:
resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==}
@ -1717,6 +2143,23 @@ packages:
svelte: 3.59.1
dev: true
/svelte-i18n/4.0.0_svelte@3.59.1:
resolution: {integrity: sha512-4vivjKZADUMRIhTs38JuBNy3unbnh9AFRxWFLxq62P4NHic+/BaIZZlAsvqsCdnp7IdJf5EoSiH6TNdItcjA6g==}
engines: {node: '>= 16'}
hasBin: true
peerDependencies:
svelte: ^3 || ^4
dependencies:
cli-color: 2.0.4
deepmerge: 4.3.1
esbuild: 0.19.12
estree-walker: 2.0.2
intl-messageformat: 10.5.14
sade: 1.8.1
svelte: 3.59.1
tiny-glob: 0.2.9
dev: false
/svelte-preprocess/5.0.4_4klotfyqh3bzvqmua74kcysa7a:
resolution: {integrity: sha512-ABia2QegosxOGsVlsSBJvoWeXy1wUKSfF7SWJdTjLAbx/Y3SrVevvvbFNQqrSJw89+lNSsM58SipmZJ5SRi5iw==}
engines: {node: '>= 14.10.0'}
@ -1941,6 +2384,21 @@ packages:
any-promise: 1.3.0
dev: true
/timers-ext/0.1.8:
resolution: {integrity: sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==}
engines: {node: '>=0.12'}
dependencies:
es5-ext: 0.10.64
next-tick: 1.1.0
dev: false
/tiny-glob/0.2.9:
resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==}
dependencies:
globalyzer: 0.1.0
globrex: 0.1.2
dev: false
/to-regex-range/5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@ -1954,7 +2412,10 @@ packages:
/tslib/2.5.3:
resolution: {integrity: sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==}
dev: true
/type/2.7.3:
resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==}
dev: false
/typescript/4.9.5:
resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==}

Loading…
Cancel
Save