Compare commits

..

119 Commits

Author SHA1 Message Date
b6784aa979 add missing deps to gh ci 2023-10-26 13:39:11 +07:00
08a7bdc6d5 fix ci again 2023-10-26 13:30:43 +07:00
6fac37a0ca improve follow/unfollow, support petname 2023-10-26 13:29:13 +07:00
37ed0f4892 fix ci 2023-10-26 13:11:06 +07:00
1cb2d8cb41 small fixes 2023-10-26 12:32:03 +07:00
3abce5e6d6 update appicon and tauri config 2023-10-26 12:24:32 +07:00
842f9e14e0 add updater v2 2023-10-26 10:33:27 +07:00
0c8dcef937 ready for alpha 2023-10-26 09:29:33 +07:00
Ren Amamiya
50f81a7d0b Merge pull request #98 from sectore/feat/flake-nix
Add Nix dev environment
2023-10-25 09:23:45 +07:00
dcacf23625 add notification widget 2023-10-25 09:23:20 +07:00
jk
b01af39445 typo + cleanup 2023-10-24 17:26:50 +02:00
507628bcaa update ui for consistent in light and dark mode 2023-10-24 21:15:59 +07:00
jk
4dfea49f71 Nix dev environment 2023-10-24 14:57:15 +02:00
854a47f266 wip 2023-10-24 13:11:10 +07:00
b1a44f2cbf wip: new composer 2023-10-22 15:48:06 +07:00
cade8c8b4c wip: multi-type composer 2023-10-21 15:58:39 +07:00
de88ca51fe update 2023-10-20 15:15:30 +07:00
7c8d8a09fd wip: nsecbunker 2023-10-20 09:36:49 +07:00
e1e54c1a98 update thread widget 2023-10-19 14:45:41 +07:00
0de72eb009 polish 2023-10-19 08:59:50 +07:00
823fb0f239 bump version 2023-10-18 14:59:33 +07:00
Ren Amamiya
3660db4887 Merge pull request #94 from luminous-devs/feat/v2
v2.0.0
2023-10-18 14:55:05 +07:00
b18ae56c36 Merge branch 'main' into feat/v2 2023-10-18 14:53:11 +07:00
939a72f945 ok fine 2023-10-18 14:49:20 +07:00
489ab6bd0b wip: polish 2023-10-18 08:43:31 +07:00
7fa1e89dc8 wip: complete new onboarding 2023-10-17 16:33:41 +07:00
3aa4f294f9 wip: new onboarding 2023-10-16 14:42:19 +07:00
cd3b9ada5a wip: new import account 2023-10-15 16:10:16 +07:00
620e763380 wip: new onboarding 2023-10-14 15:19:49 +07:00
0777c483e5 wip: fix ui for macos 2023-10-13 09:12:30 +07:00
893663561d wip: fix light mode 2023-10-13 08:35:07 +07:00
35650a40f2 wip 2023-10-12 09:13:06 +07:00
Phong
3b46e71525 wip: native secure store 2023-10-11 14:46:35 +07:00
Phong
2fcbf1987b rework macos version 2023-10-11 13:45:56 +07:00
Ren Amamiya
c3f399ea0b rollback to virtua v0.9.1 2023-10-11 08:04:20 +07:00
Ren Amamiya
770a63de63 wip 2023-10-10 15:49:23 +07:00
Ren Amamiya
bc4c3b9803 wip 2023-10-10 11:51:01 +07:00
Ren Amamiya
043c1b1220 wip: update color palette 2023-10-10 08:25:31 +07:00
Ren Amamiya
d20ee26e22 wip: add harmony color palette 2023-10-09 19:05:50 +07:00
Ren Amamiya
8930300fb5 wip 2023-10-09 15:17:15 +07:00
Ren Amamiya
140b8a47bf wip: ui 2023-10-09 11:30:52 +07:00
Ren Amamiya
ced23341d2 wip: new sidebar 2023-10-08 18:01:19 +07:00
Ren Amamiya
0946e9125e wip: ui 2023-10-08 14:14:39 +07:00
Ren Amamiya
bce76bd41c wip: ui 2023-10-08 09:31:11 +07:00
Ren Amamiya
cb91373d33 wip: dark mode - light mode 2023-10-07 09:06:33 +07:00
Ren Amamiya
c71bfb3f6d wip: fully migrate to tauri v2 2023-10-06 09:08:37 +07:00
9627c40d75 wip: tauri v2 2023-10-06 08:30:59 +07:00
Ren Amamiya
1240353e30 wip 2023-10-06 07:40:50 +07:00
Ren Amamiya
cef6b9aca9 wip: new chat layout 2023-10-05 14:55:12 +07:00
Ren Amamiya
508a746578 wip 2023-10-05 07:28:35 +07:00
Ren Amamiya
222ef2ca32 fix: vidstack bug 2023-10-04 20:30:41 +07:00
Ren Amamiya
32843018aa chore(release): v1.2.7 2023-10-04 14:27:48 +07:00
Ren Amamiya
f80dd78a8e wip 2023-10-04 14:11:45 +07:00
Ren Amamiya
9df4835be3 fix ci 2023-10-04 11:17:04 +07:00
Ren Amamiya
480580890e wip: new chat screen 2023-10-04 11:15:10 +07:00
Ren Amamiya
ca57ef1760 Merge branch 'main' into feat/nip28 2023-10-04 07:48:52 +07:00
Ren Amamiya
8e39bca57c fix build 2023-10-04 07:48:20 +07:00
Ren Amamiya
8d9ec0dcfd update config 2023-10-03 18:43:39 +07:00
Ren Amamiya
428d52f175 wip 2023-10-03 16:24:09 +07:00
Ren Amamiya
cdeb5afd28 upgrade to tauri v1.5.1 2023-10-03 07:38:23 +07:00
Ren Amamiya
1f3ba09cec new app icon 2023-10-02 15:36:20 +07:00
Ren Amamiya
4915b833e7 small fixed 2023-10-01 15:00:38 +07:00
Ren Amamiya
674e5f0339 finish relay manegament screen 2023-10-01 09:02:14 +07:00
Ren Amamiya
11ed618a7f wip: relay manegament screen 2023-09-30 19:07:17 +07:00
Ren Amamiya
a2e3247432 wip 2023-09-30 15:12:33 +07:00
Ren Amamiya
09b3eeda99 small fixes and bump version 2023-09-29 12:40:02 +07:00
Ren Amamiya
700f3eb85f upgrade to tauri v1.5.0 2023-09-29 09:19:40 +07:00
Ren Amamiya
2f87ed8949 polish widget code 2023-09-29 09:11:38 +07:00
Ren Amamiya
cb3c95b133 clean up and improve perf 2023-09-28 16:18:04 +07:00
Ren Amamiya
4f4e2f5ccd wip: multi account 2023-09-28 14:00:52 +07:00
Ren Amamiya
0e6fc65b08 Merge pull request #91 from luminous-devs/feat/ui-patch
v1.2.6
2023-09-28 08:28:34 +07:00
Ren Amamiya
876d351358 update dependencies and polish 2023-09-28 08:22:38 +07:00
Ren Amamiya
c80414a72d small fixes and support $ boost sign 2023-09-28 07:29:05 +07:00
Ren Amamiya
7cef6efa6f fix crash on windows 2023-09-27 18:24:58 +07:00
Ren Amamiya
74ff49b8db fix app window is not resizable 2023-09-27 16:11:33 +07:00
Ren Amamiya
2b50fc438f improve startup time 2023-09-27 14:53:01 +07:00
Ren Amamiya
b339e842ca perf improve 2023-09-27 08:32:19 +07:00
Ren Amamiya
1d93f8cf6a clean up 2023-09-26 09:40:02 +07:00
Ren Amamiya
236131087a polish 2023-09-26 09:05:39 +07:00
Ren Amamiya
a66770989b wip: finish browse users 2023-09-25 14:35:47 +07:00
Ren Amamiya
9ff74599eb customize traffic light on macOS 2023-09-25 07:43:13 +07:00
Ren Amamiya
c049fa8865 wip: update browse user screen 2023-09-24 15:42:49 +07:00
Ren Amamiya
41b12746a7 wip 2023-09-24 09:13:42 +07:00
Ren Amamiya
50f90ddcc2 wip 2023-09-24 07:55:27 +07:00
Ren Amamiya
c9bc7b81dd wip: browse user 2023-09-22 14:13:55 +07:00
Ren Amamiya
18a9ba3fb0 wip: replace ndk cache with metadata table 2023-09-21 16:11:35 +07:00
Ren Amamiya
413571ee7f wip 2023-09-21 15:28:01 +07:00
Ren Amamiya
17fe3bb1f6 wip: timeline 2023-09-21 09:11:45 +07:00
Ren Amamiya
0e5adb246f resizable widget 2023-09-20 14:31:14 +07:00
Ren Amamiya
296136203a update dependencies 2023-09-20 08:22:02 +07:00
Ren Amamiya
1bbfebc2b8 fix ci again 2023-09-19 16:31:38 +07:00
Ren Amamiya
d84e97b0d4 fix ci 2023-09-19 15:59:50 +07:00
Ren Amamiya
824aa8fa28 back to pnpm, bun is fun but cannot generate tauri build 2023-09-19 15:56:33 +07:00
Ren Amamiya
2b34ef3b7a fix build error 2023-09-19 15:29:26 +07:00
Ren Amamiya
4fa8f40e6a improve nwc 2023-09-19 15:06:12 +07:00
Ren Amamiya
c1bddeb6ed bump version 2023-09-19 11:20:15 +07:00
Ren Amamiya
5c2bfa0ea3 improve notification 2023-09-19 11:15:35 +07:00
Ren Amamiya
60e93965ea respect user's relay list (kind 10002) 2023-09-19 08:01:57 +07:00
Ren Amamiya
380d1fb930 temporary using default relays 2023-09-18 15:42:17 +07:00
Ren Amamiya
53aa13c8aa clean up messy code 2023-09-18 09:50:15 +07:00
Ren Amamiya
13f5190ba1 update dependencies and better handle repost 2023-09-17 16:14:04 +07:00
Ren Amamiya
c590e290e0 Merge pull request #87 from luminous-devs/feat/improve-onboarding
merge now, improve later
2023-09-17 08:44:16 +07:00
Ren Amamiya
cdf86a2613 polish 2023-09-17 08:43:42 +07:00
Ren Amamiya
8726e22b38 replace nostr.com with njump.me 2023-09-17 08:03:29 +07:00
Ren Amamiya
1206486016 partial support replaceable event 2023-09-16 16:06:01 +07:00
Ren Amamiya
11ad281d72 wip 2023-09-16 08:57:24 +07:00
Ren Amamiya
fe4bfa1699 wip: learn nostr widget 2023-09-16 07:47:44 +07:00
Ren Amamiya
c6a0636e8c add complete screen 2023-09-15 10:29:39 +07:00
Ren Amamiya
d3db6492d9 update onboarding 2023-09-15 08:58:09 +07:00
Ren Amamiya
8f8617d8f9 update import key flow 2023-09-14 16:51:38 +07:00
Ren Amamiya
8e513404c3 update create account flow 2023-09-14 09:20:36 +07:00
Ren Amamiya
5a6dd172b1 small fixes 2023-09-13 11:10:24 +07:00
Ren Amamiya
fa0d7cac31 hide nwc secret in frontend 2023-09-12 16:16:57 +07:00
Ren Amamiya
432b2ae185 polish nwc connection flow 2023-09-12 16:00:41 +07:00
Ren Amamiya
fb8a6581dd replace pnpm with bun 2023-09-12 08:43:12 +07:00
Ren Amamiya
a4f221f868 wip: redesign nwc 2023-09-12 08:27:29 +07:00
Ren Amamiya
602d012efe fix alby connection 2023-09-11 07:52:43 +07:00
Ren Amamiya
5bf816eba2 fully suport alby nostr-wallet-connect 2023-09-10 16:25:35 +07:00
Ren Amamiya
a33c9d3517 wip: integrate alby 2023-09-10 07:19:36 +07:00
317 changed files with 13366 additions and 11249 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

View File

@@ -32,7 +32,7 @@ jobs:
if: matrix.settings.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y build-essential libssl-dev libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
sudo apt-get install -y build-essential libssl-dev javascriptcoregtk-4.1 libayatana-appindicator3-dev libsoup-3.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev webkit2gtk-4.1 librsvg2-dev patchelf
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
@@ -59,6 +59,7 @@ jobs:
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with:
tagName: v__VERSION__
releaseName: 'App v__VERSION__'

4
.gitignore vendored
View File

@@ -14,9 +14,11 @@ out
*.local
.next
.vscode
pnpm-lock.yaml
*.db
*.db-journal
bun.lockb
.direnv
# Editor directories and files
.vscode/*

View File

@@ -8,6 +8,12 @@ Download Lume for your platform here: [https://github.com/luminous-devs/lume/rel
Supported platform: macOS, Windows and Linux
### Prerequisites
- PNPM or Bun (experiment)
- Tauri: https://tauri.app/v1/guides/getting-started/prerequisites#setting-up-macos
### Develop
Clone project
@@ -22,20 +28,23 @@ Install packages
pnpm install
```
Run dev
Run dev build
```
pnpm tauri dev
```
Build
Generate production build
```
pnpm tauri build
```
(Advance) - Generate SQLite migration
#### Nix
```
pnpm add-migrate <migrate_name>
```
Requirements:
1. [Install Nix](https://zero-to-flakes.com/install)
1. [Setup `direnv`](https://zero-to-flakes.com/direnv)
`cd` into the root folder of the project to enter `nix develop` shell. Run `direnv allow` (only once). Then run `pnpm` or `bun` (experimental) commands as described above.

130
flake.lock generated Normal file
View File

@@ -0,0 +1,130 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1694529238,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1697723726,
"narHash": "sha256-SaTWPkI8a5xSHX/rrKzUe+/uVNy6zCGMXgoeMb7T9rg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "7c9cc5a6e5d38010801741ac830a3f8fd667a7a0",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1681358109,
"narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1697940838,
"narHash": "sha256-eyk92QqAoRNC0V99KOcKcBZjLPixxNBS0PRc4KlSQVs=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "a3e829c06eadf848f13d109c7648570ce37ebccd",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

71
flake.nix Normal file
View File

@@ -0,0 +1,71 @@
# Nix.flake to build Lume based on Tauri's Guides:
# Prerequisites -> Installing -> Setting Up Linux -> NixOS
# https://tauri.app/v1/guides/getting-started/prerequisites/#1-system-dependencies
#
# To build Rust backend of Tauri `rust-overlay` is used
# https://github.com/oxalica/rust-overlay
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
rust-overlay.url = "github:oxalica/rust-overlay";
};
outputs = { self, nixpkgs, flake-utils, rust-overlay }:
flake-utils.lib.eachDefaultSystem (system:
let
overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs {
inherit system overlays;
};
libraries = with pkgs;[
webkitgtk
gtk3
cairo
gdk-pixbuf
glib
dbus
openssl_3
librsvg
];
packages = with pkgs; [
curl
wget
pkg-config
dbus
openssl_3
glib
gtk3
libsoup
webkitgtk
librsvg
];
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
extensions = [ "rust-src" ]; # needed by rust-analyzer
};
in
{
devShells.default = pkgs.mkShell {
buildInputs = [
rustToolchain
pkgs.nodejs
pkgs.nodePackages.pnpm
pkgs.bun # experimental in Lume
] ++ packages;
shellHook =
''
export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath libraries}:$LD_LIBRARY_PATH
export XDG_DATA_DIRS=${pkgs.gsettings-desktop-schemas}/share/gsettings-schemas/${pkgs.gsettings-desktop-schemas.name}:${pkgs.gtk3}/share/gsettings-schemas/${pkgs.gtk3.name}:$XDG_DATA_DIRS
'';
# Avoid white screen running with Nix
# https://github.com/tauri-apps/tauri/issues/4315#issuecomment-1207755694
WEBKIT_DISABLE_COMPOSITING_MODE = 1;
};
});
}

View File

@@ -1,11 +1,11 @@
<html lang="en" class="dark">
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lume</title>
</head>
<body class="relative cursor-default select-none overflow-hidden font-sans antialiased h-screen w-screen text-white">
<body class="relative cursor-default select-none overflow-hidden font-sans antialiased h-screen w-screen text-neutral-950 dark:text-neutral-50">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

View File

@@ -2,7 +2,7 @@
"name": "lume",
"description": "the communication app",
"private": true,
"version": "1.2.4",
"version": "2.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
@@ -18,86 +18,109 @@
"**/*.{ts, tsx, css, md, html, json}": "prettier --cache --write"
},
"dependencies": {
"@ctrl/magnet-link": "^3.1.2",
"@getalby/sdk": "^2.4.0",
"@nostr-dev-kit/ndk": "^1.0.0",
"@nostr-fetch/adapter-ndk": "^0.12.2",
"@radix-ui/react-alert-dialog": "^1.0.4",
"@evilmartians/harmony": "^1.1.0",
"@getalby/sdk": "^2.5.0",
"@nostr-dev-kit/ndk": "^2.0.3",
"@nostr-dev-kit/ndk-cache-dexie": "^2.0.3",
"@nostr-fetch/adapter-ndk": "^0.13.0",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-tooltip": "^1.0.6",
"@tanstack/react-query": "^4.35.0",
"@tanstack/react-virtual": "3.0.0-beta.54",
"@tauri-apps/api": "^1.4.0",
"@tiptap/extension-image": "^2.1.8",
"@tiptap/extension-mention": "^2.1.8",
"@tiptap/extension-placeholder": "^2.1.8",
"@tiptap/pm": "^2.1.8",
"@tiptap/react": "^2.1.8",
"@tiptap/starter-kit": "^2.1.8",
"@tiptap/suggestion": "^2.1.8",
"@void-cat/api": "^1.0.7",
"dayjs": "^1.11.9",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-toolbar": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-query": "4.36.1",
"@tauri-apps/api": "2.0.0-alpha.8",
"@tauri-apps/cli": "2.0.0-alpha.15",
"@tauri-apps/plugin-app": "2.0.0-alpha.1",
"@tauri-apps/plugin-clipboard-manager": "2.0.0-alpha.1",
"@tauri-apps/plugin-dialog": "2.0.0-alpha.1",
"@tauri-apps/plugin-fs": "2.0.0-alpha.1",
"@tauri-apps/plugin-http": "2.0.0-alpha.1",
"@tauri-apps/plugin-notification": "2.0.0-alpha.1",
"@tauri-apps/plugin-os": "2.0.0-alpha.2",
"@tauri-apps/plugin-process": "2.0.0-alpha.1",
"@tauri-apps/plugin-shell": "2.0.0-alpha.1",
"@tauri-apps/plugin-sql": "2.0.0-alpha.1",
"@tauri-apps/plugin-updater": "2.0.0-alpha.1",
"@tauri-apps/plugin-upload": "2.0.0-alpha.1",
"@tauri-apps/plugin-window": "2.0.0-alpha.1",
"@tiptap/extension-character-count": "^2.1.12",
"@tiptap/extension-document": "^2.1.12",
"@tiptap/extension-image": "^2.1.12",
"@tiptap/extension-mention": "^2.1.12",
"@tiptap/extension-paragraph": "^2.1.12",
"@tiptap/extension-placeholder": "^2.1.12",
"@tiptap/extension-text": "^2.1.12",
"@tiptap/pm": "^2.1.12",
"@tiptap/react": "^2.1.12",
"@tiptap/starter-kit": "^2.1.12",
"@tiptap/suggestion": "^2.1.12",
"dayjs": "^1.11.10",
"destr": "^2.0.1",
"get-urls": "^12.1.0",
"framer-motion": "^10.16.4",
"html-to-text": "^9.0.5",
"light-bolt11-decoder": "^3.0.0",
"lru-cache": "^10.0.1",
"media-chrome": "^1.4.4",
"million": "^2.6.4",
"minidenticons": "^4.2.0",
"nostr-fetch": "^0.13.0",
"nostr-tools": "^1.14.2",
"nostr-tools": "^1.17.0",
"qrcode.react": "^3.1.0",
"re-resizable": "^6.9.11",
"react": "^18.2.0",
"react-currency-input-field": "^3.6.11",
"react-dom": "^18.2.0",
"react-hook-form": "^7.46.1",
"react-hook-form": "^7.47.0",
"react-hotkeys-hook": "^4.4.1",
"react-markdown": "^8.0.7",
"react-player": "^2.13.0",
"react-router-dom": "^6.15.0",
"react-textarea-autosize": "^8.5.3",
"react-virtuoso": "^4.5.1",
"react-router-dom": "^6.17.0",
"react-string-replace": "^1.1.1",
"reactflow": "^11.9.4",
"remark-gfm": "^3.0.1",
"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",
"sonner": "^1.0.3",
"tailwind-scrollbar": "^3.0.5",
"tauri-controls": "^0.2.0",
"tippy.js": "^6.3.7",
"zustand": "^4.4.1"
"tiptap-markdown": "^0.8.2",
"virtua": "^0.15.0",
"zustand": "^4.4.3"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.10",
"@tauri-apps/cli": "^1.4.0",
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
"@types/html-to-text": "^9.0.1",
"@types/node": "^20.5.9",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"@types/youtube-player": "^5.5.7",
"@typescript-eslint/eslint-plugin": "^6.6.0",
"@typescript-eslint/parser": "^6.6.0",
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.15",
"@types/html-to-text": "^9.0.3",
"@types/node": "^20.8.7",
"@types/react": "^18.2.29",
"@types/react-dom": "^18.2.14",
"@types/youtube-player": "^5.5.9",
"@typescript-eslint/eslint-plugin": "^6.8.0",
"@typescript-eslint/parser": "^6.8.0",
"@vitejs/plugin-react-swc": "^3.4.0",
"autoprefixer": "^10.4.16",
"clsx": "^2.0.0",
"cross-env": "^7.0.3",
"csstype": "^3.1.2",
"encoding": "^0.1.13",
"eslint": "^8.48.0",
"eslint": "^8.51.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-simple-import-sort": "^10.0.0",
"husky": "^8.0.3",
"lint-staged": "^14.0.1",
"postcss": "^8.4.29",
"postcss": "^8.4.31",
"prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.5.4",
"prettier-plugin-tailwindcss": "^0.5.6",
"prop-types": "^15.8.1",
"tailwind-merge": "^1.14.0",
"tailwindcss": "^3.3.3",
"typescript": "^5.2.2",
"vite": "^4.4.9",
"vite-tsconfig-paths": "^4.2.0"
"vite": "^4.5.0",
"vite-tsconfig-paths": "^4.2.1"
}
}

4682
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

BIN
public/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

2518
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "lume"
version = "1.2.4"
version = "2.0.0"
description = "the communication app"
authors = ["Ren Amamiya"]
license = "GPL-3.0"
@@ -8,48 +8,40 @@ repository = "https://github.com/luminous-devs/lume"
edition = "2021"
rust-version = "1.66"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.4", features = [] }
tauri-build = { version = "2.0.0-alpha", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.4", features = [
"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 = { version = "2.0.0-alpha", features = [
"macos-private-api",
"native-tls-vendored",
] }
tauri-plugin-sql = { git = "hhttps://github.com/tauri-apps/plugins-workspace", branch = "v1", features = [
tauri-plugin-app = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-cli = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-clipboard-manager = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-dialog = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-fs = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-http = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-notification = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-os = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-process = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-shell = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-updater = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-window = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-upload = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-sql = { git = "hhttps://github.com/tauri-apps/plugins-workspace", branch = "v2", features = [
"sqlite",
] }
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tauri-plugin-stronghold = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tauri-plugin-upload = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
window-vibrancy = { git = "https://github.com/tauri-apps/window-vibrancy", branch = "dev" }
sqlx-cli = { version = "0.7.0", default-features = false, features = [
"sqlite",
] }
rust-argon2 = "1.0"
webpage = { version = "1.6.0", features = ["serde"] }
keyring = "2"
[features]
# by default Tauri runs in production mode

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 731 B

After

Width:  |  Height:  |  Size: 886 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 709 B

After

Width:  |  Height:  |  Size: 894 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 921 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 828 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -1,45 +1,29 @@
-- Add migration script here
-- create accounts table
-- is_active (multi-account feature), value:
-- 0: false
-- 1: true
CREATE TABLE
accounts (
id INTEGER NOT NULL PRIMARY KEY,
npub TEXT NOT NULL UNIQUE,
id TEXT NOT NULL PRIMARY KEY,
pubkey TEXT NOT NULL UNIQUE,
privkey TEXT NOT NULL,
follows JSON,
follows TEXT,
circles TEXT,
is_active INTEGER NOT NULL DEFAULT 0,
last_login_at NUMBER NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- create notes table
CREATE TABLE
notes (
id INTEGER NOT NULL PRIMARY KEY,
event_id TEXT NOT NULL UNIQUE,
events (
id TEXT NOT NULL PRIMARY KEY,
account_id INTEGER NOT NULL,
pubkey TEXT NOT NULL,
kind INTEGER NOT NULL DEFAULT 1,
tags JSON,
content TEXT NOT NULL,
event TEXT NOT NULL,
author TEXT NOT NULL,
kind NUMBER NOT NULL DEFAULt 1,
root_id TEXT,
reply_id TEXT,
created_at INTEGER NOT NULL,
parent_id TEXT,
FOREIGN KEY (account_id) REFERENCES accounts (id)
);
-- create channels table
CREATE TABLE
channels (
id INTEGER NOT NULL PRIMARY KEY,
event_id TEXT NOT NULL UNIQUE,
name TEXT,
about TEXT,
picture TEXT,
created_at INTEGER NOT NULL
);
-- create settings table
CREATE TABLE
settings (
@@ -49,11 +33,23 @@ CREATE TABLE
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- create metadata table
CREATE TABLE
metadata (
id TEXT NOT NULL PRIMARY KEY,
pubkey TEXT NOT NULL,
widgets (
id INTEGER NOT NULL PRIMARY KEY,
account_id INTEGER NOT NULL,
kind INTEGER NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (account_id) REFERENCES accounts (id)
);
CREATE TABLE
relays (
id INTEGER NOT NULL PRIMARY KEY,
account_id INTEGER NOT NULL,
relay TEXT NOT NULL UNIQUE,
purpose TEXT NOT NULL DEFAULT '',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (account_id) REFERENCES accounts (id)
);

View File

@@ -1,12 +0,0 @@
-- Add migration script here
-- create chats table
CREATE TABLE
chats (
id INTEGER NOT NULL PRIMARY KEY,
event_id TEXT NOT NULL UNIQUE,
receiver_pubkey INTEGER NOT NULL,
sender_pubkey TEXT NOT NULL,
content TEXT NOT NULL,
tags JSON,
created_at INTEGER NOT NULL
);

View File

@@ -1,14 +0,0 @@
-- Add migration script here
INSERT INTO
settings (key, value)
VALUES
("last_login", "0"),
(
"relays",
'["wss://relayable.org","wss://relay.damus.io","wss://relay.nostr.band/all","wss://relay.nostrgraph.net","wss://nostr.mutinywallet.com"]'
),
("auto_start", "0"),
("cache_time", "86400000"),
("compose_shortcut", "meta+n"),
("add_imageblock_shortcut", "meta+i"),
("add_feedblock_shortcut", "meta+f")

View File

@@ -1,3 +0,0 @@
-- Add migration script here
-- add pubkey to channel
ALTER TABLE channels ADD pubkey TEXT NOT NULL DEFAULT '';

View File

@@ -1,38 +0,0 @@
-- Add migration script here
INSERT
OR IGNORE INTO channels (
event_id,
pubkey,
name,
about,
picture,
created_at
)
VALUES
(
"e3cadf5beca1b2af1cddaa41a633679bedf263e3de1eb229c6686c50d85df753",
"126103bfddc8df256b6e0abfd7f3797c80dcc4ea88f7c2f87dd4104220b4d65f",
"lume-general",
"General channel for Lume",
"https://void.cat/d/UNyxBmAh1MUx5gQTX95jyf.webp",
1681898574
);
INSERT
OR IGNORE INTO channels (
event_id,
pubkey,
name,
about,
picture,
created_at
)
VALUES
(
"25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb",
"ed1d0e1f743a7d19aa2dfb0162df73bacdbc699f67cc55bb91a98c35f7deac69",
"Nostr",
"",
"https://cloudflare-ipfs.com/ipfs/QmTN4Eas9atUULVbEAbUU8cowhtvK7g3t7jfKztY7wc8eP?.png",
1661333723
);

View File

@@ -1,11 +0,0 @@
-- Add migration script here
-- create blacklist table
CREATE TABLE
blacklist (
id INTEGER NOT NULL PRIMARY KEY,
account_id INTEGER NOT NULL,
content TEXT NOT NULL UNIQUE,
status INTEGER NOT NULL DEFAULT 0,
kind INTEGER NOT NULL,
FOREIGN KEY (account_id) REFERENCES accounts (id)
);

View File

@@ -1,11 +0,0 @@
-- Add migration script here
CREATE TABLE
blocks (
id INTEGER NOT NULL PRIMARY KEY,
account_id INTEGER NOT NULL,
kind INTEGER NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (account_id) REFERENCES accounts (id)
);

View File

@@ -1,15 +0,0 @@
-- Add migration script here
CREATE TABLE
channel_messages (
id INTEGER NOT NULL PRIMARY KEY,
channel_id TEXT NOT NULL,
event_id TEXT NOT NULL UNIQUE,
pubkey TEXT NOT NULL,
kind INTEGER NOT NULL,
content TEXT NOT NULL,
tags JSON,
mute BOOLEAN DEFAULT 0,
hide BOOLEAN DEFAULT 0,
created_at INTEGER NOT NULL,
FOREIGN KEY (channel_id) REFERENCES channels (event_id)
);

View File

@@ -1,13 +0,0 @@
-- Add migration script here
CREATE TABLE
replies (
id INTEGER NOT NULL PRIMARY KEY,
parent_id TEXT NOT NULL,
event_id TEXT NOT NULL UNIQUE,
pubkey TEXT NOT NULL,
kind INTEGER NOT NULL DEFAULT 1,
tags JSON,
content TEXT NOT NULL,
created_at INTEGER NOT NULL,
FOREIGN KEY (parent_id) REFERENCES notes (event_id)
);

View File

@@ -1,6 +0,0 @@
-- Add migration script here
DROP TABLE IF EXISTS blacklist;
DROP TABLE IF EXISTS channel_messages;
DROP TABLE IF EXISTS channels;

View File

@@ -1,6 +0,0 @@
-- Add migration script here
UPDATE settings
SET
value = '["wss://relayable.org","wss://relay.damus.io","wss://relay.nostr.band/all","wss://nostr.mutinywallet.com"]'
WHERE
key = 'relays';

View File

@@ -1,2 +0,0 @@
-- Add migration script here
ALTER TABLE accounts ADD network JSON;

View File

@@ -1,10 +0,0 @@
-- Add migration script here
CREATE TABLE
relays (
id INTEGER NOT NULL PRIMARY KEY,
account_id INTEGER NOT NULL,
relay TEXT NOT NULL,
purpose TEXT NOT NULL DEFAULT '',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (account_id) REFERENCES accounts (id)
);

View File

@@ -1,3 +0,0 @@
-- Add migration script here
ALTER TABLE blocks
RENAME TO widgets;

View File

@@ -1,13 +0,0 @@
-- Add migration script here
CREATE TABLE
events (
id TEXT NOT NULL PRIMARY KEY,
account_id INTEGER NOT NULL,
event TEXT NOT NULL,
author TEXT NOT NULL,
kind NUMBER NOT NULL DEFAULt 1,
root_id TEXT,
reply_id TEXT,
created_at INTEGER NOT NULL,
FOREIGN KEY (account_id) REFERENCES accounts (id)
);

View File

@@ -1,8 +0,0 @@
-- Add migration script here
DROP TABLE IF EXISTS notes;
DROP TABLE IF EXISTS chats;
DROP TABLE IF EXISTS metadata;
DROP TABLE IF EXISTS replies;

View File

@@ -1,3 +0,0 @@
-- Add migration script here
ALTER TABLE accounts
ADD COLUMN last_login_at NUMBER NOT NULL DEFAULT 0;

View File

@@ -3,14 +3,12 @@
windows_subsystem = "windows"
)]
use keyring::Entry;
use std::time::Duration;
use tauri::Manager;
use tauri_plugin_autostart::MacosLauncher;
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)]
struct Payload {
@@ -84,159 +82,63 @@ async fn opengraph(url: String) -> OpenGraphResponse {
}
#[tauri::command]
async fn close_splashscreen(window: tauri::Window) {
// Close splashscreen
if let Some(splashscreen) = window.get_window("splashscreen") {
splashscreen.close().unwrap();
fn secure_save(key: String, value: String) -> Result<(), ()> {
let entry = Entry::new("lume", &key).expect("Failed to create entry");
let _ = entry.set_password(&value);
Ok(())
}
#[tauri::command]
fn secure_load(key: String) -> Result<String, String> {
let entry = Entry::new("lume", &key).expect("Failed to create entry");
if let Ok(password) = entry.get_password() {
Ok(password)
} else {
Err("not found".to_string())
}
// Show main window
window.get_window("main").unwrap().show().unwrap();
}
#[tauri::command]
fn secure_remove(key: String) -> Result<(), ()> {
let entry = Entry::new("lume", &key).expect("Failed to create entry");
let _ = entry.delete_password();
Ok(())
}
fn main() {
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");
#[cfg(desktop)]
app
.handle()
.plugin(tauri_plugin_updater::Builder::new().build())?;
Ok(())
})
.plugin(tauri_plugin_app::init())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_upload::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_window::init())
.plugin(
tauri_plugin_sql::Builder::default()
.add_migrations(
"sqlite:lume.db",
vec![
Migration {
version: 20230418013219,
description: "initial data",
sql: include_str!("../migrations/20230418013219_initial_data.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230418080146,
description: "create chats",
sql: include_str!("../migrations/20230418080146_create_chats.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230420040005,
description: "insert last login to settings",
sql: include_str!("../migrations/20230420040005_insert_last_login_to_settings.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230425023912,
description: "add pubkey to channel",
sql: include_str!("../migrations/20230425023912_add_pubkey_to_channel.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230425024708,
description: "add default channels",
sql: include_str!("../migrations/20230425024708_add_default_channels.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230425050745,
description: "create blacklist",
sql: include_str!("../migrations/20230425050745_add_blacklist_model.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230521092300,
description: "create block",
sql: include_str!("../migrations/20230521092300_add_block_model.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230617003135,
description: "add channel messages",
sql: include_str!("../migrations/20230617003135_add_channel_messages.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230619082415,
description: "add replies",
sql: include_str!("../migrations/20230619082415_add_replies.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230718072634,
description: "clean up",
sql: include_str!("../migrations/20230718072634_clean_up_old_tables.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230725010250,
description: "update default relays",
sql: include_str!("../migrations/20230725010250_update_default_relays.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230804083544,
description: "add network to accounts",
sql: include_str!("../migrations/20230804083544_add_network_to_account.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230808085847,
description: "add relays",
sql: include_str!("../migrations/20230808085847_add_relays_table.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230811074423,
description: "rename blocks to widgets",
sql: include_str!("../migrations/20230811074423_rename_blocks_to_widgets.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230814083543,
description: "add events",
sql: include_str!("../migrations/20230814083543_add_events_table.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230816090508,
description: "clean up tables",
sql: include_str!("../migrations/20230816090508_clean_up_tables.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230817014932,
description: "add last login to account",
sql: include_str!("../migrations/20230817014932_add_last_login_time_to_account.sql"),
kind: MigrationKind::Up,
},
],
"sqlite:lume_v2.db",
vec![Migration {
version: 20230418013219,
description: "initial data",
sql: include_str!("../migrations/20230418013219_initial_data.sql"),
kind: MigrationKind::Up,
}],
)
.build(),
)
.plugin(
tauri_plugin_stronghold::Builder::new(|password| {
let config = argon2::Config {
lanes: 2,
mem_cost: 50_000,
time_cost: 30,
thread_mode: argon2::ThreadMode::from_threads(2),
variant: argon2::Variant::Argon2id,
..Default::default()
};
let key = argon2::hash_raw(
password.as_ref(),
b"LUME_NEED_RUST_DEVELOPER_HELP_MAKE_SALT_RANDOM",
&config,
)
.expect("failed to hash password");
key.to_vec()
})
.build(),
)
.plugin(tauri_plugin_autostart::init(
MacosLauncher::LaunchAgent,
Some(vec!["--flag1", "--flag2"]),
@@ -249,7 +151,12 @@ fn main() {
}))
.plugin(tauri_plugin_upload::init())
.plugin(tauri_plugin_store::Builder::default().build())
.invoke_handler(tauri::generate_handler![close_splashscreen, opengraph])
.invoke_handler(tauri::generate_handler![
opengraph,
secure_save,
secure_load,
secure_remove
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@@ -1,83 +1,48 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"build": {
"beforeBuildCommand": "pnpm build",
"beforeDevCommand": "pnpm dev",
"beforeBuildCommand": "pnpm run build",
"beforeDevCommand": "pnpm run dev",
"devPath": "http://localhost:3000",
"distDir": "../dist",
"withGlobalTauri": true
},
"package": {
"productName": "Lume",
"version": "1.2.4"
"version": "2.0.0"
},
"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": {
"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
},
"clipboard": {
"all": false,
"writeText": true,
"readText": true
},
"notification": {
"all": true
}
},
"bundle": {
"active": true,
"appimage": {
@@ -102,30 +67,21 @@
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"license": "../LICENSE",
"minimumSystemVersion": "10.15.0",
"providerShortName": null,
"signingIdentity": null,
"minimumSystemVersion": "10.15.0"
"signingIdentity": null
},
"resources": [],
"shortDescription": "",
"targets": "all",
"updater": {},
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
},
"updater": {
"endpoints": [
"https://lus.reya3772.workers.dev/v1/{{target}}/{{arch}}/{{current_version}}",
"https://lus.reya3772.workers.dev/{{target}}/{{current_version}}"
]
},
"security": {
"csp": {
"content-security-policy": "upgrade-insecure-requests"
}
},
"macOSPrivateApi": true
}
}
}

View File

@@ -2,30 +2,19 @@
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"tauri": {
"windows": [
{
"width": 400,
"height": 500,
"decorations": false,
"title": "Lume",
"center": true,
"resizable": false,
"label": "splashscreen",
"url": "splashscreen"
},
{
"width": 1080,
"height": 800,
"minWidth": 1080,
"minWidth": 560,
"minHeight": 800,
"resizable": true,
"theme": "Dark",
"title": "Lume",
"transparent": false,
"center": true,
"fullscreen": false,
"hiddenTitle": true,
"visible": false,
"fileDropEnabled": true
"fileDropEnabled": true,
"decorations": false,
"transparent": false
}
]
}

View File

@@ -2,33 +2,23 @@
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"tauri": {
"windows": [
{
"width": 400,
"height": 500,
"decorations": true,
"title": "Lume",
"titleBarStyle": "Overlay",
"hiddenTitle": true,
"center": true,
"resizable": false,
"label": "splashscreen",
"url": "splashscreen"
},
{
"width": 1080,
"height": 800,
"minWidth": 1080,
"minWidth": 560,
"minHeight": 800,
"resizable": true,
"theme": "Dark",
"title": "Lume",
"titleBarStyle": "Overlay",
"transparent": true,
"center": true,
"fullscreen": false,
"hiddenTitle": true,
"visible": false,
"fileDropEnabled": true
"fileDropEnabled": true,
"decorations": true,
"transparent": true,
"windowEffects": {
"effects": ["sidebar"]
}
}
]
}

View File

@@ -2,30 +2,22 @@
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"tauri": {
"windows": [
{
"width": 400,
"height": 500,
"decorations": false,
"title": "Lume",
"center": true,
"resizable": false,
"label": "splashscreen",
"url": "splashscreen"
},
{
"width": 1080,
"height": 800,
"minWidth": 1080,
"minWidth": 560,
"minHeight": 800,
"resizable": true,
"theme": "Dark",
"title": "Lume",
"transparent": false,
"center": true,
"fullscreen": false,
"hiddenTitle": true,
"visible": false,
"fileDropEnabled": true
"fileDropEnabled": true,
"decorations": false,
"transparent": true,
"windowEffects": {
"effects": ["micaLight", "micaDark"]
}
}
]
}

66
src/app.css Normal file
View File

@@ -0,0 +1,66 @@
@import 'reactflow/dist/style.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer utilities {
.break-p {
word-break: break-word;
word-wrap: break-word;
overflow-wrap: break-word;
}
}
html {
font-size: 14px;
}
a {
@apply cursor-default no-underline !important;
}
button {
@apply cursor-default focus:outline-none;
}
input::-ms-reveal,
input::-ms-clear {
display: none;
}
::-webkit-input-placeholder {
line-height: normal;
}
.border {
background-clip: padding-box;
}
.player {
--brand-color: #f5f5f5;
--focus-color: #4e9cf6;
--audio-brand: var(--brand-color);
--audio-focus-ring-color: var(--focus-color);
--audio-border-radius: 2px;
--video-brand: var(--brand-color);
--video-focus-ring-color: var(--focus-color);
--video-border-radius: 8px;
width: 100%;
}
.player[data-view-type='video'] {
aspect-ratio: 16 /9;
}
.ProseMirror p.is-empty::before {
@apply text-neutral-600 dark:text-neutral-400;
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}
.ProseMirror img.ProseMirror-selectednode {
@apply outline-blue-500;
}

View File

@@ -1,274 +1,260 @@
import { message } from '@tauri-apps/api/dialog';
import { RouterProvider, createBrowserRouter, redirect } from 'react-router-dom';
import { message } from '@tauri-apps/plugin-dialog';
import { RouterProvider, createBrowserRouter, defer, redirect } from 'react-router-dom';
import { ReactFlowProvider } from 'reactflow';
import { AuthCreateScreen } from '@app/auth/create';
import { AuthImportScreen } from '@app/auth/import';
import { OnboardingScreen } from '@app/auth/onboarding';
import { ChatsScreen } from '@app/chats';
import { ErrorScreen } from '@app/error';
import { ExploreScreen } from '@app/explore';
import { NewScreen } from '@app/new';
import { useStorage } from '@libs/storage/provider';
import { Frame } from '@shared/frame';
import { LoaderIcon } from '@shared/icons';
import { AppLayout } from '@shared/layouts/app';
import { AuthLayout } from '@shared/layouts/auth';
import { NoteLayout } from '@shared/layouts/note';
import { SettingsLayout } from '@shared/layouts/settings';
import { checkActiveAccount } from '@utils/checkActiveAccount';
import './index.css';
async function Loader() {
try {
const account = await checkActiveAccount();
const stronghold = sessionStorage.getItem('stronghold');
const privkey = JSON.parse(stronghold).state.privkey || null;
const onboarding = localStorage.getItem('onboarding');
const step = JSON.parse(onboarding).state.step || null;
if (!account) {
return redirect('/auth/welcome');
} else {
if (step) {
return redirect(step);
}
if (!privkey) {
return redirect('/auth/unlock');
}
}
return null;
} catch (e) {
await message(e, { title: 'An unexpected error has occurred', type: 'error' });
}
}
const router = createBrowserRouter([
{
path: '/',
element: <AppLayout />,
errorElement: <ErrorScreen />,
loader: Loader,
children: [
{
path: '',
async lazy() {
const { SpaceScreen } = await import('@app/space');
return { Component: SpaceScreen };
},
},
{
path: 'users/:pubkey',
async lazy() {
const { UserScreen } = await import('@app/users');
return { Component: UserScreen };
},
},
{
path: 'chats/:pubkey',
async lazy() {
const { ChatScreen } = await import('@app/chats');
return { Component: ChatScreen };
},
},
{
path: 'notifications',
async lazy() {
const { NotificationScreen } = await import('@app/notifications');
return { Component: NotificationScreen };
},
},
],
},
{
path: '/notes',
element: <NoteLayout />,
errorElement: <ErrorScreen />,
children: [
{
path: 'text/:id',
async lazy() {
const { TextNoteScreen } = await import('@app/notes/text');
return { Component: TextNoteScreen };
},
},
{
path: 'article/:id',
async lazy() {
const { ArticleNoteScreen } = await import('@app/notes/article');
return { Component: ArticleNoteScreen };
},
},
],
},
{
path: '/splashscreen',
errorElement: <ErrorScreen />,
async lazy() {
const { SplashScreen } = await import('@app/splash');
return { Component: SplashScreen };
},
},
{
path: '/auth',
element: <AuthLayout />,
errorElement: <ErrorScreen />,
children: [
{
path: 'welcome',
async lazy() {
const { WelcomeScreen } = await import('@app/auth/welcome');
return { Component: WelcomeScreen };
},
},
{
path: 'import',
element: <AuthImportScreen />,
errorElement: <ErrorScreen />,
children: [
{
path: '',
async lazy() {
const { ImportStep1Screen } = await import('@app/auth/import/step-1');
return { Component: ImportStep1Screen };
},
},
{
path: 'step-2',
async lazy() {
const { ImportStep2Screen } = await import('@app/auth/import/step-2');
return { Component: ImportStep2Screen };
},
},
{
path: 'step-3',
async lazy() {
const { ImportStep3Screen } = await import('@app/auth/import/step-3');
return { Component: ImportStep3Screen };
},
},
],
},
{
path: 'create',
element: <AuthCreateScreen />,
errorElement: <ErrorScreen />,
children: [
{
path: '',
async lazy() {
const { CreateStep1Screen } = await import('@app/auth/create/step-1');
return { Component: CreateStep1Screen };
},
},
{
path: 'step-2',
async lazy() {
const { CreateStep2Screen } = await import('@app/auth/create/step-2');
return { Component: CreateStep2Screen };
},
},
{
path: 'step-3',
async lazy() {
const { CreateStep3Screen } = await import('@app/auth/create/step-3');
return { Component: CreateStep3Screen };
},
},
],
},
{
path: 'onboarding',
element: <OnboardingScreen />,
errorElement: <ErrorScreen />,
children: [
{
path: '',
async lazy() {
const { OnboardStep1Screen } = await import('@app/auth/onboarding/step-1');
return { Component: OnboardStep1Screen };
},
},
{
path: 'step-2',
async lazy() {
const { OnboardStep2Screen } = await import('@app/auth/onboarding/step-2');
return { Component: OnboardStep2Screen };
},
},
{
path: 'step-3',
async lazy() {
const { OnboardStep3Screen } = await import('@app/auth/onboarding/step-3');
return { Component: OnboardStep3Screen };
},
},
],
},
{
path: 'unlock',
async lazy() {
const { UnlockScreen } = await import('@app/auth/unlock');
return { Component: UnlockScreen };
},
},
{
path: 'migrate',
async lazy() {
const { MigrateScreen } = await import('@app/auth/migrate');
return { Component: MigrateScreen };
},
},
{
path: 'reset',
async lazy() {
const { ResetScreen } = await import('@app/auth/reset');
return { Component: ResetScreen };
},
},
{
path: 'hard-reset',
async lazy() {
const { HardResetScreen } = await import('@app/auth/hardReset');
return { Component: HardResetScreen };
},
},
],
},
{
path: '/settings',
element: <SettingsLayout />,
errorElement: <ErrorScreen />,
children: [
{
path: '',
async lazy() {
const { GeneralSettingsScreen } = await import('@app/settings/general');
return { Component: GeneralSettingsScreen };
},
},
{
path: 'backup',
async lazy() {
const { AccountSettingsScreen } = await import('@app/settings/account');
return { Component: AccountSettingsScreen };
},
},
],
},
]);
import './app.css';
export default function App() {
const { db } = useStorage();
const accountLoader = async () => {
try {
// redirect to welcome screen if none user exist
const totalAccount = await db.checkAccount();
if (totalAccount === 0) return redirect('/auth/welcome');
return null;
} catch (e) {
await message(e, { title: 'An unexpected error has occurred', type: 'error' });
}
};
const relayLoader = async ({ params }) => {
return defer({
relay: fetch(`https://${params.url}`, {
method: 'GET',
headers: {
Accept: 'application/nostr+json',
},
}).then((res) => res.json()),
});
};
const router = createBrowserRouter([
{
path: '/',
element: <AppLayout />,
errorElement: <ErrorScreen />,
loader: accountLoader,
children: [
{
path: '',
async lazy() {
const { SpaceScreen } = await import('@app/space');
return { Component: SpaceScreen };
},
},
{
path: 'users/:pubkey',
async lazy() {
const { UserScreen } = await import('@app/users');
return { Component: UserScreen };
},
},
{
path: 'nwc',
async lazy() {
const { NWCScreen } = await import('@app/nwc');
return { Component: NWCScreen };
},
},
{
path: 'relays',
async lazy() {
const { RelaysScreen } = await import('@app/relays');
return { Component: RelaysScreen };
},
},
{
path: 'relays/:url',
loader: relayLoader,
async lazy() {
const { RelayScreen } = await import('@app/relays/relay');
return { Component: RelayScreen };
},
},
{
path: 'explore',
element: (
<ReactFlowProvider>
<ExploreScreen />
</ReactFlowProvider>
),
errorElement: <ErrorScreen />,
},
{
path: 'chats',
element: <ChatsScreen />,
errorElement: <ErrorScreen />,
children: [
{
path: 'chat/:pubkey',
async lazy() {
const { ChatScreen } = await import('@app/chats/chat');
return { Component: ChatScreen };
},
},
],
},
],
},
{
path: '/new',
element: <NewScreen />,
errorElement: <ErrorScreen />,
children: [
{
path: '',
async lazy() {
const { NewPostScreen } = await import('@app/new/post');
return { Component: NewPostScreen };
},
},
{
path: 'article',
async lazy() {
const { NewArticleScreen } = await import('@app/new/article');
return { Component: NewArticleScreen };
},
},
{
path: 'file',
async lazy() {
const { NewFileScreen } = await import('@app/new/file');
return { Component: NewFileScreen };
},
},
],
},
{
path: '/notes',
element: <NoteLayout />,
errorElement: <ErrorScreen />,
children: [
{
path: 'text/:id',
async lazy() {
const { TextNoteScreen } = await import('@app/notes/text');
return { Component: TextNoteScreen };
},
},
{
path: 'article/:id',
async lazy() {
const { ArticleNoteScreen } = await import('@app/notes/article');
return { Component: ArticleNoteScreen };
},
},
],
},
{
path: '/auth',
element: <AuthLayout />,
errorElement: <ErrorScreen />,
children: [
{
path: 'welcome',
async lazy() {
const { WelcomeScreen } = await import('@app/auth/welcome');
return { Component: WelcomeScreen };
},
},
{
path: 'create',
async lazy() {
const { CreateAccountScreen } = await import('@app/auth/create');
return { Component: CreateAccountScreen };
},
},
{
path: 'import',
async lazy() {
const { ImportAccountScreen } = await import('@app/auth/import');
return { Component: ImportAccountScreen };
},
},
{
path: 'onboarding',
element: <OnboardingScreen />,
errorElement: <ErrorScreen />,
children: [
{
path: '',
async lazy() {
const { OnboardingListScreen } = await import(
'@app/auth/onboarding/list'
);
return { Component: OnboardingListScreen };
},
},
{
path: 'enrich',
async lazy() {
const { OnboardEnrichScreen } = await import(
'@app/auth/onboarding/enrich'
);
return { Component: OnboardEnrichScreen };
},
},
{
path: 'hashtag',
async lazy() {
const { OnboardHashtagScreen } = await import(
'@app/auth/onboarding/hashtag'
);
return { Component: OnboardHashtagScreen };
},
},
],
},
],
},
{
path: '/settings',
element: <SettingsLayout />,
errorElement: <ErrorScreen />,
children: [
{
path: '',
async lazy() {
const { GeneralSettingsScreen } = await import('@app/settings/general');
return { Component: GeneralSettingsScreen };
},
},
{
path: 'backup',
async lazy() {
const { AccountSettingsScreen } = await import('@app/settings/account');
return { Component: AccountSettingsScreen };
},
},
],
},
]);
return (
<RouterProvider
router={router}
fallbackElement={
<Frame className="flex h-full w-full items-center justify-center">
<div className="flex h-full w-full items-center justify-center">
<LoaderIcon className="h-6 w-6 animate-spin text-white" />
</Frame>
</div>
}
future={{ v7_startTransition: true }}
/>
);
}

View File

@@ -0,0 +1,50 @@
import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notification';
import { CheckCircleIcon } from '@shared/icons';
import { useOnboarding } from '@stores/onboarding';
export function AllowNotification() {
const [notification, setNotification] = useOnboarding((state) => [
state.notification,
state.toggleNotification,
]);
const allow = async () => {
let permissionGranted = await isPermissionGranted();
if (!permissionGranted) {
const permission = await requestPermission();
permissionGranted = permission === 'granted';
}
if (permissionGranted) {
setNotification();
}
};
return (
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
<div className="flex items-start justify-between gap-2">
<div>
<h5 className="font-semibold">Allow notification</h5>
<p className="text-sm">
By allowing Lume to send notifications in your OS settings, you will receive
notification messages when someone interacts with you or your content.
</p>
</div>
{notification ? (
<div className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-teal-500 text-white">
<CheckCircleIcon className="h-4 w-4" />
</div>
) : (
<button
type="button"
onClick={allow}
className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
>
Allow
</button>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,100 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { LRUCache } from 'lru-cache';
import { useState } from 'react';
import { toast } from 'sonner';
import { useNDK } from '@libs/ndk/provider';
import { useStorage } from '@libs/storage/provider';
import { CheckCircleIcon, LoaderIcon } from '@shared/icons';
import { useOnboarding } from '@stores/onboarding';
export function Circle() {
const { db } = useStorage();
const { ndk } = useNDK();
const [circle, setCircle] = useOnboarding((state) => [
state.circle,
state.toggleCircle,
]);
const [loading, setLoading] = useState(false);
const enableLinks = async () => {
setLoading(true);
const users = ndk.getUser({ hexpubkey: db.account.pubkey });
const follows = await users.follows();
if (follows.size === 0) {
setLoading(false);
return toast('You need to follow at least 1 account');
}
const lru = new LRUCache<string, string, void>({ max: 300 });
const followsAsArr = [];
// add user's follows to lru
follows.forEach((user) => {
lru.set(user.pubkey, user.pubkey);
followsAsArr.push(user.pubkey);
});
// get follows from follows
const events = await ndk.fetchEvents({
kinds: [NDKKind.Contacts],
authors: followsAsArr,
limit: 300,
});
events.forEach((event: NDKEvent) => {
event.tags.forEach((tag) => {
if (tag[0] === 'p') lru.set(tag[1], tag[1]);
});
});
// get lru values
const circleList = [...lru.values()] as string[];
// update db
await db.updateAccount('follows', JSON.stringify(followsAsArr));
await db.updateAccount('circles', JSON.stringify(circleList));
db.account.follows = followsAsArr;
db.account.circles = circleList;
// clear lru
lru.clear();
// done
await db.createSetting('circles', '1');
setCircle();
};
return (
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
<div className="flex items-start justify-between gap-2">
<div>
<h5 className="font-semibold">Enable Circle</h5>
<p className="text-sm">
Beside newsfeed from your follows, you will see more content from all people
that followed by your follows.
</p>
</div>
{circle ? (
<div className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-teal-500 text-white">
<CheckCircleIcon className="h-4 w-4" />
</div>
) : (
<button
type="button"
onClick={enableLinks}
className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
>
{loading ? <LoaderIcon className="h-4 w-4 animate-spin" /> : 'Enable'}
</button>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,47 @@
import { useStorage } from '@libs/storage/provider';
import { CheckCircleIcon } from '@shared/icons';
import { useOnboarding } from '@stores/onboarding';
export function OutboxModel() {
const { db } = useStorage();
const [outbox, setOutbox] = useOnboarding((state) => [
state.outbox,
state.toggleOutbox,
]);
const enableOutbox = async () => {
await db.createSetting('outbox', '1');
setOutbox();
};
return (
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
<div className="flex items-start justify-between gap-2">
<div>
<h5 className="font-semibold">Enable Outbox (experiment)</h5>
<p className="text-sm">
When you request information about a user, Lume will automatically query the
user&apos;s outbox relays and subsequent queries will favour using those
relays for queries with that user&apos;s pubkey.
</p>
</div>
{outbox ? (
<div className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-teal-500 text-white">
<CheckCircleIcon className="h-4 w-4" />
</div>
) : (
<button
type="button"
onClick={enableOutbox}
className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
>
Enable
</button>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,35 @@
import { Link } from 'react-router-dom';
import { CheckCircleIcon } from '@shared/icons';
import { useOnboarding } from '@stores/onboarding';
export function FavoriteHashtag() {
const hashtag = useOnboarding((state) => state.hashtag);
return (
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
<div className="flex items-start justify-between">
<div>
<h5 className="font-semibold">Favorite hashtag</h5>
<p className="text-sm">
By adding favorite hashtag, Lume will display all contents related to this
hashtag as a column
</p>
</div>
{hashtag ? (
<div className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-teal-500 text-white">
<CheckCircleIcon className="h-4 w-4" />
</div>
) : (
<Link
to="/auth/onboarding/hashtag"
className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
>
Add
</Link>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,58 @@
import { useQuery } from '@tanstack/react-query';
import { useNDK } from '@libs/ndk/provider';
import { useStorage } from '@libs/storage/provider';
import { LoaderIcon } from '@shared/icons';
import { User } from '@shared/user';
export function FollowList() {
const { db } = useStorage();
const { ndk } = useNDK();
const { status, data } = useQuery(
['follows'],
async () => {
const user = ndk.getUser({ hexpubkey: db.account.pubkey });
const follows = await user.follows();
const followsAsArr = [];
follows.forEach((user) => {
followsAsArr.push(user.pubkey);
});
// update db
await db.updateAccount('follows', JSON.stringify(followsAsArr));
await db.updateAccount('circles', JSON.stringify(followsAsArr));
db.account.follows = followsAsArr;
db.account.circles = followsAsArr;
return followsAsArr;
},
{
refetchOnWindowFocus: false,
}
);
return (
<div className="relative rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
<h5 className="font-semibold">Your follows</h5>
<div className="mt-2 flex w-full items-center justify-center">
{status === 'loading' ? (
<LoaderIcon className="h-4 w-4 animate-spin text-neutral-900 dark:text-neutral-100" />
) : (
<div className="isolate flex -space-x-2">
{data.slice(0, 16).map((item) => (
<User key={item} pubkey={item} variant="stacked" />
))}
{data.length > 16 ? (
<div className="inline-flex h-8 w-8 items-center justify-center rounded-full bg-neutral-200 text-neutral-900 ring-1 ring-neutral-200 dark:bg-neutral-800 dark:text-neutral-100 dark:ring-neutral-800">
<span className="text-xs font-medium">+{data.length}</span>
</div>
) : null}
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,35 @@
import { Link } from 'react-router-dom';
import { CheckCircleIcon } from '@shared/icons';
import { useOnboarding } from '@stores/onboarding';
export function SuggestFollow() {
const enrich = useOnboarding((state) => state.enrich);
return (
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
<div className="flex items-start justify-between">
<div>
<h5 className="font-semibold">Enrich your network</h5>
<p className="text-sm">
Follow more people to stay up to date with everything happening around the
world.
</p>
</div>
{enrich ? (
<div className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-teal-500 text-white">
<CheckCircleIcon className="h-4 w-4" />
</div>
) : (
<Link
to="/auth/onboarding/enrich"
className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
>
Check
</Link>
)}
</div>
</div>
);
}

View File

@@ -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}
</p>
<span className="max-w-[15rem] truncate leading-tight text-white/50">
{displayNpub(pubkey, 16)}
</span>
</div>
</div>
);
}

View File

@@ -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 || displayNpub(pubkey, 16)}
</span>
</div>
</div>
);
}

305
src/app/auth/create.tsx Normal file
View File

@@ -0,0 +1,305 @@
import { NDKEvent, NDKKind, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
import { downloadDir } from '@tauri-apps/api/path';
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
import { message, save } from '@tauri-apps/plugin-dialog';
import { writeTextFile } from '@tauri-apps/plugin-fs';
import { motion } from 'framer-motion';
import { minidenticon } from 'minidenticons';
import { generatePrivateKey, getPublicKey, nip19 } from 'nostr-tools';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
import { useNDK } from '@libs/ndk/provider';
import { useStorage } from '@libs/storage/provider';
import { AvatarUploader } from '@shared/avatarUploader';
import { ArrowLeftIcon, LoaderIcon } from '@shared/icons';
import { User } from '@shared/user';
export function CreateAccountScreen() {
const [picture, setPicture] = useState('');
const [downloaded, setDownloaded] = useState(false);
const [loading, setLoading] = useState(false);
const [keys, setKeys] = useState<null | {
npub: string;
nsec: string;
pubkey: string;
privkey: string;
}>(null);
const {
register,
handleSubmit,
formState: { isDirty, isValid },
} = useForm();
const { db } = useStorage();
const { ndk } = useNDK();
const navigate = useNavigate();
const svgURI =
'data:image/svg+xml;utf8,' +
encodeURIComponent(minidenticon('lume new account', 90, 50));
const onSubmit = async (data: { name: string; about: string }) => {
try {
setLoading(true);
const profile = {
...data,
name: data.name,
display_name: data.name,
bio: data.about,
picture: picture,
avatar: picture,
};
const userPrivkey = generatePrivateKey();
const userPubkey = getPublicKey(userPrivkey);
const userNpub = nip19.npubEncode(userPubkey);
const userNsec = nip19.nsecEncode(userPrivkey);
const signer = new NDKPrivateKeySigner(userPrivkey);
ndk.signer = signer;
const event = new NDKEvent(ndk);
event.content = JSON.stringify(profile);
event.kind = NDKKind.Metadata;
event.created_at = Math.floor(Date.now() / 1000);
event.pubkey = userPubkey;
event.tags = [];
const publish = await event.publish();
if (publish) {
await db.createAccount(userNpub, userPubkey);
await db.secureSave(userPubkey, userPrivkey);
setKeys({
npub: userNpub,
nsec: userNsec,
pubkey: userPubkey,
privkey: userPrivkey,
});
setLoading(false);
} else {
toast('Create account failed');
setLoading(false);
}
} catch (e) {
return toast(e);
}
};
const copyNsec = async () => {
await writeText(keys.nsec);
};
const download = async () => {
try {
const downloadPath = await downloadDir();
const fileName = `nostr_keys_${new Date().toISOString()}.txt`;
const filePath = await save({
defaultPath: downloadPath + '/' + fileName,
});
if (filePath) {
await writeTextFile(
filePath,
`Nostr account, generated by Lume (lume.nu)\nPublic key: ${keys.npub}\nPrivate key: ${keys.nsec}`
);
setDownloaded(true);
} // else { user cancel action }
} catch (e) {
await message(e, { title: 'Cannot download account keys', type: 'error' });
}
};
return (
<div className="relative flex h-full w-full items-center justify-center">
<div className="absolute left-[8px] top-2">
{!keys ? (
<button
onClick={() => navigate(-1)}
className="inline-flex items-center gap-2 text-sm font-medium"
>
<div className="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-neutral-200 text-neutral-800 dark:bg-neutral-800 dark:text-neutral-200">
<ArrowLeftIcon className="h-5 w-5" />
</div>
Back
</button>
) : null}
</div>
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
<h1 className="text-center text-2xl font-semibold text-neutral-900 dark:text-neutral-100">
Let&apos;s set up your account.
</h1>
<div className="flex flex-col gap-3">
{!keys ? (
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col">
<input type={'hidden'} {...register('picture')} value={picture} />
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<span className="font-semibold">Avatar</span>
<div className="relative flex h-36 w-full items-center justify-center rounded-lg bg-neutral-200 dark:bg-neutral-800">
{picture.length > 0 ? (
<img
src={picture}
alt="user's avatar"
className="h-14 w-14 rounded-xl"
/>
) : (
<img
src={svgURI}
alt="user's avatar"
className="h-14 w-14 rounded-xl bg-black dark:bg-white"
/>
)}
<div className="absolute bottom-2 right-2">
<AvatarUploader setPicture={setPicture} />
</div>
</div>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="name" className="font-semibold">
Name *
</label>
<input
type={'text'}
{...register('name', {
required: true,
minLength: 1,
})}
spellCheck={false}
className="h-11 rounded-lg bg-neutral-200 px-3 placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
/>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="about" className="font-semibold">
Bio
</label>
<textarea
{...register('about')}
spellCheck={false}
className="relative h-20 w-full resize-none rounded-lg bg-neutral-200 px-3 py-2 !outline-none placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
/>
</div>
<button
type="submit"
disabled={!isDirty || !isValid}
className="inline-flex h-9 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50"
>
{loading ? (
<LoaderIcon className="h-5 w-4 animate-spin" />
) : (
'Create and Continue'
)}
</button>
</div>
</form>
</div>
) : (
<>
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{
opacity: 1,
y: 0,
}}
className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200"
>
<User pubkey={keys.pubkey} variant="simple" />
</motion.div>
<motion.div
initial={{ opacity: 0, y: 80 }}
animate={{
opacity: 1,
y: 0,
}}
className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200"
>
<div className="flex flex-col gap-1.5">
<h5 className="font-semibold">Backup account</h5>
<div>
<p className="mb-2 select-text text-sm text-neutral-800 dark:text-neutral-200">
Your private key is your password. If you lose this key, you will
lose access to your account! Copy it and keep it in a safe place.{' '}
<span className="text-red-600">
There is no way to reset your private key.
</span>
</p>
<p className="select-text text-sm text-neutral-800 dark:text-neutral-200">
Public key is used for sharing with other people so that they can
find you using the public key.
</p>
</div>
<div className="mt-3 flex flex-col gap-3">
<div className="flex flex-col gap-1">
<label htmlFor="nsec" className="text-sm font-semibold">
Private key
</label>
<div className="relative w-full">
<input
readOnly
value={
keys.nsec.substring(0, 10) + '**************************'
}
className="h-11 w-full rounded-lg bg-neutral-200 px-3 placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
/>
<div className="absolute right-0 top-0 inline-flex h-11 items-center justify-center px-2">
<button
type="button"
onClick={copyNsec}
className="rounded-md bg-neutral-300 px-2 py-1 text-sm font-medium hover:bg-neutral-400 dark:bg-neutral-700 dark:hover:bg-neutral-600"
>
Copy
</button>
</div>
</div>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="nsec" className="text-sm font-semibold">
Public key
</label>
<input
readOnly
value={keys.npub}
className="h-11 rounded-lg bg-neutral-200 px-3 placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
/>
</div>
</div>
{!downloaded ? (
<button
type="button"
onClick={() => download()}
className="inline-flex h-9 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
>
Download account keys
</button>
) : null}
</div>
</motion.div>
</>
)}
{downloaded ? (
<motion.button
initial={{ opacity: 0, y: 50 }}
animate={{
opacity: 1,
y: 0,
}}
className="inline-flex h-9 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
type="button"
onClick={() => navigate('/auth/onboarding', { state: { newuser: true } })}
>
Finish
</motion.button>
) : null}
</div>
</div>
</div>
);
}

View File

@@ -1,22 +0,0 @@
import { useEffect } from 'react';
import { Outlet } from 'react-router-dom';
import { useOnboarding } from '@stores/onboarding';
import { useStronghold } from '@stores/stronghold';
export function AuthCreateScreen() {
const [step, tmpPrivkey] = useOnboarding((state) => [state.step, state.tempPrivkey]);
const setPrivkey = useStronghold((state) => state.setPrivkey);
useEffect(() => {
if (step) {
setPrivkey(tmpPrivkey);
}
}, [tmpPrivkey]);
return (
<div className="flex h-full w-full items-center justify-center">
<Outlet />
</div>
);
}

View File

@@ -1,149 +0,0 @@
import { BaseDirectory, writeTextFile } from '@tauri-apps/api/fs';
import { generatePrivateKey, getPublicKey, nip19 } from 'nostr-tools';
import { useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useStorage } from '@libs/storage/provider';
import { Button } from '@shared/button';
import { EyeOffIcon, EyeOnIcon, LoaderIcon } from '@shared/icons';
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
import { useOnboarding } from '@stores/onboarding';
import { useStronghold } from '@stores/stronghold';
export function CreateStep1Screen() {
const { db } = useStorage();
const navigate = useNavigate();
const setPrivkey = useStronghold((state) => state.setPrivkey);
const setTempPrivkey = useOnboarding((state) => state.setTempPrivkey);
const setPubkey = useOnboarding((state) => state.setPubkey);
const setStep = useOnboarding((state) => state.setStep);
const [privkeyInput, setPrivkeyInput] = useState('password');
const [loading, setLoading] = useState(false);
const [downloaded, setDownloaded] = useState(false);
const privkey = useMemo(() => generatePrivateKey(), []);
const pubkey = getPublicKey(privkey);
const npub = nip19.npubEncode(pubkey);
const nsec = nip19.nsecEncode(privkey);
// toggle private key
const showPrivateKey = () => {
if (privkeyInput === 'password') {
setPrivkeyInput('text');
} else {
setPrivkeyInput('password');
}
};
const download = async () => {
await writeTextFile(
`nostr_keys_${new Date().toISOString().slice(0, 10)}.txt`,
`Generated by Lume (lume.nu)\nPublic key: ${npub}\nPrivate key: ${nsec}`,
{
dir: BaseDirectory.Download,
}
);
setDownloaded(true);
};
const submit = () => {
setLoading(true);
// update state
setPrivkey(privkey);
setTempPrivkey(privkey); // only use if user close app and reopen it
setPubkey(pubkey);
// save to database
db.createAccount(npub, pubkey);
// redirect to next step
navigate('/auth/create/step-2', { replace: true });
};
useEffect(() => {
// save current step, if user close app and reopen it
setStep('/auth/create');
}, []);
return (
<div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center">
<h1 className="text-xl font-semibold text-white">Save your access key!</h1>
</div>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<span className="text-base font-semibold text-white/50">Public Key</span>
<input
readOnly
value={npub}
className="relative h-11 w-full rounded-lg bg-white/10 px-3.5 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/50"
/>
</div>
<div className="flex flex-col gap-1">
<span className="text-base font-semibold text-white/50">Private Key</span>
<div className="relative">
<input
readOnly
type={privkeyInput}
value={nsec}
className="relative h-11 w-full rounded-lg bg-white/10 py-1 pl-3.5 pr-11 text-white !outline-none 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 className="flex flex-col gap-2">
<button
type="button"
onClick={() => submit()}
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
>
{loading ? (
<>
<span className="w-5" />
<span>Creating...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : (
<>
<span className="w-5" />
<span>I have saved my key, continue</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</button>
{downloaded ? (
<span className="inline-flex h-11 w-full items-center justify-center text-sm text-white/50">
Saved in Download folder
</span>
) : (
<Button preset="large-alt" onClick={() => download()}>
Download
</Button>
)}
</div>
</div>
</div>
);
}

View File

@@ -1,151 +0,0 @@
import { appConfigDir } from '@tauri-apps/api/path';
import { useEffect, useState } from 'react';
import { Resolver, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { Stronghold } from 'tauri-plugin-stronghold-api';
import { useStorage } from '@libs/storage/provider';
import { EyeOffIcon, EyeOnIcon, LoaderIcon } from '@shared/icons';
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
import { useOnboarding } from '@stores/onboarding';
import { useStronghold } from '@stores/stronghold';
type FormValues = {
password: string;
};
const resolver: Resolver<FormValues> = async (values) => {
return {
values: values.password ? values : {},
errors: !values.password
? {
password: {
type: 'required',
message: 'This is required.',
},
}
: {},
};
};
export function CreateStep2Screen() {
const navigate = useNavigate();
const setStep = useOnboarding((state) => state.setStep);
const pubkey = useOnboarding((state) => state.pubkey);
const privkey = useStronghold((state) => state.privkey);
const [passwordInput, setPasswordInput] = useState('password');
const [loading, setLoading] = useState(false);
const { db } = useStorage();
// toggle private key
const showPassword = () => {
if (passwordInput === 'password') {
setPasswordInput('text');
} else {
setPasswordInput('password');
}
};
const {
register,
setError,
handleSubmit,
formState: { errors, isDirty, isValid },
} = useForm<FormValues>({ resolver });
const onSubmit = async (data: { [x: string]: string }) => {
setLoading(true);
if (data.password.length > 3) {
const dir = await appConfigDir();
const stronghold = await Stronghold.load(`${dir}/lume.stronghold`, data.password);
if (!db.secureDB) db.secureDB = stronghold;
// save privkey to secure storage
await db.secureSave(pubkey, privkey);
// redirect to next step
navigate('/auth/create/step-3', { replace: true });
} else {
setLoading(false);
setError('password', {
type: 'custom',
message: 'Password is required and must be greater than 3',
});
}
};
useEffect(() => {
// save current step, if user close app and reopen it
setStep('/auth/create/step-2');
}, []);
return (
<div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center">
<h1 className="text-xl font-semibold text-white">
Set password to secure your key
</h1>
</div>
<div className="flex flex-col gap-4">
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-3">
<div className="flex flex-col gap-1">
<div className="relative">
<input
{...register('password', { required: true })}
type={passwordInput}
className="relative h-11 w-full rounded-lg bg-white/10 px-3.5 py-1 text-center text-white !outline-none backdrop-blur-xl placeholder:text-white/50"
/>
<button
type="button"
onClick={() => showPassword()}
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 backdrop-blur-xl hover:bg-white/10"
>
{passwordInput === 'password' ? (
<EyeOffIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
) : (
<EyeOnIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
)}
</button>
</div>
<div className="text-sm text-white/50">
<p>
Password is use to secure your key store in local machine, when you move
to other clients, you just need to copy your private key as nsec or
hexstring
</p>
</div>
<span className="text-sm text-red-400">
{errors.password && <p>{errors.password.message}</p>}
</span>
</div>
<div className="flex items-center justify-center">
<button
type="submit"
disabled={!isDirty || !isValid}
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
>
{loading ? (
<>
<span className="w-5" />
<span>Creating...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : (
<>
<span className="w-5" />
<span>Continue</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</button>
</div>
</form>
</div>
</div>
);
}

View File

@@ -1,170 +0,0 @@
import { NDKKind } from '@nostr-dev-kit/ndk';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { AvatarUploader } from '@shared/avatarUploader';
import { BannerUploader } from '@shared/bannerUploader';
import { LoaderIcon } from '@shared/icons';
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
import { Image } from '@shared/image';
import { useOnboarding } from '@stores/onboarding';
import { useNostr } from '@utils/hooks/useNostr';
export function CreateStep3Screen() {
const navigate = useNavigate();
const setStep = useOnboarding((state) => state.setStep);
const [loading, setLoading] = useState(false);
const [picture, setPicture] = useState('https://void.cat/d/5VKmKyuHyxrNMf9bWSVPih');
const [banner, setBanner] = useState('');
const { publish } = useNostr();
const {
register,
handleSubmit,
formState: { isDirty, isValid },
} = useForm();
const onSubmit = async (data: { name: string; about: string; website: string }) => {
setLoading(true);
try {
const profile = {
...data,
name: data.name,
display_name: data.name,
bio: data.about,
website: data.website,
};
const event = await publish({
content: JSON.stringify(profile),
kind: NDKKind.Metadata,
tags: [],
});
if (event) {
navigate('/auth/onboarding', { replace: true });
}
} catch (e) {
console.log('error: ', e);
setLoading(false);
}
};
useEffect(() => {
// save current step, if user close app and reopen it
setStep('/auth/create/step-3');
}, []);
return (
<div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center">
<h1 className="text-xl font-semibold text-white">Create your profile</h1>
</div>
<div className="w-full overflow-hidden rounded-xl bg-white/10 backdrop-blur-xl">
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col">
<input type={'hidden'} {...register('picture')} value={picture} />
<input type={'hidden'} {...register('banner')} value={banner} />
<div className="relative">
<div className="relative h-44 w-full bg-white/10 backdrop-blur-xl">
{banner ? (
<Image
src={banner}
alt="user's banner"
className="h-full w-full object-cover"
/>
) : (
<div className="h-full w-full bg-black/50" />
)}
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
<BannerUploader setBanner={setBanner} />
</div>
</div>
<div className="mb-5 px-4">
<div className="relative z-10 -mt-7 h-14 w-14">
<Image
src={picture}
alt="user's avatar"
className="h-14 w-14 rounded-lg object-cover ring-2 ring-white/10"
/>
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
<AvatarUploader setPicture={setPicture} />
</div>
</div>
</div>
</div>
<div className="flex flex-col gap-4 px-4 pb-4">
<div className="flex flex-col gap-1">
<label
htmlFor="name"
className="text-sm font-semibold uppercase tracking-wider text-white/50"
>
Name *
</label>
<input
type={'text'}
{...register('name', {
required: true,
minLength: 4,
})}
spellCheck={false}
className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/50"
/>
</div>
<div className="flex flex-col gap-1">
<label
htmlFor="about"
className="text-sm font-semibold uppercase tracking-wider text-white/50"
>
Bio
</label>
<textarea
{...register('about')}
spellCheck={false}
className="relative h-20 w-full resize-none rounded-lg bg-white/10 px-3 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/50"
/>
</div>
<div className="flex flex-col gap-1">
<label
htmlFor="website"
className="text-sm font-semibold uppercase tracking-wider text-white/50"
>
Website
</label>
<input
type={'text'}
{...register('website', {
required: false,
})}
spellCheck={false}
className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/50"
/>
</div>
<button
type="submit"
disabled={!isDirty || !isValid}
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
>
{loading ? (
<>
<span className="w-5" />
<span>Creating...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : (
<>
<span className="w-5" />
<span>Continue</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</button>
</div>
</form>
</div>
</div>
);
}

View File

@@ -1,7 +0,0 @@
export function HardResetScreen() {
return (
<div>
<p>hard reset</p>
</div>
);
}

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