Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bbfebc2b8 | ||
|
|
d84e97b0d4 | ||
|
|
824aa8fa28 | ||
|
|
2b34ef3b7a | ||
|
|
4fa8f40e6a | ||
|
|
c1bddeb6ed | ||
|
|
5c2bfa0ea3 | ||
|
|
60e93965ea | ||
|
|
380d1fb930 | ||
|
|
53aa13c8aa | ||
|
|
13f5190ba1 | ||
|
|
c590e290e0 | ||
|
|
cdf86a2613 | ||
|
|
8726e22b38 | ||
|
|
1206486016 | ||
|
|
11ad281d72 | ||
|
|
fe4bfa1699 | ||
|
|
c6a0636e8c | ||
|
|
d3db6492d9 | ||
|
|
8f8617d8f9 | ||
|
|
8e513404c3 | ||
|
|
5a6dd172b1 | ||
|
|
fa0d7cac31 | ||
|
|
432b2ae185 | ||
|
|
fb8a6581dd | ||
|
|
a4f221f868 | ||
|
|
602d012efe | ||
|
|
5bf816eba2 | ||
|
|
a33c9d3517 | ||
|
|
1553f5ced2 | ||
|
|
41901b2174 | ||
|
|
177e4c1ff7 | ||
|
|
10036500cb | ||
|
|
a1fa777f8c | ||
|
|
472925bb05 | ||
|
|
8eb11efb34 | ||
|
|
48066a4018 | ||
|
|
5c8850ea8f | ||
|
|
09aea3cff5 | ||
|
|
45c5a890b9 | ||
|
|
69a3e85cb3 | ||
|
|
224439f62b | ||
|
|
2389ad5fdc | ||
|
|
4019623d99 | ||
|
|
57c17ffbf9 | ||
|
|
98d2ccfc86 | ||
|
|
c74a81cfdb | ||
|
|
3ebcf4a981 | ||
|
|
5d45027776 | ||
|
|
21ea8309c7 | ||
|
|
431331174a | ||
|
|
c26cfc038d | ||
|
|
39b7b34bb7 | ||
|
|
a4cf65e7c2 | ||
|
|
37668393f1 | ||
|
|
4309f734b6 | ||
|
|
b4957bae1f | ||
|
|
7a3b19bf7b | ||
|
|
1931373515 | ||
|
|
28939d1733 | ||
|
|
e6d35bc635 | ||
|
|
cc315a190a |
@@ -1,4 +1,4 @@
|
|||||||
name: 'publish'
|
name: 'Publish'
|
||||||
on: workflow_dispatch
|
on: workflow_dispatch
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
- name: setup node
|
- name: setup node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 18
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
targets: aarch64-apple-darwin
|
targets: aarch64-apple-darwin
|
||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
if: matrix.settings.platform == 'ubuntu-22.04'
|
if: matrix.settings.platform == 'ubuntu-22.04'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libglib2.0-dev libsoup-3.0 libgtk-3-dev webkit2gtk-4.1 javascriptcoregtk-4.1 libappindicator3-dev librsvg2-dev patchelf
|
sudo apt-get install -y build-essential libssl-dev libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v2
|
uses: pnpm/action-setup@v2
|
||||||
with:
|
with:
|
||||||
@@ -66,3 +66,4 @@ jobs:
|
|||||||
releaseDraft: true
|
releaseDraft: true
|
||||||
prerelease: false
|
prerelease: false
|
||||||
args: ${{ matrix.settings.args }}
|
args: ${{ matrix.settings.args }}
|
||||||
|
includeDebug: true
|
||||||
|
|||||||
+1
-1
@@ -14,9 +14,9 @@ out
|
|||||||
*.local
|
*.local
|
||||||
.next
|
.next
|
||||||
.vscode
|
.vscode
|
||||||
pnpm-lock.yaml
|
|
||||||
*.db
|
*.db
|
||||||
*.db-journal
|
*.db-journal
|
||||||
|
bun.lockb
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ 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
|
||||||
|
|
||||||
|
- Bun: https://bun.sh/docs/installation
|
||||||
|
|
||||||
|
- Tauri: https://tauri.app/v1/guides/getting-started/prerequisites#setting-up-macos
|
||||||
|
|
||||||
### Develop
|
### Develop
|
||||||
|
|
||||||
Clone project
|
Clone project
|
||||||
@@ -19,23 +25,17 @@ git clone https://github.com/luminous-devs/lume.git && cd lume
|
|||||||
Install packages
|
Install packages
|
||||||
|
|
||||||
```
|
```
|
||||||
pnpm install
|
bun install
|
||||||
```
|
```
|
||||||
|
|
||||||
Run dev
|
Run dev build
|
||||||
|
|
||||||
```
|
```
|
||||||
pnpm tauri dev
|
bun tauri dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Build
|
Generate production build
|
||||||
|
|
||||||
```
|
```
|
||||||
pnpm tauri build
|
bun tauri build
|
||||||
```
|
```
|
||||||
|
|
||||||
(Advance) - Generate SQLite migration
|
|
||||||
|
|
||||||
```
|
|
||||||
pnpm add-migrate <migrate_name>
|
|
||||||
```
|
|
||||||
+31
-44
@@ -2,7 +2,7 @@
|
|||||||
"name": "lume",
|
"name": "lume",
|
||||||
"description": "the communication app",
|
"description": "the communication app",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.2.1",
|
"version": "1.2.5",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
@@ -18,8 +18,8 @@
|
|||||||
"**/*.{ts, tsx, css, md, html, json}": "prettier --cache --write"
|
"**/*.{ts, tsx, css, md, html, json}": "prettier --cache --write"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ctrl/magnet-link": "^3.1.2",
|
"@getalby/sdk": "^2.4.0",
|
||||||
"@nostr-dev-kit/ndk": "^0.8.23",
|
"@nostr-dev-kit/ndk": "^1.0.0",
|
||||||
"@nostr-fetch/adapter-ndk": "^0.12.2",
|
"@nostr-fetch/adapter-ndk": "^0.12.2",
|
||||||
"@radix-ui/react-alert-dialog": "^1.0.4",
|
"@radix-ui/react-alert-dialog": "^1.0.4",
|
||||||
"@radix-ui/react-collapsible": "^1.0.3",
|
"@radix-ui/react-collapsible": "^1.0.3",
|
||||||
@@ -27,33 +27,16 @@
|
|||||||
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||||
"@radix-ui/react-popover": "^1.0.6",
|
"@radix-ui/react-popover": "^1.0.6",
|
||||||
"@radix-ui/react-tooltip": "^1.0.6",
|
"@radix-ui/react-tooltip": "^1.0.6",
|
||||||
"@tanstack/react-query": "^4.33.0",
|
"@tanstack/react-query": "^4.35.0",
|
||||||
"@tanstack/react-virtual": "3.0.0-beta.54",
|
"@tanstack/react-virtual": "3.0.0-beta.54",
|
||||||
"@tauri-apps/api": "2.0.0-alpha.6",
|
"@tauri-apps/api": "^1.4.0",
|
||||||
"@tauri-apps/cli": "2.0.0-alpha.11",
|
"@tiptap/extension-image": "^2.1.8",
|
||||||
"@tauri-apps/plugin-app": "github:tauri-apps/tauri-plugin-app#v2",
|
"@tiptap/extension-mention": "^2.1.8",
|
||||||
"@tauri-apps/plugin-autostart": "github:tauri-apps/tauri-plugin-autostart#v2",
|
"@tiptap/extension-placeholder": "^2.1.8",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "github:tauri-apps/tauri-plugin-clipboard-manager#v2",
|
"@tiptap/pm": "^2.1.8",
|
||||||
"@tauri-apps/plugin-dialog": "github:tauri-apps/tauri-plugin-dialog#v2",
|
"@tiptap/react": "^2.1.8",
|
||||||
"@tauri-apps/plugin-fs": "github:tauri-apps/tauri-plugin-fs#v2",
|
"@tiptap/starter-kit": "^2.1.8",
|
||||||
"@tauri-apps/plugin-http": "github:tauri-apps/tauri-plugin-http#v2",
|
"@tiptap/suggestion": "^2.1.8",
|
||||||
"@tauri-apps/plugin-notification": "github:tauri-apps/tauri-plugin-notification#v2",
|
|
||||||
"@tauri-apps/plugin-os": "github:tauri-apps/tauri-plugin-os#v2",
|
|
||||||
"@tauri-apps/plugin-process": "github:tauri-apps/tauri-plugin-process#v2",
|
|
||||||
"@tauri-apps/plugin-shell": "github:tauri-apps/tauri-plugin-shell#v2",
|
|
||||||
"@tauri-apps/plugin-sql": "github:tauri-apps/tauri-plugin-sql#v2",
|
|
||||||
"@tauri-apps/plugin-store": "github:tauri-apps/tauri-plugin-store#v2",
|
|
||||||
"@tauri-apps/plugin-stronghold": "github:tauri-apps/tauri-plugin-stronghold#v2",
|
|
||||||
"@tauri-apps/plugin-upload": "github:tauri-apps/tauri-plugin-upload#v2",
|
|
||||||
"@tauri-apps/plugin-window": "github:tauri-apps/tauri-plugin-window#v2",
|
|
||||||
"@tiptap/extension-image": "^2.1.7",
|
|
||||||
"@tiptap/extension-mention": "^2.1.7",
|
|
||||||
"@tiptap/extension-placeholder": "^2.1.7",
|
|
||||||
"@tiptap/pm": "^2.1.7",
|
|
||||||
"@tiptap/react": "^2.1.7",
|
|
||||||
"@tiptap/starter-kit": "^2.1.7",
|
|
||||||
"@tiptap/suggestion": "^2.1.7",
|
|
||||||
"@void-cat/api": "^1.0.7",
|
|
||||||
"dayjs": "^1.11.9",
|
"dayjs": "^1.11.9",
|
||||||
"destr": "^2.0.1",
|
"destr": "^2.0.1",
|
||||||
"get-urls": "^12.1.0",
|
"get-urls": "^12.1.0",
|
||||||
@@ -61,53 +44,57 @@
|
|||||||
"light-bolt11-decoder": "^3.0.0",
|
"light-bolt11-decoder": "^3.0.0",
|
||||||
"lru-cache": "^10.0.1",
|
"lru-cache": "^10.0.1",
|
||||||
"minidenticons": "^4.2.0",
|
"minidenticons": "^4.2.0",
|
||||||
"nostr-fetch": "^0.12.2",
|
"nostr-fetch": "^0.13.0",
|
||||||
"nostr-tools": "^1.14.2",
|
"nostr-tools": "^1.14.2",
|
||||||
"qrcode.react": "^3.1.0",
|
"qrcode.react": "^3.1.0",
|
||||||
"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.45.4",
|
"react-hook-form": "^7.46.1",
|
||||||
"react-hotkeys-hook": "^4.4.1",
|
|
||||||
"react-markdown": "^8.0.7",
|
"react-markdown": "^8.0.7",
|
||||||
"react-player": "^2.12.0",
|
"react-player": "^2.13.0",
|
||||||
"react-router-dom": "^6.15.0",
|
"react-router-dom": "^6.15.0",
|
||||||
"react-textarea-autosize": "^8.5.3",
|
"react-textarea-autosize": "^8.5.3",
|
||||||
"react-virtuoso": "^4.5.0",
|
"react-virtuoso": "^4.5.1",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"tippy.js": "^6.3.7",
|
"tauri-plugin-sql-api": "github:tauri-apps/tauri-plugin-sql#v1",
|
||||||
|
"tauri-plugin-store-api": "github:tauri-apps/tauri-plugin-store#v1",
|
||||||
|
"tauri-plugin-stronghold-api": "github:tauri-apps/tauri-plugin-stronghold#v1",
|
||||||
|
"tauri-plugin-upload-api": "github:tauri-apps/tauri-plugin-upload#v1",
|
||||||
"zustand": "^4.4.1"
|
"zustand": "^4.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.10",
|
||||||
|
"@tauri-apps/cli": "^1.4.0",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
|
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
|
||||||
"@types/html-to-text": "^9.0.1",
|
"@types/html-to-text": "^9.0.1",
|
||||||
"@types/node": "^20.5.6",
|
"@types/node": "^20.6.0",
|
||||||
"@types/react": "^18.2.21",
|
"@types/react": "^18.2.21",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
"@types/youtube-player": "^5.5.7",
|
"@types/youtube-player": "^5.5.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.4.1",
|
"@typescript-eslint/eslint-plugin": "^6.6.0",
|
||||||
"@typescript-eslint/parser": "^6.4.1",
|
"@typescript-eslint/parser": "^6.6.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||||
"autoprefixer": "^10.4.15",
|
"autoprefixer": "^10.4.15",
|
||||||
"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.48.0",
|
"eslint": "^8.49.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.0.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.2",
|
||||||
"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": "^14.0.1",
|
||||||
"postcss": "^8.4.28",
|
"postcss": "^8.4.29",
|
||||||
"prettier": "^3.0.2",
|
"prettier": "^3.0.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.5.3",
|
"prettier-plugin-tailwindcss": "^0.5.4",
|
||||||
"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": "^5.2.2",
|
||||||
"vite": "^4.4.9",
|
"vite": "^4.4.9",
|
||||||
"vite-tsconfig-paths": "^4.2.0"
|
"vite-tsconfig-paths": "^4.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1277
-2170
File diff suppressed because it is too large
Load Diff
Generated
+737
-921
File diff suppressed because it is too large
Load Diff
+37
-29
@@ -1,51 +1,59 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lume"
|
name = "lume"
|
||||||
version = "1.2.1"
|
version = "1.2.5"
|
||||||
description = "nostr client"
|
description = "the communication app"
|
||||||
authors = ["Ren Amamiya"]
|
authors = ["Ren Amamiya"]
|
||||||
license = "GPL-3.0"
|
license = "GPL-3.0"
|
||||||
repository = "https://github.com/luminous-devs/lume"
|
repository = "https://github.com/luminous-devs/lume"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.71"
|
rust-version = "1.66"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# 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.8", features = [] }
|
tauri-build = { version = "1.4", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
tauri = { git = "https://git.nextgraph.org/NextGraph/tauri.git", branch = "alpha.11-nextgraph", features = [
|
tauri = { version = "1.4", features = [
|
||||||
"protocol-asset",
|
"window-close",
|
||||||
|
"window-print",
|
||||||
|
"window-create",
|
||||||
"macos-private-api",
|
"macos-private-api",
|
||||||
"no-ipc-custom-protocol",
|
"fs-read-dir",
|
||||||
|
"fs-read-file",
|
||||||
|
"window-start-dragging",
|
||||||
|
"path-all",
|
||||||
|
"http-all",
|
||||||
|
"clipboard-write-text",
|
||||||
|
"os-all",
|
||||||
|
"notification-all",
|
||||||
|
"clipboard-read-text",
|
||||||
|
"window-set-resizable",
|
||||||
|
"window-set-size",
|
||||||
|
"shell-open",
|
||||||
|
"fs-write-file",
|
||||||
|
"app-all",
|
||||||
|
"fs-remove-file",
|
||||||
|
"window-center",
|
||||||
|
"dialog-all",
|
||||||
|
"http-multipart",
|
||||||
] }
|
] }
|
||||||
tauri-plugin-single-instance = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
|
tauri-plugin-sql = { git = "hhttps://github.com/tauri-apps/plugins-workspace", branch = "v1", features = [
|
||||||
tauri-plugin-autostart = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
|
"sqlite",
|
||||||
tauri-plugin-stronghold = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
|
] }
|
||||||
tauri-plugin-upload = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
|
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||||
tauri-plugin-updater = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
|
tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||||
tauri-plugin-dialog = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
|
tauri-plugin-stronghold = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||||
tauri-plugin-http = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
|
tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||||
tauri-plugin-fs = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
|
tauri-plugin-upload = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||||
tauri-plugin-clipboard-manager = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
|
window-vibrancy = { git = "https://github.com/tauri-apps/window-vibrancy", branch = "dev" }
|
||||||
tauri-plugin-notification = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
|
|
||||||
tauri-plugin-app = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
|
|
||||||
tauri-plugin-process = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
|
|
||||||
tauri-plugin-os = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
|
|
||||||
tauri-plugin-window = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
|
|
||||||
tauri-plugin-store = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
|
|
||||||
tauri-plugin-shell = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
|
|
||||||
sqlx-cli = { version = "0.7.0", default-features = false, features = [
|
sqlx-cli = { version = "0.7.0", default-features = false, features = [
|
||||||
"sqlite",
|
"sqlite",
|
||||||
] }
|
] }
|
||||||
rust-argon2 = "1.0"
|
rust-argon2 = "1.0"
|
||||||
|
webpage = { version = "1.6.0", features = ["serde"] }
|
||||||
[dependencies.tauri-plugin-sql]
|
|
||||||
git = "https://github.com/luminous-devs/plugins-workspace"
|
|
||||||
branch = "v2"
|
|
||||||
features = ["sqlite"]
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# by default Tauri runs in production mode
|
# by default Tauri runs in production mode
|
||||||
@@ -53,7 +61,7 @@ features = ["sqlite"]
|
|||||||
default = ["custom-protocol"]
|
default = ["custom-protocol"]
|
||||||
# this feature is used used for production builds where `devPath` points to the filesystem
|
# this feature is used used for production builds where `devPath` points to the filesystem
|
||||||
# DO NOT remove this
|
# DO NOT remove this
|
||||||
custom-protocol = []
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
|
|
||||||
# Optimized for bundle size. If you want faster builds comment out/delete this section.
|
# Optimized for bundle size. If you want faster builds comment out/delete this section.
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
CREATE UNIQUE INDEX unique_relay ON relays (relay);
|
||||||
+86
-12
@@ -6,6 +6,11 @@
|
|||||||
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 std::time::Duration;
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial};
|
||||||
|
|
||||||
#[derive(Clone, serde::Serialize)]
|
#[derive(Clone, serde::Serialize)]
|
||||||
struct Payload {
|
struct Payload {
|
||||||
@@ -13,6 +18,71 @@ 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]
|
||||||
|
async fn opengraph(url: String) -> OpenGraphResponse {
|
||||||
|
let result = fetch_opengraph(url).await;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn close_splashscreen(window: tauri::Window) {
|
async fn close_splashscreen(window: tauri::Window) {
|
||||||
// Close splashscreen
|
// Close splashscreen
|
||||||
@@ -25,6 +95,15 @@ async fn close_splashscreen(window: tauri::Window) {
|
|||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
|
.setup(|app| {
|
||||||
|
let window = app.get_window("main").unwrap();
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
apply_vibrancy(&window, NSVisualEffectMaterial::HudWindow, None, None)
|
||||||
|
.expect("Unsupported platform! 'apply_vibrancy' is only supported on macOS");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
.plugin(
|
.plugin(
|
||||||
tauri_plugin_sql::Builder::default()
|
tauri_plugin_sql::Builder::default()
|
||||||
.add_migrations(
|
.add_migrations(
|
||||||
@@ -132,6 +211,12 @@ fn main() {
|
|||||||
sql: include_str!("../migrations/20230817014932_add_last_login_time_to_account.sql"),
|
sql: include_str!("../migrations/20230817014932_add_last_login_time_to_account.sql"),
|
||||||
kind: MigrationKind::Up,
|
kind: MigrationKind::Up,
|
||||||
},
|
},
|
||||||
|
Migration {
|
||||||
|
version: 20230918235335,
|
||||||
|
description: "add unique to relay",
|
||||||
|
sql: include_str!("../migrations/20230918235335_add_uniq_to_relay.sql"),
|
||||||
|
kind: MigrationKind::Up,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.build(),
|
.build(),
|
||||||
@@ -168,20 +253,9 @@ 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())
|
||||||
.plugin(tauri_plugin_shell::init())
|
.invoke_handler(tauri::generate_handler![close_splashscreen, opengraph])
|
||||||
.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");
|
||||||
}
|
}
|
||||||
|
|||||||
+86
-42
@@ -1,47 +1,86 @@
|
|||||||
{
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeBuildCommand": "pnpm build",
|
"beforeBuildCommand": "pnpm run build",
|
||||||
"beforeDevCommand": "pnpm dev",
|
"beforeDevCommand": "pnpm run dev",
|
||||||
"devPath": "http://localhost:3000",
|
"devPath": "http://localhost:3000",
|
||||||
"distDir": "../dist",
|
"distDir": "../dist",
|
||||||
"withGlobalTauri": true
|
"withGlobalTauri": true
|
||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "Lume",
|
"productName": "Lume",
|
||||||
"version": "1.2.1"
|
"version": "1.2.5"
|
||||||
},
|
|
||||||
"plugins": {
|
|
||||||
"fs": {
|
|
||||||
"scope": [
|
|
||||||
"$APPDATA/*",
|
|
||||||
"$DATA/*",
|
|
||||||
"$LOCALDATA/*",
|
|
||||||
"$DESKTOP/*",
|
|
||||||
"$DOCUMENT/*",
|
|
||||||
"$DOWNLOAD/*",
|
|
||||||
"$HOME/*",
|
|
||||||
"$PICTURE/*",
|
|
||||||
"$PUBLIC/*",
|
|
||||||
"$VIDEO/*"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"http": {
|
|
||||||
"scope": [
|
|
||||||
"http://**/",
|
|
||||||
"https://**/"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"shell": {
|
|
||||||
"open": true
|
|
||||||
},
|
|
||||||
"updater": {
|
|
||||||
"endpoints": [
|
|
||||||
"https://lus.reya3772.workers.dev/v1/{{target}}/{{arch}}/{{current_version}}",
|
|
||||||
"https://lus.reya3772.workers.dev/{{target}}/{{current_version}}"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
|
"allowlist": {
|
||||||
|
"app": {
|
||||||
|
"all": true,
|
||||||
|
"show": true,
|
||||||
|
"hide": true
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"all": true
|
||||||
|
},
|
||||||
|
"dialog": {
|
||||||
|
"all": true,
|
||||||
|
"ask": true,
|
||||||
|
"confirm": true,
|
||||||
|
"message": true,
|
||||||
|
"open": true,
|
||||||
|
"save": true
|
||||||
|
},
|
||||||
|
"fs": {
|
||||||
|
"all": false,
|
||||||
|
"removeFile": true,
|
||||||
|
"writeFile": true,
|
||||||
|
"readDir": true,
|
||||||
|
"readFile": true,
|
||||||
|
"scope": [
|
||||||
|
"$APPDATA/*",
|
||||||
|
"$DATA/*",
|
||||||
|
"$LOCALDATA/*",
|
||||||
|
"$DESKTOP/*",
|
||||||
|
"$DOCUMENT/*",
|
||||||
|
"$DOWNLOAD/*",
|
||||||
|
"$HOME/*",
|
||||||
|
"$PICTURE/*",
|
||||||
|
"$PUBLIC/*",
|
||||||
|
"$VIDEO/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"http": {
|
||||||
|
"all": true,
|
||||||
|
"scope": [
|
||||||
|
"http://**",
|
||||||
|
"https://**"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"shell": {
|
||||||
|
"all": false,
|
||||||
|
"open": true
|
||||||
|
},
|
||||||
|
"os": {
|
||||||
|
"all": true
|
||||||
|
},
|
||||||
|
"window": {
|
||||||
|
"all": false,
|
||||||
|
"center": true,
|
||||||
|
"setResizable": true,
|
||||||
|
"setSize": true,
|
||||||
|
"startDragging": true,
|
||||||
|
"create": true,
|
||||||
|
"close": true,
|
||||||
|
"print": true
|
||||||
|
},
|
||||||
|
"clipboard": {
|
||||||
|
"all": false,
|
||||||
|
"writeText": true,
|
||||||
|
"readText": true
|
||||||
|
},
|
||||||
|
"notification": {
|
||||||
|
"all": true
|
||||||
|
}
|
||||||
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"appimage": {
|
"appimage": {
|
||||||
@@ -79,19 +118,24 @@
|
|||||||
"timestampUrl": ""
|
"timestampUrl": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"updater": {
|
||||||
|
"endpoints": [
|
||||||
|
"https://lus.reya3772.workers.dev/v1/{{target}}/{{arch}}/{{current_version}}",
|
||||||
|
"https://lus.reya3772.workers.dev/{{target}}/{{current_version}}"
|
||||||
|
]
|
||||||
|
},
|
||||||
"security": {
|
"security": {
|
||||||
"csp": {
|
"csp": {
|
||||||
"connect-src": "ipc: https://ipc.localhost",
|
|
||||||
"content-security-policy": "upgrade-insecure-requests"
|
"content-security-policy": "upgrade-insecure-requests"
|
||||||
},
|
},
|
||||||
"freezePrototype": false,
|
"dangerousRemoteDomainIpcAccess": [
|
||||||
"assetProtocol": {
|
{
|
||||||
"enable": true,
|
"scheme": "https",
|
||||||
"scope": {
|
"domain": "nwc.getalby.com",
|
||||||
"allow": ["$APPCONFIG/*.db", "$RESOURCE/**"],
|
"windows": ["alby"],
|
||||||
"deny": ["$APPCONFIG/*.stronghold"]
|
"enableTauriAPI": true
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
},
|
},
|
||||||
"macOSPrivateApi": true
|
"macOSPrivateApi": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
@@ -27,11 +28,7 @@
|
|||||||
"fullscreen": false,
|
"fullscreen": false,
|
||||||
"hiddenTitle": true,
|
"hiddenTitle": true,
|
||||||
"visible": false,
|
"visible": false,
|
||||||
"fileDropEnabled": true,
|
"fileDropEnabled": true
|
||||||
"windowEffects": {
|
|
||||||
"effects": ["hudWindow"],
|
|
||||||
"state": "followsWindowActiveState"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
@@ -19,16 +20,12 @@
|
|||||||
"resizable": true,
|
"resizable": true,
|
||||||
"theme": "Dark",
|
"theme": "Dark",
|
||||||
"title": "Lume",
|
"title": "Lume",
|
||||||
"transparent": true,
|
"transparent": false,
|
||||||
"center": true,
|
"center": true,
|
||||||
"fullscreen": false,
|
"fullscreen": false,
|
||||||
"hiddenTitle": true,
|
"hiddenTitle": true,
|
||||||
"visible": false,
|
"visible": false,
|
||||||
"fileDropEnabled": true,
|
"fileDropEnabled": true
|
||||||
"windowEffects": {
|
|
||||||
"effects": ["micaDark", "micaLight", "acrylic"],
|
|
||||||
"state": "followsWindowActiveState"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-21
@@ -1,3 +1,4 @@
|
|||||||
|
import { message } from '@tauri-apps/api/dialog';
|
||||||
import { RouterProvider, createBrowserRouter, redirect } from 'react-router-dom';
|
import { RouterProvider, createBrowserRouter, redirect } from 'react-router-dom';
|
||||||
|
|
||||||
import { AuthCreateScreen } from '@app/auth/create';
|
import { AuthCreateScreen } from '@app/auth/create';
|
||||||
@@ -5,6 +6,7 @@ import { AuthImportScreen } from '@app/auth/import';
|
|||||||
import { OnboardingScreen } from '@app/auth/onboarding';
|
import { OnboardingScreen } from '@app/auth/onboarding';
|
||||||
import { ErrorScreen } from '@app/error';
|
import { ErrorScreen } from '@app/error';
|
||||||
|
|
||||||
|
import { Frame } from '@shared/frame';
|
||||||
import { LoaderIcon } from '@shared/icons';
|
import { LoaderIcon } from '@shared/icons';
|
||||||
import { AppLayout } from '@shared/layouts/app';
|
import { AppLayout } from '@shared/layouts/app';
|
||||||
import { AuthLayout } from '@shared/layouts/auth';
|
import { AuthLayout } from '@shared/layouts/auth';
|
||||||
@@ -18,18 +20,20 @@ import './index.css';
|
|||||||
async function Loader() {
|
async function Loader() {
|
||||||
try {
|
try {
|
||||||
const account = await checkActiveAccount();
|
const account = await checkActiveAccount();
|
||||||
|
|
||||||
const stronghold = sessionStorage.getItem('stronghold');
|
const stronghold = sessionStorage.getItem('stronghold');
|
||||||
const privkey = JSON.parse(stronghold).state.privkey || null;
|
const privkey = JSON.parse(stronghold).state.privkey || null;
|
||||||
|
|
||||||
const onboarding = localStorage.getItem('onboarding');
|
const onboarding = localStorage.getItem('onboarding');
|
||||||
const step = JSON.parse(onboarding).state.step || null;
|
const step = JSON.parse(onboarding).state.step || null;
|
||||||
|
|
||||||
if (step) {
|
|
||||||
return redirect(step);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return redirect('/auth/welcome');
|
return redirect('/auth/welcome');
|
||||||
} else {
|
} else {
|
||||||
|
if (step) {
|
||||||
|
return redirect(step);
|
||||||
|
}
|
||||||
|
|
||||||
if (!privkey) {
|
if (!privkey) {
|
||||||
return redirect('/auth/unlock');
|
return redirect('/auth/unlock');
|
||||||
}
|
}
|
||||||
@@ -37,7 +41,7 @@ async function Loader() {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error('App failed to load');
|
await message(e, { title: 'An unexpected error has occurred', type: 'error' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +80,13 @@ const router = createBrowserRouter([
|
|||||||
return { Component: NotificationScreen };
|
return { Component: NotificationScreen };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'nwc',
|
||||||
|
async lazy() {
|
||||||
|
const { NWCScreen } = await import('@app/nwc');
|
||||||
|
return { Component: NWCScreen };
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -194,15 +205,15 @@ const router = createBrowserRouter([
|
|||||||
return { Component: OnboardStep2Screen };
|
return { Component: OnboardStep2Screen };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'step-3',
|
|
||||||
async lazy() {
|
|
||||||
const { OnboardStep3Screen } = await import('@app/auth/onboarding/step-3');
|
|
||||||
return { Component: OnboardStep3Screen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'complete',
|
||||||
|
async lazy() {
|
||||||
|
const { CompleteScreen } = await import('@app/auth/complete');
|
||||||
|
return { Component: CompleteScreen };
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'unlock',
|
path: 'unlock',
|
||||||
async lazy() {
|
async lazy() {
|
||||||
@@ -224,13 +235,6 @@ const router = createBrowserRouter([
|
|||||||
return { Component: ResetScreen };
|
return { Component: ResetScreen };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'hard-reset',
|
|
||||||
async lazy() {
|
|
||||||
const { HardResetScreen } = await import('@app/auth/hardReset');
|
|
||||||
return { Component: HardResetScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -261,9 +265,9 @@ export default function App() {
|
|||||||
<RouterProvider
|
<RouterProvider
|
||||||
router={router}
|
router={router}
|
||||||
fallbackElement={
|
fallbackElement={
|
||||||
<div className="flex h-full w-full items-center justify-center bg-black/90 backdrop-blur-xl">
|
<Frame className="flex h-full w-full items-center justify-center">
|
||||||
<LoaderIcon className="h-6 w-6 animate-spin text-white" />
|
<LoaderIcon className="h-6 w-6 animate-spin text-white" />
|
||||||
</div>
|
</Frame>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
export function CompleteScreen() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [count, setCount] = useState(5);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let counter: NodeJS.Timeout;
|
||||||
|
|
||||||
|
if (count > 0) {
|
||||||
|
counter = setTimeout(() => setCount(count - 1), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count === 0) {
|
||||||
|
navigate('/', { replace: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(counter);
|
||||||
|
};
|
||||||
|
}, [count]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex h-full w-full flex-col items-center justify-center">
|
||||||
|
<div className="mx-auto flex max-w-xl flex-col gap-1.5 text-center">
|
||||||
|
<h1 className="text-2xl font-light leading-none text-white">
|
||||||
|
<span className="font-semibold">You're ready</span>, redirecting in {count}
|
||||||
|
</h1>
|
||||||
|
<p className="text-white/70">
|
||||||
|
Thank you for using Lume. Lume doesn't use telemetry. If you encounter any
|
||||||
|
problems, please submit a report via the "Report Issue" button.
|
||||||
|
<br />
|
||||||
|
You can find it while using the application.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-6 left-1/2 flex -translate-x-1/2 transform items-center justify-center">
|
||||||
|
<img src="/lume.png" alt="lume" className="h-auto w-1/5" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { Image } from '@shared/image';
|
|
||||||
|
|
||||||
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 backdrop-blur-xl" />
|
|
||||||
<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 backdrop-blur-xl" />
|
|
||||||
<span className="h-3 w-1/3 animate-pulse rounded bg-white/10 backdrop-blur-xl" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2.5">
|
|
||||||
<Image
|
|
||||||
src={user?.picture || user?.image}
|
|
||||||
alt={pubkey}
|
|
||||||
className="h-10 w-10 shrink-0 rounded-lg object-cover"
|
|
||||||
/>
|
|
||||||
<div className="flex w-full flex-1 flex-col items-start text-start">
|
|
||||||
<p className="max-w-[15rem] truncate font-medium leading-tight text-white">
|
|
||||||
{user?.name || user?.display_name || user?.nip05}
|
|
||||||
</p>
|
|
||||||
<span className="max-w-[15rem] truncate leading-tight text-white/50">
|
|
||||||
{displayNpub(pubkey, 16)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { Image } from '@shared/image';
|
|
||||||
|
|
||||||
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 backdrop-blur-xl" />
|
|
||||||
<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 backdrop-blur-xl" />
|
|
||||||
<span className="h-3 w-1/3 animate-pulse rounded bg-white/10 backdrop-blur-xl" />
|
|
||||||
</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}
|
|
||||||
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,13 +1,14 @@
|
|||||||
import { BaseDirectory, writeTextFile } from '@tauri-apps/plugin-fs';
|
import { writeText } from '@tauri-apps/api/clipboard';
|
||||||
|
import { message, save } from '@tauri-apps/api/dialog';
|
||||||
|
import { writeTextFile } from '@tauri-apps/api/fs';
|
||||||
|
import { downloadDir } from '@tauri-apps/api/path';
|
||||||
import { generatePrivateKey, getPublicKey, nip19 } from 'nostr-tools';
|
import { generatePrivateKey, getPublicKey, nip19 } from 'nostr-tools';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { Button } from '@shared/button';
|
import { CopyIcon } from '@shared/icons';
|
||||||
import { EyeOffIcon, EyeOnIcon, LoaderIcon } from '@shared/icons';
|
|
||||||
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
|
|
||||||
|
|
||||||
import { useOnboarding } from '@stores/onboarding';
|
import { useOnboarding } from '@stores/onboarding';
|
||||||
import { useStronghold } from '@stores/stronghold';
|
import { useStronghold } from '@stores/stronghold';
|
||||||
@@ -21,8 +22,8 @@ export function CreateStep1Screen() {
|
|||||||
const setPubkey = useOnboarding((state) => state.setPubkey);
|
const setPubkey = useOnboarding((state) => state.setPubkey);
|
||||||
const setStep = useOnboarding((state) => state.setStep);
|
const setStep = useOnboarding((state) => state.setStep);
|
||||||
|
|
||||||
const [privkeyInput, setPrivkeyInput] = useState('password');
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
const [downloaded, setDownloaded] = useState(false);
|
const [downloaded, setDownloaded] = useState(false);
|
||||||
|
|
||||||
const privkey = useMemo(() => generatePrivateKey(), []);
|
const privkey = useMemo(() => generatePrivateKey(), []);
|
||||||
@@ -30,27 +31,39 @@ export function CreateStep1Screen() {
|
|||||||
const npub = nip19.npubEncode(pubkey);
|
const npub = nip19.npubEncode(pubkey);
|
||||||
const nsec = nip19.nsecEncode(privkey);
|
const nsec = nip19.nsecEncode(privkey);
|
||||||
|
|
||||||
// toggle private key
|
const download = async () => {
|
||||||
const showPrivateKey = () => {
|
try {
|
||||||
if (privkeyInput === 'password') {
|
const downloadPath = await downloadDir();
|
||||||
setPrivkeyInput('text');
|
const fileName = `nostr_keys_${new Date().toISOString()}.txt`;
|
||||||
} else {
|
const filePath = await save({
|
||||||
setPrivkeyInput('password');
|
defaultPath: downloadPath + '/' + fileName,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filePath) {
|
||||||
|
await writeTextFile(
|
||||||
|
filePath,
|
||||||
|
`Generated by Lume (lume.nu)\nPublic key: ${npub}\nPrivate key: ${nsec}`
|
||||||
|
);
|
||||||
|
|
||||||
|
setDownloaded(true);
|
||||||
|
} // else { user cancel action }
|
||||||
|
} catch (e) {
|
||||||
|
await message(e, { title: 'Cannot download account keys', type: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const download = async () => {
|
const copyPrivkey = async () => {
|
||||||
await writeTextFile(
|
try {
|
||||||
`nostr_keys_${new Date().toISOString().slice(0, 10)}.txt`,
|
await writeText(nsec);
|
||||||
`Generated by Lume (lume.nu)\nPublic key: ${npub}\nPrivate key: ${nsec}`,
|
setCopied(true);
|
||||||
{
|
|
||||||
dir: BaseDirectory.Download,
|
setTimeout(() => setCopied(false), 3000);
|
||||||
}
|
} catch (e) {
|
||||||
);
|
await message(e, { title: 'Cannot copy private key', type: 'error' });
|
||||||
setDownloaded(true);
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const submit = () => {
|
const submit = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
// update state
|
// update state
|
||||||
@@ -59,7 +72,7 @@ export function CreateStep1Screen() {
|
|||||||
setPubkey(pubkey);
|
setPubkey(pubkey);
|
||||||
|
|
||||||
// save to database
|
// save to database
|
||||||
db.createAccount(npub, pubkey);
|
await db.createAccount(npub, pubkey);
|
||||||
|
|
||||||
// redirect to next step
|
// redirect to next step
|
||||||
navigate('/auth/create/step-2', { replace: true });
|
navigate('/auth/create/step-2', { replace: true });
|
||||||
@@ -67,81 +80,73 @@ export function CreateStep1Screen() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// save current step, if user close app and reopen it
|
// save current step, if user close app and reopen it
|
||||||
setStep('/auth/create/step-1');
|
setStep('/auth/create');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-md">
|
<div className="mx-auto w-full max-w-md">
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-4 border-b border-white/10 pb-4">
|
||||||
<h1 className="text-xl font-semibold text-white">Save your access key!</h1>
|
<h1 className="mb-2 text-center text-2xl font-semibold text-white">
|
||||||
|
This is your new Nostr account
|
||||||
|
</h1>
|
||||||
|
<p className="mb-2 text-white/70">
|
||||||
|
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>
|
||||||
|
<p className="text-white/70">
|
||||||
|
Public key is used for sharing with other people so that they can find you using
|
||||||
|
the public key.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-8">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-3">
|
||||||
<span className="text-base font-semibold text-white/50">Public Key</span>
|
<div className="flex flex-col gap-1">
|
||||||
<input
|
<span className="font-medium text-white">Private Key</span>
|
||||||
readOnly
|
<div className="relative">
|
||||||
value={npub}
|
<input
|
||||||
className="relative h-11 w-full rounded-lg bg-white/10 px-3.5 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/50"
|
readOnly
|
||||||
/>
|
value={nsec.substring(0, 5) + '**************************************'}
|
||||||
</div>
|
className="relative h-12 w-full rounded-lg border-t border-white/10 bg-white/20 py-1 pl-3.5 pr-11 text-white !outline-none backdrop-blur-xl placeholder:text-white/70"
|
||||||
<div className="flex flex-col gap-1">
|
/>
|
||||||
<span className="text-base font-semibold text-white/50">Private Key</span>
|
<button
|
||||||
<div className="relative">
|
type="button"
|
||||||
|
onClick={() => copyPrivkey()}
|
||||||
|
className="group absolute right-2 top-1/2 inline-flex h-7 -translate-y-1/2 transform items-center gap-1.5 rounded-md bg-white/20 px-2.5 text-sm hover:bg-white/30"
|
||||||
|
>
|
||||||
|
<CopyIcon className="h-4 w-4 text-white/70 group-hover:text-white" />
|
||||||
|
{copied ? 'Copied' : 'Copy'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="font-medium text-white">Public Key</span>
|
||||||
<input
|
<input
|
||||||
readOnly
|
readOnly
|
||||||
type={privkeyInput}
|
value={npub}
|
||||||
value={nsec}
|
className="relative h-12 w-full rounded-lg border-t border-white/10 bg-white/20 px-3.5 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/70"
|
||||||
className="relative h-11 w-full rounded-lg bg-white/10 py-1 pl-3.5 pr-11 text-white !outline-none backdrop-blur-xl 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 backdrop-blur-xl 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>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => submit()}
|
onClick={() => download()}
|
||||||
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"
|
className="inline-flex h-12 w-full items-center justify-center rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{downloaded ? 'Downloaded' : 'Download account keys'}
|
||||||
<>
|
|
||||||
<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>
|
</button>
|
||||||
{downloaded ? (
|
<button
|
||||||
<span className="inline-flex h-11 w-full items-center justify-center text-sm text-white/50">
|
type="button"
|
||||||
Saved in Download folder
|
onClick={() => submit()}
|
||||||
</span>
|
className="inline-flex h-12 w-full items-center justify-center rounded-lg border-t border-white/10 bg-white/20 px-6 font-medium leading-none text-white hover:bg-white/30 focus:outline-none"
|
||||||
) : (
|
>
|
||||||
<Button preset="large-alt" onClick={() => download()}>
|
{loading ? 'Creating...' : 'Continue'}
|
||||||
Download
|
</button>
|
||||||
</Button>
|
<span className="text-center text-sm text-white/50">
|
||||||
)}
|
By clicking 'Continue', you are ensuring that your keys are saved in
|
||||||
|
a safe place. You cannot recover these keys if they are lost.
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { appConfigDir } from '@tauri-apps/api/path';
|
import { appConfigDir } from '@tauri-apps/api/path';
|
||||||
import { Stronghold } from '@tauri-apps/plugin-stronghold';
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Resolver, useForm } from 'react-hook-form';
|
import { Resolver, useForm } from 'react-hook-form';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Stronghold } from 'tauri-plugin-stronghold-api';
|
||||||
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
@@ -61,9 +61,9 @@ export function CreateStep2Screen() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
if (data.password.length > 3) {
|
if (data.password.length > 3) {
|
||||||
const dir = await appConfigDir();
|
const dir = await appConfigDir();
|
||||||
const stronghold = await Stronghold.load(`${dir}/lume.stronghold`, data.password);
|
const stronghold = await Stronghold.load(`${dir}lume.stronghold`, data.password);
|
||||||
|
|
||||||
db.secureDB = stronghold;
|
if (!db.secureDB) db.secureDB = stronghold;
|
||||||
|
|
||||||
// save privkey to secure storage
|
// save privkey to secure storage
|
||||||
await db.secureSave(pubkey, privkey);
|
await db.secureSave(pubkey, privkey);
|
||||||
@@ -86,10 +86,16 @@ export function CreateStep2Screen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-md">
|
<div className="mx-auto w-full max-w-md">
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-4 border-b border-white/10 pb-4">
|
||||||
<h1 className="text-xl font-semibold text-white">
|
<h1 className="mb-2 text-center text-2xl font-semibold text-white">
|
||||||
Set password to secure your key
|
Set password to secure your key
|
||||||
</h1>
|
</h1>
|
||||||
|
<p className="text-white/70">
|
||||||
|
Password is not related to your Nostr account. It is only used to secure your
|
||||||
|
keys stored on your local machine and to unlock the app (like unlocking your
|
||||||
|
phone with a passcode). When you move to other Nostr clients, you just need to
|
||||||
|
copy your private key.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-3">
|
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-3">
|
||||||
@@ -98,12 +104,13 @@ export function CreateStep2Screen() {
|
|||||||
<input
|
<input
|
||||||
{...register('password', { required: true })}
|
{...register('password', { required: true })}
|
||||||
type={passwordInput}
|
type={passwordInput}
|
||||||
className="relative h-11 w-full rounded-lg bg-white/10 px-3.5 py-1 text-center text-white !outline-none backdrop-blur-xl placeholder:text-white/50"
|
placeholder="Enter password"
|
||||||
|
className="relative h-12 w-full rounded-lg border-t border-white/10 bg-white/20 px-3.5 py-1 text-center tracking-widest text-white !outline-none backdrop-blur-xl placeholder:tracking-normal placeholder:text-white/70"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => showPassword()}
|
onClick={() => showPassword()}
|
||||||
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 backdrop-blur-xl hover:bg-white/10"
|
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 backdrop-blur-xl hover:bg-white/20"
|
||||||
>
|
>
|
||||||
{passwordInput === 'password' ? (
|
{passwordInput === 'password' ? (
|
||||||
<EyeOffIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
|
<EyeOffIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
|
||||||
@@ -112,13 +119,6 @@ export function CreateStep2Screen() {
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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">
|
<span className="text-sm text-red-400">
|
||||||
{errors.password && <p>{errors.password.message}</p>}
|
{errors.password && <p>{errors.password.message}</p>}
|
||||||
</span>
|
</span>
|
||||||
@@ -127,12 +127,12 @@ export function CreateStep2Screen() {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!isDirty || !isValid}
|
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"
|
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg border-t border-white/10 bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<>
|
<>
|
||||||
<span className="w-5" />
|
<span className="w-5" />
|
||||||
<span>Creating...</span>
|
<span>Securing your account...</span>
|
||||||
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import { NDKKind } from '@nostr-dev-kit/ndk';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { AvatarUploader } from '@shared/avatarUploader';
|
import { AvatarUploader } from '@shared/avatarUploader';
|
||||||
import { BannerUploader } from '@shared/bannerUploader';
|
import { BannerUploader } from '@shared/bannerUploader';
|
||||||
import { LoaderIcon } from '@shared/icons';
|
import { LoaderIcon } from '@shared/icons';
|
||||||
@@ -9,6 +12,7 @@ import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
|
|||||||
import { Image } from '@shared/image';
|
import { Image } from '@shared/image';
|
||||||
|
|
||||||
import { useOnboarding } from '@stores/onboarding';
|
import { useOnboarding } from '@stores/onboarding';
|
||||||
|
import { WidgetKinds } from '@stores/widgets';
|
||||||
|
|
||||||
import { useNostr } from '@utils/hooks/useNostr';
|
import { useNostr } from '@utils/hooks/useNostr';
|
||||||
|
|
||||||
@@ -20,6 +24,7 @@ export function CreateStep3Screen() {
|
|||||||
const [picture, setPicture] = useState('https://void.cat/d/5VKmKyuHyxrNMf9bWSVPih');
|
const [picture, setPicture] = useState('https://void.cat/d/5VKmKyuHyxrNMf9bWSVPih');
|
||||||
const [banner, setBanner] = useState('');
|
const [banner, setBanner] = useState('');
|
||||||
|
|
||||||
|
const { db } = useStorage();
|
||||||
const { publish } = useNostr();
|
const { publish } = useNostr();
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -40,10 +45,13 @@ export function CreateStep3Screen() {
|
|||||||
|
|
||||||
const event = await publish({
|
const event = await publish({
|
||||||
content: JSON.stringify(profile),
|
content: JSON.stringify(profile),
|
||||||
kind: 0,
|
kind: NDKKind.Metadata,
|
||||||
tags: [],
|
tags: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// create default widget
|
||||||
|
await db.createWidget(WidgetKinds.other.learnNostr, 'Learn Nostr', '');
|
||||||
|
|
||||||
if (event) {
|
if (event) {
|
||||||
navigate('/auth/onboarding', { replace: true });
|
navigate('/auth/onboarding', { replace: true });
|
||||||
}
|
}
|
||||||
@@ -60,30 +68,41 @@ export function CreateStep3Screen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-md">
|
<div className="mx-auto w-full max-w-md">
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-4 border-b border-white/10 pb-4">
|
||||||
<h1 className="text-xl font-semibold text-white">Create your profile</h1>
|
<h1 className="mb-2 text-center text-2xl font-semibold text-white">
|
||||||
|
Personalize your Nostr profile
|
||||||
|
</h1>
|
||||||
|
<p className="text-white/70">
|
||||||
|
Nostr profile is synchronous across all Nostr clients. If you create a profile
|
||||||
|
on Lume, it will also work well with other Nostr clients. If you update your
|
||||||
|
profile on another Nostr client, it will also sync to Lume.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full overflow-hidden rounded-xl bg-white/10 backdrop-blur-xl">
|
<div className="w-full overflow-hidden rounded-xl bg-white/10 backdrop-blur-xl">
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col">
|
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col">
|
||||||
<input type={'hidden'} {...register('picture')} value={picture} />
|
<input type={'hidden'} {...register('picture')} value={picture} />
|
||||||
<input type={'hidden'} {...register('banner')} value={banner} />
|
<input type={'hidden'} {...register('banner')} value={banner} />
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="relative h-44 w-full bg-white/10 backdrop-blur-xl">
|
<div className="relative h-36 w-full bg-white/10 backdrop-blur-xl">
|
||||||
<Image
|
{banner ? (
|
||||||
src={banner}
|
<Image
|
||||||
alt="user's banner"
|
src={banner}
|
||||||
className="h-full w-full object-cover"
|
alt="user's banner"
|
||||||
/>
|
className="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="h-full w-full bg-white/20" />
|
||||||
|
)}
|
||||||
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
|
<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} />
|
<BannerUploader setBanner={setBanner} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-5 px-4">
|
<div className="mb-5 px-4">
|
||||||
<div className="relative z-10 -mt-7 h-14 w-14">
|
<div className="relative z-10 -mt-8 h-16 w-16">
|
||||||
<Image
|
<Image
|
||||||
src={picture}
|
src={picture}
|
||||||
alt="user's avatar"
|
alt="user's avatar"
|
||||||
className="h-14 w-14 rounded-lg object-cover ring-2 ring-white/10"
|
className="h-16 w-16 rounded-lg object-cover ring-2 ring-white/20"
|
||||||
/>
|
/>
|
||||||
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
|
<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} />
|
<AvatarUploader setPicture={setPicture} />
|
||||||
@@ -93,55 +112,45 @@ export function CreateStep3Screen() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4 px-4 pb-4">
|
<div className="flex flex-col gap-4 px-4 pb-4">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label
|
<label htmlFor="name" className="font-medium text-white">
|
||||||
htmlFor="name"
|
|
||||||
className="text-sm font-semibold uppercase tracking-wider text-white/50"
|
|
||||||
>
|
|
||||||
Name *
|
Name *
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type={'text'}
|
type={'text'}
|
||||||
{...register('name', {
|
{...register('name', {
|
||||||
required: true,
|
required: true,
|
||||||
minLength: 4,
|
minLength: 1,
|
||||||
})}
|
})}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/50"
|
className="relative h-12 w-full rounded-lg bg-white/20 px-3 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/70"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label
|
<label htmlFor="about" className="font-medium text-white">
|
||||||
htmlFor="about"
|
|
||||||
className="text-sm font-semibold uppercase tracking-wider text-white/50"
|
|
||||||
>
|
|
||||||
Bio
|
Bio
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
{...register('about')}
|
{...register('about')}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative h-20 w-full resize-none rounded-lg bg-white/10 px-3 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/50"
|
className="relative h-20 w-full resize-none rounded-lg bg-white/20 px-3 py-2 text-white !outline-none backdrop-blur-xl placeholder:text-white/70"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label
|
<label htmlFor="website" className="font-medium text-white">
|
||||||
htmlFor="website"
|
|
||||||
className="text-sm font-semibold uppercase tracking-wider text-white/50"
|
|
||||||
>
|
|
||||||
Website
|
Website
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type={'text'}
|
|
||||||
{...register('website', {
|
{...register('website', {
|
||||||
required: false,
|
required: false,
|
||||||
})}
|
})}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/50"
|
className="relative h-12 w-full rounded-lg bg-white/20 px-3 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/70"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!isDirty || !isValid}
|
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"
|
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg border-t border-white/10 bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
export function HardResetScreen() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<p>hard reset</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { LoaderIcon } from '@shared/icons';
|
import { EyeOffIcon, EyeOnIcon, LoaderIcon } from '@shared/icons';
|
||||||
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
|
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
|
||||||
|
|
||||||
import { useOnboarding } from '@stores/onboarding';
|
import { useOnboarding } from '@stores/onboarding';
|
||||||
@@ -37,6 +37,7 @@ export function ImportStep1Screen() {
|
|||||||
const setStep = useOnboarding((state) => state.setStep);
|
const setStep = useOnboarding((state) => state.setStep);
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [passwordInput, setPasswordInput] = useState('password');
|
||||||
|
|
||||||
const { db } = useStorage();
|
const { db } = useStorage();
|
||||||
const {
|
const {
|
||||||
@@ -64,12 +65,13 @@ export function ImportStep1Screen() {
|
|||||||
setPubkey(pubkey);
|
setPubkey(pubkey);
|
||||||
|
|
||||||
// add account to local database
|
// add account to local database
|
||||||
db.createAccount(npub, pubkey);
|
await db.createAccount(npub, pubkey);
|
||||||
|
|
||||||
// redirect to step 2
|
// redirect to step 2 with delay 1.2s
|
||||||
navigate('/auth/import/step-2', { replace: true });
|
setTimeout(() => navigate('/auth/import/step-2', { replace: true }), 1200);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
setLoading(false);
|
||||||
setError('privkey', {
|
setError('privkey', {
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
message: 'Private key is invalid, please check again',
|
message: 'Private key is invalid, please check again',
|
||||||
@@ -77,27 +79,53 @@ export function ImportStep1Screen() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// toggle private key
|
||||||
|
const showPassword = () => {
|
||||||
|
if (passwordInput === 'password') {
|
||||||
|
setPasswordInput('text');
|
||||||
|
} else {
|
||||||
|
setPasswordInput('password');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// save current step, if user close app and reopen it
|
// save current step, if user close app and reopen it
|
||||||
setStep('/auth/import/step-1');
|
setStep('/auth/import');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-md">
|
<div className="mx-auto w-full max-w-md">
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-4 pb-4">
|
||||||
<h1 className="text-xl font-semibold text-white">Import your key</h1>
|
<h1 className="text-center text-2xl font-semibold text-white">
|
||||||
|
Import your Nostr key
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col gap-3">
|
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col gap-3">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="text-base font-semibold text-white/50">Private key</span>
|
<label htmlFor="privkey" className="font-medium text-white">
|
||||||
<input
|
Insert your nostr private key, in nsec or hex format
|
||||||
{...register('privkey', { required: true, minLength: 32 })}
|
</label>
|
||||||
type={'password'}
|
<div className="relative">
|
||||||
placeholder="nsec or hexstring"
|
<input
|
||||||
className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/50"
|
{...register('privkey', { required: true, minLength: 32 })}
|
||||||
/>
|
type={passwordInput}
|
||||||
<span className="text-sm text-red-400">
|
placeholder="nsec1..."
|
||||||
|
className="relative h-12 w-full rounded-lg border-t border-white/10 bg-white/20 px-3 py-1 text-white backdrop-blur-xl placeholder:text-white/70 focus:outline-none"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => showPassword()}
|
||||||
|
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 backdrop-blur-xl hover:bg-white/20"
|
||||||
|
>
|
||||||
|
{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>
|
||||||
|
<span className="text-sm text-red-500">
|
||||||
{errors.privkey && <p>{errors.privkey.message}</p>}
|
{errors.privkey && <p>{errors.privkey.message}</p>}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -105,12 +133,12 @@ export function ImportStep1Screen() {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!isDirty || !isValid}
|
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"
|
className="inline-flex h-12 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 ? (
|
{loading ? (
|
||||||
<>
|
<>
|
||||||
<span className="w-5" />
|
<span className="w-5" />
|
||||||
<span>Creating...</span>
|
<span>Importing...</span>
|
||||||
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { appConfigDir } from '@tauri-apps/api/path';
|
import { appConfigDir } from '@tauri-apps/api/path';
|
||||||
import { Stronghold } from '@tauri-apps/plugin-stronghold';
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Resolver, useForm } from 'react-hook-form';
|
import { Resolver, useForm } from 'react-hook-form';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Stronghold } from 'tauri-plugin-stronghold-api';
|
||||||
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
@@ -61,9 +61,9 @@ export function ImportStep2Screen() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
if (data.password.length > 3) {
|
if (data.password.length > 3) {
|
||||||
const dir = await appConfigDir();
|
const dir = await appConfigDir();
|
||||||
const stronghold = await Stronghold.load(`${dir}/lume.stronghold`, data.password);
|
const stronghold = await Stronghold.load(`${dir}lume.stronghold`, data.password);
|
||||||
|
|
||||||
db.secureDB = stronghold;
|
if (!db.secureDB) db.secureDB = stronghold;
|
||||||
|
|
||||||
// save privkey to secure storage
|
// save privkey to secure storage
|
||||||
await db.secureSave(pubkey, privkey);
|
await db.secureSave(pubkey, privkey);
|
||||||
@@ -86,10 +86,16 @@ export function ImportStep2Screen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-md">
|
<div className="mx-auto w-full max-w-md">
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-4 border-b border-white/10 pb-4">
|
||||||
<h1 className="text-xl font-semibold text-white">
|
<h1 className="mb-2 text-center text-2xl font-semibold text-white">
|
||||||
Set password to secure your key
|
Set password to secure your key
|
||||||
</h1>
|
</h1>
|
||||||
|
<p className="text-white/70">
|
||||||
|
Password is not related to your Nostr account. It is only used to secure your
|
||||||
|
keys stored on your local machine and to unlock the app (like unlocking your
|
||||||
|
phone with a passcode). When you move to other Nostr clients, you only need to
|
||||||
|
copy your private key.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-3">
|
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-3">
|
||||||
@@ -98,12 +104,13 @@ export function ImportStep2Screen() {
|
|||||||
<input
|
<input
|
||||||
{...register('password', { required: true })}
|
{...register('password', { required: true })}
|
||||||
type={passwordInput}
|
type={passwordInput}
|
||||||
className="relative h-11 w-full rounded-lg bg-white/10 px-3.5 py-1 text-center text-white !outline-none backdrop-blur-xl placeholder:text-white/50"
|
placeholder="Enter password"
|
||||||
|
className="relative h-12 w-full rounded-lg border-t border-white/10 bg-white/20 px-3.5 py-1 text-center tracking-widest text-white !outline-none backdrop-blur-xl placeholder:tracking-normal placeholder:text-white/70"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => showPassword()}
|
onClick={() => showPassword()}
|
||||||
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 backdrop-blur-xl hover:bg-white/10"
|
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 backdrop-blur-xl hover:bg-white/20"
|
||||||
>
|
>
|
||||||
{passwordInput === 'password' ? (
|
{passwordInput === 'password' ? (
|
||||||
<EyeOffIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
|
<EyeOffIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
|
||||||
@@ -112,11 +119,6 @@ export function ImportStep2Screen() {
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-white/50">
|
|
||||||
Password is use to unlock app and 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>
|
|
||||||
<span className="text-sm text-red-400">
|
<span className="text-sm text-red-400">
|
||||||
{errors.password && <p>{errors.password.message}</p>}
|
{errors.password && <p>{errors.password.message}</p>}
|
||||||
</span>
|
</span>
|
||||||
@@ -125,12 +127,12 @@ export function ImportStep2Screen() {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!isDirty || !isValid}
|
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"
|
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg border-t border-white/10 bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<>
|
<>
|
||||||
<span className="w-5" />
|
<span className="w-5" />
|
||||||
<span>Creating...</span>
|
<span>Securing your account...</span>
|
||||||
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { User } from '@app/auth/components/user';
|
|
||||||
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
|
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
|
||||||
|
import { User } from '@shared/user';
|
||||||
|
|
||||||
import { useOnboarding } from '@stores/onboarding';
|
import { useOnboarding } from '@stores/onboarding';
|
||||||
|
import { WidgetKinds } from '@stores/widgets';
|
||||||
|
|
||||||
import { useNostr } from '@utils/hooks/useNostr';
|
import { useNostr } from '@utils/hooks/useNostr';
|
||||||
|
|
||||||
@@ -15,11 +15,11 @@ export function ImportStep3Screen() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const setStep = useOnboarding((state) => state.setStep);
|
const setStep = useOnboarding((state) => state.setStep);
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const { db } = useStorage();
|
const { db } = useStorage();
|
||||||
const { fetchUserData, prefetchEvents } = useNostr();
|
const { fetchUserData, prefetchEvents } = useNostr();
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
try {
|
try {
|
||||||
// show loading indicator
|
// show loading indicator
|
||||||
@@ -29,6 +29,9 @@ export function ImportStep3Screen() {
|
|||||||
const user = await fetchUserData();
|
const user = await fetchUserData();
|
||||||
const data = await prefetchEvents();
|
const data = await prefetchEvents();
|
||||||
|
|
||||||
|
// create default widget
|
||||||
|
await db.createWidget(WidgetKinds.other.learnNostr, 'Learn Nostr', '');
|
||||||
|
|
||||||
// redirect to next step
|
// redirect to next step
|
||||||
if (user.status === 'ok' && data.status === 'ok') {
|
if (user.status === 'ok' && data.status === 'ok') {
|
||||||
navigate('/auth/onboarding/step-2', { replace: true });
|
navigate('/auth/onboarding/step-2', { replace: true });
|
||||||
@@ -49,17 +52,19 @@ export function ImportStep3Screen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-md">
|
<div className="mx-auto w-full max-w-md">
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-4 pb-4">
|
||||||
<h1 className="text-xl font-semibold">
|
<h1 className="text-center text-2xl font-semibold text-white">
|
||||||
{loading ? 'Prefetching data...' : 'Continue with'}
|
{loading ? 'Downloading...' : 'Your Nostr profile'}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full rounded-xl bg-white/10 p-4 backdrop-blur-xl">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="rounded-lg border-t border-white/10 bg-white/20 px-3 py-3">
|
||||||
<User pubkey={db.account.pubkey} />
|
<User pubkey={db.account.pubkey} variant="simple" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
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"
|
className="inline-flex h-12 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"
|
||||||
onClick={() => submit()}
|
onClick={() => submit()}
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -76,6 +81,10 @@ export function ImportStep3Screen() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
<span className="text-center text-sm text-white/50">
|
||||||
|
By clicking 'Continue', Lume will download your old relay list and
|
||||||
|
all events from the last 24 hours. It may take a bit
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { appConfigDir } from '@tauri-apps/api/path';
|
import { appConfigDir } from '@tauri-apps/api/path';
|
||||||
import { Stronghold } from '@tauri-apps/plugin-stronghold';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Resolver, useForm } from 'react-hook-form';
|
import { Resolver, useForm } from 'react-hook-form';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Stronghold } from 'tauri-plugin-stronghold-api';
|
||||||
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { User } from '@app/auth/components/user';
|
|
||||||
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { ArrowRightCircleIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons';
|
import { ArrowRightCircleIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons';
|
||||||
|
import { User } from '@shared/user';
|
||||||
|
|
||||||
import { useOnboarding } from '@stores/onboarding';
|
import { useOnboarding } from '@stores/onboarding';
|
||||||
|
|
||||||
@@ -19,7 +18,7 @@ export function OnboardStep1Screen() {
|
|||||||
|
|
||||||
const { publish, fetchUserData, prefetchEvents } = useNostr();
|
const { publish, fetchUserData, prefetchEvents } = useNostr();
|
||||||
const { db } = useStorage();
|
const { db } = useStorage();
|
||||||
const { status, data } = useQuery(['trending-profiles'], async () => {
|
const { status, data } = useQuery(['trending-profiles-widget'], async () => {
|
||||||
const res = await fetch('https://api.nostr.band/v0/trending/profiles');
|
const res = await fetch('https://api.nostr.band/v0/trending/profiles');
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error('Error');
|
throw new Error('Error');
|
||||||
@@ -68,45 +67,51 @@ export function OnboardStep1Screen() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-md">
|
<div className="flex h-full w-full flex-col justify-center">
|
||||||
<div className="mb-8 text-center">
|
<div className="mx-auto mb-4 w-full max-w-md border-b border-white/10 pb-4">
|
||||||
<h1 className="text-xl font-semibold text-white">
|
<h1 className="mb-2 text-center text-2xl font-semibold text-white">
|
||||||
{loading ? 'Prefetching data...' : 'Enrich your network'}
|
{loading ? 'Prefetching data...' : 'Enrich your network'}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-white/50">Choose account you want to follow</p>
|
<p className="text-white/70">
|
||||||
|
Choose the account you want to follow. These accounts are trending in the last
|
||||||
|
24 hours. If none of the accounts interest you, you can explore more options and
|
||||||
|
add them later.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="scrollbar-hide flex w-full flex-nowrap items-center gap-4 overflow-x-auto px-4">
|
||||||
<div className="scrollbar-hide flex h-[500px] w-full flex-col overflow-y-auto rounded-xl bg-white/10 py-2 backdrop-blur-xl">
|
{status === 'loading' ? (
|
||||||
{status === 'loading' ? (
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
|
</div>
|
||||||
</div>
|
) : (
|
||||||
) : (
|
data?.profiles.map((item: { pubkey: string; profile: { content: string } }) => (
|
||||||
data?.profiles.map(
|
<button
|
||||||
(item: { pubkey: string; profile: { content: string } }) => (
|
key={item.pubkey}
|
||||||
<button
|
type="button"
|
||||||
key={item.pubkey}
|
onClick={() => toggleFollow(item.pubkey)}
|
||||||
type="button"
|
className="relative h-[300px] shrink-0 grow-0 basis-[250px] rounded-lg border-t border-white/10 bg-white/20 px-4 py-4 hover:bg-white/30"
|
||||||
onClick={() => toggleFollow(item.pubkey)}
|
>
|
||||||
className="inline-flex transform items-center justify-between px-4 py-2 hover:bg-white/20"
|
<User
|
||||||
>
|
pubkey={item.pubkey}
|
||||||
<User pubkey={item.pubkey} fallback={item.profile?.content} />
|
variant="large"
|
||||||
{follows.includes(item.pubkey) && (
|
embedProfile={item.profile?.content}
|
||||||
<div>
|
/>
|
||||||
<CheckCircleIcon className="h-4 w-4 text-green-400" />
|
{follows.includes(item.pubkey) && (
|
||||||
</div>
|
<div className="absolute right-2 top-2">
|
||||||
)}
|
<CheckCircleIcon className="h-4 w-4 text-green-400" />
|
||||||
</button>
|
</div>
|
||||||
)
|
)}
|
||||||
)
|
</button>
|
||||||
)}
|
))
|
||||||
</div>
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mx-auto mt-4 w-full max-w-md">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={submit}
|
onClick={submit}
|
||||||
disabled={loading || follows.length === 0}
|
disabled={loading || follows.length === 0}
|
||||||
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 disabled:opacity-50"
|
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg border-t border-white/10 bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<>
|
<>
|
||||||
@@ -122,12 +127,19 @@ export function OnboardStep1Screen() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
<Link
|
{!loading ? (
|
||||||
to="/auth/onboarding/step-2"
|
<Link
|
||||||
className="inline-flex h-11 w-full items-center justify-center rounded-lg px-6 font-medium leading-none text-white backdrop-blur-xl hover:bg-white/10 focus:outline-none"
|
to="/auth/onboarding/step-2"
|
||||||
>
|
className="inline-flex h-12 w-full items-center justify-center rounded-lg border-t border-white/10 bg-white/20 px-6 font-medium leading-none text-white backdrop-blur-xl hover:bg-white/30 focus:outline-none"
|
||||||
Skip, you can add later
|
>
|
||||||
</Link>
|
Skip, you can add later
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span className="text-center text-sm text-white/50">
|
||||||
|
By clicking 'Continue', Lume will download all events related to
|
||||||
|
your follows from the last 24 hours. It may take a bit
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { message } from '@tauri-apps/api/dialog';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ import { WidgetKinds } from '@stores/widgets';
|
|||||||
const data = [
|
const data = [
|
||||||
{ hashtag: '#bitcoin' },
|
{ hashtag: '#bitcoin' },
|
||||||
{ hashtag: '#nostr' },
|
{ hashtag: '#nostr' },
|
||||||
|
{ hashtag: '#nostrdesign' },
|
||||||
{ hashtag: '#zap' },
|
{ hashtag: '#zap' },
|
||||||
{ hashtag: '#LFG' },
|
{ hashtag: '#LFG' },
|
||||||
{ hashtag: '#zapchain' },
|
{ hashtag: '#zapchain' },
|
||||||
@@ -19,6 +21,10 @@ const data = [
|
|||||||
{ hashtag: '#hodl' },
|
{ hashtag: '#hodl' },
|
||||||
{ hashtag: '#stacksats' },
|
{ hashtag: '#stacksats' },
|
||||||
{ hashtag: '#nokyc' },
|
{ hashtag: '#nokyc' },
|
||||||
|
{ hashtag: '#meme' },
|
||||||
|
{ hashtag: '#memes' },
|
||||||
|
{ hashtag: '#memestr' },
|
||||||
|
{ hashtag: '#penisbutter' },
|
||||||
{ hashtag: '#anime' },
|
{ hashtag: '#anime' },
|
||||||
{ hashtag: '#waifu' },
|
{ hashtag: '#waifu' },
|
||||||
{ hashtag: '#manga' },
|
{ hashtag: '#manga' },
|
||||||
@@ -28,8 +34,8 @@ const data = [
|
|||||||
|
|
||||||
export function OnboardStep2Screen() {
|
export function OnboardStep2Screen() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const setStep = useOnboarding((state) => state.setStep);
|
|
||||||
|
|
||||||
|
const [setStep, clearStep] = useOnboarding((state) => [state.setStep, state.clearStep]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [tags, setTags] = useState(new Set<string>());
|
const [tags, setTags] = useState(new Set<string>());
|
||||||
|
|
||||||
@@ -47,17 +53,34 @@ export function OnboardStep2Screen() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const skip = async () => {
|
||||||
|
// update last login
|
||||||
|
await db.updateLastLogin();
|
||||||
|
|
||||||
|
// clear local storage
|
||||||
|
clearStep();
|
||||||
|
|
||||||
|
navigate('/auth/complete', { replace: true });
|
||||||
|
};
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
await db.createWidget(WidgetKinds.hashtag, tag, tag.replace('#', ''));
|
await db.createWidget(WidgetKinds.global.hashtag, tag, tag.replace('#', ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate('/auth/onboarding/step-3', { replace: true });
|
// update last login
|
||||||
} catch {
|
await db.updateLastLogin();
|
||||||
console.log('error');
|
|
||||||
|
// clear local storage
|
||||||
|
clearStep();
|
||||||
|
|
||||||
|
navigate('/auth/complete', { replace: true });
|
||||||
|
} catch (e) {
|
||||||
|
setLoading(false);
|
||||||
|
await message(e, { title: 'Lume', type: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -68,20 +91,23 @@ export function OnboardStep2Screen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-md">
|
<div className="mx-auto w-full max-w-md">
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-4 border-b border-white/10 pb-4">
|
||||||
<h1 className="text-xl font-semibold text-white">
|
<h1 className="mb-2 text-center text-2xl font-semibold text-white">
|
||||||
Choose {tags.size}/3 your favorite tags
|
Choose {tags.size}/3 your favorite hashtags
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-white/50">Customize your space which hashtag widget</p>
|
<p className="text-white/70">
|
||||||
|
Hashtags are an easy way to discover more content. By adding a hashtag, Lume
|
||||||
|
will show all related posts. You can always add more later.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="scrollbar-hide flex h-[500px] w-full flex-col overflow-y-auto rounded-xl bg-white/10 backdrop-blur-xl">
|
<div className="scrollbar-hide flex h-[450px] w-full flex-col divide-y divide-white/5 overflow-y-auto rounded-xl bg-white/20 backdrop-blur-xl">
|
||||||
{data.map((item: { hashtag: string }) => (
|
{data.map((item: { hashtag: string }) => (
|
||||||
<button
|
<button
|
||||||
key={item.hashtag}
|
key={item.hashtag}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => toggleTag(item.hashtag)}
|
onClick={() => toggleTag(item.hashtag)}
|
||||||
className="inline-flex transform items-center justify-between bg-white/10 px-4 py-2 backdrop-blur-xl hover:bg-white/20"
|
className="inline-flex transform items-center justify-between px-4 py-2 hover:bg-white/10"
|
||||||
>
|
>
|
||||||
<p className="text-white">{item.hashtag}</p>
|
<p className="text-white">{item.hashtag}</p>
|
||||||
{tags.has(item.hashtag) && (
|
{tags.has(item.hashtag) && (
|
||||||
@@ -97,7 +123,7 @@ export function OnboardStep2Screen() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={submit}
|
onClick={submit}
|
||||||
disabled={loading || tags.size === 0 || tags.size > 3}
|
disabled={loading || tags.size === 0 || tags.size > 3}
|
||||||
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 disabled:opacity-50"
|
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg border-t border-white/10 bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<>
|
<>
|
||||||
@@ -113,12 +139,15 @@ export function OnboardStep2Screen() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
<Link
|
{!loading ? (
|
||||||
to="/auth/onboarding/step-3"
|
<button
|
||||||
className="inline-flex h-11 w-full items-center justify-center rounded-lg px-6 font-medium leading-none text-white backdrop-blur-xl hover:bg-white/10 focus:outline-none"
|
type="button"
|
||||||
>
|
onClick={() => skip()}
|
||||||
Skip, you can add later
|
className="inline-flex h-12 w-full items-center justify-center rounded-lg border-t border-white/10 bg-white/20 font-medium leading-none text-white backdrop-blur-xl hover:bg-white/30 focus:outline-none"
|
||||||
</Link>
|
>
|
||||||
|
Skip, you can add later
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,12 +2,11 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { UserRelay } from '@app/auth/components/userRelay';
|
|
||||||
|
|
||||||
import { useNDK } from '@libs/ndk/provider';
|
import { useNDK } from '@libs/ndk/provider';
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { ArrowRightCircleIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons';
|
import { ArrowRightCircleIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons';
|
||||||
|
import { User } from '@shared/user';
|
||||||
|
|
||||||
import { FULL_RELAYS } from '@stores/constants';
|
import { FULL_RELAYS } from '@stores/constants';
|
||||||
import { useOnboarding } from '@stores/onboarding';
|
import { useOnboarding } from '@stores/onboarding';
|
||||||
@@ -49,6 +48,8 @@ export function OnboardStep3Screen() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const relaysAsArray = Array.from(data?.keys() || []);
|
||||||
|
|
||||||
const toggleRelay = (relay: string) => {
|
const toggleRelay = (relay: string) => {
|
||||||
if (relays.has(relay)) {
|
if (relays.has(relay)) {
|
||||||
setRelays((prev) => {
|
setRelays((prev) => {
|
||||||
@@ -61,8 +62,9 @@ export function OnboardStep3Screen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const submit = async (skip?: boolean) => {
|
const submit = async (skip?: boolean) => {
|
||||||
setLoading(true);
|
|
||||||
try {
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
if (!skip) {
|
if (!skip) {
|
||||||
for (const relay of relays) {
|
for (const relay of relays) {
|
||||||
await db.createRelay(relay);
|
await db.createRelay(relay);
|
||||||
@@ -87,8 +89,6 @@ export function OnboardStep3Screen() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const relaysAsArray = Array.from(data?.keys() || []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// save current step, if user close app and reopen it
|
// save current step, if user close app and reopen it
|
||||||
setStep('/auth/onboarding/step-3');
|
setStep('/auth/onboarding/step-3');
|
||||||
@@ -118,10 +118,11 @@ export function OnboardStep3Screen() {
|
|||||||
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
|
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
|
||||||
</div>
|
</div>
|
||||||
) : relaysAsArray.length === 0 ? (
|
) : relaysAsArray.length === 0 ? (
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
<div className="flex h-full w-full items-center justify-center px-6">
|
||||||
<p className="text-center text-white/50">
|
<p className="text-center text-white/50">
|
||||||
Can't found any relays, you can skip this step and use default relays
|
Lume couldn't find any relays from your follows.
|
||||||
instead
|
<br />
|
||||||
|
You can skip this step and use default relays instead.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -134,7 +135,7 @@ export function OnboardStep3Screen() {
|
|||||||
>
|
>
|
||||||
<div className="flex flex-col items-start gap-1">
|
<div className="flex flex-col items-start gap-1">
|
||||||
<p className="max-w-[15rem] truncate">{item.replace(/\/+$/, '')}</p>
|
<p className="max-w-[15rem] truncate">{item.replace(/\/+$/, '')}</p>
|
||||||
<UserRelay pubkey={data.get(item)} />
|
<User pubkey={data.get(item)} variant="mention" />
|
||||||
</div>
|
</div>
|
||||||
{relays.has(item) && (
|
{relays.has(item) && (
|
||||||
<div className="pt-1.5">
|
<div className="pt-1.5">
|
||||||
@@ -145,7 +146,7 @@ export function OnboardStep3Screen() {
|
|||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
{relays.size > 5 && (
|
{relays.size > 5 && (
|
||||||
<div className="sticky bottom-0 left-0 inline-flex w-full items-center justify-center bg-white/10 px-4 py-2 backdrop-blur-2xl backdrop-blur-xl">
|
<div className="sticky bottom-0 left-0 inline-flex w-full items-center justify-center bg-white/10 px-4 py-2 backdrop-blur-2xl">
|
||||||
<p className="text-sm text-orange-400">
|
<p className="text-sm text-orange-400">
|
||||||
Using too much relay can cause high resource usage
|
Using too much relay can cause high resource usage
|
||||||
</p>
|
</p>
|
||||||
@@ -178,7 +179,7 @@ export function OnboardStep3Screen() {
|
|||||||
onClick={() => submit(true)}
|
onClick={() => submit(true)}
|
||||||
className="inline-flex h-11 w-full items-center justify-center rounded-lg px-6 font-medium leading-none text-white backdrop-blur-xl hover:bg-white/10 focus:outline-none"
|
className="inline-flex h-11 w-full items-center justify-center rounded-lg px-6 font-medium leading-none text-white backdrop-blur-xl hover:bg-white/10 focus:outline-none"
|
||||||
>
|
>
|
||||||
Skip, use default relays
|
Skip, use Lume default relays
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+15
-9
@@ -1,9 +1,9 @@
|
|||||||
import { appConfigDir } from '@tauri-apps/api/path';
|
import { appConfigDir } from '@tauri-apps/api/path';
|
||||||
import { Stronghold } from '@tauri-apps/plugin-stronghold';
|
|
||||||
import { getPublicKey, nip19 } from 'nostr-tools';
|
import { getPublicKey, nip19 } from 'nostr-tools';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Resolver, useForm } from 'react-hook-form';
|
import { Resolver, useForm } from 'react-hook-form';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
|
import { Stronghold } from 'tauri-plugin-stronghold-api';
|
||||||
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
@@ -116,28 +116,28 @@ export function ResetScreen() {
|
|||||||
</div>
|
</div>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col gap-3">
|
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col gap-3">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label htmlFor="privkey" className="font-medium text-white/50">
|
<label htmlFor="privkey" className="font-medium text-white">
|
||||||
Private key
|
Private key
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
{...register('privkey', { required: true })}
|
{...register('privkey', { required: true })}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="nsec..."
|
placeholder="nsec1..."
|
||||||
className="relative h-12 w-full rounded-lg bg-white/10 px-3.5 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/10"
|
className="relative h-12 w-full rounded-lg border-t border-white/10 bg-white/20 px-3.5 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/70"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label htmlFor="password" className="font-medium text-white/50">
|
<label htmlFor="password" className="font-medium text-white">
|
||||||
Set a new password to protect your key
|
Set a new password to protect your key
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
{...register('password', { required: true })}
|
{...register('password', { required: true })}
|
||||||
type={passwordInput}
|
type={passwordInput}
|
||||||
placeholder="min. 4 characters"
|
placeholder="Min. 4 characters"
|
||||||
className="relative h-12 w-full rounded-lg bg-white/10 px-3.5 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/10"
|
className="relative h-12 w-full rounded-lg border-t border-white/10 bg-white/20 px-3.5 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/70"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -155,7 +155,7 @@ export function ResetScreen() {
|
|||||||
{errors.password && <p>{errors.password.message}</p>}
|
{errors.password && <p>{errors.password.message}</p>}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex flex-col items-center justify-center">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!isDirty || !isValid}
|
disabled={!isDirty || !isValid}
|
||||||
@@ -167,6 +167,12 @@ export function ResetScreen() {
|
|||||||
'Continue →'
|
'Continue →'
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
<Link
|
||||||
|
to="/auth/unlock"
|
||||||
|
className="mt-1 inline-flex h-12 w-full items-center justify-center rounded-lg text-center text-white/70 hover:bg-white/20"
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+15
-11
@@ -1,14 +1,13 @@
|
|||||||
import { appConfigDir } from '@tauri-apps/api/path';
|
import { appConfigDir } from '@tauri-apps/api/path';
|
||||||
import { Stronghold } from '@tauri-apps/plugin-stronghold';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Resolver, useForm } from 'react-hook-form';
|
import { Resolver, useForm } from 'react-hook-form';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
|
import { Stronghold } from 'tauri-plugin-stronghold-api';
|
||||||
import { User } from '@app/auth/components/user';
|
|
||||||
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { ArrowRightCircleIcon, EyeOffIcon, EyeOnIcon, LoaderIcon } from '@shared/icons';
|
import { ArrowRightCircleIcon, EyeOffIcon, EyeOnIcon, LoaderIcon } from '@shared/icons';
|
||||||
|
import { User } from '@shared/user';
|
||||||
|
|
||||||
import { useStronghold } from '@stores/stronghold';
|
import { useStronghold } from '@stores/stronghold';
|
||||||
|
|
||||||
@@ -33,6 +32,7 @@ const resolver: Resolver<FormValues> = async (values) => {
|
|||||||
export function UnlockScreen() {
|
export function UnlockScreen() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const setPrivkey = useStronghold((state) => state.setPrivkey);
|
const setPrivkey = useStronghold((state) => state.setPrivkey);
|
||||||
|
const setWalletConnectURL = useStronghold((state) => state.setWalletConnectURL);
|
||||||
|
|
||||||
const [showPassword, setShowPassword] = useState<boolean>(false);
|
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
@@ -55,8 +55,10 @@ export function UnlockScreen() {
|
|||||||
if (!db.secureDB) db.secureDB = stronghold;
|
if (!db.secureDB) db.secureDB = stronghold;
|
||||||
|
|
||||||
const privkey = await db.secureLoad(db.account.pubkey);
|
const privkey = await db.secureLoad(db.account.pubkey);
|
||||||
|
const uri = await db.secureLoad('walletConnectURL', 'nwc');
|
||||||
|
|
||||||
setPrivkey(privkey);
|
if (privkey) setPrivkey(privkey);
|
||||||
|
if (uri) setWalletConnectURL(uri);
|
||||||
// redirect to home
|
// redirect to home
|
||||||
navigate('/', { replace: true });
|
navigate('/', { replace: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -71,20 +73,22 @@ export function UnlockScreen() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
<div className="mx-auto w-full max-w-md">
|
<div className="mx-auto w-full max-w-md">
|
||||||
<div className="mb-6 text-center">
|
<div className="mb-4 pb-4">
|
||||||
<h1 className="text-2xl font-semibold text-white">Enter password to unlock</h1>
|
<h1 className="text-center text-2xl font-semibold text-white">
|
||||||
|
Enter password to unlock
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col">
|
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col">
|
||||||
<div className="flex flex-col rounded-lg bg-white/5">
|
<div className="flex flex-col rounded-lg bg-white/5">
|
||||||
<div className="w-full rounded-t-lg border-b border-white/10 bg-white/5 p-4">
|
<div className="w-full rounded-t-lg border-b border-white/10 bg-white/5 p-4">
|
||||||
<User pubkey={db.account.pubkey} />
|
<User pubkey={db.account.pubkey} variant="simple" />
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
{...register('password', { required: true, minLength: 4 })}
|
{...register('password', { required: true, minLength: 4 })}
|
||||||
type={showPassword ? 'text' : 'password'}
|
type={showPassword ? 'text' : 'password'}
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
className="relative h-12 w-full rounded-b-lg bg-white/10 py-1 text-center text-white !outline-none backdrop-blur-xl placeholder:text-white/50"
|
className="relative h-12 w-full rounded-b-lg bg-white/10 py-1 text-center tracking-widest text-white !outline-none backdrop-blur-xl placeholder:tracking-normal placeholder:text-white/50"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -106,12 +110,12 @@ export function UnlockScreen() {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!isDirty || !isValid}
|
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 disabled:opacity-50"
|
className="inline-flex h-12 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 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<>
|
<>
|
||||||
<span className="w-5" />
|
<span className="w-5" />
|
||||||
<span>Decryting...</span>
|
<span>Unlocking...</span>
|
||||||
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -124,7 +128,7 @@ export function UnlockScreen() {
|
|||||||
</button>
|
</button>
|
||||||
<Link
|
<Link
|
||||||
to="/auth/reset"
|
to="/auth/reset"
|
||||||
className="mt-1 inline-flex h-11 w-full items-center justify-center rounded-lg text-center text-white/50 hover:bg-white/10"
|
className="mt-1 inline-flex h-12 w-full items-center justify-center rounded-lg text-center text-white/70 hover:bg-white/20"
|
||||||
>
|
>
|
||||||
Reset password
|
Reset password
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
+10
-10
@@ -1,4 +1,4 @@
|
|||||||
import { LogicalSize, getCurrent } from '@tauri-apps/plugin-window';
|
import { LogicalSize, getCurrent } from '@tauri-apps/api/window';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
@@ -28,19 +28,19 @@ export function WelcomeScreen() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-full flex-col justify-between bg-white/10 backdrop-blur-xl">
|
<div className="flex h-screen w-full flex-col justify-between">
|
||||||
<div className="flex flex-col gap-10 pt-16">
|
<div className="flex flex-col gap-10 pt-16">
|
||||||
<div className="sflex flex-col gap-2 text-center">
|
<div className="flex flex-col gap-1.5 text-center">
|
||||||
<h1 className="text-3xl font-medium text-white">Welcome to Lume</h1>
|
<h1 className="text-3xl font-semibold text-white">Welcome to Lume</h1>
|
||||||
<h3 className="mx-auto w-2/3 text-white/50">
|
<p className="mx-auto w-2/3 leading-tight text-white/50">
|
||||||
Let's get you up and connecting with all peoples around the world on
|
Let's get you up and connecting with all peoples around the world on
|
||||||
Nostr
|
Nostr
|
||||||
</h3>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex w-full flex-col items-center gap-3 px-4 pb-10">
|
<div className="inline-flex w-full flex-col items-center gap-3 px-4 pb-10">
|
||||||
<Link
|
<Link
|
||||||
to="/auth/import"
|
to="/auth/import"
|
||||||
className="inline-flex h-11 w-2/3 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"
|
className="inline-flex h-12 w-3/4 items-center justify-between gap-2 rounded-lg border-t border-white/10 bg-fuchsia-500 px-4 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
|
||||||
>
|
>
|
||||||
<span className="w-5" />
|
<span className="w-5" />
|
||||||
<span>Login with private key</span>
|
<span>Login with private key</span>
|
||||||
@@ -48,14 +48,14 @@ export function WelcomeScreen() {
|
|||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/auth/create"
|
to="/auth/create"
|
||||||
className="inline-flex h-11 w-2/3 items-center justify-center gap-2 rounded-lg bg-white/10 px-6 font-medium leading-none text-white/50 backdrop-blur-xl hover:bg-white/20 focus:outline-none"
|
className="inline-flex h-12 w-3/4 items-center justify-center gap-2 rounded-lg border-t border-white/10 bg-white/20 font-medium leading-none text-white backdrop-blur-xl hover:bg-white/30 focus:outline-none"
|
||||||
>
|
>
|
||||||
Create new key
|
Create new key
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-1 items-end justify-center pb-10">
|
<div className="flex flex-1 items-end justify-center pb-6">
|
||||||
<img src="/lume.png" alt="lume" className="h-auto w-1/3" />
|
<img src="/lume.png" alt="lume" className="h-auto w-1/4" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function ChatsListItem({ pubkey }: { pubkey: string }) {
|
|||||||
'flex h-10 items-center gap-2.5 rounded-r-lg border-l-2 pl-4 pr-2',
|
'flex h-10 items-center gap-2.5 rounded-r-lg border-l-2 pl-4 pr-2',
|
||||||
isActive
|
isActive
|
||||||
? 'border-fuchsia-500 bg-white/5 text-white'
|
? 'border-fuchsia-500 bg-white/5 text-white'
|
||||||
: 'border-transparent text-white/80'
|
: 'border-transparent text-white/70'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -38,7 +38,7 @@ export function ChatsListItem({ pubkey }: { pubkey: string }) {
|
|||||||
/>
|
/>
|
||||||
<div className="inline-flex w-full flex-1 items-center justify-between">
|
<div className="inline-flex w-full flex-1 items-center justify-between">
|
||||||
<h5 className="max-w-[10rem] truncate">
|
<h5 className="max-w-[10rem] truncate">
|
||||||
{user?.nip05 || user?.name || user?.display_name || displayNpub(pubkey, 16)}
|
{user?.name || user?.display_name || displayNpub(pubkey, 16)}
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|||||||
@@ -36,10 +36,6 @@ export function ChatsList() {
|
|||||||
<div className="relative h-7 w-7 shrink-0 animate-pulse rounded bg-white/10 backdrop-blur-xl" />
|
<div className="relative h-7 w-7 shrink-0 animate-pulse rounded bg-white/10 backdrop-blur-xl" />
|
||||||
<div className="h-4 w-full animate-pulse rounded bg-white/10 backdrop-blur-xl" />
|
<div className="h-4 w-full animate-pulse rounded bg-white/10 backdrop-blur-xl" />
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex h-10 items-center gap-2.5 border-l-2 border-transparent pl-4">
|
|
||||||
<div className="relative h-7 w-7 shrink-0 animate-pulse rounded bg-white/10 backdrop-blur-xl" />
|
|
||||||
<div className="h-4 w-full animate-pulse rounded bg-white/10 backdrop-blur-xl" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function ChatMessageItem({
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-min min-h-min w-full select-text flex-col px-5 py-3 backdrop-blur-xl hover:bg-white/10">
|
<div className="flex h-min min-h-min w-full select-text flex-col px-5 py-3 backdrop-blur-xl hover:bg-white/10">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<User pubkey={message.pubkey} time={message.created_at} isChat={true} />
|
<User pubkey={message.pubkey} time={message.created_at} variant="chat" />
|
||||||
<div className="-mt-[20px] pl-[49px]">
|
<div className="-mt-[20px] pl-[49px]">
|
||||||
<p className="select-text whitespace-pre-line break-words text-base text-white">
|
<p className="select-text whitespace-pre-line break-words text-base text-white">
|
||||||
{message.content}
|
{message.content}
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import { Dispatch, SetStateAction, useState } from 'react';
|
|||||||
|
|
||||||
import { LoaderIcon, MediaIcon } from '@shared/icons';
|
import { LoaderIcon, MediaIcon } from '@shared/icons';
|
||||||
|
|
||||||
import { useImageUploader } from '@utils/hooks/useUploader';
|
import { useNostr } from '@utils/hooks/useNostr';
|
||||||
|
|
||||||
export function MediaUploader({
|
export function MediaUploader({
|
||||||
setState,
|
setState,
|
||||||
}: {
|
}: {
|
||||||
setState: Dispatch<SetStateAction<string>>;
|
setState: Dispatch<SetStateAction<string>>;
|
||||||
}) {
|
}) {
|
||||||
const upload = useImageUploader();
|
const { upload } = useNostr();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const uploadMedia = async () => {
|
const uploadMedia = async () => {
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import * as Dialog from '@radix-ui/react-dialog';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { User } from '@app/auth/components/user';
|
|
||||||
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { CancelIcon, PlusIcon } from '@shared/icons';
|
import { CancelIcon, PlusIcon } from '@shared/icons';
|
||||||
|
import { User } from '@shared/user';
|
||||||
|
|
||||||
export function NewMessageModal() {
|
export function NewMessageModal() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -35,7 +34,7 @@ export function NewMessageModal() {
|
|||||||
</button>
|
</button>
|
||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
<Dialog.Portal className="relative z-10">
|
<Dialog.Portal className="relative z-10">
|
||||||
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xl" />
|
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-2xl" />
|
||||||
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
||||||
<div className="relative h-min w-full max-w-xl rounded-xl bg-white/10 backdrop-blur-xl">
|
<div className="relative h-min w-full max-w-xl rounded-xl bg-white/10 backdrop-blur-xl">
|
||||||
<div className="h-min w-full shrink-0 border-b border-white/10 bg-white/5 px-5 py-5">
|
<div className="h-min w-full shrink-0 border-b border-white/10 bg-white/5 px-5 py-5">
|
||||||
@@ -44,7 +43,7 @@ export function NewMessageModal() {
|
|||||||
<Dialog.Title className="text-lg font-semibold leading-none text-white">
|
<Dialog.Title className="text-lg font-semibold leading-none text-white">
|
||||||
New chat
|
New chat
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<Dialog.Close className="inline-flex h-6 w-6 items-center justify-center rounded-md backdrop-blur-xl hover:bg-white/10">
|
<Dialog.Close className="inline-flex h-6 w-6 items-center justify-center rounded-md hover:bg-white/10">
|
||||||
<CancelIcon className="h-4 w-4 text-white/50" />
|
<CancelIcon className="h-4 w-4 text-white/50" />
|
||||||
</Dialog.Close>
|
</Dialog.Close>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,17 +53,17 @@ export function NewMessageModal() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-[500px] flex-col overflow-y-auto overflow-x-hidden pb-2 pt-2">
|
<div className="flex h-[500px] flex-col overflow-y-auto overflow-x-hidden pb-2 pt-2">
|
||||||
{db.account?.follows?.map((follow) => (
|
{db.account?.follows?.map((pubkey) => (
|
||||||
<div
|
<div
|
||||||
key={follow}
|
key={pubkey}
|
||||||
className="group flex items-center justify-between px-4 py-2 backdrop-blur-xl hover:bg-white/10"
|
className="group flex items-center justify-between px-4 py-2 hover:bg-white/10"
|
||||||
>
|
>
|
||||||
<User pubkey={follow} />
|
<User pubkey={pubkey} variant="simple" />
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => openChat(follow)}
|
onClick={() => openChat(pubkey)}
|
||||||
className="hidden w-max rounded bg-white/10 px-3 py-1 text-sm font-medium backdrop-blur-xl hover:bg-fuchsia-500 group-hover:inline-flex"
|
className="hidden w-max rounded bg-white/10 px-3 py-1 text-sm font-medium hover:bg-fuchsia-500 group-hover:inline-flex"
|
||||||
>
|
>
|
||||||
Chat
|
Chat
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { Image } from '@shared/image';
|
import { Image } from '@shared/image';
|
||||||
|
import { NIP05 } from '@shared/nip05';
|
||||||
|
|
||||||
import { useProfile } from '@utils/hooks/useProfile';
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
import { displayNpub } from '@utils/shortenKey';
|
import { displayNpub } from '@utils/shortenKey';
|
||||||
@@ -23,9 +24,17 @@ export function ChatSidebar({ pubkey }: { pubkey: string }) {
|
|||||||
<h3 className="text-lg font-semibold leading-none">
|
<h3 className="text-lg font-semibold leading-none">
|
||||||
{user?.display_name || user?.name}
|
{user?.display_name || user?.name}
|
||||||
</h3>
|
</h3>
|
||||||
<h5 className="leading-none text-white/50">
|
{user?.nip05 ? (
|
||||||
{user?.nip05 || displayNpub(pubkey, 16)}
|
<NIP05
|
||||||
</h5>
|
pubkey={pubkey}
|
||||||
|
nip05={user?.nip05}
|
||||||
|
className="leading-none text-white/50"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="leading-none text-white/50">
|
||||||
|
{displayNpub(pubkey, 16)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="leading-tight">{user?.bio || user?.about}</p>
|
<p className="leading-tight">{user?.bio || user?.about}</p>
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ import * as Dialog from '@radix-ui/react-dialog';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { User } from '@app/auth/components/user';
|
|
||||||
|
|
||||||
import { CancelIcon, StrangersIcon } from '@shared/icons';
|
import { CancelIcon, StrangersIcon } from '@shared/icons';
|
||||||
|
import { User } from '@shared/user';
|
||||||
|
|
||||||
import { compactNumber } from '@utils/number';
|
import { compactNumber } from '@utils/number';
|
||||||
|
|
||||||
@@ -35,7 +34,7 @@ export function UnknownsModal({ data }: { data: string[] }) {
|
|||||||
</button>
|
</button>
|
||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
<Dialog.Portal className="relative z-10">
|
<Dialog.Portal className="relative z-10">
|
||||||
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xl" />
|
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-2xl" />
|
||||||
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
||||||
<div className="relative h-min w-full max-w-xl rounded-xl bg-white/10 backdrop-blur-xl">
|
<div className="relative h-min w-full max-w-xl rounded-xl bg-white/10 backdrop-blur-xl">
|
||||||
<div className="h-min w-full shrink-0 border-b border-white/10 bg-white/5 px-5 py-5">
|
<div className="h-min w-full shrink-0 border-b border-white/10 bg-white/5 px-5 py-5">
|
||||||
@@ -44,7 +43,7 @@ export function UnknownsModal({ data }: { data: string[] }) {
|
|||||||
<Dialog.Title className="text-lg font-semibold leading-none text-white">
|
<Dialog.Title className="text-lg font-semibold leading-none text-white">
|
||||||
{data.length} unknowns
|
{data.length} unknowns
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<Dialog.Close className="inline-flex h-6 w-6 items-center justify-center rounded-md backdrop-blur-xl hover:bg-white/10">
|
<Dialog.Close className="inline-flex h-6 w-6 items-center justify-center rounded-md hover:bg-white/10">
|
||||||
<CancelIcon className="h-4 w-4 text-white/50" />
|
<CancelIcon className="h-4 w-4 text-white/50" />
|
||||||
</Dialog.Close>
|
</Dialog.Close>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,14 +56,14 @@ export function UnknownsModal({ data }: { data: string[] }) {
|
|||||||
{data.map((pubkey) => (
|
{data.map((pubkey) => (
|
||||||
<div
|
<div
|
||||||
key={pubkey}
|
key={pubkey}
|
||||||
className="group flex items-center justify-between px-4 py-2 backdrop-blur-xl hover:bg-white/10"
|
className="group flex items-center justify-between px-4 py-2 hover:bg-white/10"
|
||||||
>
|
>
|
||||||
<User pubkey={pubkey} />
|
<User pubkey={pubkey} variant="simple" />
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => openChat(pubkey)}
|
onClick={() => openChat(pubkey)}
|
||||||
className="hidden w-max rounded bg-white/10 px-3 py-1 text-sm font-medium backdrop-blur-xl hover:bg-fuchsia-500 group-hover:inline-flex"
|
className="hidden w-max rounded bg-white/10 px-3 py-1 text-sm font-medium hover:bg-fuchsia-500 group-hover:inline-flex"
|
||||||
>
|
>
|
||||||
Chat
|
Chat
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
+26
-13
@@ -1,40 +1,53 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useLocation, useRouteError } from 'react-router-dom';
|
import { useLocation, useRouteError } from 'react-router-dom';
|
||||||
|
|
||||||
interface IRouteError {
|
import { Frame } from '@shared/frame';
|
||||||
|
|
||||||
|
interface RouteError {
|
||||||
statusText: string;
|
statusText: string;
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IDebugInfo {
|
interface DebugInfo {
|
||||||
os: null | string;
|
os: null | string;
|
||||||
version: null | string;
|
version: null | string;
|
||||||
|
appDir: null | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ErrorScreen() {
|
export function ErrorScreen() {
|
||||||
const error = useRouteError() as IRouteError;
|
const error = useRouteError() as RouteError;
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const [debugInfo, setDebugInfo] = useState<IDebugInfo>({ os: null, version: null });
|
const [debugInfo, setDebugInfo] = useState<DebugInfo>({
|
||||||
|
os: null,
|
||||||
|
version: null,
|
||||||
|
appDir: null,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getInformation() {
|
async function getInformation() {
|
||||||
const { platform, version } = await import('@tauri-apps/plugin-os');
|
const { platform, version } = await import('@tauri-apps/api/os');
|
||||||
const { getVersion } = await import('@tauri-apps/plugin-app');
|
const { getVersion } = await import('@tauri-apps/api/app');
|
||||||
|
const { appConfigDir } = await import('@tauri-apps/api/path');
|
||||||
|
|
||||||
const platformName = await platform();
|
const platformName = await platform();
|
||||||
const osVersion = await version();
|
const osVersion = await version();
|
||||||
const appVersion = await getVersion();
|
const appVersion = await getVersion();
|
||||||
|
const appDir = await appConfigDir();
|
||||||
|
|
||||||
setDebugInfo({ os: platformName + ' ' + osVersion, version: appVersion });
|
setDebugInfo({
|
||||||
|
os: platformName + ' ' + osVersion,
|
||||||
|
version: appVersion,
|
||||||
|
appDir: appDir,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getInformation();
|
getInformation();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full items-center justify-center bg-black/90 backdrop-blur-xl">
|
<Frame className="flex h-full items-center justify-center">
|
||||||
<div className="flex max-w-lg flex-col gap-4">
|
<div className="flex w-full flex-col gap-4 px-4 md:max-w-lg md:px-0">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<h1 className="mb-1 text-2xl font-semibold text-white">
|
<h1 className="mb-1 text-2xl font-semibold text-white">
|
||||||
Sorry, an unexpected error has occurred.
|
Sorry, an unexpected error has occurred.
|
||||||
@@ -57,24 +70,24 @@ export function ErrorScreen() {
|
|||||||
href="https://github.com/luminous-devs/lume/issues/new"
|
href="https://github.com/luminous-devs/lume/issues/new"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-white/10 text-sm font-medium text-white backdrop-blur-xl hover:bg-white/20"
|
className="inline-flex h-11 w-full items-center justify-center rounded-lg text-sm font-medium text-white backdrop-blur-xl hover:bg-white/10"
|
||||||
>
|
>
|
||||||
Click here to report the issue on GitHub
|
Click here to report the issue on GitHub
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-white/10 text-sm font-medium text-white backdrop-blur-xl hover:bg-white/20"
|
className="inline-flex h-11 w-full items-center justify-center rounded-lg text-sm font-medium text-white backdrop-blur-xl hover:bg-white/10"
|
||||||
>
|
>
|
||||||
Reload app
|
Reload app
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-white/10 text-sm font-medium text-white backdrop-blur-xl hover:bg-white/20"
|
className="inline-flex h-11 w-full items-center justify-center rounded-lg text-sm font-medium text-white backdrop-blur-xl hover:bg-white/10"
|
||||||
>
|
>
|
||||||
Reset app
|
Reset app
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Frame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-17
@@ -1,7 +1,7 @@
|
|||||||
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
||||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
import { writeText } from '@tauri-apps/api/clipboard';
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
import { EventPointer } from 'nostr-tools/lib/nip19';
|
import { AddressPointer, EventPointer } from 'nostr-tools/lib/nip19';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
@@ -13,11 +13,11 @@ import {
|
|||||||
NoteActions,
|
NoteActions,
|
||||||
NoteReplyForm,
|
NoteReplyForm,
|
||||||
NoteStats,
|
NoteStats,
|
||||||
ThreadUser,
|
|
||||||
UnknownNote,
|
UnknownNote,
|
||||||
} from '@shared/notes';
|
} from '@shared/notes';
|
||||||
import { RepliesList } from '@shared/notes/replies/list';
|
import { RepliesList } from '@shared/notes/replies/list';
|
||||||
import { NoteSkeleton } from '@shared/notes/skeleton';
|
import { NoteSkeleton } from '@shared/notes/skeleton';
|
||||||
|
import { User } from '@shared/user';
|
||||||
|
|
||||||
import { useEvent } from '@utils/hooks/useEvent';
|
import { useEvent } from '@utils/hooks/useEvent';
|
||||||
|
|
||||||
@@ -27,13 +27,15 @@ export function ArticleNoteScreen() {
|
|||||||
|
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const { db } = useStorage();
|
const { db } = useStorage();
|
||||||
const { status, data } = useEvent(id);
|
|
||||||
|
const naddr = id.startsWith('naddr') ? (nip19.decode(id).data as AddressPointer) : null;
|
||||||
|
const { status, data } = useEvent(id, naddr);
|
||||||
|
|
||||||
const [isCopy, setIsCopy] = useState(false);
|
const [isCopy, setIsCopy] = useState(false);
|
||||||
|
|
||||||
const share = async () => {
|
const share = async () => {
|
||||||
await writeText(
|
await writeText(
|
||||||
'https://nostr.com/' +
|
'https://njump.me/' +
|
||||||
nip19.neventEncode({ id: data.id, author: data.pubkey } as EventPointer)
|
nip19.neventEncode({ id: data.id, author: data.pubkey } as EventPointer)
|
||||||
);
|
);
|
||||||
// update state
|
// update state
|
||||||
@@ -98,21 +100,27 @@ export function ArticleNoteScreen() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-min w-full px-3">
|
<>
|
||||||
<div className="rounded-xl bg-white/10 px-3 pt-3 backdrop-blur-xl">
|
<div className="h-min w-full px-3">
|
||||||
<ThreadUser pubkey={data.pubkey} time={data.created_at} />
|
<div className="rounded-xl border-t border-white/10 bg-white/20 px-3 pt-3">
|
||||||
<div className="mt-2">{renderKind(data)}</div>
|
<User pubkey={data.pubkey} time={data.created_at} variant="thread" />
|
||||||
<div>
|
<div className="mt-2">{renderKind(data)}</div>
|
||||||
<NoteActions id={id} pubkey={data.pubkey} extraButtons={false} />
|
<div>
|
||||||
<NoteStats id={id} />
|
<NoteActions
|
||||||
|
id={data.id}
|
||||||
|
pubkey={data.pubkey}
|
||||||
|
extraButtons={false}
|
||||||
|
/>
|
||||||
|
<NoteStats id={data.id} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div ref={replyRef} className="px-3">
|
||||||
|
<NoteReplyForm id={data.id} pubkey={db.account.pubkey} />
|
||||||
|
<RepliesList id={data.id} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<div ref={replyRef} className="px-3">
|
|
||||||
<NoteReplyForm id={id} pubkey={db.account.pubkey} />
|
|
||||||
<RepliesList id={id} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-1" />
|
<div className="col-span-1" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
||||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
import { writeText } from '@tauri-apps/api/clipboard';
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
import { EventPointer } from 'nostr-tools/lib/nip19';
|
import { EventPointer } from 'nostr-tools/lib/nip19';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
@@ -15,11 +15,11 @@ import {
|
|||||||
NoteReplyForm,
|
NoteReplyForm,
|
||||||
NoteStats,
|
NoteStats,
|
||||||
TextNote,
|
TextNote,
|
||||||
ThreadUser,
|
|
||||||
UnknownNote,
|
UnknownNote,
|
||||||
} from '@shared/notes';
|
} from '@shared/notes';
|
||||||
import { RepliesList } from '@shared/notes/replies/list';
|
import { RepliesList } from '@shared/notes/replies/list';
|
||||||
import { NoteSkeleton } from '@shared/notes/skeleton';
|
import { NoteSkeleton } from '@shared/notes/skeleton';
|
||||||
|
import { User } from '@shared/user';
|
||||||
|
|
||||||
import { useEvent } from '@utils/hooks/useEvent';
|
import { useEvent } from '@utils/hooks/useEvent';
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ export function TextNoteScreen() {
|
|||||||
|
|
||||||
const share = async () => {
|
const share = async () => {
|
||||||
await writeText(
|
await writeText(
|
||||||
'https://nostr.com/' +
|
'https://njump.me/' +
|
||||||
nip19.neventEncode({ id: data.id, author: data.pubkey } as EventPointer)
|
nip19.neventEncode({ id: data.id, author: data.pubkey } as EventPointer)
|
||||||
);
|
);
|
||||||
// update state
|
// update state
|
||||||
@@ -51,7 +51,7 @@ export function TextNoteScreen() {
|
|||||||
const renderKind = (event: NDKEvent) => {
|
const renderKind = (event: NDKEvent) => {
|
||||||
switch (event.kind) {
|
switch (event.kind) {
|
||||||
case NDKKind.Text:
|
case NDKKind.Text:
|
||||||
return <TextNote event={event} />;
|
return <TextNote content={event.content} />;
|
||||||
case NDKKind.Article:
|
case NDKKind.Article:
|
||||||
return <ArticleNote event={event} />;
|
return <ArticleNote event={event} />;
|
||||||
case 1063:
|
case 1063:
|
||||||
@@ -106,7 +106,7 @@ export function TextNoteScreen() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="h-min w-full px-3">
|
<div className="h-min w-full px-3">
|
||||||
<div className="rounded-xl bg-white/10 px-3 pt-3 backdrop-blur-xl">
|
<div className="rounded-xl bg-white/10 px-3 pt-3 backdrop-blur-xl">
|
||||||
<ThreadUser pubkey={data.pubkey} time={data.created_at} />
|
<User pubkey={data.pubkey} time={data.created_at} variant="thread" />
|
||||||
<div className="mt-2">{renderKind(data)}</div>
|
<div className="mt-2">{renderKind(data)}</div>
|
||||||
<div>
|
<div>
|
||||||
<NoteActions id={id} pubkey={data.pubkey} extraButtons={false} />
|
<NoteActions id={id} pubkey={data.pubkey} extraButtons={false} />
|
||||||
|
|||||||
@@ -1,32 +1,25 @@
|
|||||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||||
import { useMemo } from 'react';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { NotiContent } from '@app/notifications/components/content';
|
|
||||||
import { NotiUser } from '@app/notifications/components/user';
|
import { NotiUser } from '@app/notifications/components/user';
|
||||||
|
|
||||||
import { formatCreatedAt } from '@utils/createdAt';
|
import { formatCreatedAt } from '@utils/createdAt';
|
||||||
import { parser } from '@utils/parser';
|
|
||||||
|
|
||||||
export function NotiMention({ event }: { event: NDKEvent }) {
|
export function NotiMention({ event }: { event: NDKEvent }) {
|
||||||
const createdAt = formatCreatedAt(event.created_at);
|
const createdAt = formatCreatedAt(event.created_at);
|
||||||
const content = useMemo(() => parser(event), [event]);
|
const rootId = event.tags.find((el) => el[0])?.[1];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-min w-full px-3 py-1.5">
|
<Link to={`/notes/text/${rootId}`} className="h-min w-full px-3">
|
||||||
<div className="relative overflow-hidden rounded-xl bg-white/10 px-3 pt-3 backdrop-blur-xl">
|
<div className="group flex items-center justify-between rounded-xl px-3 py-3 hover:bg-white/10">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-start gap-1">
|
<NotiUser pubkey={event.pubkey} />
|
||||||
<NotiUser pubkey={event.pubkey} />
|
<p className="leading-none text-white/50">has mention you · {createdAt}</p>
|
||||||
<p className="leading-none text-white/50">has reply you post · {createdAt}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="f- relative z-10 -mt-6 flex gap-3">
|
|
||||||
<div className="h-11 w-11 shrink-0" />
|
|
||||||
<div className="mb-2 mt-3 w-full cursor-default rounded-lg bg-white/10 px-3 py-3 backdrop-blur-xl">
|
|
||||||
<NotiContent content={content} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<span className="hidden text-sm font-semibold text-fuchsia-500 group-hover:block">
|
||||||
|
View
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,27 @@
|
|||||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { SimpleNote } from '@app/notifications/components/simpleNote';
|
|
||||||
import { NotiUser } from '@app/notifications/components/user';
|
import { NotiUser } from '@app/notifications/components/user';
|
||||||
|
|
||||||
import { formatCreatedAt } from '@utils/createdAt';
|
import { formatCreatedAt } from '@utils/createdAt';
|
||||||
|
|
||||||
export function NotiReaction({ event }: { event: NDKEvent }) {
|
export function NotiReaction({ event }: { event: NDKEvent }) {
|
||||||
const root = event.tags.find((e) => e[0] === 'e')?.[1];
|
|
||||||
const createdAt = formatCreatedAt(event.created_at);
|
const createdAt = formatCreatedAt(event.created_at);
|
||||||
|
const rootId = event.tags.find((el) => el[0])?.[1];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-min w-full px-3 py-1.5">
|
<Link to={`/notes/text/${rootId}`} className="h-min w-full px-3">
|
||||||
<div className="relative overflow-hidden rounded-xl bg-white/10 px-3 pt-3 backdrop-blur-xl">
|
<div className="group flex items-center justify-between rounded-xl px-3 py-3 hover:bg-white/10">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-start gap-1">
|
<NotiUser pubkey={event.pubkey} />
|
||||||
<NotiUser pubkey={event.pubkey} />
|
<p className="leading-none text-white/50">
|
||||||
<p className="leading-none text-white/50">
|
reacted {event.content} · {createdAt}
|
||||||
reacted {event.content} · {createdAt}
|
</p>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="relative z-10 -mt-6 flex gap-3">
|
|
||||||
<div className="h-11 w-11 shrink-0" />
|
|
||||||
<div className="flex-1">{root && <SimpleNote id={root} />}</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<span className="hidden text-sm font-semibold text-fuchsia-500 group-hover:block">
|
||||||
|
View
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { SimpleNote } from '@app/notifications/components/simpleNote';
|
|
||||||
import { NotiUser } from '@app/notifications/components/user';
|
import { NotiUser } from '@app/notifications/components/user';
|
||||||
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
@@ -10,29 +10,24 @@ import { formatCreatedAt } from '@utils/createdAt';
|
|||||||
export function NotiRepost({ event }: { event: NDKEvent }) {
|
export function NotiRepost({ event }: { event: NDKEvent }) {
|
||||||
const { db } = useStorage();
|
const { db } = useStorage();
|
||||||
|
|
||||||
const root = event.tags.find((e) => e[0] === 'e')?.[1];
|
|
||||||
const createdAt = formatCreatedAt(event.created_at);
|
const createdAt = formatCreatedAt(event.created_at);
|
||||||
|
const rootId = event.tags.find((el) => el[0])?.[1];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-min w-full px-3 py-1.5">
|
<Link to={`/notes/text/${rootId}`} className="h-min w-full px-3">
|
||||||
<div className="relative overflow-hidden rounded-xl bg-white/10 px-3 pt-3 backdrop-blur-xl">
|
<div className="group flex items-center justify-between rounded-xl px-3 py-3 hover:bg-white/10">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-start gap-1">
|
<NotiUser pubkey={event.pubkey} />
|
||||||
<NotiUser pubkey={event.pubkey} />
|
<p className="leading-none text-white/50">
|
||||||
<p className="leading-none text-white/50">
|
repost{' '}
|
||||||
repost{' '}
|
{event.pubkey !== db.account.pubkey ? 'a post that mention you' : 'your post'}{' '}
|
||||||
{event.pubkey !== db.account.pubkey
|
· {createdAt}
|
||||||
? 'a post that mention you'
|
</p>
|
||||||
: 'your post'}{' '}
|
|
||||||
· {createdAt}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="relative z-10 -mt-6 flex gap-3">
|
|
||||||
<div className="h-11 w-11 shrink-0" />
|
|
||||||
<div className="flex-1">{root && <SimpleNote id={root} />}</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<span className="hidden text-sm font-semibold text-fuchsia-500 group-hover:block">
|
||||||
|
View
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const SimpleNote = memo(function SimpleNote({ id }: { id: string }) {
|
|||||||
const openThread = (event, thread: string) => {
|
const openThread = (event, thread: string) => {
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
if (selection.toString().length === 0) {
|
if (selection.toString().length === 0) {
|
||||||
setWidget(db, { kind: WidgetKinds.thread, title: 'Thread', content: thread });
|
setWidget(db, { kind: WidgetKinds.local.thread, title: 'Thread', content: thread });
|
||||||
} else {
|
} else {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,7 @@ export const SimpleNote = memo(function SimpleNote({ id }: { id: string }) {
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className="mb-2 mt-3 cursor-default rounded-lg bg-white/10 px-3 py-3 backdrop-blur-xl"
|
className="mb-2 mt-3 cursor-default rounded-lg bg-white/10 px-3 py-3 backdrop-blur-xl"
|
||||||
>
|
>
|
||||||
<User pubkey={data.pubkey} time={data.created_at} size="small" />
|
<User pubkey={data.pubkey} time={data.created_at} variant="mention" />
|
||||||
<div className="markdown">
|
<div className="markdown">
|
||||||
<p>
|
<p>
|
||||||
{data.content.length > 200
|
{data.content.length > 200
|
||||||
|
|||||||
@@ -18,14 +18,14 @@ export function NotiUser({ pubkey }: { pubkey: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex shrink-0 items-start justify-start gap-3">
|
<div className="flex shrink-0 items-center justify-start gap-2">
|
||||||
<Image
|
<Image
|
||||||
src={user?.picture || user?.image}
|
src={user?.picture || user?.image}
|
||||||
alt={pubkey}
|
alt={pubkey}
|
||||||
className="h-11 w-11 shrink-0 rounded-lg object-cover"
|
className="h-8 w-8 shrink-0 rounded-md object-cover"
|
||||||
/>
|
/>
|
||||||
<span className="max-w-[10rem] flex-1 truncate font-medium leading-none text-white">
|
<span className="max-w-[10rem] truncate font-medium leading-none text-white">
|
||||||
{user?.nip05 || user?.name || user?.display_name || displayNpub(pubkey, 16)}
|
{user?.name || user?.display_name || displayNpub(pubkey, 16)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useCallback, useEffect } from 'react';
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import { NotiMention } from '@app/notifications/components/mention';
|
import { NotiMention } from '@app/notifications/components/mention';
|
||||||
import { NotiReaction } from '@app/notifications/components/reaction';
|
import { NotiReaction } from '@app/notifications/components/reaction';
|
||||||
@@ -11,18 +10,19 @@ import { useStorage } from '@libs/storage/provider';
|
|||||||
import { LoaderIcon } from '@shared/icons';
|
import { LoaderIcon } from '@shared/icons';
|
||||||
import { TitleBar } from '@shared/titleBar';
|
import { TitleBar } from '@shared/titleBar';
|
||||||
|
|
||||||
|
import { useActivities } from '@stores/activities';
|
||||||
|
|
||||||
import { useNostr } from '@utils/hooks/useNostr';
|
import { useNostr } from '@utils/hooks/useNostr';
|
||||||
|
|
||||||
export function NotificationScreen() {
|
export function NotificationScreen() {
|
||||||
const { db } = useStorage();
|
const { db } = useStorage();
|
||||||
const { fetchActivities } = useNostr();
|
const { fetchActivities } = useNostr();
|
||||||
const { status, data } = useQuery(
|
|
||||||
['notifications', db.account.pubkey],
|
const [activities, setActivities, clearTotalNewActivities] = useActivities((state) => [
|
||||||
async () => {
|
state.activities,
|
||||||
return await fetchActivities();
|
state.setActivities,
|
||||||
},
|
state.clearTotalNewActivities,
|
||||||
{ refetchOnWindowFocus: false }
|
]);
|
||||||
);
|
|
||||||
|
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
(event: NDKEvent) => {
|
(event: NDKEvent) => {
|
||||||
@@ -37,34 +37,44 @@ export function NotificationScreen() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[data]
|
[activities]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getActivities() {
|
||||||
|
const events = await fetchActivities();
|
||||||
|
setActivities(events, db.account.last_login_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
getActivities();
|
||||||
|
|
||||||
|
// clear total new activities
|
||||||
|
clearTotalNewActivities();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="scrollbar-hide h-full w-full overflow-y-auto bg-white/10 backdrop-blur-xl">
|
<div className="scrollbar-hide h-full w-full overflow-y-auto bg-white/10 backdrop-blur-xl">
|
||||||
<div className="grid h-full grid-cols-3">
|
<div className="grid h-full grid-cols-3">
|
||||||
<div className="col-span-2 flex flex-col border-r border-white/5">
|
<div className="col-span-2 flex flex-col border-r border-white/5">
|
||||||
<TitleBar title="Activities in the last 24 hours" />
|
<TitleBar title="Activities in the last 24 hours" />
|
||||||
<div className="flex h-full flex-col gap-1.5">
|
<div className="flex h-full flex-col">
|
||||||
<div className="flex h-full flex-col">
|
{!activities ? (
|
||||||
{status === 'loading' ? (
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
<div className="flex flex-col items-center gap-1.5">
|
||||||
<div className="flex flex-col items-center gap-1.5">
|
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
||||||
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
<p className="text-sm font-medium text-white/50">Loading</p>
|
||||||
<p className="text-sm font-medium text-white/50">Loading</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : data?.length < 1 ? (
|
</div>
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center">
|
) : activities.length <= 1 ? (
|
||||||
<p className="mb-1 text-4xl">🎉</p>
|
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||||
<p className="font-medium text-white/50">
|
<p className="mb-1 text-4xl">🎉</p>
|
||||||
Yo!, no new activities around you in the last 24 hours
|
<p className="font-medium text-white/50">
|
||||||
</p>
|
Yo!, no new activities around you in the last 24 hours
|
||||||
</div>
|
</p>
|
||||||
) : (
|
</div>
|
||||||
data.map((event) => renderItem(event))
|
) : (
|
||||||
)}
|
activities.map((event) => renderItem(event))
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import { webln } from '@getalby/sdk';
|
||||||
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
|
import { message } from '@tauri-apps/api/dialog';
|
||||||
|
import { WebviewWindow } from '@tauri-apps/api/window';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AlbyIcon,
|
||||||
|
ArrowRightCircleIcon,
|
||||||
|
CancelIcon,
|
||||||
|
CheckCircleIcon,
|
||||||
|
LoaderIcon,
|
||||||
|
} from '@shared/icons';
|
||||||
|
|
||||||
|
import { useStronghold } from '@stores/stronghold';
|
||||||
|
|
||||||
|
export function NWCAlby() {
|
||||||
|
const { db } = useStorage();
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [isLoading, setIsloading] = useState(false);
|
||||||
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
|
|
||||||
|
const setWalletConnectURL = useStronghold((state) => state.setWalletConnectURL);
|
||||||
|
|
||||||
|
const initAlby = async () => {
|
||||||
|
try {
|
||||||
|
setIsloading(true);
|
||||||
|
|
||||||
|
const provider = webln.NostrWebLNProvider.withNewSecret();
|
||||||
|
const walletConnectURL = provider.getNostrWalletConnectUrl(true);
|
||||||
|
|
||||||
|
// get auth url
|
||||||
|
const authURL = provider.getAuthorizationUrl({ name: 'Lume' });
|
||||||
|
|
||||||
|
// open auth window
|
||||||
|
const webview = new WebviewWindow('alby', {
|
||||||
|
title: 'Connect Alby',
|
||||||
|
url: authURL.href,
|
||||||
|
center: true,
|
||||||
|
width: 400,
|
||||||
|
height: 650,
|
||||||
|
});
|
||||||
|
|
||||||
|
webview.listen('tauri://close-requested', async () => {
|
||||||
|
await db.secureSave('walletConnectURL', walletConnectURL, 'nwc');
|
||||||
|
setWalletConnectURL(walletConnectURL);
|
||||||
|
setIsConnected(true);
|
||||||
|
setIsloading(false);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setIsloading(false);
|
||||||
|
await message(e.toString(), { title: 'Connect Alby', type: 'error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="inline-flex items-center gap-2.5">
|
||||||
|
<div className="inline-flex h-11 w-11 items-center justify-center rounded-md bg-orange-200">
|
||||||
|
<AlbyIcon className="h-8 w-8" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 className="font-semibold leading-tight text-white">Alby</h5>
|
||||||
|
<p className="text-sm leading-tight text-white/50">Require alby account</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Dialog.Trigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex h-9 w-min items-center justify-center rounded-md border-t border-white/10 bg-white/20 px-3 text-sm font-medium text-white hover:bg-green-500"
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</button>
|
||||||
|
</Dialog.Trigger>
|
||||||
|
</div>
|
||||||
|
<Dialog.Portal className="relative z-10">
|
||||||
|
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-2xl" />
|
||||||
|
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
||||||
|
<div className="relative h-min w-full max-w-xl rounded-xl bg-white/10 backdrop-blur-xl">
|
||||||
|
<div className="h-min w-full shrink-0 rounded-t-xl border-b border-white/10 bg-white/5 px-5 py-5">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Dialog.Title className="text-lg font-semibold leading-none text-white">
|
||||||
|
Alby integration (Beta)
|
||||||
|
</Dialog.Title>
|
||||||
|
<Dialog.Close className="inline-flex h-6 w-6 items-center justify-center rounded-md backdrop-blur-xl hover:bg-white/10">
|
||||||
|
<CancelIcon className="h-4 w-4 text-white/50" />
|
||||||
|
</Dialog.Close>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3 px-5 py-5">
|
||||||
|
<div className="relative flex h-40 items-center justify-center gap-4">
|
||||||
|
<div className="inline-flex h-16 w-16 items-end justify-center rounded-lg bg-black pb-2">
|
||||||
|
<img src="/lume.png" className="w-1/3" alt="Lume Logo" />
|
||||||
|
</div>
|
||||||
|
<div className="w-20 border border-dashed border-white/5" />
|
||||||
|
<div className="inline-flex h-16 w-16 items-center justify-center rounded-lg bg-white">
|
||||||
|
<AlbyIcon className="h-8 w-8" />
|
||||||
|
</div>
|
||||||
|
{isConnected ? (
|
||||||
|
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transform">
|
||||||
|
<CheckCircleIcon className="h-5 w-5 text-green-500" />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<p className="text-sm text-white/50">
|
||||||
|
When you click "Connect", a new window will open and you need
|
||||||
|
to click the "Connect Wallet" button to grant Lume permission
|
||||||
|
to integrate with your Alby account.
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-white/50">
|
||||||
|
All information will be encrypted and stored on the local machine.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => initAlby()}
|
||||||
|
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 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<span className="w-5" />
|
||||||
|
<span>Connecting...</span>
|
||||||
|
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
||||||
|
</>
|
||||||
|
) : isConnected ? (
|
||||||
|
<>
|
||||||
|
<span className="w-5" />
|
||||||
|
<span>Connected</span>
|
||||||
|
<CheckCircleIcon className="h-5 w-5" />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="w-5" />
|
||||||
|
<span>Connect</span>
|
||||||
|
<ArrowRightCircleIcon className="h-5 w-5" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Resolver, useForm } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
|
import { ArrowRightCircleIcon, CancelIcon, LoaderIcon, WorldIcon } from '@shared/icons';
|
||||||
|
|
||||||
|
import { useStronghold } from '@stores/stronghold';
|
||||||
|
|
||||||
|
type FormValues = {
|
||||||
|
uri: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolver: Resolver<FormValues> = async (values) => {
|
||||||
|
return {
|
||||||
|
values: values.uri ? values : {},
|
||||||
|
errors: !values.uri
|
||||||
|
? {
|
||||||
|
uri: {
|
||||||
|
type: 'required',
|
||||||
|
message: 'This is required.',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function NWCOther() {
|
||||||
|
const { db } = useStorage();
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
setError,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors, isDirty, isValid },
|
||||||
|
} = useForm<FormValues>({ resolver });
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [isLoading, setIsloading] = useState(false);
|
||||||
|
|
||||||
|
const setWalletConnectURL = useStronghold((state) => state.setWalletConnectURL);
|
||||||
|
|
||||||
|
const onSubmit = async (data: { [x: string]: string }) => {
|
||||||
|
try {
|
||||||
|
if (!data.uri.startsWith('nostr+walletconnect:')) {
|
||||||
|
setError('uri', {
|
||||||
|
type: 'custom',
|
||||||
|
message:
|
||||||
|
'Connect URI is required and must start with format nostr+walletconnect:, please check again',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsloading(true);
|
||||||
|
|
||||||
|
const uriObj = new URL(data.uri);
|
||||||
|
const params = new URLSearchParams(uriObj.search);
|
||||||
|
|
||||||
|
if (params.has('relay') && params.has('secret')) {
|
||||||
|
await db.secureSave('walletConnectURL', data.uri, 'nwc');
|
||||||
|
setWalletConnectURL(data.uri);
|
||||||
|
setIsloading(false);
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setIsloading(false);
|
||||||
|
setError('uri', {
|
||||||
|
type: 'custom',
|
||||||
|
message:
|
||||||
|
'Connect URI is required and must start with format nostr+walletconnect:, please check again',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<div className="flex items-center justify-between pt-4">
|
||||||
|
<div className="inline-flex items-center gap-2.5">
|
||||||
|
<div className="inline-flex h-11 w-11 items-center justify-center rounded-md bg-white/10">
|
||||||
|
<WorldIcon className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 className="font-semibold leading-tight text-white">URI String</h5>
|
||||||
|
<p className="text-sm leading-tight text-white/50">
|
||||||
|
Using format nostr+walletconnect:
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Dialog.Trigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex h-9 w-min items-center justify-center rounded-md border-t border-white/10 bg-white/20 px-3 text-sm font-medium text-white hover:bg-green-500"
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</button>
|
||||||
|
</Dialog.Trigger>
|
||||||
|
</div>
|
||||||
|
<Dialog.Portal className="relative z-10">
|
||||||
|
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-2xl" />
|
||||||
|
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
||||||
|
<div className="relative h-min w-full max-w-xl rounded-xl bg-white/10 backdrop-blur-xl">
|
||||||
|
<div className="h-min w-full shrink-0 rounded-t-xl border-b border-white/10 bg-white/5 px-5 py-5">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Dialog.Title className="text-lg font-semibold leading-none text-white">
|
||||||
|
Nostr Wallet Connect
|
||||||
|
</Dialog.Title>
|
||||||
|
<Dialog.Close className="inline-flex h-6 w-6 items-center justify-center rounded-md backdrop-blur-xl hover:bg-white/10">
|
||||||
|
<CancelIcon className="h-4 w-4 text-white/50" />
|
||||||
|
</Dialog.Close>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
className="mb-0 flex flex-col gap-3 px-5 py-5"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label
|
||||||
|
htmlFor="uri"
|
||||||
|
className="text-sm font-semibold uppercase tracking-wider text-white/50"
|
||||||
|
>
|
||||||
|
Connect URI
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
{...register('uri', { required: true })}
|
||||||
|
placeholder="nostr+walletconnect:"
|
||||||
|
spellCheck={false}
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="off"
|
||||||
|
className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/50"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-red-400">
|
||||||
|
{errors.uri && <p>{errors.uri.message}</p>}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1 text-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 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<span className="w-5" />
|
||||||
|
<span>Connecting...</span>
|
||||||
|
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="w-5" />
|
||||||
|
<span>Connect</span>
|
||||||
|
<ArrowRightCircleIcon className="h-5 w-5" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<span className="text-sm text-white/50">
|
||||||
|
All information will be encrypted and stored on the local machine.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { NWCAlby } from '@app/nwc/components/alby';
|
||||||
|
import { NWCOther } from '@app/nwc/components/other';
|
||||||
|
|
||||||
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
|
import { CheckCircleIcon } from '@shared/icons';
|
||||||
|
|
||||||
|
import { useStronghold } from '@stores/stronghold';
|
||||||
|
|
||||||
|
export function NWCScreen() {
|
||||||
|
const { db } = useStorage();
|
||||||
|
|
||||||
|
const [walletConnectURL, setWalletConnectURL] = useStronghold((state) => [
|
||||||
|
state.walletConnectURL,
|
||||||
|
state.setWalletConnectURL,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const remove = async () => {
|
||||||
|
setWalletConnectURL('');
|
||||||
|
await db.secureSave('walletConnectURL', '', 'nwc');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<div className="flex w-full flex-col gap-5">
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-2xl font-bold leading-tight">
|
||||||
|
Nostr Wallet Connect (Beta)
|
||||||
|
</h3>
|
||||||
|
<p className="leading-tight text-white/70">
|
||||||
|
Sending tips easily via Bitcoin Lightning.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mx-auto max-w-lg">
|
||||||
|
{!walletConnectURL ? (
|
||||||
|
<div className="flex w-full flex-col gap-4 divide-y divide-white/5 rounded-xl border-t border-white/10 bg-white/20 p-3">
|
||||||
|
<NWCAlby />
|
||||||
|
<NWCOther />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex w-full flex-col rounded-xl border-t border-white/10 bg-white/20 p-3">
|
||||||
|
<div className="mb-1 inline-flex items-center gap-1.5 text-sm text-green-500">
|
||||||
|
<CheckCircleIcon className="h-4 w-4" />
|
||||||
|
<p>You're using nostr wallet connect</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<textarea
|
||||||
|
readOnly
|
||||||
|
value={walletConnectURL.substring(0, 120) + '****'}
|
||||||
|
className="relative h-40 w-full resize-none rounded-lg bg-white/20 px-3 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/50"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => remove()}
|
||||||
|
className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg bg-white/10 px-6 font-medium leading-none text-red-500 hover:bg-white/20 focus:outline-none disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Remove connection
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="mt-5 flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<h5 className="text-sm font-bold text-white">Introduction</h5>
|
||||||
|
<p className="text-sm text-white/70">
|
||||||
|
Nostr Wallet Connect (NWC) is a way for applications like Nostr clients to
|
||||||
|
access a remote Lightning wallet through a standardized protocol.
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-white/70">
|
||||||
|
To learn more about the details have a look at{' '}
|
||||||
|
<a
|
||||||
|
href="https://github.com/getAlby/nips/blob/7-wallet-connect-patch/47.md"
|
||||||
|
target="_blank"
|
||||||
|
className="text-fuchsia-300"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
the specs (NIP47)
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<h5 className="text-sm font-bold text-white">About tipping</h5>
|
||||||
|
<p className="text-sm text-white/70">
|
||||||
|
Also known as Zap in other Nostr client.
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-white/70">
|
||||||
|
Lume doesn't take any commission or platform fees when you tip
|
||||||
|
someone.
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-white/70">Lume doesn't hold your Bitcoin</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<h5 className="text-sm font-bold text-white">
|
||||||
|
Recommend wallet that support NWC
|
||||||
|
</h5>
|
||||||
|
<p className="text-sm text-white/70">
|
||||||
|
Mutiny Wallet:{' '}
|
||||||
|
<a
|
||||||
|
href="https://www.mutinywallet.com/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="text-fuchsia-300"
|
||||||
|
>
|
||||||
|
website
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-white/70">
|
||||||
|
Self hosted NWC on Umbrel :{' '}
|
||||||
|
<a
|
||||||
|
href="https://apps.umbrel.com/app/alby-nostr-wallet-connect"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="text-fuchsia-300"
|
||||||
|
>
|
||||||
|
website
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getVersion } from '@tauri-apps/plugin-app';
|
import { getVersion } from '@tauri-apps/api/app';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export function VersionSetting() {
|
export function VersionSetting() {
|
||||||
|
|||||||
@@ -2,9 +2,18 @@ import { useCallback } from 'react';
|
|||||||
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ArticleIcon,
|
||||||
|
FileIcon,
|
||||||
|
FollowsIcon,
|
||||||
|
GroupFeedsIcon,
|
||||||
|
HashtagIcon,
|
||||||
|
ThreadsIcon,
|
||||||
|
TrendingIcon,
|
||||||
|
} from '@shared/icons';
|
||||||
import { TitleBar } from '@shared/titleBar';
|
import { TitleBar } from '@shared/titleBar';
|
||||||
|
|
||||||
import { DefaultWidgets, useWidgets } from '@stores/widgets';
|
import { DefaultWidgets, WidgetKinds, useWidgets } from '@stores/widgets';
|
||||||
|
|
||||||
import { Widget, WidgetGroup, WidgetGroupItem } from '@utils/types';
|
import { Widget, WidgetGroup, WidgetGroupItem } from '@utils/types';
|
||||||
|
|
||||||
@@ -20,16 +29,63 @@ export function WidgetList({ params }: { params: Widget }) {
|
|||||||
removeWidget(db, params.id);
|
removeWidget(db, params.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderIcon = useCallback(
|
||||||
|
(kind: number) => {
|
||||||
|
switch (kind) {
|
||||||
|
case WidgetKinds.tmp.xfeed:
|
||||||
|
return <GroupFeedsIcon className="h-5 w-5 text-white" />;
|
||||||
|
case WidgetKinds.local.follows:
|
||||||
|
return <FollowsIcon className="h-5 w-5 text-white" />;
|
||||||
|
case WidgetKinds.local.files:
|
||||||
|
case WidgetKinds.global.files:
|
||||||
|
return <FileIcon className="h-5 w-5 text-white" />;
|
||||||
|
case WidgetKinds.local.articles:
|
||||||
|
case WidgetKinds.global.articles:
|
||||||
|
return <ArticleIcon className="h-5 w-5 text-white" />;
|
||||||
|
case WidgetKinds.tmp.xhashtag:
|
||||||
|
return <HashtagIcon className="h-5 w-4 text-white" />;
|
||||||
|
case WidgetKinds.nostrBand.trendingAccounts:
|
||||||
|
case WidgetKinds.nostrBand.trendingNotes:
|
||||||
|
return <TrendingIcon className="h-5 w-4 text-white" />;
|
||||||
|
case WidgetKinds.other.learnNostr:
|
||||||
|
return <ThreadsIcon className="h-5 w-4 text-white" />;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[DefaultWidgets]
|
||||||
|
);
|
||||||
|
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
(row: WidgetGroup) => {
|
(row: WidgetGroup) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<h3 className="font-medium text-white/50">{row.title}</h3>
|
<h3 className="font-medium text-white/50">{row.title}</h3>
|
||||||
<div className="grid grid-cols-3 gap-6">
|
<div className="flex flex-col divide-y divide-white/5 overflow-hidden rounded-xl bg-white/10">
|
||||||
{row.data.map((item, index) => (
|
{row.data.map((item, index) => (
|
||||||
<button onClick={() => openWidget(item)} key={index}>
|
<button
|
||||||
<div className="inline-flex aspect-square h-full w-full transform-gpu flex-col items-center justify-center gap-2.5 rounded-2xl bg-white/5 hover:bg-white/10">
|
onClick={() => openWidget(item)}
|
||||||
<h5 className="text-sm font-medium">{item.title}</h5>
|
key={index}
|
||||||
|
className="flex items-center gap-2.5 px-4 hover:bg-white/10"
|
||||||
|
>
|
||||||
|
{item.icon ? (
|
||||||
|
<div className="h-10 w-10 shrink-0 rounded-md">
|
||||||
|
<img
|
||||||
|
src={item.icon}
|
||||||
|
alt={item.title}
|
||||||
|
className="h-10 w-10 object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-white/10">
|
||||||
|
{renderIcon(item.kind)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="inline-flex h-16 w-full flex-col items-start justify-center gap-1">
|
||||||
|
<h5 className="line-clamp-1 font-medium leading-none">{item.title}</h5>
|
||||||
|
<p className="line-clamp-1 text-xs leading-none text-white/50">
|
||||||
|
{item.description}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -41,10 +97,26 @@ export function WidgetList({ params }: { params: Widget }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-full shrink-0 grow-0 basis-[400px] overflow-hidden">
|
<div className="relative h-full shrink-0 grow-0 basis-[400px] bg-white/10">
|
||||||
<TitleBar id={params.id} title="Add widget" />
|
<TitleBar id={params.id} title="Add widget" />
|
||||||
<div className="flex flex-col gap-8 px-3">
|
<div className="scrollbar-hide h-full overflow-y-auto pb-20">
|
||||||
{DefaultWidgets.map((row: WidgetGroup) => renderItem(row))}
|
<div className="flex flex-col gap-6 px-3">
|
||||||
|
{DefaultWidgets.map((row: WidgetGroup) => renderItem(row))}
|
||||||
|
<div className="border-t border-white/5 pt-6">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled
|
||||||
|
className="inline-flex h-14 w-full items-center justify-center gap-2.5 rounded-xl bg-white/5 text-sm font-medium text-white/50"
|
||||||
|
>
|
||||||
|
Build your own widget{' '}
|
||||||
|
<div className="-rotate-3 transform rounded-md border border-white/20 bg-white/10 px-1.5 py-1">
|
||||||
|
<span className="bg-gradient-to-t from-fuchsia-200 via-red-200 to-orange-300 bg-clip-text text-xs text-transparent">
|
||||||
|
Coming soon
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ import {
|
|||||||
GlobalArticlesWidget,
|
GlobalArticlesWidget,
|
||||||
GlobalFilesWidget,
|
GlobalFilesWidget,
|
||||||
GlobalHashtagWidget,
|
GlobalHashtagWidget,
|
||||||
|
LearnNostrWidget,
|
||||||
LocalArticlesWidget,
|
LocalArticlesWidget,
|
||||||
LocalFeedsWidget,
|
LocalFeedsWidget,
|
||||||
LocalFilesWidget,
|
LocalFilesWidget,
|
||||||
|
LocalFollowsWidget,
|
||||||
LocalNetworkWidget,
|
LocalNetworkWidget,
|
||||||
LocalThreadWidget,
|
LocalThreadWidget,
|
||||||
LocalUserWidget,
|
LocalUserWidget,
|
||||||
@@ -40,6 +42,8 @@ export function SpaceScreen() {
|
|||||||
switch (widget.kind) {
|
switch (widget.kind) {
|
||||||
case WidgetKinds.local.network:
|
case WidgetKinds.local.network:
|
||||||
return <LocalNetworkWidget key={widget.id} />;
|
return <LocalNetworkWidget key={widget.id} />;
|
||||||
|
case WidgetKinds.local.follows:
|
||||||
|
return <LocalFollowsWidget key={widget.id} params={widget} />;
|
||||||
case WidgetKinds.local.feeds:
|
case WidgetKinds.local.feeds:
|
||||||
return <LocalFeedsWidget key={widget.id} params={widget} />;
|
return <LocalFeedsWidget key={widget.id} params={widget} />;
|
||||||
case WidgetKinds.local.files:
|
case WidgetKinds.local.files:
|
||||||
@@ -66,8 +70,10 @@ export function SpaceScreen() {
|
|||||||
return <XfeedsWidget key={widget.id} params={widget} />;
|
return <XfeedsWidget key={widget.id} params={widget} />;
|
||||||
case WidgetKinds.tmp.list:
|
case WidgetKinds.tmp.list:
|
||||||
return <WidgetList key={widget.id} params={widget} />;
|
return <WidgetList key={widget.id} params={widget} />;
|
||||||
|
case WidgetKinds.other.learnNostr:
|
||||||
|
return <LearnNostrWidget key={widget.id} params={widget} />;
|
||||||
default:
|
default:
|
||||||
break;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[widgets]
|
[widgets]
|
||||||
|
|||||||
+29
-26
@@ -1,3 +1,4 @@
|
|||||||
|
import { message } from '@tauri-apps/api/dialog';
|
||||||
import { invoke } from '@tauri-apps/api/tauri';
|
import { invoke } from '@tauri-apps/api/tauri';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
@@ -10,48 +11,53 @@ import { useNostr } from '@utils/hooks/useNostr';
|
|||||||
|
|
||||||
export function SplashScreen() {
|
export function SplashScreen() {
|
||||||
const { db } = useStorage();
|
const { db } = useStorage();
|
||||||
const { ndk, relayUrls } = useNDK();
|
const { ndk } = useNDK();
|
||||||
const { fetchUserData, prefetchEvents } = useNostr();
|
const { fetchUserData, prefetchEvents } = useNostr();
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
const [errorMessage, setErrorMessage] = useState<null | string>(null);
|
|
||||||
|
|
||||||
const skip = async () => {
|
const skip = async () => {
|
||||||
await invoke('close_splashscreen');
|
await invoke('close_splashscreen');
|
||||||
};
|
};
|
||||||
|
|
||||||
const prefetch = async () => {
|
const prefetch = async () => {
|
||||||
const onboarding = localStorage.getItem('onboarding');
|
|
||||||
const step = JSON.parse(onboarding).state.step || null;
|
|
||||||
if (step) await invoke('close_splashscreen');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await fetchUserData();
|
const [user, events] = await Promise.all([fetchUserData(), prefetchEvents()]);
|
||||||
const data = await prefetchEvents();
|
|
||||||
|
|
||||||
if (user.status === 'ok' && data.status === 'ok') {
|
if (user.status === 'ok' && events.status === 'ok') {
|
||||||
|
// update last login = current time
|
||||||
await db.updateLastLogin();
|
await db.updateLastLogin();
|
||||||
|
// close splash screen and open main app screen
|
||||||
await invoke('close_splashscreen');
|
await invoke('close_splashscreen');
|
||||||
} else {
|
|
||||||
setIsLoading(false);
|
|
||||||
setErrorMessage(user.message);
|
|
||||||
console.log('fetch failed, error: ', user.message);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setErrorMessage(e);
|
await message(e, {
|
||||||
console.log('prefetch failed, error: ', e);
|
title: 'An unexpected error has occurred',
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!db.account) {
|
async function initial() {
|
||||||
invoke('close_splashscreen');
|
if (!db.account) {
|
||||||
|
await invoke('close_splashscreen');
|
||||||
|
} else {
|
||||||
|
const onboarding = localStorage.getItem('onboarding');
|
||||||
|
const step = JSON.parse(onboarding).state.step || null;
|
||||||
|
|
||||||
|
if (step) {
|
||||||
|
await invoke('close_splashscreen');
|
||||||
|
} else {
|
||||||
|
console.log('prefetching...');
|
||||||
|
prefetch();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ndk && db.account) {
|
if (ndk) {
|
||||||
console.log('prefetching...');
|
initial();
|
||||||
prefetch();
|
|
||||||
}
|
}
|
||||||
}, [ndk, db.account]);
|
}, [ndk, db.account]);
|
||||||
|
|
||||||
@@ -64,9 +70,7 @@ export function SplashScreen() {
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex flex-col gap-1 text-center">
|
<div className="flex flex-col gap-1 text-center">
|
||||||
<h3 className="text-lg font-semibold leading-none text-white">
|
<h3 className="text-lg font-semibold leading-none text-white">
|
||||||
{!ndk
|
{!ndk ? 'Connecting to relay...' : 'Fetching events from the last login.'}
|
||||||
? 'Connecting to relay...'
|
|
||||||
: `Connected to ${relayUrls.length} relays`}
|
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-white/50">
|
<p className="text-sm text-white/50">
|
||||||
This may take a few seconds, please don't close app.
|
This may take a few seconds, please don't close app.
|
||||||
@@ -75,15 +79,14 @@ export function SplashScreen() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="mt-2 flex flex-col gap-1 text-center">
|
<div className="mt-2 flex flex-col gap-1 text-center">
|
||||||
<h3 className="text-lg font-semibold leading-none text-white">
|
<h3 className="text-lg font-semibold leading-none text-white">
|
||||||
Something wrong!
|
An unexpected error has occurred
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-white/50">{errorMessage}</p>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={skip}
|
onClick={skip}
|
||||||
className="mx-auto mt-4 inline-flex h-10 w-max items-center justify-center rounded-md bg-white/10 px-8 text-sm font-medium leading-none text-white backdrop-blur-xl hover:bg-white/20"
|
className="mx-auto mt-4 inline-flex h-10 w-max items-center justify-center rounded-md bg-white/10 px-8 text-sm font-medium leading-none text-white backdrop-blur-xl hover:bg-white/20"
|
||||||
>
|
>
|
||||||
Skip
|
Skip this step
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NDKEvent, NDKUserProfile } from '@nostr-dev-kit/ndk';
|
import { NDKEvent, NDKUserProfile } from '@nostr-dev-kit/ndk';
|
||||||
import * as Dialog from '@radix-ui/react-dialog';
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { fetch } from '@tauri-apps/api/http';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
@@ -13,6 +14,12 @@ import { Image } from '@shared/image';
|
|||||||
|
|
||||||
import { useNostr } from '@utils/hooks/useNostr';
|
import { useNostr } from '@utils/hooks/useNostr';
|
||||||
|
|
||||||
|
interface NIP05 {
|
||||||
|
names: {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function EditProfileModal() {
|
export function EditProfileModal() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -47,19 +54,26 @@ export function EditProfileModal() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const verifyNIP05 = async (nip05: string) => {
|
const verifyNIP05 = async (nip05: string) => {
|
||||||
const url = nip05.split('@');
|
const localPath = nip05.split('@')[0];
|
||||||
const username = url[0];
|
const service = nip05.split('@')[1];
|
||||||
const service = url[1];
|
const verifyURL = `https://${service}/.well-known/nostr.json?name=${localPath}`;
|
||||||
const verifyURL = `https://${service}/.well-known/nostr.json?name=${username}`;
|
|
||||||
|
|
||||||
const res = await fetch(verifyURL, {
|
const res = await fetch(verifyURL, {
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 10,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json; charset=utf-8',
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(res);
|
if (!res.ok) throw new Error(`Failed to fetch NIP-05 service: ${nip05}`);
|
||||||
return true;
|
|
||||||
|
const data = res.data as NIP05;
|
||||||
|
if (data.names) {
|
||||||
|
if (data.names[localPath] !== db.account.pubkey) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async (data: NDKUserProfile) => {
|
const onSubmit = async (data: NDKUserProfile) => {
|
||||||
@@ -131,7 +145,7 @@ export function EditProfileModal() {
|
|||||||
</button>
|
</button>
|
||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
<Dialog.Portal className="relative z-10">
|
<Dialog.Portal className="relative z-10">
|
||||||
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xl" />
|
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-2xl" />
|
||||||
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
||||||
<div className="relative h-min w-full max-w-xl rounded-xl bg-white/10 backdrop-blur-xl">
|
<div className="relative h-min w-full max-w-xl rounded-xl bg-white/10 backdrop-blur-xl">
|
||||||
<div className="h-min w-full shrink-0 border-b border-white/10 bg-white/5 px-5 py-5">
|
<div className="h-min w-full shrink-0 border-b border-white/10 bg-white/5 px-5 py-5">
|
||||||
@@ -275,7 +289,7 @@ export function EditProfileModal() {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!isValid}
|
disabled={!isValid}
|
||||||
className="inline-flex h-11 w-full transform items-center justify-center gap-1 rounded-md bg-fuchsia-500 font-medium text-white hover:bg-fuchsia-600 focus:outline-none active:translate-y-1 disabled:pointer-events-none disabled:opacity-50"
|
className="inline-flex h-11 w-full transform items-center justify-center gap-1 rounded-lg bg-fuchsia-500 font-medium text-white hover:bg-fuchsia-600 focus:outline-none active:translate-y-1 disabled:pointer-events-none disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-white" />
|
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-white" />
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ import { UserStats } from '@app/users/components/stats';
|
|||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { Image } from '@shared/image';
|
import { Image } from '@shared/image';
|
||||||
|
import { NIP05 } from '@shared/nip05';
|
||||||
|
|
||||||
import { useNostr } from '@utils/hooks/useNostr';
|
import { useNostr } from '@utils/hooks/useNostr';
|
||||||
import { useProfile } from '@utils/hooks/useProfile';
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
import { shortenKey } from '@utils/shortenKey';
|
import { displayNpub } from '@utils/shortenKey';
|
||||||
|
|
||||||
export function UserProfile({ pubkey }: { pubkey: string }) {
|
export function UserProfile({ pubkey }: { pubkey: string }) {
|
||||||
const { db } = useStorage();
|
const { db } = useStorage();
|
||||||
@@ -57,7 +58,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
|
|||||||
className="h-full w-full object-cover"
|
className="h-full w-full object-cover"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full w-full bg-black/50" />
|
<div className="h-full w-full bg-black" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="-mt-7 flex w-full flex-col items-center px-5">
|
<div className="-mt-7 flex w-full flex-col items-center px-5">
|
||||||
@@ -68,13 +69,21 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
|
|||||||
/>
|
/>
|
||||||
<div className="mt-2 flex flex-1 flex-col gap-6">
|
<div className="mt-2 flex flex-1 flex-col gap-6">
|
||||||
<div className="flex flex-col items-center gap-1">
|
<div className="flex flex-col items-center gap-1">
|
||||||
<div className="inline-flex flex-col gap-1.5">
|
<div className="inline-flex flex-col items-center gap-1.5">
|
||||||
<h5 className="text-center text-xl font-semibold leading-none">
|
<h5 className="text-center text-xl font-semibold leading-none">
|
||||||
{user.display_name || user.displayName || user.name || 'No name'}
|
{user.display_name || user.displayName || user.name || 'No name'}
|
||||||
</h5>
|
</h5>
|
||||||
<span className="max-w-[15rem] truncate text-center leading-none text-white/50">
|
{user.nip05 ? (
|
||||||
{user.nip05 || user.username || shortenKey(pubkey)}
|
<NIP05
|
||||||
</span>
|
pubkey={pubkey}
|
||||||
|
nip05={user?.nip05}
|
||||||
|
className="max-w-[15rem] truncate text-sm leading-none text-white/50"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="max-w-[15rem] truncate text-sm leading-none text-white/50">
|
||||||
|
{displayNpub(pubkey, 16)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
{user.about || user.bio ? (
|
{user.about || user.bio ? (
|
||||||
@@ -82,7 +91,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
|
|||||||
{user.about || user.bio}
|
{user.about || user.bio}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<div />
|
||||||
)}
|
)}
|
||||||
<UserStats pubkey={pubkey} />
|
<UserStats pubkey={pubkey} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export function UserScreen() {
|
|||||||
ref={virtualizer.measureElement}
|
ref={virtualizer.measureElement}
|
||||||
>
|
>
|
||||||
<NoteWrapper event={event}>
|
<NoteWrapper event={event}>
|
||||||
<TextNote event={event} />
|
<TextNote content={event.content} />
|
||||||
</NoteWrapper>
|
</NoteWrapper>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+30
-23
@@ -2,20 +2,21 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
html {
|
.scrollbar-hide::-webkit-scrollbar {
|
||||||
font-size: 14px;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
.scrollbar-hide {
|
||||||
@apply cursor-default no-underline !important;
|
-ms-overflow-style: none;
|
||||||
}
|
scrollbar-width: none;
|
||||||
|
|
||||||
button {
|
|
||||||
@apply cursor-default focus:outline-none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown {
|
.markdown {
|
||||||
@apply prose prose-white max-w-none select-text hyphens-auto text-white prose-p:mb-2 prose-p:mt-0 prose-p:break-words prose-p:[word-break:break-word] prose-p:last:mb-0 prose-a:break-words prose-a:break-all prose-a:font-normal hover:prose-a:text-fuchsia-500 prose-blockquote:mb-1 prose-blockquote:mt-1 prose-blockquote:border-l-[2px] prose-blockquote:border-fuchsia-500 prose-blockquote:pl-2 prose-pre:whitespace-pre-wrap prose-pre:break-words prose-pre:break-all prose-ol:m-0 prose-ol:mb-1 prose-ul:mb-1 prose-ul:mt-1 prose-img:mb-2 prose-img:mt-3 prose-hr:mx-0 prose-hr:my-2;
|
@apply prose prose-white max-w-none select-text hyphens-auto text-white prose-headings:mb-1 prose-headings:mt-3 prose-p:mb-0 prose-p:mt-0 prose-p:break-words prose-p:[word-break:break-word] prose-p:last:mb-0 prose-a:break-words prose-a:break-all prose-a:font-normal hover:prose-a:text-fuchsia-500 prose-blockquote:mb-1 prose-blockquote:mt-1 prose-blockquote:border-l-[2px] prose-blockquote:border-fuchsia-500 prose-blockquote:pl-2 prose-pre:whitespace-pre-wrap prose-pre:break-words prose-pre:break-all prose-pre:bg-white/10 prose-ol:m-0 prose-ol:mb-1 prose-ul:mb-1 prose-ul:mt-1 prose-img:mb-2 prose-img:mt-3 prose-hr:mx-0 prose-hr:my-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-article {
|
||||||
|
@apply prose prose-white max-w-none select-text hyphens-auto text-white/80 prose-headings:mb-1 prose-headings:mt-3 prose-headings:text-white prose-p:mb-2 prose-p:mt-0 prose-p:break-words prose-p:[word-break:break-word] prose-a:break-words prose-a:break-all prose-a:font-normal hover:prose-a:text-fuchsia-500 prose-blockquote:mb-1 prose-blockquote:mt-1 prose-blockquote:border-l-[2px] prose-blockquote:border-fuchsia-500 prose-blockquote:pl-2 prose-pre:whitespace-pre-wrap prose-pre:break-words prose-pre:break-all prose-pre:bg-white/10 prose-ol:m-0 prose-ol:mb-1 prose-ul:mb-1 prose-ul:mt-1 prose-img:mb-2 prose-img:mt-3 prose-hr:mx-0 prose-hr:my-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror p.is-empty::before {
|
.ProseMirror p.is-empty::before {
|
||||||
@@ -30,25 +31,31 @@ button {
|
|||||||
@apply outline-fuchsia-500;
|
@apply outline-fuchsia-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
@apply cursor-default no-underline !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
@apply cursor-default focus:outline-none;
|
||||||
|
}
|
||||||
|
|
||||||
iframe {
|
iframe {
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* For Webkit-based browsers (Chrome, Safari and Opera) */
|
span[data-slate-placeholder] {
|
||||||
.scrollbar-hide::-webkit-scrollbar {
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::-ms-reveal,
|
||||||
|
input::-ms-clear {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* For IE, Edge and Firefox */
|
::-webkit-input-placeholder {
|
||||||
.scrollbar-hide {
|
line-height: normal;
|
||||||
-ms-overflow-style: none; /* IE and Edge */
|
|
||||||
scrollbar-width: none; /* Firefox */
|
|
||||||
}
|
|
||||||
|
|
||||||
.border {
|
|
||||||
background-clip: padding-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
span[data-slate-placeholder] {
|
|
||||||
@apply top-0;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { NDKCacheAdapter } from '@nostr-dev-kit/ndk';
|
import { NDKCacheAdapter } from '@nostr-dev-kit/ndk';
|
||||||
import { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk';
|
import { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk';
|
||||||
import { Store } from '@tauri-apps/plugin-store';
|
import { Store } from 'tauri-plugin-store-api';
|
||||||
|
|
||||||
export default class TauriAdapter implements NDKCacheAdapter {
|
export default class TauriAdapter implements NDKCacheAdapter {
|
||||||
public store: Store;
|
public store: Store;
|
||||||
|
|||||||
+34
-50
@@ -1,85 +1,69 @@
|
|||||||
// inspire by: https://github.com/nostr-dev-kit/ndk-react/
|
// inspire by: https://github.com/nostr-dev-kit/ndk-react/
|
||||||
import NDK from '@nostr-dev-kit/ndk';
|
import NDK from '@nostr-dev-kit/ndk';
|
||||||
|
import { message } from '@tauri-apps/api/dialog';
|
||||||
|
import { fetch } from '@tauri-apps/api/http';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import TauriAdapter from '@libs/ndk/cache';
|
import TauriAdapter from '@libs/ndk/cache';
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { FULL_RELAYS } from '@stores/constants';
|
|
||||||
|
|
||||||
export const NDKInstance = () => {
|
export const NDKInstance = () => {
|
||||||
const { db } = useStorage();
|
|
||||||
|
|
||||||
const [ndk, setNDK] = useState<NDK | undefined>(undefined);
|
const [ndk, setNDK] = useState<NDK | undefined>(undefined);
|
||||||
const [relayUrls, setRelayUrls] = useState<string[]>([]);
|
const [relayUrls, setRelayUrls] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const { db } = useStorage();
|
||||||
const cacheAdapter = useMemo(() => new TauriAdapter(), [ndk]);
|
const cacheAdapter = useMemo(() => new TauriAdapter(), [ndk]);
|
||||||
|
|
||||||
// TODO: fully support NIP-11
|
// TODO: fully support NIP-11
|
||||||
async function verifyRelays(relays: string[]) {
|
async function getExplicitRelays() {
|
||||||
try {
|
try {
|
||||||
const urls: string[] = relays.map((relay) => {
|
// get relays
|
||||||
if (relay.startsWith('ws')) {
|
const relays = await db.getExplicitRelayUrls();
|
||||||
return relay.replace('ws', 'http');
|
const requests = relays.map((relay) => {
|
||||||
}
|
const url = new URL(relay);
|
||||||
if (relay.startsWith('wss')) {
|
return fetch(`https://${url.hostname + url.pathname}`, {
|
||||||
return relay.replace('wss', 'https');
|
method: 'GET',
|
||||||
}
|
timeout: 10,
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/nostr+json',
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => controller.abort('timeout'), 5000);
|
|
||||||
|
|
||||||
const requests = urls.map((url) =>
|
|
||||||
fetch(url, {
|
|
||||||
headers: { Accept: 'application/nostr+json' },
|
|
||||||
signal: controller.signal,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const responses = await Promise.all(requests);
|
const responses = await Promise.all(requests);
|
||||||
const errors = responses.filter((response) => !response.ok);
|
const successes = responses.filter((res) => res.ok);
|
||||||
|
|
||||||
if (errors.length > 0) {
|
const verifiedRelays: string[] = successes.map((res) => {
|
||||||
throw errors.map((response) => Error(response.statusText));
|
// TODO: support payment
|
||||||
}
|
// @ts-expect-error, not have type yet
|
||||||
|
if (!res.data.limitation?.payment_required) {
|
||||||
const verifiedRelays: string[] = responses.map((res) => {
|
const url = new URL(res.url);
|
||||||
if (res.url.startsWith('http')) {
|
if (url.protocol === 'http:') return `ws://${url.hostname + url.pathname}`;
|
||||||
return res.url.replace('htto', 'ws');
|
if (url.protocol === 'https:') return `wss://${url.hostname + url.pathname}`;
|
||||||
}
|
|
||||||
if (res.url.startsWith('https')) {
|
|
||||||
return res.url.replace('https', 'wss');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// clear timeout
|
// return all validated relays
|
||||||
clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
// return all validate relays
|
|
||||||
return verifiedRelays;
|
return verifiedRelays;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('ndk instance error: ', e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initNDK() {
|
async function initNDK() {
|
||||||
let explicitRelayUrls: string[];
|
const explicitRelayUrls = await getExplicitRelays();
|
||||||
const explicitRelayUrlsFromDB = await db.getExplicitRelayUrls();
|
const instance = new NDK({
|
||||||
console.log('relays in db: ', explicitRelayUrlsFromDB);
|
explicitRelayUrls,
|
||||||
|
cacheAdapter,
|
||||||
if (explicitRelayUrlsFromDB) {
|
});
|
||||||
explicitRelayUrls = await verifyRelays(explicitRelayUrlsFromDB);
|
|
||||||
} else {
|
|
||||||
explicitRelayUrls = await verifyRelays(FULL_RELAYS);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('ndk cache adapter: ', cacheAdapter);
|
|
||||||
const instance = new NDK({ explicitRelayUrls, cacheAdapter });
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await instance.connect(10000);
|
await instance.connect(10000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error('NDK instance init failed: ', error);
|
await message(`NDK instance init failed: ${error}`, {
|
||||||
|
title: 'Lume',
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setNDK(instance);
|
setNDK(instance);
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import { PropsWithChildren, createContext, useContext } from 'react';
|
|||||||
import { NDKInstance } from '@libs/ndk/instance';
|
import { NDKInstance } from '@libs/ndk/instance';
|
||||||
|
|
||||||
interface NDKContext {
|
interface NDKContext {
|
||||||
ndk: NDK;
|
ndk: undefined | NDK;
|
||||||
relayUrls: string[];
|
relayUrls: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const NDKContext = createContext<NDKContext>({
|
const NDKContext = createContext<NDKContext>({
|
||||||
ndk: new NDK({}),
|
ndk: undefined,
|
||||||
relayUrls: [],
|
relayUrls: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,44 +1,55 @@
|
|||||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||||
import { BaseDirectory, removeFile } from '@tauri-apps/plugin-fs';
|
import { BaseDirectory, removeFile } from '@tauri-apps/api/fs';
|
||||||
import Database from '@tauri-apps/plugin-sql';
|
import { Platform } from '@tauri-apps/api/os';
|
||||||
import { Stronghold } from '@tauri-apps/plugin-stronghold';
|
import Database from 'tauri-plugin-sql-api';
|
||||||
|
import { Stronghold } from 'tauri-plugin-stronghold-api';
|
||||||
|
|
||||||
|
import { FULL_RELAYS } from '@stores/constants';
|
||||||
|
|
||||||
import { Account, DBEvent, Relays, Widget } from '@utils/types';
|
import { Account, DBEvent, Relays, Widget } from '@utils/types';
|
||||||
|
|
||||||
export class LumeStorage {
|
export class LumeStorage {
|
||||||
public db: Database;
|
public db: Database;
|
||||||
|
public platform: Platform;
|
||||||
public secureDB: Stronghold;
|
public secureDB: Stronghold;
|
||||||
public account: Account | null = null;
|
public account: Account | null = null;
|
||||||
|
|
||||||
constructor(sqlite: Database, stronghold?: Stronghold) {
|
constructor(sqlite: Database, platform: Platform, stronghold?: Stronghold) {
|
||||||
this.db = sqlite;
|
this.db = sqlite;
|
||||||
|
this.platform = platform ?? undefined;
|
||||||
this.secureDB = stronghold ?? undefined;
|
this.secureDB = stronghold ?? undefined;
|
||||||
this.account = null;
|
this.account = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getSecureClient() {
|
private async getSecureClient(key?: string) {
|
||||||
try {
|
try {
|
||||||
return await this.secureDB.loadClient('lume');
|
return await this.secureDB.loadClient(key ?? 'lume');
|
||||||
} catch {
|
} catch {
|
||||||
return await this.secureDB.createClient('lume');
|
return await this.secureDB.createClient(key ?? 'lume');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async secureSave(key: string, value: string) {
|
public async secureSave(key: string, value: string, clientKey?: string) {
|
||||||
if (!this.secureDB) throw new Error("Stronghold isn't initialize");
|
if (!this.secureDB) throw new Error("Stronghold isn't initialize");
|
||||||
|
|
||||||
const client = await this.getSecureClient();
|
const client = await this.getSecureClient(clientKey);
|
||||||
const store = client.getStore();
|
const store = client.getStore();
|
||||||
|
console.log('insert key: ', key);
|
||||||
|
|
||||||
await store.insert(key, Array.from(new TextEncoder().encode(value)));
|
await store.insert(key, Array.from(new TextEncoder().encode(value)));
|
||||||
return await this.secureDB.save();
|
await this.secureDB.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async secureLoad(key: string) {
|
public async secureLoad(key: string, clientKey?: string) {
|
||||||
if (!this.secureDB) throw new Error("Stronghold isn't initialize");
|
if (!this.secureDB) throw new Error("Stronghold isn't initialize");
|
||||||
|
|
||||||
const client = await this.getSecureClient();
|
const client = await this.getSecureClient(clientKey);
|
||||||
const store = client.getStore();
|
const store = client.getStore();
|
||||||
|
console.log('get key: ', key);
|
||||||
|
|
||||||
const value = await store.get(key);
|
const value = await store.get(key);
|
||||||
|
if (!value) return null;
|
||||||
|
|
||||||
const decoded = new TextDecoder().decode(new Uint8Array(value));
|
const decoded = new TextDecoder().decode(new Uint8Array(value));
|
||||||
return decoded;
|
return decoded;
|
||||||
}
|
}
|
||||||
@@ -134,18 +145,29 @@ export class LumeStorage {
|
|||||||
return await this.db.execute('DELETE FROM widgets WHERE id = $1;', [id]);
|
return await this.db.execute('DELETE FROM widgets WHERE id = $1;', [id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createEvent(
|
public async createEvent(event: NDKEvent) {
|
||||||
id: string,
|
let root: string;
|
||||||
event: string,
|
let reply: string;
|
||||||
author: string,
|
|
||||||
kind: number,
|
if (event.tags?.[0]?.[0] === 'e' && !event.tags?.[0]?.[3]) {
|
||||||
root_id: string,
|
root = event.tags[0][1];
|
||||||
reply_id: string,
|
} else {
|
||||||
created_at: number
|
root = event.tags.find((el) => el[3] === 'root')?.[1];
|
||||||
) {
|
reply = event.tags.find((el) => el[3] === 'reply')?.[1];
|
||||||
|
}
|
||||||
|
|
||||||
return await this.db.execute(
|
return await this.db.execute(
|
||||||
'INSERT OR IGNORE INTO events (id, account_id, event, author, kind, root_id, reply_id, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8);',
|
'INSERT OR IGNORE INTO events (id, account_id, event, author, kind, root_id, reply_id, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8);',
|
||||||
[id, this.account.id, event, author, kind, root_id, reply_id, created_at]
|
[
|
||||||
|
event.id,
|
||||||
|
this.account.id,
|
||||||
|
JSON.stringify(event),
|
||||||
|
event.pubkey,
|
||||||
|
event.kind,
|
||||||
|
root,
|
||||||
|
reply,
|
||||||
|
event.created_at,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,13 +283,13 @@ export class LumeStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getExplicitRelayUrls() {
|
public async getExplicitRelayUrls() {
|
||||||
if (!this.account) return null;
|
if (!this.account) return FULL_RELAYS;
|
||||||
|
|
||||||
const result: Relays[] = await this.db.select(
|
const result: Relays[] = await this.db.select(
|
||||||
`SELECT * FROM relays WHERE account_id = "${this.account.id}" ORDER BY id DESC LIMIT 50;`
|
`SELECT * FROM relays WHERE account_id = "${this.account.id}" ORDER BY id DESC LIMIT 50;`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.length < 1) return null;
|
if (!result || result.length < 1) return FULL_RELAYS;
|
||||||
return result.map((el) => el.relay);
|
return result.map((el) => el.relay);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,6 +310,15 @@ export class LumeStorage {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async accountLogout() {
|
||||||
|
await this.db.execute("UPDATE accounts SET is_active = '0' WHERE id = $1;", [
|
||||||
|
this.account.id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.account = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public async close() {
|
public async close() {
|
||||||
return this.db.close();
|
return this.db.close();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import Database from '@tauri-apps/plugin-sql';
|
import { message } from '@tauri-apps/api/dialog';
|
||||||
|
import { platform } from '@tauri-apps/api/os';
|
||||||
|
import { appConfigDir } from '@tauri-apps/api/path';
|
||||||
import { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react';
|
import { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react';
|
||||||
|
import Database from 'tauri-plugin-sql-api';
|
||||||
|
|
||||||
import { LumeStorage } from '@libs/storage/instance';
|
import { LumeStorage } from '@libs/storage/instance';
|
||||||
|
|
||||||
@@ -15,11 +18,22 @@ const StorageProvider = ({ children }: PropsWithChildren<object>) => {
|
|||||||
const [db, setDB] = useState<LumeStorage>(undefined);
|
const [db, setDB] = useState<LumeStorage>(undefined);
|
||||||
|
|
||||||
async function initLumeStorage() {
|
async function initLumeStorage() {
|
||||||
const sqlite = await Database.load('sqlite:lume.db');
|
try {
|
||||||
const lumeStorage = new LumeStorage(sqlite);
|
const dir = await appConfigDir();
|
||||||
|
const sqlite = await Database.load('sqlite:lume.db');
|
||||||
|
const platformName = await platform();
|
||||||
|
const lumeStorage = new LumeStorage(sqlite, platformName);
|
||||||
|
|
||||||
if (!lumeStorage.account) await lumeStorage.getActiveAccount();
|
console.log('App config dir: ', dir);
|
||||||
setDB(lumeStorage);
|
|
||||||
|
if (!lumeStorage.account) await lumeStorage.getActiveAccount();
|
||||||
|
setDB(lumeStorage);
|
||||||
|
} catch (e) {
|
||||||
|
await message(`Cannot initialize database: ${e}`, {
|
||||||
|
title: 'Lume',
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { useStorage } from '@libs/storage/provider';
|
|||||||
import { AccountMoreActions } from '@shared/accounts/more';
|
import { AccountMoreActions } from '@shared/accounts/more';
|
||||||
import { Image } from '@shared/image';
|
import { Image } from '@shared/image';
|
||||||
|
|
||||||
|
import { useActivities } from '@stores/activities';
|
||||||
|
|
||||||
import { useNostr } from '@utils/hooks/useNostr';
|
import { useNostr } from '@utils/hooks/useNostr';
|
||||||
import { useProfile } from '@utils/hooks/useProfile';
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
import { sendNativeNotification } from '@utils/notification';
|
import { sendNativeNotification } from '@utils/notification';
|
||||||
@@ -16,6 +18,8 @@ export function ActiveAccount() {
|
|||||||
const { status, user } = useProfile(db.account.pubkey);
|
const { status, user } = useProfile(db.account.pubkey);
|
||||||
const { sub } = useNostr();
|
const { sub } = useNostr();
|
||||||
|
|
||||||
|
const addActivity = useActivities((state) => state.addActivity);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const filter: NDKFilter = {
|
const filter: NDKFilter = {
|
||||||
'#p': [db.account.pubkey],
|
'#p': [db.account.pubkey],
|
||||||
@@ -30,11 +34,13 @@ export function ActiveAccount() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
sub(filter, async (event) => {
|
sub(filter, async (event) => {
|
||||||
|
addActivity(event);
|
||||||
|
|
||||||
switch (event.kind) {
|
switch (event.kind) {
|
||||||
case NDKKind.Text:
|
case NDKKind.Text:
|
||||||
return await sendNativeNotification('Mention');
|
return await sendNativeNotification('Mention');
|
||||||
case NDKKind.Contacts:
|
case NDKKind.Contacts:
|
||||||
return await sendNativeNotification("You've new follower");
|
return await sendNativeNotification("You've a new follower");
|
||||||
case NDKKind.Repost:
|
case NDKKind.Repost:
|
||||||
return await sendNativeNotification('Repost');
|
return await sendNativeNotification('Repost');
|
||||||
case NDKKind.Reaction:
|
case NDKKind.Reaction:
|
||||||
@@ -42,7 +48,6 @@ export function ActiveAccount() {
|
|||||||
case NDKKind.Zap:
|
case NDKKind.Zap:
|
||||||
return await sendNativeNotification('Zap');
|
return await sendNativeNotification('Zap');
|
||||||
default:
|
default:
|
||||||
console.log('[notify] new event: ', event);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -50,8 +55,8 @@ export function ActiveAccount() {
|
|||||||
|
|
||||||
if (status === 'loading') {
|
if (status === 'loading') {
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex h-10 items-center gap-2.5 rounded-md px-2">
|
<div className="inline-flex h-16 items-center gap-2.5 border-l-2 border-transparent pb-2 pl-4 pr-2">
|
||||||
<div className="relative h-7 w-7 shrink-0 animate-pulse rounded bg-white/10 backdrop-blur-xl" />
|
<div className="relative h-10 w-10 shrink-0 animate-pulse rounded bg-white/10 backdrop-blur-xl" />
|
||||||
<div className="h-2.5 w-2/3 animate-pulse rounded bg-white/10 backdrop-blur-xl" />
|
<div className="h-2.5 w-2/3 animate-pulse rounded bg-white/10 backdrop-blur-xl" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -67,7 +72,7 @@ export function ActiveAccount() {
|
|||||||
/>
|
/>
|
||||||
<div className="flex w-full flex-1 flex-col items-start gap-1.5">
|
<div className="flex w-full flex-1 flex-col items-start gap-1.5">
|
||||||
<p className="max-w-[10rem] truncate font-bold leading-none text-white">
|
<p className="max-w-[10rem] truncate font-bold leading-none text-white">
|
||||||
{user?.name || user?.display_name || user?.nip05}
|
{user?.name || user?.display_name}
|
||||||
</p>
|
</p>
|
||||||
<span className="max-w-[8rem] truncate text-sm leading-none text-white/50">
|
<span className="max-w-[8rem] truncate text-sm leading-none text-white/50">
|
||||||
{displayNpub(db.account.pubkey, 16)}
|
{displayNpub(db.account.pubkey, 16)}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useState } from 'react';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { HorizontalDotsIcon } from '@shared/icons';
|
import { HorizontalDotsIcon } from '@shared/icons';
|
||||||
|
import { Logout } from '@shared/logout';
|
||||||
|
|
||||||
export function AccountMoreActions({ pubkey }: { pubkey: string }) {
|
export function AccountMoreActions({ pubkey }: { pubkey: string }) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -18,7 +19,7 @@ export function AccountMoreActions({ pubkey }: { pubkey: string }) {
|
|||||||
</button>
|
</button>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Portal>
|
<DropdownMenu.Portal>
|
||||||
<DropdownMenu.Content className="flex w-[200px] flex-col overflow-hidden rounded-xl bg-white/10 p-2 backdrop-blur-3xl focus:outline-none">
|
<DropdownMenu.Content className="flex w-[200px] flex-col overflow-hidden rounded-xl border border-white/10 bg-white/20 p-2 backdrop-blur-3xl focus:outline-none">
|
||||||
<DropdownMenu.Item asChild>
|
<DropdownMenu.Item asChild>
|
||||||
<Link
|
<Link
|
||||||
to={`/users/${pubkey}`}
|
to={`/users/${pubkey}`}
|
||||||
@@ -44,9 +45,7 @@ export function AccountMoreActions({ pubkey }: { pubkey: string }) {
|
|||||||
</Link>
|
</Link>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item asChild>
|
<DropdownMenu.Item asChild>
|
||||||
<button className="inline-flex h-10 items-center rounded-lg px-2 text-sm font-medium text-white hover:bg-white/10">
|
<Logout />
|
||||||
Logout
|
|
||||||
</button>
|
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Portal>
|
</DropdownMenu.Portal>
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import { Dispatch, SetStateAction, useState } from 'react';
|
|||||||
|
|
||||||
import { LoaderIcon, PlusIcon } from '@shared/icons';
|
import { LoaderIcon, PlusIcon } from '@shared/icons';
|
||||||
|
|
||||||
import { useImageUploader } from '@utils/hooks/useUploader';
|
import { useNostr } from '@utils/hooks/useNostr';
|
||||||
|
|
||||||
export function AvatarUploader({
|
export function AvatarUploader({
|
||||||
setPicture,
|
setPicture,
|
||||||
}: {
|
}: {
|
||||||
setPicture: Dispatch<SetStateAction<string>>;
|
setPicture: Dispatch<SetStateAction<string>>;
|
||||||
}) {
|
}) {
|
||||||
const upload = useImageUploader();
|
const { upload } = useNostr();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const uploadAvatar = async () => {
|
const uploadAvatar = async () => {
|
||||||
|
|||||||
@@ -2,23 +2,21 @@ import { Dispatch, SetStateAction, useState } from 'react';
|
|||||||
|
|
||||||
import { LoaderIcon, PlusIcon } from '@shared/icons';
|
import { LoaderIcon, PlusIcon } from '@shared/icons';
|
||||||
|
|
||||||
import { useImageUploader } from '@utils/hooks/useUploader';
|
import { useNostr } from '@utils/hooks/useNostr';
|
||||||
|
|
||||||
export function BannerUploader({
|
export function BannerUploader({
|
||||||
setBanner,
|
setBanner,
|
||||||
}: {
|
}: {
|
||||||
setBanner: Dispatch<
|
setBanner: Dispatch<SetStateAction<string>>;
|
||||||
SetStateAction<{ url: undefined | string; error?: undefined | string }>
|
|
||||||
>;
|
|
||||||
}) {
|
}) {
|
||||||
const upload = useImageUploader();
|
const { upload } = useNostr();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const uploadBanner = async () => {
|
const uploadBanner = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const image = await upload(null);
|
const image = await upload(null);
|
||||||
if (image.url) {
|
if (image.url) {
|
||||||
setBanner(image);
|
setBanner(image.url);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
@@ -27,13 +25,14 @@ export function BannerUploader({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => uploadBanner()}
|
onClick={() => uploadBanner()}
|
||||||
className="inline-flex h-full w-full items-center justify-center bg-black/40 hover:bg-black/50"
|
className="inline-flex h-full w-full flex-col items-center justify-center gap-1 bg-black/40 hover:bg-black/50"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoaderIcon className="h-8 w-8 animate-spin text-white" />
|
<LoaderIcon className="h-6 w-6 animate-spin text-white" />
|
||||||
) : (
|
) : (
|
||||||
<PlusIcon className="h-8 w-8 text-white" />
|
<PlusIcon className="h-6 w-6 text-white" />
|
||||||
)}
|
)}
|
||||||
|
<p className="text-sm font-medium text-white/70">Add a banner image</p>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,28 @@
|
|||||||
|
import { message } from '@tauri-apps/api/dialog';
|
||||||
import Image from '@tiptap/extension-image';
|
import Image from '@tiptap/extension-image';
|
||||||
import Mention from '@tiptap/extension-mention';
|
|
||||||
import Placeholder from '@tiptap/extension-placeholder';
|
import Placeholder from '@tiptap/extension-placeholder';
|
||||||
import { EditorContent, useEditor } from '@tiptap/react';
|
import { EditorContent, useEditor } from '@tiptap/react';
|
||||||
import StarterKit from '@tiptap/starter-kit';
|
import StarterKit from '@tiptap/starter-kit';
|
||||||
import { convert } from 'html-to-text';
|
import { convert } from 'html-to-text';
|
||||||
import { nip19 } from 'nostr-tools';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
import { Suggestion } from '@shared/composer';
|
import { MediaUploader, MentionPopup } from '@shared/composer';
|
||||||
import { CancelIcon, LoaderIcon, PlusCircleIcon } from '@shared/icons';
|
import { CancelIcon, LoaderIcon } from '@shared/icons';
|
||||||
import { MentionNote } from '@shared/notes';
|
import { MentionNote } from '@shared/notes';
|
||||||
|
|
||||||
import { useComposer } from '@stores/composer';
|
import { useComposer } from '@stores/composer';
|
||||||
|
|
||||||
import { useNostr } from '@utils/hooks/useNostr';
|
import { useNostr } from '@utils/hooks/useNostr';
|
||||||
import { useImageUploader } from '@utils/hooks/useUploader';
|
|
||||||
import { sendNativeNotification } from '@utils/notification';
|
import { sendNativeNotification } from '@utils/notification';
|
||||||
|
|
||||||
export function Composer() {
|
export function Composer() {
|
||||||
const { publish } = useNostr();
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
const [status, setStatus] = useState<null | 'loading' | 'done'>(null);
|
|
||||||
const [reply, clearReply] = useComposer((state) => [state.reply, state.clearReply]);
|
const [reply, clearReply] = useComposer((state) => [state.reply, state.clearReply]);
|
||||||
|
|
||||||
const expand = useComposer((state) => state.expand)
|
const { publish } = useNostr();
|
||||||
const upload = useImageUploader();
|
|
||||||
|
|
||||||
|
const expand = useComposer((state) => state.expand);
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
extensions: [
|
extensions: [
|
||||||
StarterKit.configure({
|
StarterKit.configure({
|
||||||
@@ -35,12 +31,6 @@ export function Composer() {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
Placeholder.configure({ placeholder: 'Type something...' }),
|
Placeholder.configure({ placeholder: 'Type something...' }),
|
||||||
Mention.configure({
|
|
||||||
suggestion: Suggestion,
|
|
||||||
renderLabel({ node }) {
|
|
||||||
return `nostr:${nip19.npubEncode(node.attrs.id.pubkey)} `;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
Image.configure({
|
Image.configure({
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
class:
|
class:
|
||||||
@@ -48,26 +38,22 @@ export function Composer() {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
content: '',
|
content: JSON.parse(localStorage.getItem('editor-content') || '{}'),
|
||||||
editorProps: {
|
editorProps: {
|
||||||
attributes: {
|
attributes: {
|
||||||
class: 'h-full markdown break-all overflow-y-auto outline-none pr-2',
|
class: 'h-full markdown break-all overflow-y-auto outline-none pr-2',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
onUpdate: ({ editor }) => {
|
||||||
|
const jsonContent = JSON.stringify(editor.getJSON());
|
||||||
|
localStorage.setItem('editor-content', jsonContent);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const uploadImage = async (file?: string) => {
|
|
||||||
const image = await upload(file, true);
|
|
||||||
if (image.url) {
|
|
||||||
editor.commands.setImage({ src: image.url });
|
|
||||||
editor.commands.createParagraphNear();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
setStatus('loading');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
// get plaintext content
|
// get plaintext content
|
||||||
const html = editor.getHTML();
|
const html = editor.getHTML();
|
||||||
const serializedContent = convert(html, {
|
const serializedContent = convert(html, {
|
||||||
@@ -108,18 +94,19 @@ export function Composer() {
|
|||||||
await publish({ content: serializedContent, kind: 1, tags });
|
await publish({ content: serializedContent, kind: 1, tags });
|
||||||
|
|
||||||
// send native notifiation
|
// send native notifiation
|
||||||
await sendNativeNotification('Publish post successfully');
|
await sendNativeNotification('Post has been published successfully.');
|
||||||
|
|
||||||
// update state
|
// update state
|
||||||
setStatus('done');
|
setLoading(false);
|
||||||
// reset editor
|
// reset editor
|
||||||
editor.commands.clearContent();
|
editor.commands.clearContent();
|
||||||
|
// reset reply
|
||||||
if (reply.id) {
|
if (reply.id) {
|
||||||
clearReply();
|
clearReply();
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setStatus(null);
|
setLoading(false);
|
||||||
console.log('failed to publish');
|
await message('Publishing post failed.', { title: 'Lume', type: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -136,7 +123,10 @@ export function Composer() {
|
|||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
className={twMerge('scrollbar-hide markdown break-all max-h-[500px] overflow-y-auto outline-none pr-2', expand ? 'min-h-[500px]' : 'min-h-[120px]')}
|
className={twMerge(
|
||||||
|
'scrollbar-hide markdown max-h-[500px] overflow-y-auto break-all pr-2 outline-none',
|
||||||
|
expand ? 'min-h-[500px]' : reply.id ? 'min-h-min' : 'min-h-[120px]'
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
{reply.id && (
|
{reply.id && (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -152,17 +142,18 @@ export function Composer() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between bg-white/5 rounded-b-xl p-2 border-t border-white/10">
|
<div className="flex items-center justify-between rounded-b-xl border-t border-white/10 bg-white/5 p-2">
|
||||||
|
<div className="inline-flex items-center gap-1">
|
||||||
|
<MediaUploader editor={editor} />
|
||||||
|
<MentionPopup editor={editor} />
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
onClick={() => submit()}
|
||||||
onClick={() => uploadImage()}
|
disabled={editor && editor.isEmpty}
|
||||||
className="ml-2 inline-flex h-10 w-10 items-center justify-center rounded-lg backdrop-blur-xl hover:bg-white/10"
|
className="inline-flex h-10 w-20 items-center justify-center rounded-lg bg-fuchsia-500 px-2 font-semibold hover:bg-fuchsia-600 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<PlusCircleIcon className="h-5 w-5 text-white" />
|
{loading === true ? (
|
||||||
</button>
|
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
||||||
<button onClick={() => submit()} className="inline-flex items-center justify-center w-max px-8 rounded-lg font-bold h-10 bg-fuchsia-500 hover:bg-fuchsia-600">
|
|
||||||
{status === 'loading' ? (
|
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
|
|
||||||
) : (
|
) : (
|
||||||
'Post'
|
'Post'
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export * from './user';
|
export * from './user';
|
||||||
export * from './modal';
|
export * from './modal';
|
||||||
export * from './composer';
|
export * from './composer';
|
||||||
export * from './mention/list';
|
|
||||||
export * from './mention/item';
|
export * from './mention/item';
|
||||||
export * from './mention/suggestion';
|
export * from './mention/popup';
|
||||||
|
export * from './mediaUploader';
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { message } from '@tauri-apps/api/dialog';
|
||||||
|
import { UnlistenFn, listen } from '@tauri-apps/api/event';
|
||||||
|
import { Editor } from '@tiptap/react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { MediaIcon } from '@shared/icons';
|
||||||
|
|
||||||
|
import { useNostr } from '@utils/hooks/useNostr';
|
||||||
|
|
||||||
|
export function MediaUploader({ editor }: { editor: Editor }) {
|
||||||
|
const { upload } = useNostr();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const uploadToNostrBuild = async (file?: string) => {
|
||||||
|
try {
|
||||||
|
// start loading
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const image = await upload(file, true);
|
||||||
|
if (image.url) {
|
||||||
|
editor.commands.setImage({ src: image.url });
|
||||||
|
editor.commands.createParagraphNear();
|
||||||
|
|
||||||
|
// stop loading
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// stop loading
|
||||||
|
setLoading(false);
|
||||||
|
await message(`Upload failed, error: ${e}`, { title: 'Lume', type: 'error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let unlisten: UnlistenFn;
|
||||||
|
|
||||||
|
async function listenDnD() {
|
||||||
|
unlisten = await listen('tauri://file-drop', (event) => {
|
||||||
|
uploadToNostrBuild(event.payload[0]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// start listen drag and drop event
|
||||||
|
listenDnD();
|
||||||
|
|
||||||
|
// clean up
|
||||||
|
return () => {
|
||||||
|
unlisten();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => uploadToNostrBuild()}
|
||||||
|
className="ml-2 inline-flex h-10 w-max items-center justify-center gap-1.5 rounded-lg px-2 text-sm font-medium text-white/80 hover:bg-white/10 hover:backdrop-blur-xl"
|
||||||
|
>
|
||||||
|
<MediaIcon className="h-5 w-5 text-white/80" />
|
||||||
|
{loading ? 'Uploading...' : 'Add media'}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,22 +1,36 @@
|
|||||||
import { Image } from '@shared/image';
|
import { Image } from '@shared/image';
|
||||||
|
|
||||||
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
import { displayNpub } from '@utils/shortenKey';
|
import { displayNpub } from '@utils/shortenKey';
|
||||||
import { Profile } from '@utils/types';
|
|
||||||
|
|
||||||
export function MentionItem({ profile }: { profile: Profile }) {
|
export function MentionItem({ pubkey }: { pubkey: string }) {
|
||||||
|
const { status, user } = useProfile(pubkey);
|
||||||
|
|
||||||
|
if (status === 'loading') {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="relative h-8 w-8 shrink-0 animate-pulse rounded bg-white/10 backdrop-blur-xl" />
|
||||||
|
<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 backdrop-blur-xl" />
|
||||||
|
<span className="h-3 w-1/3 animate-pulse rounded bg-white/10 backdrop-blur-xl" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex h-11 items-center justify-start gap-2.5 px-2 hover:bg-white/10">
|
||||||
<Image
|
<Image
|
||||||
src={profile.picture || profile.image}
|
src={user.picture || user.image}
|
||||||
alt={profile.pubkey}
|
alt={pubkey}
|
||||||
className="shirnk-0 h-8 w-8 rounded-md object-cover"
|
className="shirnk-0 h-8 w-8 rounded-md object-cover"
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col gap-px">
|
<div className="flex flex-col items-start gap-px">
|
||||||
<h5 className="max-w-[15rem] text-sm font-medium leading-none text-white">
|
<h5 className="max-w-[10rem] truncate text-sm font-medium leading-none text-white">
|
||||||
{profile.ident}
|
{user.display_name || user.displayName || user.name}
|
||||||
</h5>
|
</h5>
|
||||||
<span className="text-sm leading-none text-white/50">
|
<span className="text-sm leading-none text-white/50">
|
||||||
{displayNpub(profile.pubkey, 16)}
|
{displayNpub(pubkey, 16)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
import { NDKUserProfile } from '@nostr-dev-kit/ndk';
|
|
||||||
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
|
|
||||||
import { twMerge } from 'tailwind-merge';
|
|
||||||
|
|
||||||
import { MentionItem } from '@shared/composer';
|
|
||||||
|
|
||||||
export const MentionList = forwardRef((props: any, ref: any) => {
|
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
||||||
|
|
||||||
const selectItem = (index) => {
|
|
||||||
const item = props.items[index];
|
|
||||||
|
|
||||||
if (item) {
|
|
||||||
props.command({ id: item });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const upHandler = () => {
|
|
||||||
setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length);
|
|
||||||
};
|
|
||||||
|
|
||||||
const downHandler = () => {
|
|
||||||
setSelectedIndex((selectedIndex + 1) % props.items.length);
|
|
||||||
};
|
|
||||||
|
|
||||||
const enterHandler = () => {
|
|
||||||
selectItem(selectedIndex);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => setSelectedIndex(0), [props.items]);
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
onKeyDown: ({ event }) => {
|
|
||||||
if (event.key === 'ArrowUp') {
|
|
||||||
upHandler();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === 'ArrowDown') {
|
|
||||||
downHandler();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
enterHandler();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex w-[250px] flex-col rounded-xl border-t border-zinc-700/50 bg-zinc-800 px-3 py-3">
|
|
||||||
{props.items.length ? (
|
|
||||||
props.items.map((item: NDKUserProfile, index: number) => (
|
|
||||||
<button
|
|
||||||
className={twMerge(
|
|
||||||
'h-11 w-full rounded-lg px-2 text-start text-sm font-medium hover:bg-zinc-700',
|
|
||||||
`${index === selectedIndex ? 'is-selected' : ''}`
|
|
||||||
)}
|
|
||||||
key={index}
|
|
||||||
onClick={() => selectItem(index)}
|
|
||||||
>
|
|
||||||
<MentionItem profile={item} />
|
|
||||||
</button>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div>No result</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
MentionList.displayName = 'MentionList';
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import * as Popover from '@radix-ui/react-popover';
|
||||||
|
import { Editor } from '@tiptap/react';
|
||||||
|
import { nip19 } from 'nostr-tools';
|
||||||
|
|
||||||
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
|
import { MentionItem } from '@shared/composer';
|
||||||
|
import { MentionIcon } from '@shared/icons';
|
||||||
|
|
||||||
|
export function MentionPopup({ editor }: { editor: Editor }) {
|
||||||
|
const { db } = useStorage();
|
||||||
|
|
||||||
|
const insertMention = (pubkey: string) => {
|
||||||
|
editor.commands.insertContent(`nostr:${nip19.npubEncode(pubkey)}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover.Root>
|
||||||
|
<Popover.Trigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex h-10 w-10 items-center justify-center rounded-lg hover:bg-white/10 hover:backdrop-blur-xl"
|
||||||
|
>
|
||||||
|
<MentionIcon className="h-5 w-5 text-white/80" />
|
||||||
|
</button>
|
||||||
|
</Popover.Trigger>
|
||||||
|
<Popover.Content className="h-full max-h-[200px] w-[250px] overflow-hidden overflow-y-auto rounded-lg bg-white/10 backdrop-blur-xl focus:outline-none">
|
||||||
|
<div className="flex flex-col gap-1 py-1">
|
||||||
|
{db.account.follows.map((item) => (
|
||||||
|
<button key={item} type="button" onClick={() => insertMention(item)}>
|
||||||
|
<MentionItem pubkey={item} />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import { ReactRenderer } from '@tiptap/react';
|
|
||||||
import tippy from 'tippy.js';
|
|
||||||
|
|
||||||
import { MentionList } from '@shared/composer';
|
|
||||||
|
|
||||||
export const Suggestion = {
|
|
||||||
items: async ({ query }) => {
|
|
||||||
const users = [];
|
|
||||||
return users
|
|
||||||
.filter((item) => item.ident.toLowerCase().startsWith(query.toLowerCase()))
|
|
||||||
.slice(0, 5);
|
|
||||||
},
|
|
||||||
|
|
||||||
render: () => {
|
|
||||||
let component;
|
|
||||||
let popup;
|
|
||||||
|
|
||||||
return {
|
|
||||||
onStart: (props) => {
|
|
||||||
component = new ReactRenderer(MentionList, {
|
|
||||||
props,
|
|
||||||
editor: props.editor,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!props.clientRect) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
popup = tippy('body', {
|
|
||||||
getReferenceClientRect: props.clientRect,
|
|
||||||
appendTo: () => document.body,
|
|
||||||
content: component.element,
|
|
||||||
showOnCreate: true,
|
|
||||||
interactive: true,
|
|
||||||
trigger: 'manual',
|
|
||||||
placement: 'bottom-start',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onUpdate(props) {
|
|
||||||
component.updateProps(props);
|
|
||||||
|
|
||||||
if (!props.clientRect) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
popup[0].setProps({
|
|
||||||
getReferenceClientRect: props.clientRect,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onKeyDown(props) {
|
|
||||||
if (props.event.key === 'Escape') {
|
|
||||||
popup[0].hide();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return component.ref?.onKeyDown(props);
|
|
||||||
},
|
|
||||||
|
|
||||||
onExit() {
|
|
||||||
popup[0].destroy();
|
|
||||||
component.destroy();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -37,7 +37,7 @@ export function ComposerModal() {
|
|||||||
</button>
|
</button>
|
||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
<Dialog.Portal className="relative z-10">
|
<Dialog.Portal className="relative z-10">
|
||||||
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xl" />
|
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-2xl" />
|
||||||
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Image } from '@shared/image';
|
import { Image } from '@shared/image';
|
||||||
|
|
||||||
import { useProfile } from '@utils/hooks/useProfile';
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
|
import { displayNpub } from '@utils/shortenKey';
|
||||||
|
|
||||||
export function ComposerUser({ pubkey }: { pubkey: string }) {
|
export function ComposerUser({ pubkey }: { pubkey: string }) {
|
||||||
const { user } = useProfile(pubkey);
|
const { user } = useProfile(pubkey);
|
||||||
@@ -13,9 +14,7 @@ export function ComposerUser({ pubkey }: { pubkey: string }) {
|
|||||||
className="h-10 w-10 shrink-0 rounded-lg"
|
className="h-10 w-10 shrink-0 rounded-lg"
|
||||||
/>
|
/>
|
||||||
<h5 className="text-base font-semibold leading-none text-white">
|
<h5 className="text-base font-semibold leading-none text-white">
|
||||||
{user?.nip05 || user?.name || (
|
{user?.name || user?.display_name || displayNpub(pubkey, 16)}
|
||||||
<div className="h-3 w-20 animate-pulse rounded-sm bg-zinc-700" />
|
|
||||||
)}
|
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { HTMLProps, ReactNode, useCallback } from 'react';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
|
export function Frame({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
lighter,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
className: HTMLProps<HTMLElement>['className'];
|
||||||
|
lighter?: boolean;
|
||||||
|
}) {
|
||||||
|
const { db } = useStorage();
|
||||||
|
|
||||||
|
const platformStyles = useCallback(() => {
|
||||||
|
switch (db.platform) {
|
||||||
|
case 'darwin':
|
||||||
|
if (lighter) return 'bg-black/80';
|
||||||
|
return 'bg-black/90';
|
||||||
|
default:
|
||||||
|
return 'bg-black';
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <div className={twMerge(className, platformStyles())}>{children}</div>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { SVGProps } from 'react';
|
||||||
|
|
||||||
|
export function AlbyIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="400"
|
||||||
|
height="578"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 400 578"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="#000"
|
||||||
|
d="M201.283 577.511c54.122 0 97.998-8.1 97.998-18.092 0-9.992-43.876-18.092-97.998-18.092-54.123 0-97.998 8.1-97.998 18.092 0 9.992 43.875 18.092 97.998 18.092z"
|
||||||
|
opacity="0.1"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
fill="#fff"
|
||||||
|
stroke="#000"
|
||||||
|
strokeWidth="15.077"
|
||||||
|
d="M295.75 471.344c50.627 0 73.67-112.102 73.67-154.608 0-33.13-22.86-53.208-52.913-53.208-29.866 0-54.113 12.843-54.414 28.747-.001 41.971-7.388 179.069 33.657 179.069zM110.837 471.344c-50.627 0-73.67-112.102-73.67-154.608 0-33.13 22.86-53.208 52.913-53.208 29.866 0 54.113 12.843 54.414 28.747.001 41.971 7.388 179.069-33.657 179.069z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
fill="#FFDF6F"
|
||||||
|
stroke="#000"
|
||||||
|
strokeWidth="15"
|
||||||
|
d="M68.83 303.262v-.002c-.054-.519.052-.82.16-1.016.127-.232.368-.508.773-.738.84-.477 2.014-.563 3.108.076 37.603 22.042 80.976 34.678 128.13 34.678 47.163 0 91.339-12.881 129.184-35.307 1.087-.645 2.26-.565 3.102-.091.407.229.65.504.779.737.109.197.216.499.163 1.019-5.854 58.014-37.322 105.977-79.618 128.054-13.969 7.293-23.576 19.962-32.013 31.089l-.452.597-.002.002c-6.857 9.046-13.063 17.147-20.648 23.116-7.584-5.969-13.791-14.07-20.648-23.116l-.001-.002-.452-.597c-8.437-11.127-18.043-23.796-32.013-31.089-42.135-21.992-73.523-69.677-79.551-127.41z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
fill="#000"
|
||||||
|
stroke="#000"
|
||||||
|
strokeWidth="15.077"
|
||||||
|
d="M201.786 346.338c73.274 0 132.674-19.8 132.674-44.225s-59.4-44.225-132.674-44.225-132.674 19.8-132.674 44.225 59.4 44.225 132.674 44.225z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
stroke="#000"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeWidth="15.077"
|
||||||
|
d="M95.245 376.491s65.44 22.112 107.546 22.112c42.105 0 107.546-22.112 107.546-22.112"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
fill="#000"
|
||||||
|
d="M77 143c-16.569 0-30-13.431-30-30 0-16.569 13.431-30 30-30 16.569 0 30 13.431 30 30 0 16.569-13.431 30-30 30z"
|
||||||
|
></path>
|
||||||
|
<path stroke="#000" strokeWidth="15" d="M72 108.5l56 56"></path>
|
||||||
|
<path
|
||||||
|
fill="#000"
|
||||||
|
d="M322 143c16.569 0 30-13.431 30-30 0-16.569-13.431-30-30-30-16.569 0-30 13.431-30 30 0 16.569 13.431 30 30 30z"
|
||||||
|
></path>
|
||||||
|
<path stroke="#000" strokeWidth="15" d="M327.5 108.5l-56 56"></path>
|
||||||
|
<path
|
||||||
|
fill="#FFDF6F"
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M85.516 292.019c-16.17-7.698-25.58-24.983-22.427-42.612C76.618 173.747 133 117 200.5 117c67.663 0 124.155 57.023 137.509 132.958 3.106 17.66-6.381 34.937-22.605 42.572C280.687 308.868 241.91 318 201 318c-41.335 0-80.493-9.323-115.484-25.981z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
fill="#000"
|
||||||
|
d="M70.472 250.728C83.544 177.62 137.582 124.5 200.5 124.5v-15c-72.082 0-130.809 60.375-144.794 138.587l14.766 2.641zM200.5 124.5c63.069 0 117.218 53.379 130.122 126.757l14.774-2.598C331.592 170.166 272.758 109.5 200.5 109.5v15zm111.71 161.244C278.472 301.621 240.783 310.5 201 310.5v15c42.037 0 81.902-9.386 117.597-26.183l-6.387-13.573zM201 310.5c-40.196 0-78.255-9.064-112.26-25.253l-6.448 13.544C118.269 315.918 158.526 325.5 201 325.5v-15zm129.622-59.243c2.49 14.159-5.091 28.219-18.412 34.487l6.387 13.573c19.128-9.002 30.52-29.497 26.799-50.658l-14.774 2.598zm-274.916-3.17c-3.778 21.124 7.524 41.629 26.586 50.704l6.447-13.544c-13.276-6.32-20.795-20.387-18.267-34.519l-14.766-2.641z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
fill="#000"
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M114.365 273.209c-13.015-5.301-20.736-19.149-16.226-32.459C112.047 199.704 152.618 170 200.5 170c47.882 0 88.453 29.704 102.361 70.75 4.51 13.31-3.211 27.158-16.226 32.459C260.053 284.035 230.973 290 200.5 290c-30.473 0-59.553-5.965-86.135-16.791z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
fill="#fff"
|
||||||
|
d="M235 254c13.807 0 25-8.954 25-20s-11.193-20-25-20-25 8.954-25 20 11.193 20 25 20zM163.432 254.012c13.807 0 25-8.954 25-20s-11.193-20-25-20-25 8.954-25 20 11.193 20 25 20z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { SVGProps } from 'react';
|
||||||
|
|
||||||
|
export function ArticleIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
d="M16.25 12V4.75a1 1 0 00-1-1H3.75a1 1 0 00-1 1v13a2.5 2.5 0 002.5 2.5H18.5M16.25 12v5.75a2.5 2.5 0 005 0V13a1 1 0 00-1-1h-4zm-9.5 3.75h5.5m-5.5-8h5.5v4.5h-5.5v-4.5z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -13,10 +13,9 @@ export function FileIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement
|
|||||||
<path
|
<path
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="1.5"
|
strokeWidth="1.5"
|
||||||
d="M8.75 6.75h6.5m-6.5 4h6.5m-6.5 4h2.5m-5.5 6.5h12.5a1 1 0 001-1V3.75a1 1 0 00-1-1H5.75a1 1 0 00-1 1v16.5a1 1 0 001 1z"
|
d="M12.75 3.25v5a1 1 0 001 1h5m-10 4h3.5m-3.5 4h6.5m-9.5-14.5h6.586a1 1 0 01.707.293l5.914 5.914a1 1 0 01.293.707V20.25a1 1 0 01-1 1H5.75a1 1 0 01-1-1V3.75a1 1 0 011-1z"
|
||||||
/>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { SVGProps } from 'react';
|
||||||
|
|
||||||
|
export function FollowsIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
d="M17.75 19.25h3.673c.581 0 1.045-.496.947-1.07-.531-3.118-2.351-5.43-5.37-5.43-.446 0-.866.05-1.26.147M11.25 7a3.25 3.25 0 11-6.5 0 3.25 3.25 0 016.5 0zm8.5.5a2.75 2.75 0 11-5.5 0 2.75 2.75 0 015.5 0zM1.87 19.18c.568-3.68 2.647-6.43 6.13-6.43 3.482 0 5.561 2.75 6.13 6.43.088.575-.375 1.07-.956 1.07H2.825c-.58 0-1.043-.495-.955-1.07z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { SVGProps } from 'react';
|
||||||
|
|
||||||
|
export function GroupFeedsIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
d="M14.425 13.18c3.361-1.396 7.598.605 8.454 6.003.09.573-.372 1.067-.952 1.067H16.75M10.75 7a3.25 3.25 0 11-6.5 0 3.25 3.25 0 016.5 0zm9 0a3.25 3.25 0 11-6.5 0 3.25 3.25 0 016.5 0zm-6.966 13.25H2.072c-.58 0-1.042-.497-.951-1.07 1.362-8.573 11.252-8.573 12.614 0 .091.573-.371 1.07-.951 1.07z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ export function HashtagIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElem
|
|||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="1.5"
|
strokeWidth="1.5"
|
||||||
d="M8.75 3.75l-2 16.5m10.5-16.5l-2 16.5M3.75 7.75h16.5m0 8.5H3.75"
|
d="M8.75 3.75l-2 16.5m10.5-16.5l-2 16.5M3.75 7.75h16.5m0 8.5H3.75"
|
||||||
/>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,3 +56,11 @@ export * from './expand';
|
|||||||
export * from './focus';
|
export * from './focus';
|
||||||
export * from './chevronUp';
|
export * from './chevronUp';
|
||||||
export * from './secure';
|
export * from './secure';
|
||||||
|
export * from './verified';
|
||||||
|
export * from './mention';
|
||||||
|
export * from './groupFeeds';
|
||||||
|
export * from './article';
|
||||||
|
export * from './follows';
|
||||||
|
export * from './alby';
|
||||||
|
export * from './stars';
|
||||||
|
export * from './nwc';
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { SVGProps } from 'react';
|
||||||
|
|
||||||
|
export function MentionIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
d="M11.85 13.251c-3.719.065-6.427 2.567-7.18 5.915-.13.575.338 1.084.927 1.084h6.901m-.647-6.999l.147-.001c.353 0 .696.022 1.03.064m-1.177-.063a7.889 7.889 0 00-1.852.249m3.028-.186c.334.042.658.104.972.186m-.972-.186a7.475 7.475 0 011.972.524m3.25 1.412v3m0 0v3m0-3h-3m3 0h3m-5.5-11.75a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { SVGProps } from 'react';
|
||||||
|
|
||||||
|
export function NwcIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M3 6.5A3.5 3.5 0 016.5 3h8.969C16.314 3 17 3.686 17 4.531V8h2.25c.966 0 1.75.784 1.75 1.75v9.5A1.75 1.75 0 0119.25 21h-7a.75.75 0 010-1.5h7a.25.25 0 00.25-.25v-9.5a.25.25 0 00-.25-.25H6a2.986 2.986 0 01-1.5-.401v2.151a.75.75 0 01-1.5 0V6.5zm1.5 0A1.5 1.5 0 006 8h9.5V4.531a.031.031 0 00-.031-.031H6.5a2 2 0 00-2 2zm-1 9.285v2.93L6 20.173l2.5-1.458v-2.93L6 14.327l-2.5 1.458zm2.122-2.975a.75.75 0 01.756 0l3.25 1.896a.75.75 0 01.372.648v3.792a.75.75 0 01-.372.648l-3.25 1.895a.75.75 0 01-.756 0l-3.25-1.895A.75.75 0 012 19.146v-3.792a.75.75 0 01.372-.648l3.25-1.896z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
></path>
|
||||||
|
<path fill="currentColor" d="M15.5 15.5a1 1 0 100-2 1 1 0 000 2z"></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { SVGProps } from 'react';
|
||||||
|
|
||||||
|
export function StarsIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="30"
|
||||||
|
height="30"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 30 30"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M13.379 11.638a.999.999 0 00.759-.759l.886-3.986c.232-1.044 1.72-1.044 1.952 0l.886 3.986c.084.38.38.675.759.76l3.986.885c1.044.232 1.044 1.72 0 1.952l-3.986.886a.999.999 0 00-.759.76l-.886 3.985c-.232 1.044-1.72 1.044-1.952 0l-.886-3.986a.999.999 0 00-.759-.759l-3.986-.886c-1.044-.232-1.044-1.72 0-1.952l3.986-.886zM8.06 19.82a.999.999 0 00.76-.759l.27-1.22c.098-.438.722-.438.819 0l.271 1.22c.084.379.38.675.759.759l1.22.271c.438.097.438.721 0 .818l-1.22.271a.999.999 0 00-.759.759l-.271 1.22c-.097.438-.721.438-.818 0l-.271-1.22a.999.999 0 00-.76-.759l-1.22-.271c-.437-.097-.437-.721.001-.818l1.22-.271z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { SVGProps } from 'react';
|
||||||
|
|
||||||
|
export function VerifiedIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm3.58 7.975a.75.75 0 00-1.16-.95l-3.976 4.859L9.03 12.47a.75.75 0 00-1.06 1.06l2 2a.75.75 0 001.11-.055l4.5-5.5z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Outlet, ScrollRestoration } from 'react-router-dom';
|
import { Outlet, ScrollRestoration } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Frame } from '@shared/frame';
|
||||||
import { Navigation } from '@shared/navigation';
|
import { Navigation } from '@shared/navigation';
|
||||||
|
|
||||||
export function AppLayout() {
|
export function AppLayout() {
|
||||||
@@ -8,14 +9,14 @@ export function AppLayout() {
|
|||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
</div>
|
</div>
|
||||||
<div className="h-full w-full flex-1 bg-black/90 backdrop-blur-xl">
|
<Frame className="h-full w-full flex-1">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<ScrollRestoration
|
<ScrollRestoration
|
||||||
getKey={(location) => {
|
getKey={(location) => {
|
||||||
return location.pathname;
|
return location.pathname;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Frame>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Frame } from '@shared/frame';
|
||||||
|
|
||||||
export function AuthLayout() {
|
export function AuthLayout() {
|
||||||
return (
|
return (
|
||||||
<div className="relative h-screen w-screen bg-black/90 backdrop-blur-xl">
|
<Frame className="relative h-screen w-screen">
|
||||||
<div className="absolute left-0 top-0 z-50 h-16 w-full" data-tauri-drag-region />
|
<div className="absolute left-0 top-0 z-50 h-16 w-full" data-tauri-drag-region />
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</Frame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Frame } from '@shared/frame';
|
||||||
|
|
||||||
export function NoteLayout() {
|
export function NoteLayout() {
|
||||||
return (
|
return (
|
||||||
<div className="relative h-screen w-screen bg-black/90 backdrop-blur-xl">
|
<Frame className="relative h-screen w-screen">
|
||||||
<div className="absolute left-0 top-0 z-50 h-16 w-full" data-tauri-drag-region />
|
<div className="absolute left-0 top-0 z-50 h-16 w-full" data-tauri-drag-region />
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</Frame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { Link, NavLink, Outlet, ScrollRestoration } from 'react-router-dom';
|
import { Link, NavLink, Outlet, ScrollRestoration } from 'react-router-dom';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
import { Frame } from '@shared/frame';
|
||||||
import { ArrowLeftIcon, SecureIcon, SettingsIcon } from '@shared/icons';
|
import { ArrowLeftIcon, SecureIcon, SettingsIcon } from '@shared/icons';
|
||||||
|
|
||||||
export function SettingsLayout() {
|
export function SettingsLayout() {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-screen">
|
<div className="flex h-screen w-screen">
|
||||||
<div className="relative flex h-full w-[232px] flex-col bg-black/80">
|
<Frame className="relative flex h-full w-[232px] flex-col" lighter>
|
||||||
<div data-tauri-drag-region className="h-11 w-full shrink-0" />
|
<div data-tauri-drag-region className="h-11 w-full shrink-0" />
|
||||||
<div className="scrollbar-hide flex h-full flex-1 flex-col gap-2 overflow-y-auto pb-32">
|
<div className="scrollbar-hide flex h-full flex-1 flex-col gap-2 overflow-y-auto pb-32">
|
||||||
<div className="inline-flex items-center gap-2 border-l-2 border-transparent pl-4">
|
<div className="inline-flex items-center gap-2 border-l-2 border-transparent pl-4">
|
||||||
@@ -55,7 +56,7 @@ export function SettingsLayout() {
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Frame>
|
||||||
<div className="h-full w-full flex-1 bg-black/90 backdrop-blur-xl">
|
<div className="h-full w-full flex-1 bg-black/90 backdrop-blur-xl">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<ScrollRestoration
|
<ScrollRestoration
|
||||||
|
|||||||
+46
-59
@@ -1,78 +1,65 @@
|
|||||||
import * as Dialog from '@radix-ui/react-dialog';
|
import * as AlertDialog from '@radix-ui/react-alert-dialog';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { relaunch } from '@tauri-apps/plugin-process';
|
|
||||||
|
|
||||||
import { CancelIcon, LogoutIcon } from '@shared/icons';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
|
import { useStronghold } from '@stores/stronghold';
|
||||||
|
|
||||||
export function Logout() {
|
export function Logout() {
|
||||||
const queryClient = useQueryClient();
|
const { db } = useStorage();
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const clearPrivkey = useStronghold((state) => state.clearPrivkey);
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
// reset database
|
// remove account
|
||||||
// await removeAll();
|
db.accountLogout();
|
||||||
// reset react query
|
// clear privkey in session storage
|
||||||
queryClient.clear();
|
clearPrivkey();
|
||||||
// navigate
|
// redirect to welcome screen
|
||||||
await relaunch();
|
navigate('/auth/welcome');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Root>
|
<AlertDialog.Root>
|
||||||
<Dialog.Trigger asChild>
|
<AlertDialog.Trigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Logout"
|
className="inline-flex h-10 items-center rounded-lg px-2 text-sm font-medium text-white hover:bg-white/10"
|
||||||
className="inline-flex h-9 w-9 transform items-center justify-center rounded-md bg-white/20 active:translate-y-1"
|
|
||||||
>
|
>
|
||||||
<LogoutIcon className="h-4 w-4 text-white" />
|
Logout
|
||||||
</button>
|
</button>
|
||||||
</Dialog.Trigger>
|
</AlertDialog.Trigger>
|
||||||
<Dialog.Portal className="relative z-10">
|
<AlertDialog.Portal className="relative z-10">
|
||||||
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xl" />
|
<AlertDialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-2xl" />
|
||||||
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
<AlertDialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
||||||
<div className="relative h-min w-full max-w-xl rounded-xl bg-white/10 backdrop-blur-xl">
|
<div className="relative h-min w-full max-w-md rounded-xl bg-white/10 backdrop-blur-xl">
|
||||||
<div className="h-min w-full shrink-0 border-b border-white/10 bg-white/5 px-5 py-6">
|
<div className="flex flex-col gap-2 border-b border-white/5 px-5 py-4">
|
||||||
<div className="flex flex-col gap-2">
|
<AlertDialog.Title className="text-lg font-semibold leading-none text-white">
|
||||||
<div className="flex items-center justify-between">
|
Are you sure!
|
||||||
<Dialog.Title className="text-lg font-semibold leading-none text-white">
|
</AlertDialog.Title>
|
||||||
Are you sure!
|
<AlertDialog.Description className="text-sm leading-tight text-white/50">
|
||||||
</Dialog.Title>
|
You can always log back in at any time. If you just want to switch
|
||||||
<Dialog.Close className="inline-flex h-6 w-6 items-center justify-center rounded-md backdrop-blur-xl hover:bg-white/10">
|
accounts, you can do that by adding an existing account.
|
||||||
<CancelIcon className="h-4 w-4 text-white/50" />
|
</AlertDialog.Description>
|
||||||
</Dialog.Close>
|
|
||||||
</div>
|
|
||||||
<Dialog.Description className="text-sm leading-tight text-white/50">
|
|
||||||
<p className="mb-2">
|
|
||||||
When logout, all local data will be wiped, and restart app then you
|
|
||||||
need to start onboarding process again when you log in.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
In the next version, Lume will support multi account, then you can
|
|
||||||
switch between all account s instead of logout
|
|
||||||
</p>
|
|
||||||
</Dialog.Description>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-full w-full flex-col items-end justify-center overflow-y-auto px-5 py-2.5">
|
<div className="flex justify-end gap-2 px-5 py-3">
|
||||||
<div className="flex items-center gap-2">
|
<AlertDialog.Cancel asChild>
|
||||||
<button
|
<button className="inline-flex h-9 items-center justify-center rounded-md px-4 text-sm font-medium leading-none text-white outline-none hover:bg-white/10 hover:backdrop-blur-xl">
|
||||||
type="button"
|
|
||||||
className="inline-flex h-9 items-center justify-center rounded-md px-3 text-sm font-medium text-white/50 backdrop-blur-xl hover:bg-white/10"
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
</AlertDialog.Cancel>
|
||||||
type="button"
|
<button
|
||||||
onClick={() => logout()}
|
type="button"
|
||||||
className="inline-flex h-9 items-center justify-center rounded-md bg-red-500 px-3 text-sm font-medium text-white hover:bg-red-600"
|
onClick={() => logout()}
|
||||||
>
|
className="inline-flex h-9 items-center justify-center rounded-md bg-white/10 px-4 text-sm font-medium leading-none text-white outline-none hover:bg-fuchsia-500"
|
||||||
Confirm
|
>
|
||||||
</button>
|
Logout
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Content>
|
</AlertDialog.Content>
|
||||||
</Dialog.Portal>
|
</AlertDialog.Portal>
|
||||||
</Dialog.Root>
|
</AlertDialog.Root>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+76
-12
@@ -4,18 +4,33 @@ import { twMerge } from 'tailwind-merge';
|
|||||||
|
|
||||||
import { ChatsList } from '@app/chats/components/list';
|
import { ChatsList } from '@app/chats/components/list';
|
||||||
|
|
||||||
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { ActiveAccount } from '@shared/accounts/active';
|
import { ActiveAccount } from '@shared/accounts/active';
|
||||||
import { ComposerModal } from '@shared/composer';
|
import { ComposerModal } from '@shared/composer';
|
||||||
import { BellIcon, NavArrowDownIcon, SpaceIcon } from '@shared/icons';
|
import { Frame } from '@shared/frame';
|
||||||
|
import { BellIcon, NavArrowDownIcon, NwcIcon, SpaceIcon } from '@shared/icons';
|
||||||
|
|
||||||
|
import { useActivities } from '@stores/activities';
|
||||||
import { useSidebar } from '@stores/sidebar';
|
import { useSidebar } from '@stores/sidebar';
|
||||||
|
|
||||||
|
import { compactNumber } from '@utils/number';
|
||||||
|
|
||||||
export function Navigation() {
|
export function Navigation() {
|
||||||
|
const { db } = useStorage();
|
||||||
|
|
||||||
|
const [totalNewActivities] = useActivities((state) => [state.totalNewActivities]);
|
||||||
const [chats, toggleChats] = useSidebar((state) => [state.chats, state.toggleChats]);
|
const [chats, toggleChats] = useSidebar((state) => [state.chats, state.toggleChats]);
|
||||||
|
const [integrations, toggleIntegrations] = useSidebar((state) => [
|
||||||
|
state.integrations,
|
||||||
|
state.toggleIntegrations,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-full w-[232px] flex-col bg-black/80">
|
<Frame className="relative flex h-full w-[232px] flex-col" lighter>
|
||||||
<div data-tauri-drag-region className="h-11 w-full shrink-0" />
|
{db.platform === 'darwin' ? (
|
||||||
|
<div data-tauri-drag-region className="h-11 w-full shrink-0" />
|
||||||
|
) : null}
|
||||||
<div className="scrollbar-hide flex h-full flex-1 flex-col gap-6 overflow-y-auto pb-32">
|
<div className="scrollbar-hide flex h-full flex-1 flex-col gap-6 overflow-y-auto pb-32">
|
||||||
<div className="flex flex-col pr-2">
|
<div className="flex flex-col pr-2">
|
||||||
<ComposerModal />
|
<ComposerModal />
|
||||||
@@ -27,7 +42,7 @@ export function Navigation() {
|
|||||||
'flex h-10 items-center gap-2.5 rounded-r-lg border-l-2 pl-4 pr-2',
|
'flex h-10 items-center gap-2.5 rounded-r-lg border-l-2 pl-4 pr-2',
|
||||||
isActive
|
isActive
|
||||||
? 'border-fuchsia-500 bg-white/5 text-white'
|
? 'border-fuchsia-500 bg-white/5 text-white'
|
||||||
: 'border-transparent text-white/80'
|
: 'border-transparent text-white/70'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -41,19 +56,68 @@ export function Navigation() {
|
|||||||
preventScrollReset={true}
|
preventScrollReset={true}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
twMerge(
|
twMerge(
|
||||||
'flex h-10 items-center gap-2.5 rounded-r-lg border-l-2 pl-4 pr-2',
|
'flex h-10 items-center justify-between rounded-r-lg border-l-2 pl-4 pr-2',
|
||||||
isActive
|
isActive
|
||||||
? 'border-fuchsia-500 bg-white/5 text-white'
|
? 'border-fuchsia-500 bg-white/5 text-white'
|
||||||
: 'border-transparent text-white/80'
|
: 'border-transparent text-white/70'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded bg-white/10 backdrop-blur-xl">
|
<div className="flex items-center gap-2.5">
|
||||||
<BellIcon className="h-4 w-4 text-white" />
|
{totalNewActivities > 0 ? (
|
||||||
</span>
|
<div className="relative inline-flex h-7 w-7 shrink-0 items-center justify-center rounded bg-fuchsia-500/20 backdrop-blur-xl">
|
||||||
Notifications
|
<p className="text-sm font-bold text-fuchsia-500">
|
||||||
|
{compactNumber.format(totalNewActivities)}
|
||||||
|
</p>
|
||||||
|
<span className="absolute right-0 top-0 block h-1 w-1 -translate-y-1/2 translate-x-1/2 transform rounded-full bg-fuchsia-500 ring-2 ring-black/80" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded bg-white/10 backdrop-blur-xl">
|
||||||
|
<BellIcon className="h-4 w-4 text-white" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
Notifications
|
||||||
|
</div>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
|
<Collapsible.Root open={integrations} onOpenChange={toggleIntegrations}>
|
||||||
|
<div className="flex flex-col gap-1 pr-2">
|
||||||
|
<Collapsible.Trigger asChild>
|
||||||
|
<button className="flex items-center gap-1 pl-[20px] pr-4">
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
'inline-flex h-5 w-5 transform items-center justify-center transition-transform duration-150 ease-in-out',
|
||||||
|
integrations ? '' : 'rotate-180'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<NavArrowDownIcon className="h-3 w-3 text-white/50" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-[11px] font-bold uppercase tracking-widest text-white/50">
|
||||||
|
Integrations
|
||||||
|
</h3>
|
||||||
|
</button>
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
<Collapsible.Content>
|
||||||
|
<NavLink
|
||||||
|
to="/nwc"
|
||||||
|
preventScrollReset={true}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
twMerge(
|
||||||
|
'flex h-10 items-center gap-2.5 rounded-r-lg border-l-2 pl-4 pr-2',
|
||||||
|
isActive
|
||||||
|
? 'border-fuchsia-500 bg-white/5 text-white'
|
||||||
|
: 'border-transparent text-white/70'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded bg-white/10 backdrop-blur-xl">
|
||||||
|
<NwcIcon className="h-4 w-4 text-white" />
|
||||||
|
</span>
|
||||||
|
Wallet Connect
|
||||||
|
</NavLink>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Root>
|
||||||
<Collapsible.Root open={chats} onOpenChange={toggleChats}>
|
<Collapsible.Root open={chats} onOpenChange={toggleChats}>
|
||||||
<div className="flex flex-col gap-1 pr-2">
|
<div className="flex flex-col gap-1 pr-2">
|
||||||
<Collapsible.Trigger asChild>
|
<Collapsible.Trigger asChild>
|
||||||
@@ -77,9 +141,9 @@ export function Navigation() {
|
|||||||
</div>
|
</div>
|
||||||
</Collapsible.Root>
|
</Collapsible.Root>
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink-0">
|
<div className="relative shrink-0">
|
||||||
<ActiveAccount />
|
<ActiveAccount />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Frame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { fetch } from '@tauri-apps/api/http';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
import { UnverifiedIcon, VerifiedIcon } from '@shared/icons';
|
||||||
|
|
||||||
|
interface NIP05 {
|
||||||
|
names: {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NIP05 = memo(function NIP05({
|
||||||
|
pubkey,
|
||||||
|
nip05,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
pubkey: string;
|
||||||
|
nip05: string;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const { status, data } = useQuery(
|
||||||
|
['nip05', nip05],
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const localPath = nip05.split('@')[0];
|
||||||
|
const service = nip05.split('@')[1];
|
||||||
|
const verifyURL = `https://${service}/.well-known/nostr.json?name=${localPath}`;
|
||||||
|
|
||||||
|
const res = await fetch(verifyURL, {
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 10,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) throw new Error(`Failed to fetch NIP-05 service: ${nip05}`);
|
||||||
|
|
||||||
|
const data = res.data as NIP05;
|
||||||
|
if (data.names) {
|
||||||
|
if (data.names[localPath] !== pubkey) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`Failed to verify NIP-05, error: ${e}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
refetchOnMount: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
staleTime: Infinity,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (status === 'loading') {
|
||||||
|
<div className="h-3 w-20 animate-pulse rounded bg-white/10" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={twMerge('leadning-none inline-flex items-center gap-1', className)}>
|
||||||
|
<p>{nip05}</p>
|
||||||
|
<div className="shrink-0">
|
||||||
|
{data === true ? (
|
||||||
|
<VerifiedIcon className="h-3 w-3 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<UnverifiedIcon className="h-3 w-3 text-red-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -28,12 +28,12 @@ export function NoteActions({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip.Provider>
|
<Tooltip.Provider>
|
||||||
<div className="-ml-1 mt-4 inline-flex w-full items-center">
|
<div className="-ml-1 mt-2 inline-flex w-full items-center">
|
||||||
<div className="inline-flex items-center gap-2">
|
<div className="inline-flex items-center gap-2">
|
||||||
<NoteReply id={id} pubkey={pubkey} root={root} />
|
<NoteReply id={id} pubkey={pubkey} root={root} />
|
||||||
<NoteReaction id={id} pubkey={pubkey} />
|
<NoteReaction id={id} pubkey={pubkey} />
|
||||||
<NoteRepost id={id} pubkey={pubkey} />
|
<NoteRepost id={id} pubkey={pubkey} />
|
||||||
<NoteZap id={id} />
|
<NoteZap id={id} pubkey={pubkey} />
|
||||||
</div>
|
</div>
|
||||||
{extraButtons && (
|
{extraButtons && (
|
||||||
<>
|
<>
|
||||||
@@ -50,7 +50,7 @@ export function NoteActions({
|
|||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
<Tooltip.Portal>
|
<Tooltip.Portal>
|
||||||
<Tooltip.Content className="-left-10 select-none rounded-md bg-black px-3.5 py-1.5 text-sm leading-none text-white will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
|
<Tooltip.Content className="-left-10 select-none rounded-md bg-black px-3.5 py-1.5 text-sm leading-none text-white will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
|
||||||
Open thread
|
Focus
|
||||||
<Tooltip.Arrow className="fill-black" />
|
<Tooltip.Arrow className="fill-black" />
|
||||||
</Tooltip.Content>
|
</Tooltip.Content>
|
||||||
</Tooltip.Portal>
|
</Tooltip.Portal>
|
||||||
@@ -61,7 +61,7 @@ export function NoteActions({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setWidget(db, {
|
setWidget(db, {
|
||||||
kind: WidgetKinds.thread,
|
kind: WidgetKinds.local.thread,
|
||||||
title: 'Thread',
|
title: 'Thread',
|
||||||
content: id,
|
content: id,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
import { writeText } from '@tauri-apps/api/clipboard';
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
import { EventPointer } from 'nostr-tools/lib/nip19';
|
import { EventPointer } from 'nostr-tools/lib/nip19';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@@ -18,8 +18,7 @@ export function MoreActions({ id, pubkey }: { id: string; pubkey: string }) {
|
|||||||
|
|
||||||
const copyLink = async () => {
|
const copyLink = async () => {
|
||||||
await writeText(
|
await writeText(
|
||||||
'https://nostr.com/' +
|
'https://njump.me/' + nip19.neventEncode({ id: id, author: pubkey } as EventPointer)
|
||||||
nip19.neventEncode({ id: id, author: pubkey } as EventPointer)
|
|
||||||
);
|
);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
};
|
};
|
||||||
@@ -45,7 +44,7 @@ export function MoreActions({ id, pubkey }: { id: string; pubkey: string }) {
|
|||||||
</Tooltip.Portal>
|
</Tooltip.Portal>
|
||||||
</Tooltip.Root>
|
</Tooltip.Root>
|
||||||
<DropdownMenu.Portal>
|
<DropdownMenu.Portal>
|
||||||
<DropdownMenu.Content className="flex w-[200px] flex-col overflow-hidden rounded-xl bg-white/10 p-2 backdrop-blur-3xl focus:outline-none">
|
<DropdownMenu.Content className="flex w-[200px] flex-col overflow-hidden rounded-xl border border-white/10 bg-white/20 p-2 backdrop-blur-3xl focus:outline-none">
|
||||||
<DropdownMenu.Item asChild>
|
<DropdownMenu.Item asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { NDKKind } from '@nostr-dev-kit/ndk';
|
||||||
import * as Popover from '@radix-ui/react-popover';
|
import * as Popover from '@radix-ui/react-popover';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ export function NoteReaction({ id, pubkey }: { id: string; pubkey: string }) {
|
|||||||
|
|
||||||
const event = await publish({
|
const event = await publish({
|
||||||
content: content,
|
content: content,
|
||||||
kind: 7,
|
kind: NDKKind.Reaction,
|
||||||
tags: [
|
tags: [
|
||||||
['e', id],
|
['e', id],
|
||||||
['p', pubkey],
|
['p', pubkey],
|
||||||
|
|||||||
@@ -1,25 +1,39 @@
|
|||||||
|
import { NDKKind } from '@nostr-dev-kit/ndk';
|
||||||
import * as AlertDialog from '@radix-ui/react-alert-dialog';
|
import * as AlertDialog from '@radix-ui/react-alert-dialog';
|
||||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||||
|
import { message } from '@tauri-apps/api/dialog';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
import { RepostIcon } from '@shared/icons';
|
import { useNDK } from '@libs/ndk/provider';
|
||||||
|
|
||||||
|
import { LoaderIcon, RepostIcon } from '@shared/icons';
|
||||||
|
|
||||||
import { useNostr } from '@utils/hooks/useNostr';
|
import { useNostr } from '@utils/hooks/useNostr';
|
||||||
|
import { sendNativeNotification } from '@utils/notification';
|
||||||
|
|
||||||
export function NoteRepost({ id, pubkey }: { id: string; pubkey: string }) {
|
export function NoteRepost({ id, pubkey }: { id: string; pubkey: string }) {
|
||||||
const { publish } = useNostr();
|
const { publish } = useNostr();
|
||||||
|
const { relayUrls } = useNDK();
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isRepost, setIsRepost] = useState(false);
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
const tags = [
|
const tags = [
|
||||||
['e', id, 'wss://relayable.org', 'root'],
|
['e', id, relayUrls[0], 'root'],
|
||||||
['p', pubkey],
|
['p', pubkey],
|
||||||
];
|
];
|
||||||
const event = await publish({ content: '', kind: 6, tags: tags });
|
const event = await publish({ content: '', kind: NDKKind.Repost, tags: tags });
|
||||||
if (event) {
|
if (event) {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
setIsRepost(true);
|
||||||
|
await sendNativeNotification('Reposted successfully', 'Lume');
|
||||||
} else {
|
} else {
|
||||||
console.log('failed reposting');
|
setIsLoading(false);
|
||||||
|
await message('Repost failed, try again later', { title: 'Lume', type: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,7 +46,12 @@ export function NoteRepost({ id, pubkey }: { id: string; pubkey: string }) {
|
|||||||
type="button"
|
type="button"
|
||||||
className="group inline-flex h-7 w-7 items-center justify-center"
|
className="group inline-flex h-7 w-7 items-center justify-center"
|
||||||
>
|
>
|
||||||
<RepostIcon className="h-5 w-5 text-white group-hover:text-blue-400" />
|
<RepostIcon
|
||||||
|
className={twMerge(
|
||||||
|
'h-5 w-5 group-hover:text-blue-400',
|
||||||
|
isRepost ? 'text-blue-400' : 'text-white'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</AlertDialog.Trigger>
|
</AlertDialog.Trigger>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
@@ -44,30 +63,34 @@ export function NoteRepost({ id, pubkey }: { id: string; pubkey: string }) {
|
|||||||
</Tooltip.Portal>
|
</Tooltip.Portal>
|
||||||
</Tooltip.Root>
|
</Tooltip.Root>
|
||||||
<AlertDialog.Portal className="relative z-10">
|
<AlertDialog.Portal className="relative z-10">
|
||||||
<AlertDialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xl" />
|
<AlertDialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-2xl" />
|
||||||
<AlertDialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
<AlertDialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
||||||
<div className="relative h-min w-full max-w-xl rounded-xl bg-white/10 backdrop-blur-xl">
|
<div className="relative h-min w-full max-w-md rounded-xl bg-white/10 backdrop-blur-xl">
|
||||||
<div className="flex flex-col gap-2 border-b border-white/5 px-5 py-4">
|
<div className="flex flex-col gap-2 border-b border-white/5 px-5 py-4">
|
||||||
<AlertDialog.Title className="text-lg font-semibold leading-none text-white">
|
<AlertDialog.Title className="text-lg font-semibold leading-none text-white">
|
||||||
Confirm repost this post?
|
Confirm repost this post?
|
||||||
</AlertDialog.Title>
|
</AlertDialog.Title>
|
||||||
<AlertDialog.Description className="text-sm leading-none text-white/50">
|
<AlertDialog.Description className="text-sm leading-tight text-white/50">
|
||||||
Reposted post will be visible to your followers, and you cannot undo this
|
Reposted post will be visible to your followers, and you cannot undo this
|
||||||
action.
|
action.
|
||||||
</AlertDialog.Description>
|
</AlertDialog.Description>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end gap-6 px-5 py-3">
|
<div className="flex justify-end gap-2 px-3 py-3">
|
||||||
<AlertDialog.Cancel asChild>
|
<AlertDialog.Cancel asChild>
|
||||||
<button className="inline-flex h-11 items-center justify-center rounded-lg bg-white/10 px-4 font-medium leading-none text-white outline-none backdrop-blur-xl">
|
<button className="inline-flex h-9 w-20 items-center justify-center rounded-md text-sm font-medium leading-none text-white outline-none hover:bg-white/10 hover:backdrop-blur-xl">
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</AlertDialog.Cancel>
|
</AlertDialog.Cancel>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => submit()}
|
onClick={() => submit()}
|
||||||
className="inline-flex h-11 items-center justify-center rounded-lg bg-fuchsia-500 px-4 font-medium leading-none text-white outline-none"
|
className="inline-flex h-9 w-28 items-center justify-center rounded-md bg-white/10 text-sm font-medium leading-none text-white outline-none hover:bg-fuchsia-500"
|
||||||
>
|
>
|
||||||
Yes, repost
|
{isLoading ? (
|
||||||
|
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
|
||||||
|
) : (
|
||||||
|
'Yes, repost'
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+179
-107
@@ -1,34 +1,96 @@
|
|||||||
import { NostrEvent } from '@nostr-dev-kit/ndk';
|
import { webln } from '@getalby/sdk';
|
||||||
|
import { SendPaymentResponse } from '@getalby/sdk/dist/types';
|
||||||
import * as Dialog from '@radix-ui/react-dialog';
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
|
import { message } from '@tauri-apps/api/dialog';
|
||||||
import { QRCodeSVG } from 'qrcode.react';
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
import { useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import CurrencyInput from 'react-currency-input-field';
|
||||||
|
import TextareaAutosize from 'react-textarea-autosize';
|
||||||
|
|
||||||
import { CancelIcon, ZapIcon } from '@shared/icons';
|
import { CancelIcon, ZapIcon } from '@shared/icons';
|
||||||
|
|
||||||
|
import { useStronghold } from '@stores/stronghold';
|
||||||
|
|
||||||
import { useEvent } from '@utils/hooks/useEvent';
|
import { useEvent } from '@utils/hooks/useEvent';
|
||||||
import { useNostr } from '@utils/hooks/useNostr';
|
import { useNostr } from '@utils/hooks/useNostr';
|
||||||
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
|
import { sendNativeNotification } from '@utils/notification';
|
||||||
|
import { compactNumber } from '@utils/number';
|
||||||
|
|
||||||
export function NoteZap({ id }: { id: string }) {
|
export function NoteZap({ id, pubkey }: { id: string; pubkey: string }) {
|
||||||
const { createZap } = useNostr();
|
const { createZap } = useNostr();
|
||||||
|
const { user } = useProfile(pubkey);
|
||||||
const { data: event } = useEvent(id);
|
const { data: event } = useEvent(id);
|
||||||
|
|
||||||
const [amount, setAmount] = useState<null | number>(null);
|
const [amount, setAmount] = useState<string>('21');
|
||||||
|
const [zapMessage, setZapMessage] = useState<string>('');
|
||||||
const [invoice, setInvoice] = useState<null | string>(null);
|
const [invoice, setInvoice] = useState<null | string>(null);
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [isCompleted, setIsCompleted] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const selected = (num: number) => {
|
const walletConnectURL = useStronghold((state) => state.walletConnectURL);
|
||||||
if (amount === num) return true;
|
const nwc = useRef(null);
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const createZapRequest = async () => {
|
const createZapRequest = async () => {
|
||||||
// @ts-expect-error, todo: fix this
|
try {
|
||||||
const res = await createZap(event as unknown as NostrEvent, amount);
|
const zapAmount = parseInt(amount) * 1000;
|
||||||
if (res) setInvoice(res);
|
const res = await createZap(event, zapAmount, zapMessage);
|
||||||
|
|
||||||
|
if (!res)
|
||||||
|
return await message('Cannot create zap request', {
|
||||||
|
title: 'Zap',
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
|
||||||
|
// user don't connect nwc, create QR Code for invoice
|
||||||
|
if (!walletConnectURL) return setInvoice(res);
|
||||||
|
|
||||||
|
// user connect nwc
|
||||||
|
nwc.current = new webln.NostrWebLNProvider({
|
||||||
|
nostrWalletConnectUrl: walletConnectURL,
|
||||||
|
});
|
||||||
|
await nwc.current.enable();
|
||||||
|
|
||||||
|
// start loading
|
||||||
|
setIsLoading(true);
|
||||||
|
// send payment via nwc
|
||||||
|
const send: SendPaymentResponse = await nwc.current.sendPayment(res);
|
||||||
|
|
||||||
|
if (send) {
|
||||||
|
await sendNativeNotification(
|
||||||
|
`You've tipped ${compactNumber.format(send.amount)} sats to ${
|
||||||
|
user?.display_name || user?.name
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// eose
|
||||||
|
nwc.current.close();
|
||||||
|
setIsCompleted(true);
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
// reset after 3 secs
|
||||||
|
const timeout = setTimeout(() => setIsCompleted(false), 3000);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
nwc.current.close();
|
||||||
|
setIsLoading(false);
|
||||||
|
await message(JSON.stringify(e), { title: 'Zap', type: 'error' });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
setAmount('21');
|
||||||
|
setZapMessage('');
|
||||||
|
setIsCompleted(false);
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Root>
|
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<Dialog.Trigger asChild>
|
<Dialog.Trigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -38,113 +100,123 @@ export function NoteZap({ id }: { id: string }) {
|
|||||||
</button>
|
</button>
|
||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
<Dialog.Portal className="relative z-10">
|
<Dialog.Portal className="relative z-10">
|
||||||
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xl" />
|
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-2xl" />
|
||||||
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
||||||
<div className="relative h-min w-full max-w-xl rounded-xl bg-white/10 backdrop-blur-xl">
|
<div className="relative h-min w-full max-w-xl rounded-xl bg-white/10 backdrop-blur-xl">
|
||||||
<div className="relative h-min w-full shrink-0 border-b border-white/5 bg-white/5 px-5 py-5">
|
<div className="inline-flex w-full shrink-0 items-center justify-between px-5 py-3">
|
||||||
<div className="flex flex-col items-center gap-1.5">
|
<div className="w-6" />
|
||||||
<Dialog.Title className="font-medium leading-none text-white">
|
<Dialog.Title className="text-center text-sm font-semibold leading-none text-white">
|
||||||
Zap (Beta)
|
Send tip to {user?.display_name || user?.name}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<Dialog.Description className="text-sm leading-none text-white/50">
|
<Dialog.Close className="inline-flex h-6 w-6 items-center justify-center rounded-md backdrop-blur-xl hover:bg-white/10">
|
||||||
Send tip with Bitcoin via Lightning
|
|
||||||
</Dialog.Description>
|
|
||||||
</div>
|
|
||||||
<Dialog.Close className="absolute right-3 top-3 inline-flex h-6 w-6 items-center justify-center rounded-md backdrop-blur-xl hover:bg-white/10">
|
|
||||||
<CancelIcon className="h-4 w-4 text-white/50" />
|
<CancelIcon className="h-4 w-4 text-white/50" />
|
||||||
</Dialog.Close>
|
</Dialog.Close>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-y-auto overflow-x-hidden px-5 py-5">
|
<div className="overflow-y-auto overflow-x-hidden px-5 pb-5">
|
||||||
{!invoice ? (
|
{!invoice ? (
|
||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-3 gap-3">
|
<div className="relative flex h-40 flex-col">
|
||||||
<button
|
<div className="inline-flex h-full flex-1 items-center justify-center gap-1">
|
||||||
type="button"
|
<CurrencyInput
|
||||||
onClick={() => setAmount(21000)}
|
placeholder="0"
|
||||||
className={twMerge(
|
defaultValue={'21'}
|
||||||
'inline-flex flex-col items-center justify-center gap-2 rounded-lg px-2 py-2 backdrop-blur-xl hover:bg-white/10',
|
value={amount}
|
||||||
`${selected(21000) && 'bg-white/10 backdrop-blur-xl'}`
|
decimalsLimit={2}
|
||||||
)}
|
min={0} // 0 sats
|
||||||
>
|
max={10000} // 1M sats
|
||||||
<img className="h-12 w-12" src="/zap.png" alt="High Voltage" />
|
maxLength={10000} // 1M sats
|
||||||
<span className="text-sm font-medium leading-none text-white/80">
|
onValueChange={(value) => setAmount(value)}
|
||||||
21 sats
|
className="w-full flex-1 bg-transparent text-right text-4xl font-semibold text-white placeholder:text-white/50 focus:outline-none"
|
||||||
|
/>
|
||||||
|
<span className="w-full flex-1 text-left text-4xl font-semibold text-white/50">
|
||||||
|
sats
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</div>
|
||||||
<button
|
<div className="inline-flex items-center justify-center gap-2">
|
||||||
type="button"
|
<button
|
||||||
onClick={() => setAmount(69000)}
|
type="button"
|
||||||
className={twMerge(
|
onClick={() => setAmount('69')}
|
||||||
'inline-flex flex-col items-center justify-center gap-2 rounded-lg px-2 py-2 backdrop-blur-xl hover:bg-white/10',
|
className="w-max rounded-full border border-white/5 bg-white/5 px-2.5 py-1 text-sm font-medium hover:bg-white/10"
|
||||||
`${selected(69000) && 'bg-white/10 backdrop-blur-xl'}`
|
>
|
||||||
)}
|
|
||||||
>
|
|
||||||
<img className="h-12 w-12" src="/zap.png" alt="High Voltage" />
|
|
||||||
<span className="text-sm font-medium leading-none text-white/80">
|
|
||||||
69 sats
|
69 sats
|
||||||
</span>
|
</button>
|
||||||
</button>
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
onClick={() => setAmount('100')}
|
||||||
onClick={() => setAmount(100000)}
|
className="w-max rounded-full border border-white/5 bg-white/5 px-2.5 py-1 text-sm font-medium hover:bg-white/10"
|
||||||
className={twMerge(
|
>
|
||||||
'inline-flex flex-col items-center justify-center gap-2 rounded-lg px-2 py-2 backdrop-blur-xl hover:bg-white/10',
|
|
||||||
`${selected(100000) && 'bg-white/10 backdrop-blur-xl'}`
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<img className="h-12 w-12" src="/zap.png" alt="High Voltage" />
|
|
||||||
<span className="text-sm font-medium leading-none text-white/80">
|
|
||||||
100 sats
|
100 sats
|
||||||
</span>
|
</button>
|
||||||
</button>
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
onClick={() => setAmount('200')}
|
||||||
onClick={() => setAmount(200000)}
|
className="w-max rounded-full border border-white/5 bg-white/5 px-2.5 py-1 text-sm font-medium hover:bg-white/10"
|
||||||
className={twMerge(
|
>
|
||||||
'inline-flex flex-col items-center justify-center gap-2 rounded-lg px-2 py-2 backdrop-blur-xl hover:bg-white/10',
|
|
||||||
`${selected(200000) && 'bg-white/10 backdrop-blur-xl'}`
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<img className="h-12 w-12" src="/zap.png" alt="High Voltage" />
|
|
||||||
<span className="text-sm font-medium leading-none text-white/80">
|
|
||||||
200 sats
|
200 sats
|
||||||
</span>
|
</button>
|
||||||
</button>
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
onClick={() => setAmount('500')}
|
||||||
onClick={() => setAmount(500000)}
|
className="w-max rounded-full border border-white/5 bg-white/5 px-2.5 py-1 text-sm font-medium hover:bg-white/10"
|
||||||
className={twMerge(
|
>
|
||||||
'inline-flex flex-col items-center justify-center gap-2 rounded-lg px-2 py-2 backdrop-blur-xl hover:bg-white/10',
|
|
||||||
`${selected(500000) && 'bg-white/10 backdrop-blur-xl'}`
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<img className="h-12 w-12" src="/zap.png" alt="High Voltage" />
|
|
||||||
<span className="text-sm font-medium leading-none text-white/80">
|
|
||||||
500 sats
|
500 sats
|
||||||
</span>
|
</button>
|
||||||
</button>
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
onClick={() => setAmount('1000')}
|
||||||
onClick={() => setAmount(1000000)}
|
className="w-max rounded-full border border-white/5 bg-white/5 px-2.5 py-1 text-sm font-medium hover:bg-white/10"
|
||||||
className={twMerge(
|
>
|
||||||
'inline-flex flex-col items-center justify-center gap-2 rounded-lg px-2 py-2 backdrop-blur-xl hover:bg-white/10',
|
1K sats
|
||||||
`${selected(1000000) && 'bg-white/10 backdrop-blur-xl'}`
|
</button>
|
||||||
)}
|
</div>
|
||||||
>
|
|
||||||
<img className="h-12 w-12" src="/zap.png" alt="High Voltage" />
|
|
||||||
<span className="text-sm font-medium leading-none text-white/80">
|
|
||||||
1000 sats
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 flex w-full">
|
<div className="mt-4 flex w-full flex-col gap-2">
|
||||||
<button
|
<TextareaAutosize
|
||||||
type="button"
|
name="zapMessage"
|
||||||
onClick={() => createZapRequest()}
|
value={zapMessage}
|
||||||
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-fuchsia-500 px-4 hover:bg-fuchsia-600"
|
onChange={(e) => setZapMessage(e.target.value)}
|
||||||
>
|
spellCheck={false}
|
||||||
Create invoice
|
autoComplete="off"
|
||||||
</button>
|
autoCorrect="off"
|
||||||
|
autoCapitalize="off"
|
||||||
|
placeholder="Enter message (optional)"
|
||||||
|
className="relative min-h-[56px] w-full resize-none rounded-lg bg-white/10 px-3 py-2 !outline-none backdrop-blur-xl placeholder:text-white/50"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{walletConnectURL ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => createZapRequest()}
|
||||||
|
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-fuchsia-500 px-4 font-medium text-white hover:bg-fuchsia-600"
|
||||||
|
>
|
||||||
|
{isCompleted ? (
|
||||||
|
<p>Successfully tipped</p>
|
||||||
|
) : isLoading ? (
|
||||||
|
<span className="flex flex-col">
|
||||||
|
<p className="mb-px leading-none">Waiting for approval</p>
|
||||||
|
<p className="text-xs leading-none text-white/70">
|
||||||
|
Go to your wallet and approve payment request
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="flex flex-col">
|
||||||
|
<p className="mb-px leading-none">Send tip</p>
|
||||||
|
<p className="text-xs leading-none text-white/70">
|
||||||
|
You're using nostr wallet connect
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => createZapRequest()}
|
||||||
|
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-fuchsia-500 px-4 font-medium hover:bg-fuchsia-600"
|
||||||
|
>
|
||||||
|
<p>Create Lightning invoice</p>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user