Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cb3ef82b2 | ||
|
|
025562f9f0 | ||
|
|
595bcc9b3c | ||
|
|
15991d07ab | ||
|
|
99fc1f0b10 | ||
|
|
1041e1ccd4 | ||
|
|
2eeb2c896d | ||
|
|
caf8fb584a | ||
|
|
58205713ab | ||
|
|
33802d32f3 |
@@ -1,5 +1,8 @@
|
|||||||
name: 'Publish'
|
name: 'publish'
|
||||||
on: workflow_dispatch
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- release
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_INCREMENTAL: 0
|
CARGO_INCREMENTAL: 0
|
||||||
@@ -14,7 +17,7 @@ jobs:
|
|||||||
settings:
|
settings:
|
||||||
- platform: 'macos-latest'
|
- platform: 'macos-latest'
|
||||||
args: '--target universal-apple-darwin'
|
args: '--target universal-apple-darwin'
|
||||||
- platform: 'ubuntu-22.04'
|
- platform: 'ubuntu-20.04'
|
||||||
args: ''
|
args: ''
|
||||||
- platform: 'windows-latest'
|
- platform: 'windows-latest'
|
||||||
args: '--target x86_64-pc-windows-msvc'
|
args: '--target x86_64-pc-windows-msvc'
|
||||||
@@ -29,14 +32,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
targets: aarch64-apple-darwin
|
targets: aarch64-apple-darwin
|
||||||
- name: install dependencies (ubuntu only)
|
- name: install dependencies (ubuntu only)
|
||||||
if: matrix.settings.platform == 'ubuntu-22.04'
|
if: matrix.settings.platform == 'ubuntu-20.04'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y build-essential libssl-dev javascriptcoregtk-4.1 libayatana-appindicator3-dev libsoup-3.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev webkit2gtk-4.1 librsvg2-dev patchelf
|
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v2
|
uses: pnpm/action-setup@v2
|
||||||
with:
|
with:
|
||||||
version: 8.x.x
|
version: 7.x.x
|
||||||
run_install: false
|
run_install: false
|
||||||
- name: Setup node and cache for package data
|
- name: Setup node and cache for package data
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
@@ -59,12 +62,10 @@ jobs:
|
|||||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
||||||
with:
|
with:
|
||||||
tagName: v__VERSION__
|
tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version
|
||||||
releaseName: 'App v__VERSION__'
|
releaseName: 'App v__VERSION__'
|
||||||
releaseBody: 'See the assets to download this version and install.'
|
releaseBody: 'See the assets to download this version and install.'
|
||||||
releaseDraft: true
|
releaseDraft: true
|
||||||
prerelease: false
|
prerelease: false
|
||||||
args: ${{ matrix.settings.args }}
|
args: ${{ matrix.settings.args }}
|
||||||
includeDebug: true
|
|
||||||
|
|||||||
@@ -14,11 +14,9 @@ out
|
|||||||
*.local
|
*.local
|
||||||
.next
|
.next
|
||||||
.vscode
|
.vscode
|
||||||
|
pnpm-lock.yaml
|
||||||
*.db
|
*.db
|
||||||
*.db-journal
|
*.db-journal
|
||||||
bun.lockb
|
|
||||||
|
|
||||||
.direnv
|
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|||||||
@@ -8,12 +8,6 @@ Download Lume for your platform here: [https://github.com/luminous-devs/lume/rel
|
|||||||
|
|
||||||
Supported platform: macOS, Windows and Linux
|
Supported platform: macOS, Windows and Linux
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- PNPM or Bun (experiment)
|
|
||||||
|
|
||||||
- Tauri: https://tauri.app/v1/guides/getting-started/prerequisites#setting-up-macos
|
|
||||||
|
|
||||||
### Develop
|
### Develop
|
||||||
|
|
||||||
Clone project
|
Clone project
|
||||||
@@ -28,23 +22,20 @@ Install packages
|
|||||||
pnpm install
|
pnpm install
|
||||||
```
|
```
|
||||||
|
|
||||||
Run dev build
|
Run dev
|
||||||
|
|
||||||
```
|
```
|
||||||
pnpm tauri dev
|
pnpm tauri dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Generate production build
|
Build
|
||||||
|
|
||||||
```
|
```
|
||||||
pnpm tauri build
|
pnpm tauri build
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Nix
|
(Advance) - Generate SQLite migration
|
||||||
|
|
||||||
Requirements:
|
```
|
||||||
|
pnpm add-migrate <migrate_name>
|
||||||
1. [Install Nix](https://zero-to-flakes.com/install)
|
```
|
||||||
1. [Setup `direnv`](https://zero-to-flakes.com/direnv)
|
|
||||||
|
|
||||||
`cd` into the root folder of the project to enter `nix develop` shell. Run `direnv allow` (only once). Then run `pnpm` or `bun` (experimental) commands as described above.
|
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"flake-utils": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1694529238,
|
|
||||||
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"flake-utils_2": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems_2"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681202837,
|
|
||||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1697723726,
|
|
||||||
"narHash": "sha256-SaTWPkI8a5xSHX/rrKzUe+/uVNy6zCGMXgoeMb7T9rg=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "7c9cc5a6e5d38010801741ac830a3f8fd667a7a0",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixos-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs_2": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681358109,
|
|
||||||
"narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixpkgs-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-utils": "flake-utils",
|
|
||||||
"nixpkgs": "nixpkgs",
|
|
||||||
"rust-overlay": "rust-overlay"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rust-overlay": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-utils": "flake-utils_2",
|
|
||||||
"nixpkgs": "nixpkgs_2"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1697940838,
|
|
||||||
"narHash": "sha256-eyk92QqAoRNC0V99KOcKcBZjLPixxNBS0PRc4KlSQVs=",
|
|
||||||
"owner": "oxalica",
|
|
||||||
"repo": "rust-overlay",
|
|
||||||
"rev": "a3e829c06eadf848f13d109c7648570ce37ebccd",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "oxalica",
|
|
||||||
"repo": "rust-overlay",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"systems": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"systems_2": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
# Nix.flake to build Lume based on Tauri's Guides:
|
|
||||||
# Prerequisites -> Installing -> Setting Up Linux -> NixOS
|
|
||||||
# https://tauri.app/v1/guides/getting-started/prerequisites/#1-system-dependencies
|
|
||||||
#
|
|
||||||
# To build Rust backend of Tauri `rust-overlay` is used
|
|
||||||
# https://github.com/oxalica/rust-overlay
|
|
||||||
|
|
||||||
{
|
|
||||||
inputs = {
|
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
|
||||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
|
||||||
};
|
|
||||||
|
|
||||||
outputs = { self, nixpkgs, flake-utils, rust-overlay }:
|
|
||||||
flake-utils.lib.eachDefaultSystem (system:
|
|
||||||
let
|
|
||||||
overlays = [ (import rust-overlay) ];
|
|
||||||
pkgs = import nixpkgs {
|
|
||||||
inherit system overlays;
|
|
||||||
};
|
|
||||||
|
|
||||||
libraries = with pkgs;[
|
|
||||||
webkitgtk
|
|
||||||
gtk3
|
|
||||||
cairo
|
|
||||||
gdk-pixbuf
|
|
||||||
glib
|
|
||||||
dbus
|
|
||||||
openssl_3
|
|
||||||
librsvg
|
|
||||||
];
|
|
||||||
|
|
||||||
packages = with pkgs; [
|
|
||||||
curl
|
|
||||||
wget
|
|
||||||
pkg-config
|
|
||||||
dbus
|
|
||||||
openssl_3
|
|
||||||
glib
|
|
||||||
gtk3
|
|
||||||
libsoup
|
|
||||||
webkitgtk
|
|
||||||
librsvg
|
|
||||||
];
|
|
||||||
|
|
||||||
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
|
|
||||||
extensions = [ "rust-src" ]; # needed by rust-analyzer
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
devShells.default = pkgs.mkShell {
|
|
||||||
buildInputs = [
|
|
||||||
rustToolchain
|
|
||||||
pkgs.nodejs
|
|
||||||
pkgs.nodePackages.pnpm
|
|
||||||
pkgs.bun # experimental in Lume
|
|
||||||
] ++ packages;
|
|
||||||
|
|
||||||
shellHook =
|
|
||||||
''
|
|
||||||
export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath libraries}:$LD_LIBRARY_PATH
|
|
||||||
export XDG_DATA_DIRS=${pkgs.gsettings-desktop-schemas}/share/gsettings-schemas/${pkgs.gsettings-desktop-schemas.name}:${pkgs.gtk3}/share/gsettings-schemas/${pkgs.gtk3.name}:$XDG_DATA_DIRS
|
|
||||||
'';
|
|
||||||
|
|
||||||
# Avoid white screen running with Nix
|
|
||||||
# https://github.com/tauri-apps/tauri/issues/4315#issuecomment-1207755694
|
|
||||||
WEBKIT_DISABLE_COMPOSITING_MODE = 1;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
<html lang="en">
|
<html lang="en" class="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Lume</title>
|
<title>Lume</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="relative cursor-default select-none overflow-hidden font-sans antialiased h-screen w-screen text-neutral-950 dark:text-neutral-50">
|
<body class="relative cursor-default select-none overflow-hidden font-sans antialiased h-screen w-screen text-white">
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.jsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "lume",
|
"name": "lume",
|
||||||
"description": "the communication app",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.0.0",
|
"version": "1.2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
@@ -18,109 +17,99 @@
|
|||||||
"**/*.{ts, tsx, css, md, html, json}": "prettier --cache --write"
|
"**/*.{ts, tsx, css, md, html, json}": "prettier --cache --write"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@evilmartians/harmony": "^1.1.0",
|
"@ctrl/magnet-link": "^3.1.2",
|
||||||
"@getalby/sdk": "^2.5.0",
|
"@headlessui/react": "^1.7.16",
|
||||||
"@nostr-dev-kit/ndk": "^2.0.3",
|
"@nostr-dev-kit/ndk": "^0.8.11",
|
||||||
"@nostr-dev-kit/ndk-cache-dexie": "^2.0.3",
|
"@nostr-fetch/adapter-ndk": "^0.11.0",
|
||||||
"@nostr-fetch/adapter-ndk": "^0.13.0",
|
"@radix-ui/react-alert-dialog": "^1.0.4",
|
||||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
|
||||||
"@radix-ui/react-avatar": "^1.0.4",
|
|
||||||
"@radix-ui/react-collapsible": "^1.0.3",
|
"@radix-ui/react-collapsible": "^1.0.3",
|
||||||
"@radix-ui/react-dialog": "^1.0.5",
|
"@radix-ui/react-dialog": "^1.0.4",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
"@radix-ui/react-popover": "^1.0.6",
|
||||||
"@radix-ui/react-hover-card": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.0.6",
|
||||||
"@radix-ui/react-popover": "^1.0.7",
|
"@tanstack/react-query": "^4.32.6",
|
||||||
"@radix-ui/react-toolbar": "^1.0.4",
|
"@tanstack/react-query-devtools": "^4.32.6",
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@tanstack/react-virtual": "3.0.0-beta.54",
|
||||||
"@tanstack/react-query": "4.36.1",
|
"@tauri-apps/api": "2.0.0-alpha.5",
|
||||||
"@tauri-apps/api": "2.0.0-alpha.8",
|
"@tauri-apps/plugin-app": "github:tauri-apps/tauri-plugin-app#v2",
|
||||||
"@tauri-apps/cli": "2.0.0-alpha.15",
|
"@tauri-apps/plugin-autostart": "github:tauri-apps/tauri-plugin-autostart#v2",
|
||||||
"@tauri-apps/plugin-app": "2.0.0-alpha.1",
|
"@tauri-apps/plugin-clipboard-manager": "github:tauri-apps/tauri-plugin-clipboard-manager#v2",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "2.0.0-alpha.1",
|
"@tauri-apps/plugin-dialog": "github:tauri-apps/tauri-plugin-dialog#v2",
|
||||||
"@tauri-apps/plugin-dialog": "2.0.0-alpha.1",
|
"@tauri-apps/plugin-fs": "github:tauri-apps/tauri-plugin-fs#v2",
|
||||||
"@tauri-apps/plugin-fs": "2.0.0-alpha.1",
|
"@tauri-apps/plugin-http": "github:tauri-apps/tauri-plugin-http#v2",
|
||||||
"@tauri-apps/plugin-http": "2.0.0-alpha.1",
|
"@tauri-apps/plugin-notification": "github:tauri-apps/tauri-plugin-notification#v2",
|
||||||
"@tauri-apps/plugin-notification": "2.0.0-alpha.1",
|
"@tauri-apps/plugin-os": "github:tauri-apps/tauri-plugin-os#v2",
|
||||||
"@tauri-apps/plugin-os": "2.0.0-alpha.2",
|
"@tauri-apps/plugin-process": "github:tauri-apps/tauri-plugin-process#v2",
|
||||||
"@tauri-apps/plugin-process": "2.0.0-alpha.1",
|
"@tauri-apps/plugin-shell": "github:tauri-apps/tauri-plugin-shell#v2",
|
||||||
"@tauri-apps/plugin-shell": "2.0.0-alpha.1",
|
"@tauri-apps/plugin-sql": "github:tauri-apps/tauri-plugin-sql#v2",
|
||||||
"@tauri-apps/plugin-sql": "2.0.0-alpha.1",
|
"@tauri-apps/plugin-store": "github:tauri-apps/tauri-plugin-store#v2",
|
||||||
"@tauri-apps/plugin-updater": "2.0.0-alpha.1",
|
"@tauri-apps/plugin-stronghold": "github:tauri-apps/tauri-plugin-stronghold#v2",
|
||||||
"@tauri-apps/plugin-upload": "2.0.0-alpha.1",
|
"@tauri-apps/plugin-upload": "github:tauri-apps/tauri-plugin-upload#v2",
|
||||||
"@tauri-apps/plugin-window": "2.0.0-alpha.1",
|
"@tauri-apps/plugin-window": "2.0.0-alpha.0",
|
||||||
"@tiptap/extension-character-count": "^2.1.12",
|
"@tiptap/extension-image": "^2.0.4",
|
||||||
"@tiptap/extension-document": "^2.1.12",
|
"@tiptap/extension-mention": "^2.0.4",
|
||||||
"@tiptap/extension-image": "^2.1.12",
|
"@tiptap/extension-placeholder": "^2.0.4",
|
||||||
"@tiptap/extension-mention": "^2.1.12",
|
"@tiptap/pm": "^2.0.4",
|
||||||
"@tiptap/extension-paragraph": "^2.1.12",
|
"@tiptap/react": "^2.0.4",
|
||||||
"@tiptap/extension-placeholder": "^2.1.12",
|
"@tiptap/starter-kit": "^2.0.4",
|
||||||
"@tiptap/extension-text": "^2.1.12",
|
"@tiptap/suggestion": "^2.0.4",
|
||||||
"@tiptap/pm": "^2.1.12",
|
"@void-cat/api": "^1.0.7",
|
||||||
"@tiptap/react": "^2.1.12",
|
"cheerio": "1.0.0-rc.12",
|
||||||
"@tiptap/starter-kit": "^2.1.12",
|
"dayjs": "^1.11.9",
|
||||||
"@tiptap/suggestion": "^2.1.12",
|
"destr": "^1.2.2",
|
||||||
"dayjs": "^1.11.10",
|
"get-urls": "^11.0.0",
|
||||||
"destr": "^2.0.1",
|
|
||||||
"framer-motion": "^10.16.4",
|
|
||||||
"html-to-text": "^9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
|
"immer": "^10.0.2",
|
||||||
"light-bolt11-decoder": "^3.0.0",
|
"light-bolt11-decoder": "^3.0.0",
|
||||||
"lru-cache": "^10.0.1",
|
"lru-cache": "^10.0.1",
|
||||||
"media-chrome": "^1.4.4",
|
"nostr-fetch": "^0.12.2",
|
||||||
"million": "^2.6.4",
|
"nostr-tools": "^1.14.0",
|
||||||
"minidenticons": "^4.2.0",
|
|
||||||
"nostr-fetch": "^0.13.0",
|
|
||||||
"nostr-tools": "^1.17.0",
|
|
||||||
"qrcode.react": "^3.1.0",
|
"qrcode.react": "^3.1.0",
|
||||||
"re-resizable": "^6.9.11",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-currency-input-field": "^3.6.11",
|
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.47.0",
|
"react-hook-form": "^7.45.4",
|
||||||
"react-hotkeys-hook": "^4.4.1",
|
"react-hotkeys-hook": "^4.4.1",
|
||||||
"react-markdown": "^8.0.7",
|
"react-markdown": "^8.0.7",
|
||||||
"react-router-dom": "^6.17.0",
|
"react-player": "^2.12.0",
|
||||||
|
"react-router-dom": "^6.15.0",
|
||||||
"react-string-replace": "^1.1.1",
|
"react-string-replace": "^1.1.1",
|
||||||
"reactflow": "^11.9.4",
|
"react-virtuoso": "^4.5.0",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"sonner": "^1.0.3",
|
|
||||||
"tailwind-scrollbar": "^3.0.5",
|
|
||||||
"tauri-controls": "^0.2.0",
|
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"tiptap-markdown": "^0.8.2",
|
"zustand": "^4.4.1"
|
||||||
"virtua": "^0.15.0",
|
|
||||||
"zustand": "^4.4.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.10",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
|
"@tauri-apps/cli": "2.0.0-alpha.10",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
|
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
|
||||||
"@types/html-to-text": "^9.0.3",
|
"@types/html-to-text": "^9.0.1",
|
||||||
"@types/node": "^20.8.7",
|
"@types/node": "^18.17.5",
|
||||||
"@types/react": "^18.2.29",
|
"@types/react": "^18.2.20",
|
||||||
"@types/react-dom": "^18.2.14",
|
"@types/react-dom": "^18.2.7",
|
||||||
"@types/youtube-player": "^5.5.9",
|
"@types/youtube-player": "^5.5.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.8.0",
|
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||||
"@typescript-eslint/parser": "^6.8.0",
|
"@typescript-eslint/parser": "^5.62.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.4.0",
|
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.14",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"csstype": "^3.1.2",
|
"csstype": "^3.1.2",
|
||||||
"encoding": "^0.1.13",
|
"encoding": "^0.1.13",
|
||||||
"eslint": "^8.51.0",
|
"eslint": "^8.47.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^8.10.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||||
"eslint-plugin-react": "^7.33.2",
|
"eslint-plugin-react": "^7.33.1",
|
||||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"lint-staged": "^14.0.1",
|
"lint-staged": "^13.2.3",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.27",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^2.8.8",
|
||||||
"prettier-plugin-tailwindcss": "^0.5.6",
|
"prettier-plugin-tailwindcss": "^0.3.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"tailwind-merge": "^1.14.0",
|
"tailwind-merge": "^1.14.0",
|
||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^3.3.3",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^4.9.5",
|
||||||
"vite": "^4.5.0",
|
"vite": "^4.4.9",
|
||||||
"vite-tsconfig-paths": "^4.2.1"
|
"vite-plugin-top-level-await": "^1.3.1",
|
||||||
|
"vite-tsconfig-paths": "^4.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 341 KiB |
@@ -1,47 +1,52 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lume"
|
name = "lume"
|
||||||
version = "2.0.0"
|
version = "1.2.0"
|
||||||
description = "the communication app"
|
description = "nostr client"
|
||||||
authors = ["Ren Amamiya"]
|
authors = ["Ren Amamiya"]
|
||||||
license = "GPL-3.0"
|
license = ""
|
||||||
repository = "https://github.com/luminous-devs/lume"
|
repository = ""
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.66"
|
rust-version = "1.71"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2.0.0-alpha", features = [] }
|
tauri-build = { version = "2.0.0-alpha.6", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
tauri = { version = "2.0.0-alpha", features = [
|
tauri = { version = "2.0.0-alpha.10", features = [
|
||||||
"macos-private-api",
|
"macos-private-api",
|
||||||
"native-tls-vendored",
|
"system-tray",
|
||||||
] }
|
] }
|
||||||
tauri-plugin-app = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
|
||||||
tauri-plugin-cli = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
|
||||||
tauri-plugin-clipboard-manager = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
|
||||||
tauri-plugin-dialog = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
|
||||||
tauri-plugin-fs = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
|
||||||
tauri-plugin-http = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
|
||||||
tauri-plugin-notification = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
|
||||||
tauri-plugin-os = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
|
||||||
tauri-plugin-process = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
|
||||||
tauri-plugin-shell = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
|
||||||
tauri-plugin-updater = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
|
||||||
tauri-plugin-window = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
|
||||||
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||||
tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||||
tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
tauri-plugin-stronghold = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||||
tauri-plugin-upload = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
tauri-plugin-upload = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||||
tauri-plugin-sql = { git = "hhttps://github.com/tauri-apps/plugins-workspace", branch = "v2", features = [
|
tauri-plugin-updater = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||||
"sqlite",
|
tauri-plugin-dialog = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||||
] }
|
tauri-plugin-http = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||||
|
tauri-plugin-fs = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||||
|
tauri-plugin-clipboard-manager = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||||
|
tauri-plugin-notification = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||||
|
tauri-plugin-app = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||||
|
tauri-plugin-process = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||||
|
tauri-plugin-os = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||||
|
tauri-plugin-window = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||||
|
tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||||
|
tauri-plugin-shell = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||||
|
window-vibrancy = { git = "https://github.com/tauri-apps/window-vibrancy", branch = "dev" }
|
||||||
sqlx-cli = { version = "0.7.0", default-features = false, features = [
|
sqlx-cli = { version = "0.7.0", default-features = false, features = [
|
||||||
"sqlite",
|
"sqlite",
|
||||||
] }
|
] }
|
||||||
webpage = { version = "1.6.0", features = ["serde"] }
|
rust-argon2 = "1.0"
|
||||||
keyring = "2"
|
rand = "0.8.5"
|
||||||
|
|
||||||
|
[dependencies.tauri-plugin-sql]
|
||||||
|
git = "https://github.com/tauri-apps/plugins-workspace"
|
||||||
|
branch = "v2"
|
||||||
|
features = ["sqlite"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# by default Tauri runs in production mode
|
# by default Tauri runs in production mode
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 886 B After Width: | Height: | Size: 731 B |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 894 B After Width: | Height: | Size: 709 B |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 921 B |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 536 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 828 B |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
@@ -1,29 +1,45 @@
|
|||||||
|
-- Add migration script here
|
||||||
-- create accounts table
|
-- create accounts table
|
||||||
|
-- is_active (multi-account feature), value:
|
||||||
|
-- 0: false
|
||||||
|
-- 1: true
|
||||||
CREATE TABLE
|
CREATE TABLE
|
||||||
accounts (
|
accounts (
|
||||||
id TEXT NOT NULL PRIMARY KEY,
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
npub TEXT NOT NULL UNIQUE,
|
||||||
pubkey TEXT NOT NULL UNIQUE,
|
pubkey TEXT NOT NULL UNIQUE,
|
||||||
follows TEXT,
|
privkey TEXT NOT NULL,
|
||||||
circles TEXT,
|
follows JSON,
|
||||||
is_active INTEGER NOT NULL DEFAULT 0,
|
is_active INTEGER NOT NULL DEFAULT 0,
|
||||||
last_login_at NUMBER NOT NULL DEFAULT 0,
|
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
-- create notes table
|
-- create notes table
|
||||||
CREATE TABLE
|
CREATE TABLE
|
||||||
events (
|
notes (
|
||||||
id TEXT NOT NULL PRIMARY KEY,
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
event_id TEXT NOT NULL UNIQUE,
|
||||||
account_id INTEGER NOT NULL,
|
account_id INTEGER NOT NULL,
|
||||||
event TEXT NOT NULL,
|
pubkey TEXT NOT NULL,
|
||||||
author TEXT NOT NULL,
|
kind INTEGER NOT NULL DEFAULT 1,
|
||||||
kind NUMBER NOT NULL DEFAULt 1,
|
tags JSON,
|
||||||
root_id TEXT,
|
content TEXT NOT NULL,
|
||||||
reply_id TEXT,
|
|
||||||
created_at INTEGER NOT NULL,
|
created_at INTEGER NOT NULL,
|
||||||
|
parent_id TEXT,
|
||||||
FOREIGN KEY (account_id) REFERENCES accounts (id)
|
FOREIGN KEY (account_id) REFERENCES accounts (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- create channels table
|
||||||
|
CREATE TABLE
|
||||||
|
channels (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
event_id TEXT NOT NULL UNIQUE,
|
||||||
|
name TEXT,
|
||||||
|
about TEXT,
|
||||||
|
picture TEXT,
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
-- create settings table
|
-- create settings table
|
||||||
CREATE TABLE
|
CREATE TABLE
|
||||||
settings (
|
settings (
|
||||||
@@ -33,23 +49,11 @@ CREATE TABLE
|
|||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- create metadata table
|
||||||
CREATE TABLE
|
CREATE TABLE
|
||||||
widgets (
|
metadata (
|
||||||
id INTEGER NOT NULL PRIMARY KEY,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
account_id INTEGER NOT NULL,
|
pubkey TEXT NOT NULL,
|
||||||
kind INTEGER NOT NULL,
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
content TEXT NOT NULL,
|
content TEXT NOT NULL,
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
FOREIGN KEY (account_id) REFERENCES accounts (id)
|
);
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE
|
|
||||||
relays (
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY,
|
|
||||||
account_id INTEGER NOT NULL,
|
|
||||||
relay TEXT NOT NULL UNIQUE,
|
|
||||||
purpose TEXT NOT NULL DEFAULT '',
|
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (account_id) REFERENCES accounts (id)
|
|
||||||
);
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
-- create chats table
|
||||||
|
CREATE TABLE
|
||||||
|
chats (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
event_id TEXT NOT NULL UNIQUE,
|
||||||
|
receiver_pubkey INTEGER NOT NULL,
|
||||||
|
sender_pubkey TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
tags JSON,
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
INSERT INTO
|
||||||
|
settings (key, value)
|
||||||
|
VALUES
|
||||||
|
("last_login", "0"),
|
||||||
|
(
|
||||||
|
"relays",
|
||||||
|
'["wss://relayable.org","wss://relay.damus.io","wss://relay.nostr.band/all","wss://relay.nostrgraph.net","wss://nostr.mutinywallet.com"]'
|
||||||
|
),
|
||||||
|
("auto_start", "0"),
|
||||||
|
("cache_time", "86400000"),
|
||||||
|
("compose_shortcut", "meta+n"),
|
||||||
|
("add_imageblock_shortcut", "meta+i"),
|
||||||
|
("add_feedblock_shortcut", "meta+f")
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
-- add pubkey to channel
|
||||||
|
ALTER TABLE channels ADD pubkey TEXT NOT NULL DEFAULT '';
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
INSERT
|
||||||
|
OR IGNORE INTO channels (
|
||||||
|
event_id,
|
||||||
|
pubkey,
|
||||||
|
name,
|
||||||
|
about,
|
||||||
|
picture,
|
||||||
|
created_at
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
"e3cadf5beca1b2af1cddaa41a633679bedf263e3de1eb229c6686c50d85df753",
|
||||||
|
"126103bfddc8df256b6e0abfd7f3797c80dcc4ea88f7c2f87dd4104220b4d65f",
|
||||||
|
"lume-general",
|
||||||
|
"General channel for Lume",
|
||||||
|
"https://void.cat/d/UNyxBmAh1MUx5gQTX95jyf.webp",
|
||||||
|
1681898574
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT
|
||||||
|
OR IGNORE INTO channels (
|
||||||
|
event_id,
|
||||||
|
pubkey,
|
||||||
|
name,
|
||||||
|
about,
|
||||||
|
picture,
|
||||||
|
created_at
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
"25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb",
|
||||||
|
"ed1d0e1f743a7d19aa2dfb0162df73bacdbc699f67cc55bb91a98c35f7deac69",
|
||||||
|
"Nostr",
|
||||||
|
"",
|
||||||
|
"https://cloudflare-ipfs.com/ipfs/QmTN4Eas9atUULVbEAbUU8cowhtvK7g3t7jfKztY7wc8eP?.png",
|
||||||
|
1661333723
|
||||||
|
);
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
-- create blacklist table
|
||||||
|
CREATE TABLE
|
||||||
|
blacklist (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
account_id INTEGER NOT NULL,
|
||||||
|
content TEXT NOT NULL UNIQUE,
|
||||||
|
status INTEGER NOT NULL DEFAULT 0,
|
||||||
|
kind INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (account_id) REFERENCES accounts (id)
|
||||||
|
);
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
CREATE TABLE
|
||||||
|
blocks (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
account_id INTEGER NOT NULL,
|
||||||
|
kind INTEGER NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (account_id) REFERENCES accounts (id)
|
||||||
|
);
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
CREATE TABLE
|
||||||
|
channel_messages (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
channel_id TEXT NOT NULL,
|
||||||
|
event_id TEXT NOT NULL UNIQUE,
|
||||||
|
pubkey TEXT NOT NULL,
|
||||||
|
kind INTEGER NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
tags JSON,
|
||||||
|
mute BOOLEAN DEFAULT 0,
|
||||||
|
hide BOOLEAN DEFAULT 0,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (channel_id) REFERENCES channels (event_id)
|
||||||
|
);
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
CREATE TABLE
|
||||||
|
replies (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
parent_id TEXT NOT NULL,
|
||||||
|
event_id TEXT NOT NULL UNIQUE,
|
||||||
|
pubkey TEXT NOT NULL,
|
||||||
|
kind INTEGER NOT NULL DEFAULT 1,
|
||||||
|
tags JSON,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (parent_id) REFERENCES notes (event_id)
|
||||||
|
);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
DROP TABLE IF EXISTS blacklist;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS channel_messages;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS channels;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
UPDATE settings
|
||||||
|
SET
|
||||||
|
value = '["wss://relayable.org","wss://relay.damus.io","wss://relay.nostr.band/all","wss://nostr.mutinywallet.com"]'
|
||||||
|
WHERE
|
||||||
|
key = 'relays';
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
ALTER TABLE accounts ADD network JSON;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
CREATE TABLE
|
||||||
|
relays (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
account_id INTEGER NOT NULL,
|
||||||
|
relay TEXT NOT NULL,
|
||||||
|
purpose TEXT NOT NULL DEFAULT '',
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (account_id) REFERENCES accounts (id)
|
||||||
|
);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
ALTER TABLE blocks
|
||||||
|
RENAME TO widgets;
|
||||||
@@ -3,12 +3,11 @@
|
|||||||
windows_subsystem = "windows"
|
windows_subsystem = "windows"
|
||||||
)]
|
)]
|
||||||
|
|
||||||
use keyring::Entry;
|
// use rand::distributions::{Alphanumeric, DistString};
|
||||||
use std::time::Duration;
|
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
use tauri_plugin_autostart::MacosLauncher;
|
use tauri_plugin_autostart::MacosLauncher;
|
||||||
use tauri_plugin_sql::{Migration, MigrationKind};
|
use tauri_plugin_sql::{Migration, MigrationKind};
|
||||||
use webpage::{Webpage, WebpageOptions};
|
use window_vibrancy::{apply_mica, apply_vibrancy, NSVisualEffectMaterial};
|
||||||
|
|
||||||
#[derive(Clone, serde::Serialize)]
|
#[derive(Clone, serde::Serialize)]
|
||||||
struct Payload {
|
struct Payload {
|
||||||
@@ -16,129 +15,134 @@ struct Payload {
|
|||||||
cwd: String,
|
cwd: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
|
||||||
struct OpenGraphResponse {
|
|
||||||
title: String,
|
|
||||||
description: String,
|
|
||||||
url: String,
|
|
||||||
image: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_opengraph(url: String) -> OpenGraphResponse {
|
|
||||||
let options = WebpageOptions {
|
|
||||||
allow_insecure: false,
|
|
||||||
max_redirections: 3,
|
|
||||||
timeout: Duration::from_secs(15),
|
|
||||||
useragent: "lume - desktop app".to_string(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = match Webpage::from_url(&url, options) {
|
|
||||||
Ok(webpage) => webpage,
|
|
||||||
Err(_) => {
|
|
||||||
return OpenGraphResponse {
|
|
||||||
title: "".to_string(),
|
|
||||||
description: "".to_string(),
|
|
||||||
url: "".to_string(),
|
|
||||||
image: "".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let html = result.html;
|
|
||||||
|
|
||||||
return OpenGraphResponse {
|
|
||||||
title: html
|
|
||||||
.opengraph
|
|
||||||
.properties
|
|
||||||
.get("title")
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_default(),
|
|
||||||
description: html
|
|
||||||
.opengraph
|
|
||||||
.properties
|
|
||||||
.get("description")
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_default(),
|
|
||||||
url: html
|
|
||||||
.opengraph
|
|
||||||
.properties
|
|
||||||
.get("url")
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_default(),
|
|
||||||
image: html
|
|
||||||
.opengraph
|
|
||||||
.images
|
|
||||||
.get(0)
|
|
||||||
.and_then(|i| Some(i.url.clone()))
|
|
||||||
.unwrap_or_default(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn opengraph(url: String) -> OpenGraphResponse {
|
async fn close_splashscreen(window: tauri::Window) {
|
||||||
let result = fetch_opengraph(url).await;
|
// Close splashscreen
|
||||||
return result;
|
if let Some(splashscreen) = window.get_window("splashscreen") {
|
||||||
}
|
splashscreen.close().unwrap();
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
fn secure_save(key: String, value: String) -> Result<(), ()> {
|
|
||||||
let entry = Entry::new("lume", &key).expect("Failed to create entry");
|
|
||||||
let _ = entry.set_password(&value);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
fn secure_load(key: String) -> Result<String, String> {
|
|
||||||
let entry = Entry::new("lume", &key).expect("Failed to create entry");
|
|
||||||
if let Ok(password) = entry.get_password() {
|
|
||||||
Ok(password)
|
|
||||||
} else {
|
|
||||||
Err("not found".to_string())
|
|
||||||
}
|
}
|
||||||
}
|
// Show main window
|
||||||
|
window.get_window("main").unwrap().show().unwrap();
|
||||||
#[tauri::command]
|
|
||||||
fn secure_remove(key: String) -> Result<(), ()> {
|
|
||||||
let entry = Entry::new("lume", &key).expect("Failed to create entry");
|
|
||||||
let _ = entry.delete_password();
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.setup(|app| {
|
|
||||||
#[cfg(desktop)]
|
|
||||||
app
|
|
||||||
.handle()
|
|
||||||
.plugin(tauri_plugin_updater::Builder::new().build())?;
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.plugin(tauri_plugin_app::init())
|
|
||||||
.plugin(tauri_plugin_clipboard_manager::init())
|
|
||||||
.plugin(tauri_plugin_dialog::init())
|
|
||||||
.plugin(tauri_plugin_fs::init())
|
|
||||||
.plugin(tauri_plugin_http::init())
|
|
||||||
.plugin(tauri_plugin_notification::init())
|
|
||||||
.plugin(tauri_plugin_os::init())
|
|
||||||
.plugin(tauri_plugin_process::init())
|
|
||||||
.plugin(tauri_plugin_shell::init())
|
|
||||||
.plugin(tauri_plugin_upload::init())
|
|
||||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
|
||||||
.plugin(tauri_plugin_window::init())
|
|
||||||
.plugin(
|
.plugin(
|
||||||
tauri_plugin_sql::Builder::default()
|
tauri_plugin_sql::Builder::default()
|
||||||
.add_migrations(
|
.add_migrations(
|
||||||
"sqlite:lume_v2.db",
|
"sqlite:lume.db",
|
||||||
vec![Migration {
|
vec![
|
||||||
version: 20230418013219,
|
Migration {
|
||||||
description: "initial data",
|
version: 20230418013219,
|
||||||
sql: include_str!("../migrations/20230418013219_initial_data.sql"),
|
description: "initial data",
|
||||||
kind: MigrationKind::Up,
|
sql: include_str!("../migrations/20230418013219_initial_data.sql"),
|
||||||
}],
|
kind: MigrationKind::Up,
|
||||||
|
},
|
||||||
|
Migration {
|
||||||
|
version: 20230418080146,
|
||||||
|
description: "create chats",
|
||||||
|
sql: include_str!("../migrations/20230418080146_create_chats.sql"),
|
||||||
|
kind: MigrationKind::Up,
|
||||||
|
},
|
||||||
|
Migration {
|
||||||
|
version: 20230420040005,
|
||||||
|
description: "insert last login to settings",
|
||||||
|
sql: include_str!("../migrations/20230420040005_insert_last_login_to_settings.sql"),
|
||||||
|
kind: MigrationKind::Up,
|
||||||
|
},
|
||||||
|
Migration {
|
||||||
|
version: 20230425023912,
|
||||||
|
description: "add pubkey to channel",
|
||||||
|
sql: include_str!("../migrations/20230425023912_add_pubkey_to_channel.sql"),
|
||||||
|
kind: MigrationKind::Up,
|
||||||
|
},
|
||||||
|
Migration {
|
||||||
|
version: 20230425024708,
|
||||||
|
description: "add default channels",
|
||||||
|
sql: include_str!("../migrations/20230425024708_add_default_channels.sql"),
|
||||||
|
kind: MigrationKind::Up,
|
||||||
|
},
|
||||||
|
Migration {
|
||||||
|
version: 20230425050745,
|
||||||
|
description: "create blacklist",
|
||||||
|
sql: include_str!("../migrations/20230425050745_add_blacklist_model.sql"),
|
||||||
|
kind: MigrationKind::Up,
|
||||||
|
},
|
||||||
|
Migration {
|
||||||
|
version: 20230521092300,
|
||||||
|
description: "create block",
|
||||||
|
sql: include_str!("../migrations/20230521092300_add_block_model.sql"),
|
||||||
|
kind: MigrationKind::Up,
|
||||||
|
},
|
||||||
|
Migration {
|
||||||
|
version: 20230617003135,
|
||||||
|
description: "add channel messages",
|
||||||
|
sql: include_str!("../migrations/20230617003135_add_channel_messages.sql"),
|
||||||
|
kind: MigrationKind::Up,
|
||||||
|
},
|
||||||
|
Migration {
|
||||||
|
version: 20230619082415,
|
||||||
|
description: "add replies",
|
||||||
|
sql: include_str!("../migrations/20230619082415_add_replies.sql"),
|
||||||
|
kind: MigrationKind::Up,
|
||||||
|
},
|
||||||
|
Migration {
|
||||||
|
version: 20230718072634,
|
||||||
|
description: "clean up",
|
||||||
|
sql: include_str!("../migrations/20230718072634_clean_up_old_tables.sql"),
|
||||||
|
kind: MigrationKind::Up,
|
||||||
|
},
|
||||||
|
Migration {
|
||||||
|
version: 20230725010250,
|
||||||
|
description: "update default relays",
|
||||||
|
sql: include_str!("../migrations/20230725010250_update_default_relays.sql"),
|
||||||
|
kind: MigrationKind::Up,
|
||||||
|
},
|
||||||
|
Migration {
|
||||||
|
version: 20230804083544,
|
||||||
|
description: "add network to accounts",
|
||||||
|
sql: include_str!("../migrations/20230804083544_add_network_to_account.sql"),
|
||||||
|
kind: MigrationKind::Up,
|
||||||
|
},
|
||||||
|
Migration {
|
||||||
|
version: 20230808085847,
|
||||||
|
description: "add relays",
|
||||||
|
sql: include_str!("../migrations/20230808085847_add_relays_table.sql"),
|
||||||
|
kind: MigrationKind::Up,
|
||||||
|
},
|
||||||
|
Migration {
|
||||||
|
version: 20230811074423,
|
||||||
|
description: "rename blocks to widgets",
|
||||||
|
sql: include_str!("../migrations/20230811074423_rename_blocks_to_widgets.sql"),
|
||||||
|
kind: MigrationKind::Up,
|
||||||
|
},
|
||||||
|
],
|
||||||
)
|
)
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
|
.plugin(
|
||||||
|
tauri_plugin_stronghold::Builder::new(|password| {
|
||||||
|
let config = argon2::Config {
|
||||||
|
lanes: 2,
|
||||||
|
mem_cost: 50_000,
|
||||||
|
time_cost: 30,
|
||||||
|
thread_mode: argon2::ThreadMode::from_threads(2),
|
||||||
|
variant: argon2::Variant::Argon2id,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// let salt = Alphanumeric.sample_string(&mut rand::thread_rng(), 12);
|
||||||
|
let key = argon2::hash_raw(
|
||||||
|
password.as_ref(),
|
||||||
|
b"LUME_NEED_RUST_DEVELOPER_HELP_MAKE_SALT_RANDOM",
|
||||||
|
&config,
|
||||||
|
)
|
||||||
|
.expect("failed to hash password");
|
||||||
|
|
||||||
|
key.to_vec()
|
||||||
|
})
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
.plugin(tauri_plugin_autostart::init(
|
.plugin(tauri_plugin_autostart::init(
|
||||||
MacosLauncher::LaunchAgent,
|
MacosLauncher::LaunchAgent,
|
||||||
Some(vec!["--flag1", "--flag2"]),
|
Some(vec!["--flag1", "--flag2"]),
|
||||||
@@ -149,14 +153,33 @@ fn main() {
|
|||||||
.emit_all("single-instance", Payload { args: argv, cwd })
|
.emit_all("single-instance", Payload { args: argv, cwd })
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}))
|
}))
|
||||||
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
.plugin(tauri_plugin_upload::init())
|
.plugin(tauri_plugin_upload::init())
|
||||||
|
.plugin(tauri_plugin_dialog::init())
|
||||||
|
.plugin(tauri_plugin_http::init())
|
||||||
|
.plugin(tauri_plugin_fs::init())
|
||||||
|
.plugin(tauri_plugin_clipboard_manager::init())
|
||||||
|
.plugin(tauri_plugin_notification::init())
|
||||||
|
.plugin(tauri_plugin_app::init())
|
||||||
|
.plugin(tauri_plugin_process::init())
|
||||||
|
.plugin(tauri_plugin_os::init())
|
||||||
|
.plugin(tauri_plugin_window::init())
|
||||||
.plugin(tauri_plugin_store::Builder::default().build())
|
.plugin(tauri_plugin_store::Builder::default().build())
|
||||||
.invoke_handler(tauri::generate_handler![
|
.plugin(tauri_plugin_shell::init())
|
||||||
opengraph,
|
.setup(|app| {
|
||||||
secure_save,
|
let window = app.get_window("main").unwrap();
|
||||||
secure_load,
|
|
||||||
secure_remove
|
#[cfg(target_os = "macos")]
|
||||||
])
|
apply_vibrancy(&window, NSVisualEffectMaterial::HudWindow, None, None)
|
||||||
|
.expect("Unsupported platform! 'apply_vibrancy' is only supported on macOS");
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
apply_mica(&window, None, None)
|
||||||
|
.expect("Unsupported platform! 'apply_blur' is only supported on Windows");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.invoke_handler(tauri::generate_handler![close_splashscreen])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
|
||||||
"build": {
|
"build": {
|
||||||
"beforeBuildCommand": "pnpm run build",
|
"beforeBuildCommand": "pnpm build",
|
||||||
"beforeDevCommand": "pnpm run dev",
|
"beforeDevCommand": "pnpm dev",
|
||||||
"devPath": "http://localhost:3000",
|
"devPath": "http://localhost:3000",
|
||||||
"distDir": "../dist",
|
"distDir": "../dist",
|
||||||
"withGlobalTauri": true
|
"withGlobalTauri": true
|
||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "Lume",
|
"productName": "Lume",
|
||||||
"version": "2.0.0"
|
"version": "1.2.0"
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"fs": {
|
"fs": {
|
||||||
@@ -67,21 +66,60 @@
|
|||||||
"entitlements": null,
|
"entitlements": null,
|
||||||
"exceptionDomain": "",
|
"exceptionDomain": "",
|
||||||
"frameworks": [],
|
"frameworks": [],
|
||||||
"license": "../LICENSE",
|
|
||||||
"minimumSystemVersion": "10.15.0",
|
|
||||||
"providerShortName": null,
|
"providerShortName": null,
|
||||||
"signingIdentity": null
|
"signingIdentity": null
|
||||||
},
|
},
|
||||||
"resources": [],
|
"resources": [],
|
||||||
"shortDescription": "",
|
"shortDescription": "",
|
||||||
"targets": "all",
|
"targets": "all",
|
||||||
"updater": {},
|
"updater": {
|
||||||
|
"active": true,
|
||||||
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEU4RjAzODFBREQ4MkM3RTEKUldUaHg0TGRHamp3NkI5bnhoOEVjanlHWFNzQ2Q3NDhubFFLUmJpSHJ1L2FqNnB3alF1Y2R3U3gK",
|
||||||
|
"windows": {
|
||||||
|
"installMode": "passive"
|
||||||
|
}
|
||||||
|
},
|
||||||
"windows": {
|
"windows": {
|
||||||
"certificateThumbprint": null,
|
"certificateThumbprint": null,
|
||||||
"digestAlgorithm": "sha256",
|
"digestAlgorithm": "sha256",
|
||||||
"timestampUrl": ""
|
"timestampUrl": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"security": {
|
||||||
|
"csp": "upgrade-insecure-requests"
|
||||||
|
},
|
||||||
|
"systemTray": {
|
||||||
|
"iconAsTemplate": true,
|
||||||
|
"iconPath": "icons/icon.png"
|
||||||
|
},
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"width": 1080,
|
||||||
|
"height": 800,
|
||||||
|
"minWidth": 1080,
|
||||||
|
"minHeight": 800,
|
||||||
|
"resizable": true,
|
||||||
|
"theme": "Dark",
|
||||||
|
"title": "Lume",
|
||||||
|
"titleBarStyle": "Overlay",
|
||||||
|
"transparent": true,
|
||||||
|
"center": true,
|
||||||
|
"fullscreen": false,
|
||||||
|
"hiddenTitle": true,
|
||||||
|
"visible": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"width": 400,
|
||||||
|
"height": 500,
|
||||||
|
"decorations": true,
|
||||||
|
"hiddenTitle": true,
|
||||||
|
"center": true,
|
||||||
|
"resizable": false,
|
||||||
|
"titleBarStyle": "Overlay",
|
||||||
|
"label": "splashscreen",
|
||||||
|
"url": "splashscreen"
|
||||||
|
}
|
||||||
|
],
|
||||||
"macOSPrivateApi": true
|
"macOSPrivateApi": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
|
||||||
"tauri": {
|
|
||||||
"windows": [
|
|
||||||
{
|
|
||||||
"width": 1080,
|
|
||||||
"height": 800,
|
|
||||||
"minWidth": 560,
|
|
||||||
"minHeight": 800,
|
|
||||||
"resizable": true,
|
|
||||||
"title": "Lume",
|
|
||||||
"center": true,
|
|
||||||
"fullscreen": false,
|
|
||||||
"hiddenTitle": true,
|
|
||||||
"fileDropEnabled": true,
|
|
||||||
"decorations": false,
|
|
||||||
"transparent": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
|
||||||
"tauri": {
|
|
||||||
"windows": [
|
|
||||||
{
|
|
||||||
"width": 1080,
|
|
||||||
"height": 800,
|
|
||||||
"minWidth": 560,
|
|
||||||
"minHeight": 800,
|
|
||||||
"resizable": true,
|
|
||||||
"title": "Lume",
|
|
||||||
"titleBarStyle": "Overlay",
|
|
||||||
"center": true,
|
|
||||||
"fullscreen": false,
|
|
||||||
"hiddenTitle": true,
|
|
||||||
"fileDropEnabled": true,
|
|
||||||
"decorations": true,
|
|
||||||
"transparent": true,
|
|
||||||
"windowEffects": {
|
|
||||||
"effects": ["sidebar"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
|
||||||
"tauri": {
|
|
||||||
"windows": [
|
|
||||||
{
|
|
||||||
"width": 1080,
|
|
||||||
"height": 800,
|
|
||||||
"minWidth": 560,
|
|
||||||
"minHeight": 800,
|
|
||||||
"resizable": true,
|
|
||||||
"title": "Lume",
|
|
||||||
"center": true,
|
|
||||||
"fullscreen": false,
|
|
||||||
"hiddenTitle": true,
|
|
||||||
"fileDropEnabled": true,
|
|
||||||
"decorations": false,
|
|
||||||
"transparent": true,
|
|
||||||
"windowEffects": {
|
|
||||||
"effects": ["micaLight", "micaDark"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
@import 'reactflow/dist/style.css';
|
|
||||||
|
|
||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
@layer utilities {
|
|
||||||
.break-p {
|
|
||||||
word-break: break-word;
|
|
||||||
word-wrap: break-word;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
@apply cursor-default no-underline !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
@apply cursor-default focus:outline-none;
|
|
||||||
}
|
|
||||||
|
|
||||||
input::-ms-reveal,
|
|
||||||
input::-ms-clear {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-input-placeholder {
|
|
||||||
line-height: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.border {
|
|
||||||
background-clip: padding-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player {
|
|
||||||
--brand-color: #f5f5f5;
|
|
||||||
--focus-color: #4e9cf6;
|
|
||||||
--audio-brand: var(--brand-color);
|
|
||||||
--audio-focus-ring-color: var(--focus-color);
|
|
||||||
--audio-border-radius: 2px;
|
|
||||||
--video-brand: var(--brand-color);
|
|
||||||
--video-focus-ring-color: var(--focus-color);
|
|
||||||
--video-border-radius: 8px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player[data-view-type='video'] {
|
|
||||||
aspect-ratio: 16 /9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror p.is-empty::before {
|
|
||||||
@apply text-neutral-600 dark:text-neutral-400;
|
|
||||||
content: attr(data-placeholder);
|
|
||||||
float: left;
|
|
||||||
height: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror img.ProseMirror-selectednode {
|
|
||||||
@apply outline-blue-500;
|
|
||||||
}
|
|
||||||
@@ -1,256 +1,247 @@
|
|||||||
import { message } from '@tauri-apps/plugin-dialog';
|
import { RouterProvider, createBrowserRouter, redirect } from 'react-router-dom';
|
||||||
import { RouterProvider, createBrowserRouter, defer, redirect } from 'react-router-dom';
|
|
||||||
import { ReactFlowProvider } from 'reactflow';
|
|
||||||
|
|
||||||
|
import { AuthCreateScreen } from '@app/auth/create';
|
||||||
|
import { AuthImportScreen } from '@app/auth/import';
|
||||||
import { OnboardingScreen } from '@app/auth/onboarding';
|
import { OnboardingScreen } from '@app/auth/onboarding';
|
||||||
import { ChatsScreen } from '@app/chats';
|
|
||||||
import { ErrorScreen } from '@app/error';
|
import { ErrorScreen } from '@app/error';
|
||||||
import { ExploreScreen } from '@app/explore';
|
|
||||||
import { NewScreen } from '@app/new';
|
|
||||||
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { getActiveAccount } from '@libs/storage';
|
||||||
|
|
||||||
|
import { AppLayout } from '@shared/appLayout';
|
||||||
|
import { AuthLayout } from '@shared/authLayout';
|
||||||
import { LoaderIcon } from '@shared/icons';
|
import { LoaderIcon } from '@shared/icons';
|
||||||
import { AppLayout } from '@shared/layouts/app';
|
import { SettingsLayout } from '@shared/settingsLayout';
|
||||||
import { AuthLayout } from '@shared/layouts/auth';
|
|
||||||
import { NoteLayout } from '@shared/layouts/note';
|
|
||||||
import { SettingsLayout } from '@shared/layouts/settings';
|
|
||||||
|
|
||||||
import './app.css';
|
import './index.css';
|
||||||
|
|
||||||
|
const appLoader = async () => {
|
||||||
|
const account = await getActiveAccount();
|
||||||
|
const stronghold = sessionStorage.getItem('stronghold');
|
||||||
|
const privkey = JSON.parse(stronghold).state.privkey || null;
|
||||||
|
const onboarding = localStorage.getItem('onboarding');
|
||||||
|
const step = JSON.parse(onboarding).state.step || null;
|
||||||
|
|
||||||
|
if (step) {
|
||||||
|
return redirect(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
return redirect('/auth/welcome');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (account && account.privkey.length > 35) {
|
||||||
|
return redirect('/auth/migrate');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (account && !privkey) {
|
||||||
|
return redirect('/auth/unlock');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = createBrowserRouter([
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
element: <AppLayout />,
|
||||||
|
errorElement: <ErrorScreen />,
|
||||||
|
loader: appLoader,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
async lazy() {
|
||||||
|
const { SpaceScreen } = await import('@app/space');
|
||||||
|
return { Component: SpaceScreen };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'trending',
|
||||||
|
async lazy() {
|
||||||
|
const { TrendingScreen } = await import('@app/trending');
|
||||||
|
return { Component: TrendingScreen };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'events/:id',
|
||||||
|
async lazy() {
|
||||||
|
const { EventScreen } = await import('@app/events');
|
||||||
|
return { Component: EventScreen };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'users/:pubkey',
|
||||||
|
async lazy() {
|
||||||
|
const { UserScreen } = await import('@app/users');
|
||||||
|
return { Component: UserScreen };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'chats/:pubkey',
|
||||||
|
async lazy() {
|
||||||
|
const { ChatScreen } = await import('@app/chats');
|
||||||
|
return { Component: ChatScreen };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/splashscreen',
|
||||||
|
errorElement: <ErrorScreen />,
|
||||||
|
async lazy() {
|
||||||
|
const { SplashScreen } = await import('@app/splash');
|
||||||
|
return { Component: SplashScreen };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/auth',
|
||||||
|
element: <AuthLayout />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'welcome',
|
||||||
|
async lazy() {
|
||||||
|
const { WelcomeScreen } = await import('@app/auth/welcome');
|
||||||
|
return { Component: WelcomeScreen };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'import',
|
||||||
|
element: <AuthImportScreen />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
async lazy() {
|
||||||
|
const { ImportStep1Screen } = await import('@app/auth/import/step-1');
|
||||||
|
return { Component: ImportStep1Screen };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'step-2',
|
||||||
|
async lazy() {
|
||||||
|
const { ImportStep2Screen } = await import('@app/auth/import/step-2');
|
||||||
|
return { Component: ImportStep2Screen };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'step-3',
|
||||||
|
async lazy() {
|
||||||
|
const { ImportStep3Screen } = await import('@app/auth/import/step-3');
|
||||||
|
return { Component: ImportStep3Screen };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'create',
|
||||||
|
element: <AuthCreateScreen />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
async lazy() {
|
||||||
|
const { CreateStep1Screen } = await import('@app/auth/create/step-1');
|
||||||
|
return { Component: CreateStep1Screen };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'step-2',
|
||||||
|
async lazy() {
|
||||||
|
const { CreateStep2Screen } = await import('@app/auth/create/step-2');
|
||||||
|
return { Component: CreateStep2Screen };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'step-3',
|
||||||
|
async lazy() {
|
||||||
|
const { CreateStep3Screen } = await import('@app/auth/create/step-3');
|
||||||
|
return { Component: CreateStep3Screen };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'onboarding',
|
||||||
|
element: <OnboardingScreen />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
async lazy() {
|
||||||
|
const { OnboardStep1Screen } = await import('@app/auth/onboarding/step-1');
|
||||||
|
return { Component: OnboardStep1Screen };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'step-2',
|
||||||
|
async lazy() {
|
||||||
|
const { OnboardStep2Screen } = await import('@app/auth/onboarding/step-2');
|
||||||
|
return { Component: OnboardStep2Screen };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'step-3',
|
||||||
|
async lazy() {
|
||||||
|
const { OnboardStep3Screen } = await import('@app/auth/onboarding/step-3');
|
||||||
|
return { Component: OnboardStep3Screen };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'unlock',
|
||||||
|
async lazy() {
|
||||||
|
const { UnlockScreen } = await import('@app/auth/unlock');
|
||||||
|
return { Component: UnlockScreen };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'migrate',
|
||||||
|
async lazy() {
|
||||||
|
const { MigrateScreen } = await import('@app/auth/migrate');
|
||||||
|
return { Component: MigrateScreen };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'reset',
|
||||||
|
async lazy() {
|
||||||
|
const { ResetScreen } = await import('@app/auth/reset');
|
||||||
|
return { Component: ResetScreen };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/settings',
|
||||||
|
element: <SettingsLayout />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'general',
|
||||||
|
async lazy() {
|
||||||
|
const { GeneralSettingsScreen } = await import('@app/settings/general');
|
||||||
|
return { Component: GeneralSettingsScreen };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'shortcuts',
|
||||||
|
async lazy() {
|
||||||
|
const { ShortcutsSettingsScreen } = await import('@app/settings/shortcuts');
|
||||||
|
return { Component: ShortcutsSettingsScreen };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'account',
|
||||||
|
async lazy() {
|
||||||
|
const { AccountSettingsScreen } = await import('@app/settings/account');
|
||||||
|
return { Component: AccountSettingsScreen };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { db } = useStorage();
|
|
||||||
|
|
||||||
const accountLoader = async () => {
|
|
||||||
try {
|
|
||||||
// redirect to welcome screen if none user exist
|
|
||||||
const totalAccount = await db.checkAccount();
|
|
||||||
if (totalAccount === 0) return redirect('/auth/welcome');
|
|
||||||
|
|
||||||
return null;
|
|
||||||
} catch (e) {
|
|
||||||
await message(e, { title: 'An unexpected error has occurred', type: 'error' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const relayLoader = async ({ params }) => {
|
|
||||||
return defer({
|
|
||||||
relay: fetch(`https://${params.url}`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/nostr+json',
|
|
||||||
},
|
|
||||||
}).then((res) => res.json()),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
|
||||||
{
|
|
||||||
path: '/',
|
|
||||||
element: <AppLayout />,
|
|
||||||
errorElement: <ErrorScreen />,
|
|
||||||
loader: accountLoader,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
async lazy() {
|
|
||||||
const { SpaceScreen } = await import('@app/space');
|
|
||||||
return { Component: SpaceScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'users/:pubkey',
|
|
||||||
async lazy() {
|
|
||||||
const { UserScreen } = await import('@app/users');
|
|
||||||
return { Component: UserScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'nwc',
|
|
||||||
async lazy() {
|
|
||||||
const { NWCScreen } = await import('@app/nwc');
|
|
||||||
return { Component: NWCScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'relays',
|
|
||||||
async lazy() {
|
|
||||||
const { RelaysScreen } = await import('@app/relays');
|
|
||||||
return { Component: RelaysScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'relays/:url',
|
|
||||||
loader: relayLoader,
|
|
||||||
async lazy() {
|
|
||||||
const { RelayScreen } = await import('@app/relays/relay');
|
|
||||||
return { Component: RelayScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'explore',
|
|
||||||
element: (
|
|
||||||
<ReactFlowProvider>
|
|
||||||
<ExploreScreen />
|
|
||||||
</ReactFlowProvider>
|
|
||||||
),
|
|
||||||
errorElement: <ErrorScreen />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'chats',
|
|
||||||
element: <ChatsScreen />,
|
|
||||||
errorElement: <ErrorScreen />,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'chat/:pubkey',
|
|
||||||
async lazy() {
|
|
||||||
const { ChatScreen } = await import('@app/chats/chat');
|
|
||||||
return { Component: ChatScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/new',
|
|
||||||
element: <NewScreen />,
|
|
||||||
errorElement: <ErrorScreen />,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
async lazy() {
|
|
||||||
const { NewPostScreen } = await import('@app/new/post');
|
|
||||||
return { Component: NewPostScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'article',
|
|
||||||
async lazy() {
|
|
||||||
const { NewArticleScreen } = await import('@app/new/article');
|
|
||||||
return { Component: NewArticleScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'file',
|
|
||||||
async lazy() {
|
|
||||||
const { NewFileScreen } = await import('@app/new/file');
|
|
||||||
return { Component: NewFileScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/notes',
|
|
||||||
element: <NoteLayout />,
|
|
||||||
errorElement: <ErrorScreen />,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'text/:id',
|
|
||||||
async lazy() {
|
|
||||||
const { TextNoteScreen } = await import('@app/notes/text');
|
|
||||||
return { Component: TextNoteScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'article/:id',
|
|
||||||
async lazy() {
|
|
||||||
const { ArticleNoteScreen } = await import('@app/notes/article');
|
|
||||||
return { Component: ArticleNoteScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/auth',
|
|
||||||
element: <AuthLayout />,
|
|
||||||
errorElement: <ErrorScreen />,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'welcome',
|
|
||||||
async lazy() {
|
|
||||||
const { WelcomeScreen } = await import('@app/auth/welcome');
|
|
||||||
return { Component: WelcomeScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'create',
|
|
||||||
async lazy() {
|
|
||||||
const { CreateAccountScreen } = await import('@app/auth/create');
|
|
||||||
return { Component: CreateAccountScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'import',
|
|
||||||
async lazy() {
|
|
||||||
const { ImportAccountScreen } = await import('@app/auth/import');
|
|
||||||
return { Component: ImportAccountScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'onboarding',
|
|
||||||
element: <OnboardingScreen />,
|
|
||||||
errorElement: <ErrorScreen />,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
async lazy() {
|
|
||||||
const { OnboardingListScreen } = await import(
|
|
||||||
'@app/auth/onboarding/list'
|
|
||||||
);
|
|
||||||
return { Component: OnboardingListScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'enrich',
|
|
||||||
async lazy() {
|
|
||||||
const { OnboardEnrichScreen } = await import(
|
|
||||||
'@app/auth/onboarding/enrich'
|
|
||||||
);
|
|
||||||
return { Component: OnboardEnrichScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'hashtag',
|
|
||||||
async lazy() {
|
|
||||||
const { OnboardHashtagScreen } = await import(
|
|
||||||
'@app/auth/onboarding/hashtag'
|
|
||||||
);
|
|
||||||
return { Component: OnboardHashtagScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/settings',
|
|
||||||
element: <SettingsLayout />,
|
|
||||||
errorElement: <ErrorScreen />,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
async lazy() {
|
|
||||||
const { GeneralSettingsScreen } = await import('@app/settings/general');
|
|
||||||
return { Component: GeneralSettingsScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'backup',
|
|
||||||
async lazy() {
|
|
||||||
const { AccountSettingsScreen } = await import('@app/settings/account');
|
|
||||||
return { Component: AccountSettingsScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RouterProvider
|
<RouterProvider
|
||||||
router={router}
|
router={router}
|
||||||
fallbackElement={
|
fallbackElement={
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
<div className="flex h-full w-full items-center justify-center bg-black/90">
|
||||||
<LoaderIcon className="h-6 w-6 animate-spin text-white" />
|
<LoaderIcon className="h-6 w-6 animate-spin text-white" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notification';
|
|
||||||
|
|
||||||
import { CheckCircleIcon } from '@shared/icons';
|
|
||||||
|
|
||||||
import { useOnboarding } from '@stores/onboarding';
|
|
||||||
|
|
||||||
export function AllowNotification() {
|
|
||||||
const [notification, setNotification] = useOnboarding((state) => [
|
|
||||||
state.notification,
|
|
||||||
state.toggleNotification,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const allow = async () => {
|
|
||||||
let permissionGranted = await isPermissionGranted();
|
|
||||||
if (!permissionGranted) {
|
|
||||||
const permission = await requestPermission();
|
|
||||||
permissionGranted = permission === 'granted';
|
|
||||||
}
|
|
||||||
if (permissionGranted) {
|
|
||||||
setNotification();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
|
|
||||||
<div className="flex items-start justify-between gap-2">
|
|
||||||
<div>
|
|
||||||
<h5 className="font-semibold">Allow notification</h5>
|
|
||||||
<p className="text-sm">
|
|
||||||
By allowing Lume to send notifications in your OS settings, you will receive
|
|
||||||
notification messages when someone interacts with you or your content.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{notification ? (
|
|
||||||
<div className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-teal-500 text-white">
|
|
||||||
<CheckCircleIcon className="h-4 w-4" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={allow}
|
|
||||||
className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
|
||||||
>
|
|
||||||
Allow
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
|
||||||
import { LRUCache } from 'lru-cache';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
import { useNDK } from '@libs/ndk/provider';
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
|
||||||
|
|
||||||
import { CheckCircleIcon, LoaderIcon } from '@shared/icons';
|
|
||||||
|
|
||||||
import { useOnboarding } from '@stores/onboarding';
|
|
||||||
|
|
||||||
export function Circle() {
|
|
||||||
const { db } = useStorage();
|
|
||||||
const { ndk } = useNDK();
|
|
||||||
|
|
||||||
const [circle, setCircle] = useOnboarding((state) => [
|
|
||||||
state.circle,
|
|
||||||
state.toggleCircle,
|
|
||||||
]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const enableLinks = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const users = ndk.getUser({ hexpubkey: db.account.pubkey });
|
|
||||||
const follows = await users.follows();
|
|
||||||
|
|
||||||
if (follows.size === 0) {
|
|
||||||
setLoading(false);
|
|
||||||
return toast('You need to follow at least 1 account');
|
|
||||||
}
|
|
||||||
|
|
||||||
const lru = new LRUCache<string, string, void>({ max: 300 });
|
|
||||||
const followsAsArr = [];
|
|
||||||
|
|
||||||
// add user's follows to lru
|
|
||||||
follows.forEach((user) => {
|
|
||||||
lru.set(user.pubkey, user.pubkey);
|
|
||||||
followsAsArr.push(user.pubkey);
|
|
||||||
});
|
|
||||||
|
|
||||||
// get follows from follows
|
|
||||||
const events = await ndk.fetchEvents({
|
|
||||||
kinds: [NDKKind.Contacts],
|
|
||||||
authors: followsAsArr,
|
|
||||||
limit: 300,
|
|
||||||
});
|
|
||||||
|
|
||||||
events.forEach((event: NDKEvent) => {
|
|
||||||
event.tags.forEach((tag) => {
|
|
||||||
if (tag[0] === 'p') lru.set(tag[1], tag[1]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// get lru values
|
|
||||||
const circleList = [...lru.values()] as string[];
|
|
||||||
|
|
||||||
// update db
|
|
||||||
await db.updateAccount('follows', JSON.stringify(followsAsArr));
|
|
||||||
await db.updateAccount('circles', JSON.stringify(circleList));
|
|
||||||
|
|
||||||
db.account.follows = followsAsArr;
|
|
||||||
db.account.circles = circleList;
|
|
||||||
|
|
||||||
// clear lru
|
|
||||||
lru.clear();
|
|
||||||
|
|
||||||
// done
|
|
||||||
await db.createSetting('circles', '1');
|
|
||||||
setCircle();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
|
|
||||||
<div className="flex items-start justify-between gap-2">
|
|
||||||
<div>
|
|
||||||
<h5 className="font-semibold">Enable Circle</h5>
|
|
||||||
<p className="text-sm">
|
|
||||||
Beside newsfeed from your follows, you will see more content from all people
|
|
||||||
that followed by your follows.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{circle ? (
|
|
||||||
<div className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-teal-500 text-white">
|
|
||||||
<CheckCircleIcon className="h-4 w-4" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={enableLinks}
|
|
||||||
className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
|
||||||
>
|
|
||||||
{loading ? <LoaderIcon className="h-4 w-4 animate-spin" /> : 'Enable'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import { useStorage } from '@libs/storage/provider';
|
|
||||||
|
|
||||||
import { CheckCircleIcon } from '@shared/icons';
|
|
||||||
|
|
||||||
import { useOnboarding } from '@stores/onboarding';
|
|
||||||
|
|
||||||
export function OutboxModel() {
|
|
||||||
const { db } = useStorage();
|
|
||||||
|
|
||||||
const [outbox, setOutbox] = useOnboarding((state) => [
|
|
||||||
state.outbox,
|
|
||||||
state.toggleOutbox,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const enableOutbox = async () => {
|
|
||||||
await db.createSetting('outbox', '1');
|
|
||||||
setOutbox();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
|
|
||||||
<div className="flex items-start justify-between gap-2">
|
|
||||||
<div>
|
|
||||||
<h5 className="font-semibold">Enable Outbox (experiment)</h5>
|
|
||||||
<p className="text-sm">
|
|
||||||
When you request information about a user, Lume will automatically query the
|
|
||||||
user's outbox relays and subsequent queries will favour using those
|
|
||||||
relays for queries with that user's pubkey.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{outbox ? (
|
|
||||||
<div className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-teal-500 text-white">
|
|
||||||
<CheckCircleIcon className="h-4 w-4" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={enableOutbox}
|
|
||||||
className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
|
||||||
>
|
|
||||||
Enable
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { CheckCircleIcon } from '@shared/icons';
|
|
||||||
|
|
||||||
import { useOnboarding } from '@stores/onboarding';
|
|
||||||
|
|
||||||
export function FavoriteHashtag() {
|
|
||||||
const hashtag = useOnboarding((state) => state.hashtag);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div>
|
|
||||||
<h5 className="font-semibold">Favorite hashtag</h5>
|
|
||||||
<p className="text-sm">
|
|
||||||
By adding favorite hashtag, Lume will display all contents related to this
|
|
||||||
hashtag as a column
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{hashtag ? (
|
|
||||||
<div className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-teal-500 text-white">
|
|
||||||
<CheckCircleIcon className="h-4 w-4" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Link
|
|
||||||
to="/auth/onboarding/hashtag"
|
|
||||||
className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
|
|
||||||
import { useNDK } from '@libs/ndk/provider';
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
|
||||||
|
|
||||||
import { LoaderIcon } from '@shared/icons';
|
|
||||||
import { User } from '@shared/user';
|
|
||||||
|
|
||||||
export function FollowList() {
|
|
||||||
const { db } = useStorage();
|
|
||||||
const { ndk } = useNDK();
|
|
||||||
const { status, data } = useQuery(
|
|
||||||
['follows'],
|
|
||||||
async () => {
|
|
||||||
const user = ndk.getUser({ hexpubkey: db.account.pubkey });
|
|
||||||
const follows = await user.follows();
|
|
||||||
const followsAsArr = [];
|
|
||||||
|
|
||||||
follows.forEach((user) => {
|
|
||||||
followsAsArr.push(user.pubkey);
|
|
||||||
});
|
|
||||||
|
|
||||||
// update db
|
|
||||||
await db.updateAccount('follows', JSON.stringify(followsAsArr));
|
|
||||||
await db.updateAccount('circles', JSON.stringify(followsAsArr));
|
|
||||||
|
|
||||||
db.account.follows = followsAsArr;
|
|
||||||
db.account.circles = followsAsArr;
|
|
||||||
|
|
||||||
return followsAsArr;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
|
|
||||||
<h5 className="font-semibold">Your follows</h5>
|
|
||||||
<div className="mt-2 flex w-full items-center justify-center">
|
|
||||||
{status === 'loading' ? (
|
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-neutral-900 dark:text-neutral-100" />
|
|
||||||
) : (
|
|
||||||
<div className="isolate flex -space-x-2">
|
|
||||||
{data.slice(0, 16).map((item) => (
|
|
||||||
<User key={item} pubkey={item} variant="stacked" />
|
|
||||||
))}
|
|
||||||
{data.length > 16 ? (
|
|
||||||
<div className="inline-flex h-8 w-8 items-center justify-center rounded-full bg-neutral-200 text-neutral-900 ring-1 ring-neutral-200 dark:bg-neutral-800 dark:text-neutral-100 dark:ring-neutral-800">
|
|
||||||
<span className="text-xs font-medium">+{data.length}</span>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { CheckCircleIcon } from '@shared/icons';
|
|
||||||
|
|
||||||
import { useOnboarding } from '@stores/onboarding';
|
|
||||||
|
|
||||||
export function SuggestFollow() {
|
|
||||||
const enrich = useOnboarding((state) => state.enrich);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div>
|
|
||||||
<h5 className="font-semibold">Enrich your network</h5>
|
|
||||||
<p className="text-sm">
|
|
||||||
Follow more people to stay up to date with everything happening around the
|
|
||||||
world.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{enrich ? (
|
|
||||||
<div className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-teal-500 text-white">
|
|
||||||
<CheckCircleIcon className="h-4 w-4" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Link
|
|
||||||
to="/auth/onboarding/enrich"
|
|
||||||
className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
|
||||||
>
|
|
||||||
Check
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Image } from '@shared/image';
|
||||||
|
|
||||||
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
|
import { displayNpub } from '@utils/shortenKey';
|
||||||
|
|
||||||
|
export function User({ pubkey, fallback }: { pubkey: string; fallback?: string }) {
|
||||||
|
const { status, user } = useProfile(pubkey, fallback);
|
||||||
|
|
||||||
|
if (status === 'loading') {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="relative h-10 w-10 shrink-0 animate-pulse rounded-md bg-white/10" />
|
||||||
|
<div className="flex w-full flex-1 flex-col items-start gap-1 text-start">
|
||||||
|
<span className="h-4 w-1/2 animate-pulse rounded bg-white/10" />
|
||||||
|
<span className="h-3 w-1/3 animate-pulse rounded bg-white/10" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="relative h-10 w-10 shrink rounded-md">
|
||||||
|
<Image
|
||||||
|
src={user?.picture || user?.image}
|
||||||
|
fallback={DEFAULT_AVATAR}
|
||||||
|
alt={pubkey}
|
||||||
|
className="h-10 w-10 rounded-md object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full flex-1 flex-col items-start text-start">
|
||||||
|
<span className="truncate font-medium leading-tight text-white">
|
||||||
|
{user?.name || user?.display_name || user?.nip05}
|
||||||
|
</span>
|
||||||
|
<span className="max-w-[15rem] truncate text-base leading-tight text-white/50">
|
||||||
|
{user?.nip05?.toLowerCase() || displayNpub(pubkey, 16)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { Image } from '@shared/image';
|
||||||
|
|
||||||
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
|
import { displayNpub } from '@utils/shortenKey';
|
||||||
|
|
||||||
|
export function UserRelay({ pubkey }: { pubkey: string }) {
|
||||||
|
const { status, user } = useProfile(pubkey);
|
||||||
|
|
||||||
|
if (status === 'loading') {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="relative h-10 w-10 shrink-0 animate-pulse rounded-md bg-white/10" />
|
||||||
|
<div className="flex w-full flex-1 flex-col items-start gap-1 text-start">
|
||||||
|
<span className="h-4 w-1/2 animate-pulse rounded bg-white/10" />
|
||||||
|
<span className="h-3 w-1/3 animate-pulse rounded bg-white/10" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="inline-flex items-center gap-2 text-white/50">
|
||||||
|
<span className="text-sm">Use by</span>
|
||||||
|
<div className="inline-flex items-center gap-1">
|
||||||
|
<Image
|
||||||
|
src={user?.picture || user?.image}
|
||||||
|
fallback={DEFAULT_AVATAR}
|
||||||
|
alt={pubkey}
|
||||||
|
className="h-5 w-5 shrink-0 rounded object-cover"
|
||||||
|
/>
|
||||||
|
<span className="truncate text-sm font-medium leading-none text-white">
|
||||||
|
{user?.name || user?.display_name || user?.nip05 || displayNpub(pubkey, 16)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,305 +0,0 @@
|
|||||||
import { NDKEvent, NDKKind, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
|
|
||||||
import { downloadDir } from '@tauri-apps/api/path';
|
|
||||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
|
||||||
import { message, save } from '@tauri-apps/plugin-dialog';
|
|
||||||
import { writeTextFile } from '@tauri-apps/plugin-fs';
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import { minidenticon } from 'minidenticons';
|
|
||||||
import { generatePrivateKey, getPublicKey, nip19 } from 'nostr-tools';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
import { useNDK } from '@libs/ndk/provider';
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
|
||||||
|
|
||||||
import { AvatarUploader } from '@shared/avatarUploader';
|
|
||||||
import { ArrowLeftIcon, LoaderIcon } from '@shared/icons';
|
|
||||||
import { User } from '@shared/user';
|
|
||||||
|
|
||||||
export function CreateAccountScreen() {
|
|
||||||
const [picture, setPicture] = useState('');
|
|
||||||
const [downloaded, setDownloaded] = useState(false);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [keys, setKeys] = useState<null | {
|
|
||||||
npub: string;
|
|
||||||
nsec: string;
|
|
||||||
pubkey: string;
|
|
||||||
privkey: string;
|
|
||||||
}>(null);
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { isDirty, isValid },
|
|
||||||
} = useForm();
|
|
||||||
const { db } = useStorage();
|
|
||||||
const { ndk } = useNDK();
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const svgURI =
|
|
||||||
'data:image/svg+xml;utf8,' +
|
|
||||||
encodeURIComponent(minidenticon('lume new account', 90, 50));
|
|
||||||
|
|
||||||
const onSubmit = async (data: { name: string; about: string }) => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const profile = {
|
|
||||||
...data,
|
|
||||||
name: data.name,
|
|
||||||
display_name: data.name,
|
|
||||||
bio: data.about,
|
|
||||||
picture: picture,
|
|
||||||
avatar: picture,
|
|
||||||
};
|
|
||||||
|
|
||||||
const userPrivkey = generatePrivateKey();
|
|
||||||
const userPubkey = getPublicKey(userPrivkey);
|
|
||||||
const userNpub = nip19.npubEncode(userPubkey);
|
|
||||||
const userNsec = nip19.nsecEncode(userPrivkey);
|
|
||||||
|
|
||||||
const signer = new NDKPrivateKeySigner(userPrivkey);
|
|
||||||
ndk.signer = signer;
|
|
||||||
|
|
||||||
const event = new NDKEvent(ndk);
|
|
||||||
event.content = JSON.stringify(profile);
|
|
||||||
event.kind = NDKKind.Metadata;
|
|
||||||
event.created_at = Math.floor(Date.now() / 1000);
|
|
||||||
event.pubkey = userPubkey;
|
|
||||||
event.tags = [];
|
|
||||||
|
|
||||||
const publish = await event.publish();
|
|
||||||
|
|
||||||
if (publish) {
|
|
||||||
await db.createAccount(userNpub, userPubkey);
|
|
||||||
await db.secureSave(userPubkey, userPrivkey);
|
|
||||||
setKeys({
|
|
||||||
npub: userNpub,
|
|
||||||
nsec: userNsec,
|
|
||||||
pubkey: userPubkey,
|
|
||||||
privkey: userPrivkey,
|
|
||||||
});
|
|
||||||
setLoading(false);
|
|
||||||
} else {
|
|
||||||
toast('Create account failed');
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return toast(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyNsec = async () => {
|
|
||||||
await writeText(keys.nsec);
|
|
||||||
};
|
|
||||||
|
|
||||||
const download = async () => {
|
|
||||||
try {
|
|
||||||
const downloadPath = await downloadDir();
|
|
||||||
const fileName = `nostr_keys_${new Date().toISOString()}.txt`;
|
|
||||||
const filePath = await save({
|
|
||||||
defaultPath: downloadPath + '/' + fileName,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (filePath) {
|
|
||||||
await writeTextFile(
|
|
||||||
filePath,
|
|
||||||
`Nostr account, generated by Lume (lume.nu)\nPublic key: ${keys.npub}\nPrivate key: ${keys.nsec}`
|
|
||||||
);
|
|
||||||
|
|
||||||
setDownloaded(true);
|
|
||||||
} // else { user cancel action }
|
|
||||||
} catch (e) {
|
|
||||||
await message(e, { title: 'Cannot download account keys', type: 'error' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative flex h-full w-full items-center justify-center">
|
|
||||||
<div className="absolute left-[8px] top-2">
|
|
||||||
{!keys ? (
|
|
||||||
<button
|
|
||||||
onClick={() => navigate(-1)}
|
|
||||||
className="inline-flex items-center gap-2 text-sm font-medium"
|
|
||||||
>
|
|
||||||
<div className="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-neutral-200 text-neutral-800 dark:bg-neutral-800 dark:text-neutral-200">
|
|
||||||
<ArrowLeftIcon className="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
|
|
||||||
<h1 className="text-center text-2xl font-semibold text-neutral-900 dark:text-neutral-100">
|
|
||||||
Let's set up your account.
|
|
||||||
</h1>
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
{!keys ? (
|
|
||||||
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col">
|
|
||||||
<input type={'hidden'} {...register('picture')} value={picture} />
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<span className="font-semibold">Avatar</span>
|
|
||||||
<div className="relative flex h-36 w-full items-center justify-center rounded-lg bg-neutral-200 dark:bg-neutral-800">
|
|
||||||
{picture.length > 0 ? (
|
|
||||||
<img
|
|
||||||
src={picture}
|
|
||||||
alt="user's avatar"
|
|
||||||
className="h-14 w-14 rounded-xl"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<img
|
|
||||||
src={svgURI}
|
|
||||||
alt="user's avatar"
|
|
||||||
className="h-14 w-14 rounded-xl bg-black dark:bg-white"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="absolute bottom-2 right-2">
|
|
||||||
<AvatarUploader setPicture={setPicture} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<label htmlFor="name" className="font-semibold">
|
|
||||||
Name *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type={'text'}
|
|
||||||
{...register('name', {
|
|
||||||
required: true,
|
|
||||||
minLength: 1,
|
|
||||||
})}
|
|
||||||
spellCheck={false}
|
|
||||||
className="h-11 rounded-lg bg-neutral-200 px-3 placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<label htmlFor="about" className="font-semibold">
|
|
||||||
Bio
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
{...register('about')}
|
|
||||||
spellCheck={false}
|
|
||||||
className="relative h-20 w-full resize-none rounded-lg bg-neutral-200 px-3 py-2 !outline-none placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={!isDirty || !isValid}
|
|
||||||
className="inline-flex h-9 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<LoaderIcon className="h-5 w-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
'Create and Continue'
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 50 }}
|
|
||||||
animate={{
|
|
||||||
opacity: 1,
|
|
||||||
y: 0,
|
|
||||||
}}
|
|
||||||
className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200"
|
|
||||||
>
|
|
||||||
<User pubkey={keys.pubkey} variant="simple" />
|
|
||||||
</motion.div>
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 80 }}
|
|
||||||
animate={{
|
|
||||||
opacity: 1,
|
|
||||||
y: 0,
|
|
||||||
}}
|
|
||||||
className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
<h5 className="font-semibold">Backup account</h5>
|
|
||||||
<div>
|
|
||||||
<p className="mb-2 select-text text-sm text-neutral-800 dark:text-neutral-200">
|
|
||||||
Your private key is your password. If you lose this key, you will
|
|
||||||
lose access to your account! Copy it and keep it in a safe place.{' '}
|
|
||||||
<span className="text-red-600">
|
|
||||||
There is no way to reset your private key.
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<p className="select-text text-sm text-neutral-800 dark:text-neutral-200">
|
|
||||||
Public key is used for sharing with other people so that they can
|
|
||||||
find you using the public key.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 flex flex-col gap-3">
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<label htmlFor="nsec" className="text-sm font-semibold">
|
|
||||||
Private key
|
|
||||||
</label>
|
|
||||||
<div className="relative w-full">
|
|
||||||
<input
|
|
||||||
readOnly
|
|
||||||
value={
|
|
||||||
keys.nsec.substring(0, 10) + '**************************'
|
|
||||||
}
|
|
||||||
className="h-11 w-full rounded-lg bg-neutral-200 px-3 placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
|
|
||||||
/>
|
|
||||||
<div className="absolute right-0 top-0 inline-flex h-11 items-center justify-center px-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={copyNsec}
|
|
||||||
className="rounded-md bg-neutral-300 px-2 py-1 text-sm font-medium hover:bg-neutral-400 dark:bg-neutral-700 dark:hover:bg-neutral-600"
|
|
||||||
>
|
|
||||||
Copy
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<label htmlFor="nsec" className="text-sm font-semibold">
|
|
||||||
Public key
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
readOnly
|
|
||||||
value={keys.npub}
|
|
||||||
className="h-11 rounded-lg bg-neutral-200 px-3 placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!downloaded ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => download()}
|
|
||||||
className="inline-flex h-9 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
Download account keys
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{downloaded ? (
|
|
||||||
<motion.button
|
|
||||||
initial={{ opacity: 0, y: 50 }}
|
|
||||||
animate={{
|
|
||||||
opacity: 1,
|
|
||||||
y: 0,
|
|
||||||
}}
|
|
||||||
className="inline-flex h-9 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
|
|
||||||
type="button"
|
|
||||||
onClick={() => navigate('/auth/onboarding', { state: { newuser: true } })}
|
|
||||||
>
|
|
||||||
Finish
|
|
||||||
</motion.button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useOnboarding } from '@stores/onboarding';
|
||||||
|
import { useStronghold } from '@stores/stronghold';
|
||||||
|
|
||||||
|
export function AuthCreateScreen() {
|
||||||
|
const [step, tmpPrivkey] = useOnboarding((state) => [state.step, state.tempPrivkey]);
|
||||||
|
const setPrivkey = useStronghold((state) => state.setPrivkey);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (step) {
|
||||||
|
setPrivkey(tmpPrivkey);
|
||||||
|
}
|
||||||
|
}, [tmpPrivkey]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { BaseDirectory, writeTextFile } from '@tauri-apps/plugin-fs';
|
||||||
|
import { generatePrivateKey, getPublicKey, nip19 } from 'nostr-tools';
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { createAccount } from '@libs/storage';
|
||||||
|
|
||||||
|
import { Button } from '@shared/button';
|
||||||
|
import { EyeOffIcon, EyeOnIcon, LoaderIcon } from '@shared/icons';
|
||||||
|
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
|
||||||
|
|
||||||
|
import { useOnboarding } from '@stores/onboarding';
|
||||||
|
import { useStronghold } from '@stores/stronghold';
|
||||||
|
|
||||||
|
export function CreateStep1Screen() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const setPrivkey = useStronghold((state) => state.setPrivkey);
|
||||||
|
const setTempPrivkey = useOnboarding((state) => state.setTempPrivkey);
|
||||||
|
const setPubkey = useOnboarding((state) => state.setPubkey);
|
||||||
|
const setStep = useOnboarding((state) => state.setStep);
|
||||||
|
|
||||||
|
const [privkeyInput, setPrivkeyInput] = useState('password');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [downloaded, setDownloaded] = useState(false);
|
||||||
|
|
||||||
|
const privkey = useMemo(() => generatePrivateKey(), []);
|
||||||
|
const pubkey = getPublicKey(privkey);
|
||||||
|
const npub = nip19.npubEncode(pubkey);
|
||||||
|
const nsec = nip19.nsecEncode(privkey);
|
||||||
|
|
||||||
|
// toggle private key
|
||||||
|
const showPrivateKey = () => {
|
||||||
|
if (privkeyInput === 'password') {
|
||||||
|
setPrivkeyInput('text');
|
||||||
|
} else {
|
||||||
|
setPrivkeyInput('password');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const download = async () => {
|
||||||
|
await writeTextFile('lume-keys.txt', `Public key: ${npub}\nPrivate key: ${nsec}`, {
|
||||||
|
dir: BaseDirectory.Download,
|
||||||
|
});
|
||||||
|
setDownloaded(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const account = useMutation({
|
||||||
|
mutationFn: (data: {
|
||||||
|
npub: string;
|
||||||
|
pubkey: string;
|
||||||
|
follows: null | string[][];
|
||||||
|
is_active: number;
|
||||||
|
}) => {
|
||||||
|
return createAccount(data.npub, data.pubkey, null, 1);
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.setQueryData(['currentAccount'], data);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
setPrivkey(privkey);
|
||||||
|
setTempPrivkey(privkey); // only use if user close app and reopen it
|
||||||
|
setPubkey(pubkey);
|
||||||
|
|
||||||
|
account.mutate({
|
||||||
|
npub,
|
||||||
|
pubkey,
|
||||||
|
follows: null,
|
||||||
|
is_active: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// redirect to next step
|
||||||
|
setTimeout(() => navigate('/auth/create/step-2', { replace: true }), 1200);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// save current step, if user close app and reopen it
|
||||||
|
setStep('/auth/create/step-1');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto w-full max-w-md">
|
||||||
|
<div className="mb-8 text-center">
|
||||||
|
<h1 className="text-xl font-semibold text-white">Save your access key!</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="text-base font-semibold text-white/50">Public Key</span>
|
||||||
|
<input
|
||||||
|
readOnly
|
||||||
|
value={npub}
|
||||||
|
className="relative h-11 w-full rounded-lg bg-white/10 px-3.5 py-1 text-white !outline-none placeholder:text-white/50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="text-base font-semibold text-white/50">Private Key</span>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
readOnly
|
||||||
|
type={privkeyInput}
|
||||||
|
value={nsec}
|
||||||
|
className="relative h-11 w-full rounded-lg bg-white/10 py-1 pl-3.5 pr-11 text-white !outline-none placeholder:text-white/50"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => showPrivateKey()}
|
||||||
|
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-white/10"
|
||||||
|
>
|
||||||
|
{privkeyInput === 'password' ? (
|
||||||
|
<EyeOffIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
|
||||||
|
) : (
|
||||||
|
<EyeOnIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-sm text-white/50">
|
||||||
|
<p>
|
||||||
|
Your private key is your password. If you lose this key, you will lose
|
||||||
|
access to your account! Copy it and keep it in a safe place. There is no way
|
||||||
|
to reset your private key.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => submit()}
|
||||||
|
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<span className="w-5" />
|
||||||
|
<span>Creating...</span>
|
||||||
|
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="w-5" />
|
||||||
|
<span>I have saved my key, continue</span>
|
||||||
|
<ArrowRightCircleIcon className="h-5 w-5" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{downloaded ? (
|
||||||
|
<span className="inline-flex h-11 w-full items-center justify-center text-sm text-white/50">
|
||||||
|
Saved in Download folder
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<Button preset="large-alt" onClick={() => download()}>
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Resolver, useForm } from 'react-hook-form';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { EyeOffIcon, EyeOnIcon, LoaderIcon } from '@shared/icons';
|
||||||
|
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
|
||||||
|
|
||||||
|
import { useOnboarding } from '@stores/onboarding';
|
||||||
|
import { useStronghold } from '@stores/stronghold';
|
||||||
|
|
||||||
|
import { useSecureStorage } from '@utils/hooks/useSecureStorage';
|
||||||
|
|
||||||
|
type FormValues = {
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolver: Resolver<FormValues> = async (values) => {
|
||||||
|
return {
|
||||||
|
values: values.password ? values : {},
|
||||||
|
errors: !values.password
|
||||||
|
? {
|
||||||
|
password: {
|
||||||
|
type: 'required',
|
||||||
|
message: 'This is required.',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CreateStep2Screen() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const setStep = useOnboarding((state) => state.setStep);
|
||||||
|
const pubkey = useOnboarding((state) => state.pubkey);
|
||||||
|
const privkey = useStronghold((state) => state.privkey);
|
||||||
|
|
||||||
|
const [passwordInput, setPasswordInput] = useState('password');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const { save } = useSecureStorage();
|
||||||
|
|
||||||
|
// toggle private key
|
||||||
|
const showPassword = () => {
|
||||||
|
if (passwordInput === 'password') {
|
||||||
|
setPasswordInput('text');
|
||||||
|
} else {
|
||||||
|
setPasswordInput('password');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
setError,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors, isDirty, isValid },
|
||||||
|
} = useForm<FormValues>({ resolver });
|
||||||
|
|
||||||
|
const onSubmit = async (data: { [x: string]: string }) => {
|
||||||
|
setLoading(true);
|
||||||
|
if (data.password.length > 3) {
|
||||||
|
// save privkey to secure storage
|
||||||
|
await save(pubkey, privkey, data.password);
|
||||||
|
|
||||||
|
// redirect to next step
|
||||||
|
navigate('/auth/create/step-3', { replace: true });
|
||||||
|
} else {
|
||||||
|
setLoading(false);
|
||||||
|
setError('password', {
|
||||||
|
type: 'custom',
|
||||||
|
message: 'Password is required and must be greater than 3',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// save current step, if user close app and reopen it
|
||||||
|
setStep('/auth/create/step-2');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto w-full max-w-md">
|
||||||
|
<div className="mb-8 text-center">
|
||||||
|
<h1 className="text-xl font-semibold text-white">
|
||||||
|
Set password to secure your key
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-3">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
{...register('password', { required: true })}
|
||||||
|
type={passwordInput}
|
||||||
|
className="relative h-11 w-full rounded-lg bg-white/10 px-3.5 py-1 text-center text-white !outline-none placeholder:text-white/50"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => showPassword()}
|
||||||
|
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-white/10"
|
||||||
|
>
|
||||||
|
{passwordInput === 'password' ? (
|
||||||
|
<EyeOffIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
|
||||||
|
) : (
|
||||||
|
<EyeOnIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-white/50">
|
||||||
|
<p>
|
||||||
|
Password is use to secure your key store in local machine, when you move
|
||||||
|
to other clients, you just need to copy your private key as nsec or
|
||||||
|
hexstring
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-red-400">
|
||||||
|
{errors.password && <p>{errors.password.message}</p>}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!isDirty || !isValid}
|
||||||
|
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<span className="w-5" />
|
||||||
|
<span>Creating...</span>
|
||||||
|
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="w-5" />
|
||||||
|
<span>Continue</span>
|
||||||
|
<ArrowRightCircleIcon className="h-5 w-5" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { AvatarUploader } from '@shared/avatarUploader';
|
||||||
|
import { BannerUploader } from '@shared/bannerUploader';
|
||||||
|
import { LoaderIcon } from '@shared/icons';
|
||||||
|
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
|
||||||
|
import { Image } from '@shared/image';
|
||||||
|
|
||||||
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
import { useOnboarding } from '@stores/onboarding';
|
||||||
|
|
||||||
|
import { useNostr } from '@utils/hooks/useNostr';
|
||||||
|
|
||||||
|
export function CreateStep3Screen() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const setStep = useOnboarding((state) => state.setStep);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [picture, setPicture] = useState(DEFAULT_AVATAR);
|
||||||
|
const [banner, setBanner] = useState('');
|
||||||
|
|
||||||
|
const { publish } = useNostr();
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isDirty, isValid },
|
||||||
|
} = useForm();
|
||||||
|
|
||||||
|
const onSubmit = async (data: { name: string; about: string; website: string }) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const profile = {
|
||||||
|
...data,
|
||||||
|
name: data.name,
|
||||||
|
display_name: data.name,
|
||||||
|
bio: data.about,
|
||||||
|
website: data.website,
|
||||||
|
};
|
||||||
|
|
||||||
|
const event = await publish({
|
||||||
|
content: JSON.stringify(profile),
|
||||||
|
kind: 0,
|
||||||
|
tags: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
queryClient.invalidateQueries(['currentAccount']);
|
||||||
|
|
||||||
|
if (event) {
|
||||||
|
setTimeout(() => navigate('/auth/onboarding', { replace: true }), 1000);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('error: ', e);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// save current step, if user close app and reopen it
|
||||||
|
setStep('/auth/create/step-3');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto w-full max-w-md">
|
||||||
|
<div className="mb-8 text-center">
|
||||||
|
<h1 className="text-xl font-semibold text-white">Create your profile</h1>
|
||||||
|
</div>
|
||||||
|
<div className="w-full overflow-hidden rounded-xl bg-white/10">
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col">
|
||||||
|
<input type={'hidden'} {...register('picture')} value={picture} />
|
||||||
|
<input type={'hidden'} {...register('banner')} value={banner} />
|
||||||
|
<div className="relative">
|
||||||
|
<div className="relative h-44 w-full bg-white/10">
|
||||||
|
<Image
|
||||||
|
src={banner}
|
||||||
|
fallback="https://void.cat/d/QY1myro5tkHVs2nY7dy74b.jpg"
|
||||||
|
alt="user's banner"
|
||||||
|
className="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
|
||||||
|
<BannerUploader setBanner={setBanner} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mb-5 px-4">
|
||||||
|
<div className="relative z-10 -mt-7 h-14 w-14">
|
||||||
|
<Image
|
||||||
|
src={picture}
|
||||||
|
fallback={DEFAULT_AVATAR}
|
||||||
|
alt="user's avatar"
|
||||||
|
className="h-14 w-14 rounded-lg object-cover ring-2 ring-white/10"
|
||||||
|
/>
|
||||||
|
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
|
||||||
|
<AvatarUploader setPicture={setPicture} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4 px-4 pb-4">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="text-sm font-semibold uppercase tracking-wider text-white/50"
|
||||||
|
>
|
||||||
|
Name *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type={'text'}
|
||||||
|
{...register('name', {
|
||||||
|
required: true,
|
||||||
|
minLength: 4,
|
||||||
|
})}
|
||||||
|
spellCheck={false}
|
||||||
|
className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-1 text-white !outline-none placeholder:text-white/50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label
|
||||||
|
htmlFor="about"
|
||||||
|
className="text-sm font-semibold uppercase tracking-wider text-white/50"
|
||||||
|
>
|
||||||
|
Bio
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
{...register('about')}
|
||||||
|
spellCheck={false}
|
||||||
|
className="relative h-20 w-full resize-none rounded-lg bg-white/10 px-3 py-1 text-white !outline-none placeholder:text-white/50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label
|
||||||
|
htmlFor="website"
|
||||||
|
className="text-sm font-semibold uppercase tracking-wider text-white/50"
|
||||||
|
>
|
||||||
|
Website
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type={'text'}
|
||||||
|
{...register('website', {
|
||||||
|
required: false,
|
||||||
|
})}
|
||||||
|
spellCheck={false}
|
||||||
|
className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-1 text-white !outline-none placeholder:text-white/50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!isDirty || !isValid}
|
||||||
|
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<span className="w-5" />
|
||||||
|
<span>Creating...</span>
|
||||||
|
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="w-5" />
|
||||||
|
<span>Continue</span>
|
||||||
|
<ArrowRightCircleIcon className="h-5 w-5" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,298 +0,0 @@
|
|||||||
import { NDKNip46Signer, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
|
|
||||||
import { readText } from '@tauri-apps/plugin-clipboard-manager';
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import { nip19 } from 'nostr-tools';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import { twMerge } from 'tailwind-merge';
|
|
||||||
|
|
||||||
import { useNDK } from '@libs/ndk/provider';
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
|
||||||
|
|
||||||
import { ArrowLeftIcon } from '@shared/icons';
|
|
||||||
import { User } from '@shared/user';
|
|
||||||
|
|
||||||
export function ImportAccountScreen() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const { db } = useStorage();
|
|
||||||
const { ndk } = useNDK();
|
|
||||||
|
|
||||||
const [npub, setNpub] = useState<string>('');
|
|
||||||
const [nsec, setNsec] = useState<string>('');
|
|
||||||
const [pubkey, setPubkey] = useState<undefined | string>(undefined);
|
|
||||||
const [created, setCreated] = useState({ ok: false, remote: false });
|
|
||||||
const [savedPrivkey, setSavedPrivkey] = useState(false);
|
|
||||||
|
|
||||||
const submitNpub = async () => {
|
|
||||||
if (npub.length < 6) return toast.error('You must enter valid npub');
|
|
||||||
if (!npub.startsWith('npub1')) return toast.error('npub must be starts with npub1');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const pubkey = nip19.decode(npub).data as string;
|
|
||||||
setPubkey(pubkey);
|
|
||||||
} catch (e) {
|
|
||||||
return toast.error(`npub invalid: ${e}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const connectNsecBunker = async () => {
|
|
||||||
if (npub.length < 6) return toast.error('You must enter valid npub');
|
|
||||||
if (!npub.startsWith('npub1')) return toast.error('npub must be starts with npub1');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const pubkey = nip19.decode(npub.split('#')[0]).data as string;
|
|
||||||
const localSigner = NDKPrivateKeySigner.generate();
|
|
||||||
await db.secureSave(pubkey + '-bunker', localSigner.privateKey);
|
|
||||||
|
|
||||||
const remoteSigner = new NDKNip46Signer(ndk, npub, localSigner);
|
|
||||||
await remoteSigner.blockUntilReady();
|
|
||||||
|
|
||||||
ndk.signer = remoteSigner;
|
|
||||||
|
|
||||||
setPubkey(pubkey);
|
|
||||||
setCreated({ ok: false, remote: true });
|
|
||||||
} catch (e) {
|
|
||||||
return toast.error(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeAccount = async () => {
|
|
||||||
setNpub('');
|
|
||||||
setPubkey('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const createAccount = async () => {
|
|
||||||
try {
|
|
||||||
await db.createAccount(npub, pubkey);
|
|
||||||
setCreated((prev) => ({ ...prev, ok: true }));
|
|
||||||
|
|
||||||
if (created.remote) navigate('/auth/onboarding', { state: { newuser: false } });
|
|
||||||
} catch (e) {
|
|
||||||
return toast(`Create account failed: ${e}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const pasteNsec = async () => {
|
|
||||||
const tempNsec = await readText();
|
|
||||||
setNsec(tempNsec);
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitNsec = async () => {
|
|
||||||
if (savedPrivkey) return;
|
|
||||||
if (nsec.length > 50 && nsec.startsWith('nsec1')) {
|
|
||||||
try {
|
|
||||||
const privkey = nip19.decode(nsec).data as string;
|
|
||||||
await db.secureSave(pubkey, privkey);
|
|
||||||
|
|
||||||
ndk.signer = new NDKPrivateKeySigner(privkey);
|
|
||||||
|
|
||||||
setSavedPrivkey(true);
|
|
||||||
} catch (e) {
|
|
||||||
return toast(`nsec invalid: ${e}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative flex h-full w-full items-center justify-center">
|
|
||||||
<div className="absolute left-[8px] top-2">
|
|
||||||
{!created ? (
|
|
||||||
<button
|
|
||||||
onClick={() => navigate(-1)}
|
|
||||||
className="inline-flex items-center gap-2 text-sm font-medium"
|
|
||||||
>
|
|
||||||
<div className="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-neutral-200 text-neutral-800 dark:bg-neutral-800 dark:text-neutral-200">
|
|
||||||
<ArrowLeftIcon className="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
|
|
||||||
<h1 className="text-center text-2xl font-semibold text-neutral-900 dark:text-neutral-100">
|
|
||||||
Import your account.
|
|
||||||
</h1>
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
<label htmlFor="npub" className="font-semibold">
|
|
||||||
Enter your public key:
|
|
||||||
</label>
|
|
||||||
<div className="flex w-full flex-col gap-2">
|
|
||||||
<input
|
|
||||||
name="npub"
|
|
||||||
type="text"
|
|
||||||
value={npub}
|
|
||||||
onChange={(e) => setNpub(e.target.value)}
|
|
||||||
spellCheck={false}
|
|
||||||
autoComplete="off"
|
|
||||||
autoCorrect="off"
|
|
||||||
autoCapitalize="off"
|
|
||||||
placeholder="npub1"
|
|
||||||
className="h-11 w-full rounded-lg bg-neutral-200 px-3 placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
|
|
||||||
/>
|
|
||||||
{!pubkey ? (
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={submitNpub}
|
|
||||||
className="h-9 w-full shrink-0 rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
Continue
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={connectNsecBunker}
|
|
||||||
className="h-9 w-full shrink-0 rounded-lg bg-neutral-200 font-semibold text-neutral-900 hover:bg-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700"
|
|
||||||
>
|
|
||||||
Continue with nsecBunker
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{pubkey ? (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 50 }}
|
|
||||||
animate={{
|
|
||||||
opacity: 1,
|
|
||||||
y: 0,
|
|
||||||
}}
|
|
||||||
className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200"
|
|
||||||
>
|
|
||||||
<h5 className="mb-1.5 font-semibold">Account found</h5>
|
|
||||||
<div className="flex w-full flex-col gap-2">
|
|
||||||
<div className="flex h-full w-full items-center justify-between rounded-lg bg-neutral-200 p-2">
|
|
||||||
<User pubkey={pubkey} variant="simple" />
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={changeAccount}
|
|
||||||
className="h-8 w-20 shrink-0 rounded-lg bg-neutral-300 text-sm font-medium text-neutral-800 hover:bg-neutral-400 dark:bg-neutral-700 dark:text-neutral-200 dark:hover:bg-neutral-600"
|
|
||||||
>
|
|
||||||
Change
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{!created.ok ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={createAccount}
|
|
||||||
className="h-9 w-full shrink-0 rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
Continue
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
) : null}
|
|
||||||
{created.ok ? (
|
|
||||||
<>
|
|
||||||
{!created.remote ? (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 50 }}
|
|
||||||
animate={{
|
|
||||||
opacity: 1,
|
|
||||||
y: 0,
|
|
||||||
}}
|
|
||||||
className="rounded-lg bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
<label htmlFor="nsec" className="font-semibold">
|
|
||||||
Enter your private key (optional):
|
|
||||||
</label>
|
|
||||||
<div className="inline-flex w-full items-center gap-2">
|
|
||||||
<div className="relative flex-1">
|
|
||||||
<input
|
|
||||||
name="nsec"
|
|
||||||
type="text"
|
|
||||||
value={nsec}
|
|
||||||
onChange={(e) => setNsec(e.target.value)}
|
|
||||||
spellCheck={false}
|
|
||||||
autoComplete="off"
|
|
||||||
autoCorrect="off"
|
|
||||||
autoCapitalize="off"
|
|
||||||
placeholder="nsec1"
|
|
||||||
className="h-11 w-full rounded-lg bg-neutral-200 px-3 placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
|
|
||||||
/>
|
|
||||||
{nsec.length < 5 ? (
|
|
||||||
<div className="absolute right-0 top-0 inline-flex h-11 items-center justify-center px-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={pasteNsec}
|
|
||||||
className="rounded-md bg-neutral-300 px-2 py-1 text-sm font-medium hover:bg-neutral-400 dark:bg-neutral-700 dark:hover:bg-neutral-600"
|
|
||||||
>
|
|
||||||
Paste
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
{nsec.length > 5 ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={submitNsec}
|
|
||||||
className={twMerge(
|
|
||||||
'h-11 w-24 shrink-0 rounded-lg font-semibold text-white',
|
|
||||||
!savedPrivkey
|
|
||||||
? 'bg-blue-500 hover:bg-blue-600'
|
|
||||||
: 'bg-teal-500 hover:bg-teal-600'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{savedPrivkey ? 'Saved' : 'Save'}
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 select-text">
|
|
||||||
<p className="text-sm">
|
|
||||||
<b>Private Key</b> is used to sign your event. For example, if you
|
|
||||||
want to make a new post or send a message to your contact, you need
|
|
||||||
to use your private key to sign this event.
|
|
||||||
</p>
|
|
||||||
<h5 className="mt-2 font-semibold">
|
|
||||||
1. In case you store private key in Lume
|
|
||||||
</h5>
|
|
||||||
<p className="text-sm">
|
|
||||||
Lume will put your private key to{' '}
|
|
||||||
<b>
|
|
||||||
{db.platform === 'macos'
|
|
||||||
? 'Apple Keychain (macOS)'
|
|
||||||
: db.platform === 'windows'
|
|
||||||
? 'Credential Manager (Windows)'
|
|
||||||
: 'Secret Service (Linux)'}
|
|
||||||
</b>
|
|
||||||
, it will be secured by your OS
|
|
||||||
</p>
|
|
||||||
<h5 className="mt-2 font-semibold">
|
|
||||||
2. In case you do not store private key in Lume
|
|
||||||
</h5>
|
|
||||||
<p className="text-sm">
|
|
||||||
When you make an event that requires a sign by your private key,
|
|
||||||
Lume will show a prompt for you to enter private key. It will be
|
|
||||||
cleared after signing and not stored anywhere.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
) : null}
|
|
||||||
<motion.button
|
|
||||||
initial={{ opacity: 0, y: 80 }}
|
|
||||||
animate={{
|
|
||||||
opacity: 1,
|
|
||||||
y: 0,
|
|
||||||
}}
|
|
||||||
className="inline-flex h-9 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
|
|
||||||
type="button"
|
|
||||||
onClick={() =>
|
|
||||||
navigate('/auth/onboarding', { state: { newuser: false } })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Continue
|
|
||||||
</motion.button>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useOnboarding } from '@stores/onboarding';
|
||||||
|
import { useStronghold } from '@stores/stronghold';
|
||||||
|
|
||||||
|
export function AuthImportScreen() {
|
||||||
|
const [step, tmpPrivkey] = useOnboarding((state) => [state.step, state.tempPrivkey]);
|
||||||
|
const setPrivkey = useStronghold((state) => state.setPrivkey);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (step) {
|
||||||
|
setPrivkey(tmpPrivkey);
|
||||||
|
}
|
||||||
|
}, [tmpPrivkey]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||