Compare commits

...

258 Commits

Author SHA1 Message Date
reya
7449000f5f chore: bump version 2024-05-23 08:01:04 +07:00
reya
dc7762ca11 fix: column lose state when navigate back 2024-05-23 08:00:33 +07:00
XIAO YU
3a3f960dde refactor: use borrowed references for adding and connecting relays (#191) 2024-05-23 07:08:39 +07:00
reya
12e066ff2e fix: web 2024-05-22 15:36:41 +07:00
reya
fe4f965ed5 chore: update website 2024-05-22 15:35:41 +07:00
reya
5d3f2264e9 chore: bump version 2024-05-22 13:26:08 +07:00
雨宮蓮
407fe40b67 refactor: account managements (#190)
* feat: add keyring-search

* feat: improve nostr connect
2024-05-22 13:24:58 +07:00
雨宮蓮
1f38eba2cc feat: immersive onboarding (#189)
* feat: change the default onboarding column

* feat: add newsfeed onboarding

* feat: add topic onboarding

* feat: add group onboarding

* chore: polish and format

* feat: rename foryou to topic

* fix: array
2024-05-22 10:44:19 +07:00
reya
9b5867f80c feat: improve tauri commands 2024-05-20 07:05:30 +07:00
reya
cac774a0c1 feat: small ui improve 2024-05-19 08:48:29 +07:00
reya
82689bf3c3 feat: add windows support for v4 2024-05-18 21:02:04 +07:00
reya
f60e438a64 feat: add custom titlebar 2024-05-18 19:07:34 +07:00
reya
ca06f2b6ed chore: clean up tauri commands 2024-05-18 14:59:34 +07:00
reya
99d9c70826 feat: improve event discovery 2024-05-18 08:16:05 +07:00
reya
60afbf090b chore: update to rust-nostr 0.31 2024-05-18 07:52:28 +07:00
reya
10ca4e6ff4 chore: bump version 2024-05-17 15:32:14 +07:00
reya
b0f387d029 fix: wrong permissions for dialog 2024-05-17 15:31:35 +07:00
reya
1a8f750640 fix: profile is not show on edit screen 2024-05-17 14:45:17 +07:00
reya
25523229a2 fix: copy to clipboard is not working properly 2024-05-16 14:24:55 +07:00
雨宮蓮
47835ed857 feat: improve ui for multi-account (#188) 2024-05-16 14:13:07 +07:00
reya
d84647bc6b chore: bump version 2024-05-15 13:58:39 +07:00
reya
7724eccd72 feat: improve tauri commands 2024-05-15 13:58:03 +07:00
reya
8ea2335225 chore: update deps 2024-05-15 10:50:35 +07:00
reya
b60d4db0df feat: improve multi-account switching 2024-05-15 10:14:21 +07:00
reya
f1e17ff3c4 fix: allow app auto restart after update 2024-05-13 15:36:47 +07:00
reya
32954f17b6 chore: bump version and update readme 2024-05-13 15:21:05 +07:00
雨宮蓮
cf70b0f882 fix: context issue in production (#187) 2024-05-13 15:18:23 +07:00
reya
135d0918b3 feat: add check for updates 2024-05-12 15:07:53 +07:00
reya
e1fbcf0460 chore: fix ci again 2024-05-12 08:57:57 +07:00
reya
99aaf3da82 fix: misconfigure in github actions 2024-05-12 08:54:24 +07:00
reya
3ef13e43f1 chore: update github ci 2024-05-12 08:38:23 +07:00
reya
8939196ae4 chore: bump version to 4.0 2024-05-12 08:20:52 +07:00
reya
571d4b4004 feat: improve search 2024-05-12 08:18:25 +07:00
reya
73f80f27fb feat: add basic relay management in rust 2024-05-11 12:28:07 +07:00
雨宮蓮
b46a5cf68f Merge pull request #186 from kasugamirai/chore
feat: Refactor code to improve error handling and readability
2024-05-10 07:13:32 +07:00
xy
8c0d03aed0 feat: Refactor code to improve error handling and readability 2024-05-09 19:09:55 +09:00
reya
777eb15b4f feat: onboarding 2024-05-09 15:06:42 +07:00
reya
c8e1b8b8bd feat: add unread notification badge to dock icon 2024-05-07 14:38:00 +07:00
reya
437cd71f7e feat: improve editor 2024-05-07 14:14:21 +07:00
reya
afb7c87fa3 feat: add bell 2024-05-07 08:29:58 +07:00
reya
c843626bca feat: add notification screen 2024-05-06 15:17:34 +07:00
reya
28337e5915 feat: improve dedup events 2024-05-04 08:29:52 +07:00
雨宮蓮
a4aef25adb final design (#184)
* feat: redesign

* feat: update other columns to new design

* chore: small fixes

* fix: better manage external webview

* feat: redesign note

* feat: update ui

* chore: update

* chore: update

* chore: polish ui

* chore: update auth ui

* feat: finalize note design

* chore: small fixes

* feat: add window management in rust

* chore: format

* feat: update ui for event screen

* feat: update event screen

* feat: final
2024-05-03 15:15:48 +07:00
reya
61d1f095d4 chore: clean up 2024-04-24 15:21:13 +07:00
雨宮蓮
f027eae52d feat: use negentropy (#182)
* feat: use negentropy

* chore: polish
2024-04-24 10:18:51 +07:00
雨宮蓮
174a3cc74e feat: add search window (NIP-50) (#181)
* feat: add search window
* chore: improve search ui
2024-04-23 15:34:08 +07:00
reya
c00a7749b4 feat: use nip44 to encrypt key-value store 2024-04-23 08:04:42 +07:00
Ren Amamiya
c755b8d137 feat: add ability change column name on the fly (#180)
Co-authored-by: reya <reya@lume.nu>
2024-04-22 14:33:14 +07:00
17766d29d6 chore: update deps 2024-04-22 13:18:34 +07:00
3b13dfeed8 chore: update github action 2024-04-20 18:13:36 +07:00
17ba79e01b chore: update app icon 2024-04-20 08:58:00 +07:00
bafad544e9 chore: update deps and bump version 2024-04-19 14:21:12 +07:00
89c36423ae feat: add nip46 2024-04-18 15:09:33 +07:00
cd31b99559 feat: improve official columns 2024-04-18 07:50:46 +07:00
f3c52237fa fix: privacy setting is not working 2024-04-17 13:50:50 +07:00
413d8d82df feat: finish settings screen 2024-04-16 15:28:38 +07:00
2eb2010d43 chore: upgrade deps 2024-04-16 08:55:36 +07:00
94d400cab2 feat: support nip-36 2024-04-16 07:49:44 +07:00
09b143cb08 feat: auto resize mini webview when main webview resized 2024-04-15 13:30:55 +07:00
e3ede34108 feat: revert to sqlite 2024-04-14 09:05:46 +07:00
ed6aca41ea feat: add new spinner component 2024-04-13 15:01:27 +07:00
89f577fbef feat: upgrade to rust nostr 0.30.0 and migrate to nostrdb 2024-04-13 08:53:31 +07:00
a14aeaeb55 feat: add empty state and polish trending column 2024-04-13 08:30:58 +07:00
reya
35cf0abda4 fix: support windows 2024-04-12 18:35:19 +07:00
8a7b246315 chore: update deps 2024-04-11 07:47:37 +07:00
Ren Amamiya
d98c6d0709 Merge pull request #175 from lumehq/v4/beta
Prepare for v4 - beta
2024-04-11 07:45:43 +07:00
bda20e8fe8 chore: small updates 2024-04-11 07:44:41 +07:00
c342daecc8 feat: improve 2024-04-10 14:11:05 +07:00
5e6692cd6d feat: re add group column 2024-04-09 14:05:50 +07:00
420be77b5c feat: add nstore 2024-04-07 15:11:20 +07:00
999073f84c feat: readd for you column 2024-04-04 13:47:15 +07:00
174b28f1a7 chore: update deps 2024-04-03 08:01:40 +07:00
763cb10e85 chore: clean up 2024-04-03 07:29:46 +07:00
89bb8d88f6 feat: settings screens 2024-04-02 13:19:26 +07:00
09aa2ecafc feat: re-add trending column 2024-03-30 10:50:45 +07:00
7271e9ea87 feat: add custom traffic light inset for macos 2024-03-29 13:26:35 +07:00
a02e496b29 chore: update tauri 2024-03-29 07:45:37 +07:00
cbbf5eaf50 feat: add create account flow 2024-03-28 15:12:43 +07:00
d3fa59d2b1 chore: polish 2024-03-27 08:36:34 +07:00
aa23e39334 feat: update new icons 2024-03-26 14:58:06 +07:00
c8e2018fd0 feat: update tray icon 2024-03-26 13:08:03 +07:00
6e489f1c49 wip: onboarding 2024-03-25 14:22:06 +07:00
a49b88ab35 Merge branch 'main' into v4/beta 2024-03-23 07:24:58 +07:00
31839531ea feat: improve open store column 2024-03-22 15:25:53 +07:00
ec0f3fabc0 feat: improve column management 2024-03-22 11:28:54 +07:00
dd7155a3a6 feat: update default column informations 2024-03-21 14:53:08 +07:00
cb565ff35b feat: readd default columns 2024-03-20 15:12:54 +07:00
5d59040224 chore: follow up 2024-03-19 15:36:20 +07:00
7fabf949c6 feat: move column manager to rust 2024-03-19 14:56:24 +07:00
ea5120e2f0 chore: upgrade tauri and bump version 2024-03-19 12:46:19 +07:00
14f07dfea8 feat: add lume store screen 2024-03-19 08:21:16 +07:00
Ren Amamiya
1de48cc640 Merge pull request #174 from fernandolguevara/minors/styling-scrollbar
feature: make scrollbar nicer
2024-03-19 07:10:28 +07:00
Fernando López Guevara
f72eb456e8 feature: make scrollbar nicer 2024-03-18 09:42:25 -03:00
05b52564e0 feat: column manager 2024-03-18 10:50:08 +07:00
c8e014f33e chore: clean up 2024-03-16 18:45:54 +07:00
46cc01e0ee feat: child webview 2024-03-16 15:05:06 +07:00
16e6d234e5 feat: space 2024-03-14 14:22:41 +07:00
3005d27403 feat: update fetch opg function 2024-03-13 10:24:45 +07:00
1be84f3139 chore: release v4.0.0-alpha+2 2024-03-13 09:40:30 +07:00
Ren Amamiya
04bde7dd43 Merge pull request #172 from lumehq/v4/functions
Some polish for v4
2024-03-13 09:39:57 +07:00
f1504d99ac feat: add mock tray menu 2024-03-13 09:19:25 +07:00
e928f2ee37 feat: polish 2024-03-13 08:40:18 +07:00
Ren Amamiya
ccab78ca11 Merge pull request #171 from lumehq/v4/zap
Add zap to v4
2024-03-11 15:39:35 +07:00
5b13c3e45c feat: auto close window if zapp successfully 2024-03-11 15:38:54 +07:00
e6b97ab9ae feat: add zap 2024-03-11 15:36:17 +07:00
bb6badfed6 feat: add zap setup screen 2024-03-10 16:39:23 +07:00
Ren Amamiya
0c4b309a11 Merge pull request #170 from lumehq/v4/home-screen
v4/home-screen
2024-03-09 10:09:29 +07:00
f4400d711f feat: improve home screen 2024-03-09 10:08:50 +07:00
a3e46aa96b fix: crash when database is not exist 2024-03-08 14:47:36 +07:00
a4fdcfdf0b feat: add local and global newsfeeds 2024-03-07 15:39:43 +07:00
25d07303a3 fix: build error in windows and linux 2024-03-06 15:32:51 +07:00
0e1e7524c9 chore: bump version 2024-03-06 15:05:17 +07:00
Ren Amamiya
32d770eb91 Merge pull request #169 from lumehq/v4/onboarding
Add basic onboarding dialog to v4
2024-03-06 15:00:07 +07:00
e93cbf78d5 feat: add npub to file associations 2024-03-06 14:40:55 +07:00
71a2290b8f feat: add backup dialog 2024-03-06 14:40:00 +07:00
95294a80cb feat: simplify account management 2024-03-06 11:11:24 +07:00
8eaf47f6d2 feat: add login dialog 2024-03-06 09:42:44 +07:00
86183d799a fix: package version in windows 2024-03-02 17:59:36 +07:00
2dbfff3c17 chore: release v4.0.0-alpha.0 2024-03-02 16:08:01 +07:00
Ren Amamiya
d1f5c372ae Merge pull request #166 from lumehq/feat/rust-nostr
Rust Nostr
2024-03-02 15:32:13 +07:00
13281455ed chore: refer old branch in readme 2024-03-02 15:31:41 +07:00
9cfb32be4d feat: update readme 2024-03-02 15:29:45 +07:00
cb0672c22a Merge branch 'main' into feat/rust-nostr 2024-03-02 15:22:11 +07:00
ca0e041731 feat: improve 2024-03-02 15:21:28 +07:00
cfcb9bc6ed feat: improve ui 2024-02-29 13:02:16 +07:00
09df8672d0 feat: add zap command 2024-02-27 15:37:49 +07:00
2403231ac4 feat: add user screen 2024-02-27 13:42:59 +07:00
98ef1927f2 feat: editor 2024-02-26 15:10:42 +07:00
63db8b1423 chore: clean up 2024-02-25 15:52:47 +07:00
2c8dd71792 feat: add simple tray icon 2024-02-25 09:06:23 +07:00
reya
fe73e1428b feat: upgrade tauri 2024-02-24 18:26:20 +07:00
88a6c3c81f feat: account switcher 2024-02-24 14:41:57 +07:00
84584a4d1f feat: add editor screen 2024-02-23 14:56:24 +07:00
64286aa354 wip 2024-02-22 14:01:29 +07:00
e9ce932646 chore: remove storage layer 2024-02-22 08:58:45 +07:00
Ren Amamiya
4c28b4879c chore: update readme 2024-02-21 15:04:30 +07:00
9127d5c5ea feat: add more metadata commands 2024-02-21 14:29:54 +07:00
090e54ec5a feat: add contact list commands 2024-02-20 14:46:21 +07:00
d0c9f93ebb wip: small updates 2024-02-20 10:26:19 +07:00
e2cdc5b576 feat: account selection screen 2024-02-19 14:13:33 +07:00
bfe35ad885 wip: multi accounts 2024-02-19 09:58:27 +07:00
0f06522c28 wip: refactor 2024-02-17 16:01:53 +07:00
f47eba5af7 feat: upgrade to rust nostr 0.28 2024-02-17 09:29:37 +07:00
f28a7ae82f wip: update design 2024-02-16 14:11:49 +07:00
296b11b7b8 wip: basic support multi windows 2024-02-16 09:58:07 +07:00
a0d9a729dd feat: add tauri devtools 2024-02-15 14:41:32 +07:00
1de8c7240d wip: desktop2 2024-02-15 14:32:40 +07:00
70126ef1b3 Merge branch 'main' into feat/rust-nostr 2024-02-15 12:48:20 +07:00
cdf29f8a54 wip: update 2024-02-15 12:47:15 +07:00
6171b9bed1 wip: tired... 2024-02-14 15:51:06 +07:00
60fd09000b wip: new design 2024-02-14 08:29:24 +07:00
4292def206 feat: add more nostr commands 2024-02-13 09:36:11 +07:00
90f149b09c chore: clean up 2024-02-13 08:57:24 +07:00
ed52105c02 wip: migrate to desktop2 2024-02-12 20:04:45 +07:00
1950cb59a2 wip: migrate to desktop2 2024-02-12 09:08:35 +07:00
Ren Amamiya
9753e6d6b4 Merge pull request #167 from anthony-robin/french-translations
Add french translations 🇫🇷
2024-02-12 07:55:46 +07:00
Anthony Robin
3488f05960 Add french translations 🇫🇷
This PR adds support for french translations into Lume.
2024-02-11 17:09:59 +01:00
c809ab6b4e feat: add desktop2 2024-02-11 09:30:18 +07:00
Ren Amamiya
c6674c4a2d Merge pull request #165 from higedamc/main
Update ja.json
2024-02-11 08:10:36 +07:00
35c5b5fb78 chore: fix build 2024-02-10 11:19:18 +07:00
739ba63e6c wip: migrate frontend to new backend 2024-02-09 15:33:27 +07:00
ec78cf8bf7 feat: migrate frontend to new backend 2024-02-08 21:24:08 +07:00
17052aeeaa feat: add new account management 2024-02-08 17:17:45 +07:00
d7bbda6e7b fix: remove native db 2024-02-07 14:26:58 +07:00
6a08c1de10 feat: add all nostr events command 2024-02-07 14:04:57 +07:00
3c4bd39384 feat: update rust nostr 2024-02-06 19:28:46 +07:00
a4069dae99 chore: clean up 2024-02-06 09:15:20 +07:00
HigeMan
55cd556cd6 Update ja.json 2024-02-06 08:45:17 +09:00
a21da11a91 feat: add desktop2 2024-02-05 14:18:27 +07:00
08fa7de01d feat: add check active account before init client 2024-02-05 13:36:10 +07:00
2a58326cd1 chore: update dependencies 2024-02-05 08:30:06 +07:00
8a17c36a5b feat: upgrade to tauri beta 2024-02-05 08:14:58 +07:00
7bd6f6a8db fix: remove surrealdb and clean up 2024-02-05 07:46:41 +07:00
3ba870be4b feat: add verify signer 2024-02-04 10:46:50 +07:00
bd2b6a3759 feat: add surreal db 2024-02-03 11:24:08 +07:00
a3a8f57bfc feat: add more nostr commands 2024-02-03 09:13:17 +07:00
fd393a4d30 feat: add basic nostr connection 2024-02-02 09:09:14 +07:00
e8bd48e51b feat: add rust nostr 2024-02-01 12:54:25 +07:00
0191180f31 chore: rename 2024-02-01 09:15:25 +07:00
60ed56b1b9 chore: bump version 2024-02-01 08:40:27 +07:00
Ren Amamiya
da722afed3 Merge pull request #154 from luminous-devs/next
v3.0.1
2024-02-01 08:36:21 +07:00
d8eb51e49c Revert "chore: update dependencies"
This reverts commit c700a45ab6.
2024-02-01 08:18:51 +07:00
c700a45ab6 chore: update dependencies 2024-02-01 07:59:25 +07:00
b806a34edb chore: remove submodule and clean up flatpak 2024-01-31 14:31:16 +07:00
21989e6fa5 fix: minor improves 2024-01-31 14:20:03 +07:00
0539c5649d feat: add waifu column and fix wrong package 2024-01-31 11:04:47 +07:00
ad488ff72d feat: add global column 2024-01-31 09:12:26 +07:00
02e0309a41 feat: refactor rust commands 2024-01-31 08:20:39 +07:00
b7f4af7883 fix: respect NIP spec 2024-01-31 07:29:47 +07:00
cc48a4f36b chore: update dependencies 2024-01-30 15:33:24 +07:00
46ed3330fc feat: add hover card to user 2024-01-30 14:45:46 +07:00
1fa1872ca6 feat: add trending notes column 2024-01-30 14:22:56 +07:00
Ren Amamiya
c389a23365 Merge pull request #153 from luminous-devs/feat/improve-updater
feat: Add new update bubble to navigation
2024-01-30 13:18:52 +07:00
eaf9bda077 feat: add new update notify to navigation 2024-01-30 13:16:39 +07:00
84a248a5a9 chore: fix typos 2024-01-30 10:01:45 +07:00
Ren Amamiya
711c1d561a Merge pull request #151 from luminous-devs/feat/multi-lang
Add support for multi-languages
2024-01-30 09:12:06 +07:00
21210b4336 feat: migrate activity screen to i18n 2024-01-30 09:10:26 +07:00
3bd480b75e feat: optimize locale loader 2024-01-30 08:55:49 +07:00
2b19650e46 feat: migrate onboarding to i18n 2024-01-30 08:13:12 +07:00
23482531c5 feat: migrate settings screen to i18n 2024-01-29 14:57:00 +07:00
cfda9ba899 feat: migrate ui components to i18n 2024-01-29 13:38:22 +07:00
698bd78684 feat: migrate note component to i18n 2024-01-29 10:22:55 +07:00
b97676dd3e feat: add translation to relay screen 2024-01-29 09:30:33 +07:00
25ae4f2201 feat: add multi-lang 2024-01-29 09:12:44 +07:00
Ren Amamiya
59435ccd13 Merge pull request #150 from jurraca/add-libappindicator
add libappindicator to runtime libs in nix shell
2024-01-28 17:35:40 +07:00
jurraca
e81912c5e9 add libappindicator to runtime libs in nix shell 2024-01-28 08:24:25 +00:00
af1b4e60d3 web: update download link 2024-01-28 08:45:04 +07:00
648cbf6f80 chore: fix ci 2024-01-28 08:05:36 +07:00
7b06a82ee7 chore: bump version and fix gh action 2024-01-28 07:40:08 +07:00
Ren Amamiya
d18de93c60 Merge pull request #148 from luminous-devs/feat/improve-perf
Improve overall performance
2024-01-27 20:01:55 +07:00
df15eb7a03 Merge branch 'main' into feat/improve-perf 2024-01-27 17:56:50 +07:00
Ren Amamiya
06674df6cc Merge pull request #137 from kogeletey/feat/package-flatpak
Package lume in flatpak
2024-01-27 17:34:47 +07:00
reya
8295625a44 feat: polish on windows 2024-01-27 15:53:19 +07:00
b11e2a4291 fix: update mention popup style 2024-01-27 11:16:17 +07:00
353c18bb76 feat: update ark provider 2024-01-27 09:08:41 +07:00
kogeletey
02b0c9e48a merge remote-tracking branch 'origin/main' into feat/package-flatpak 2024-01-26 19:49:27 +03:00
kogeletey
ff73c8ac88 feat: supporting hash of github actions cache 2024-01-26 19:37:12 +03:00
Ren Amamiya
bc48391a1a Merge pull request #147 from luminous-devs/feat/improve-design
Improve overal design
2024-01-26 14:21:27 +07:00
b0a443c002 feat: polish 2024-01-26 14:15:25 +07:00
bef1f136ad chore: bump version 2024-01-26 12:43:19 +07:00
9ba584bf14 feat: improve onboarding 2024-01-26 10:17:23 +07:00
kogeletey
43509fc943 feat: supported flatpak version v3 2024-01-25 15:00:08 +03:00
kogeletey
4a99eb94e2 feat: supported flatpak 2024-01-25 14:59:48 +03:00
74426e13c8 wip: improve onboarding 2024-01-25 15:25:40 +07:00
bd45c36072 feat: fix typos and other stuffs 2024-01-25 09:49:04 +07:00
c13aefcd15 feat: update dependencies 2024-01-25 08:35:17 +07:00
167caee8bc feat: improve ui contrast 2024-01-25 08:14:25 +07:00
d527078d5c feat: redesign column header 2024-01-24 15:08:55 +07:00
Ren Amamiya
763ace5ddf Merge pull request #145 from luminous-devs/feat/search
feat: add search based on NIP-50
2024-01-24 13:36:17 +07:00
057c57b70f feat: add empty state to search dialog 2024-01-24 13:34:27 +07:00
cb71786ac1 feat: add basic search dialog 2024-01-23 13:07:24 +07:00
Ren Amamiya
67afeac198 Merge pull request #143 from luminous-devs/apps/web
Add landing page
2024-01-22 14:41:24 +07:00
f4ee25de8e feat: add landing page 2024-01-22 14:40:39 +07:00
445a218a9e Merge branch 'main' into next 2024-01-21 09:54:47 +07:00
f09139ffbe final 2024-01-21 09:43:46 +07:00
446721729b feat: polish 2024-01-20 20:14:26 +07:00
reya
e0250d7f5c chore: fix some issues on windows 2024-01-20 10:15:27 +07:00
9fcdac4edb feat: add suggest screen 2024-01-20 14:51:13 +07:00
b726ae3c7c feat: add for you column 2024-01-20 09:06:00 +07:00
a3460418f6 feat: add interest screen to onboarding 2024-01-19 14:53:26 +07:00
f65175f11e feat: polish 2024-01-19 08:24:13 +07:00
16efd495a0 chore: clean up 2024-01-19 07:45:28 +07:00
ed6423e4aa feat: polish 2024-01-18 15:09:16 +07:00
0e9418949b feat: add instant zap 2024-01-18 13:56:35 +07:00
240fe8bc7c feat: fix relay manager 2024-01-18 09:41:53 +07:00
c3482cddd8 feat: improve zap 2024-01-18 08:22:35 +07:00
d13e7b3ef6 feat: polish 2024-01-18 07:37:40 +07:00
47800bd2ff chore: upgrade tauri 2024-01-17 13:07:01 +07:00
c0305db5fc chore: update and fix dependencies 2024-01-17 12:39:04 +07:00
0b745cb40e feat: add reply form 2024-01-17 12:24:04 +07:00
a20f5ca15d feat: polish 2024-01-17 09:30:32 +07:00
c29b4e173e feat: polish 2024-01-17 08:50:43 +07:00
33dd8b1d8a feat: better error handler 2024-01-16 19:56:07 +07:00
1503d90bd5 fix: tauri cache 2024-01-16 14:51:06 +07:00
6581ffb92b feat: polish 2024-01-16 14:49:00 +07:00
939dfd9cc1 feat: fix errros 2024-01-16 08:37:42 +07:00
Ren Amamiya
a98ffd4887 Merge pull request #135 from fernandolguevara/feat-open-notes-edit-rail-title
feat(rail): edit title & open user notes
2024-01-07 07:39:18 +07:00
Fernando López Guevara
2e23b3ae06 feat(rail): edit title & open user notes 2024-01-06 11:38:34 -03:00
613 changed files with 46039 additions and 23077 deletions

44
.dockerignore Normal file
View File

@@ -0,0 +1,44 @@
# Dependencies
**/node_modules
.pnp
.pnp.js
# Local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Testing
coverage
# Turbo
.turbo
# Vercel
.vercel
# Build Outputs
**/.next/
**/out/
**/build
**/dist
**/target
# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Misc
.DS_Store
*.pem
# Unnecessary files
**/.git/
.github/
flatpak/*.xml
flatpak/*.desktop
flatpak/*.yml

72
.github/workflows/flatpak-bundle.yml vendored Normal file
View File

@@ -0,0 +1,72 @@
name: Flatpak
on: workflow_dispatch
jobs:
prepare-repo:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: 'recursive'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: cache of container
id: cache-container
uses: actions/cache@v3
with:
path: prepare-dist
key: ${{ runner.os }}-container-${{ hashFiles('prepare-dist') }}
- name: Run latest-tag
id: latest-tag
uses: oprypin/find-latest-tag@v1
with:
repository:
lumehq/lume
#FIXME: lumehq after merged fix, now it just won't find tags
# repository: ${{ github.repository }}
- name: Build container
# if: steps.cache-container.outputs.cache-hit != 'true'
run: |
docker buildx build -t flatpak-prepare-lume --build-arg=${{steps.latest-tag.outputs.tag}} --rm --output=. --target=final -f flatpak/Containerfile .
- name: Copy flatpak files content
run: |
cp -r flatpak/*.xml flatpak/*.desktop flatpak/*.yml prepare-dist
- uses: actions/upload-artifact@v4
with:
name: repo-dist
path: prepare-dist
flatpak:
name: flatpak-bundle
needs: prepare-repo
runs-on: ubuntu-latest
container:
image: bilelmoussaoui/flatpak-github-actions:gnome-45
options: --privileged
steps:
- uses: actions/download-artifact@v4
with:
name: repo-dist
- uses: actions/checkout@v4
with:
repository: flathub/shared-modules
path: shared-modules
- uses: flatpak/flatpak-github-actions/flatpak-builder@v6
with:
bundle: lume.flatpak
manifest-path: nu.lume.Lume.yml
restore-cache: false
# cache-key: flatpak-builder-${{ github.sha }}
- name: Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
append_body: true
files: lume.flatpak
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: geekyeggo/delete-artifact@v4
with:
name: repo-dist
token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,53 +1,54 @@
name: 'Publish'
name: "Publish"
on: workflow_dispatch
env:
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
RUSTFLAGS: '-W unreachable-pub -W rust-2021-compatibility'
RUSTFLAGS: "-W unreachable-pub -W rust-2021-compatibility"
jobs:
publish-tauri:
strategy:
fail-fast: false
matrix:
settings:
- platform: 'macos-latest'
args: '--target universal-apple-darwin'
- platform: 'ubuntu-22.04'
args: ''
- platform: 'windows-latest'
args: '--target x86_64-pc-windows-msvc'
runs-on: ${{ matrix.settings.platform }}
include:
- platform: "macos-latest" # for Arm based macs (M1 and above).
args: "--target aarch64-apple-darwin"
- platform: "macos-latest" # for Intel based macs.
args: "--target x86_64-apple-darwin"
#- platform: 'ubuntu-22.04'
# args: ''
#- platform: 'windows-latest'
# args: '--target x86_64-pc-windows-msvc'
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v3
- name: setup node
uses: actions/setup-node@v3
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 18
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin
- name: install dependencies (ubuntu only)
if: matrix.settings.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y build-essential libssl-dev javascriptcoregtk-4.1 libayatana-appindicator3-dev libsoup-3.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev webkit2gtk-4.1 librsvg2-dev patchelf
- name: Install pnpm
node-version: "lts/*"
- name: Install PNPM
uses: pnpm/action-setup@v2
with:
version: 8.x.x
run_install: false
- name: Setup node and cache for package data
uses: actions/setup-node@v3
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
node-version: 'lts/*'
cache: 'pnpm'
cache-dependency-path: pnpm-lock.yaml
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- run: pnpm install
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: Install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y build-essential libssl-dev javascriptcoregtk-4.1 libayatana-appindicator3-dev libsoup-3.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev webkit2gtk-4.1 librsvg2-dev patchelf
- name: Install frontend dependencies
run: pnpm install
- uses: tauri-apps/tauri-action@dev
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -62,9 +63,9 @@ jobs:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with:
tagName: v__VERSION__
releaseName: 'v__VERSION__'
releaseBody: 'See the assets to download this version and install.'
releaseName: "v__VERSION__"
releaseBody: "See the assets to download this version and install."
releaseDraft: true
prerelease: false
args: ${{ matrix.settings.args }}
includeDebug: true
args: ${{ matrix.args }}
includeDebug: false

16
.gitignore vendored
View File

@@ -13,26 +13,26 @@ node_modules
.env.production.local
# Testing
coverage
coverage/
# Turbo
.turbo
.turbo/
# Vercel
.vercel
.vercel/
# Build Outputs
.next/
out/
build
dist
build/
dist/
# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
*.log.*
# Misc
.DS_Store
*.pem
.vscode/
ndb/

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "modules/depot"]
path = modules/depot
url = https://github.com/luminous-devs/depot.git

View File

@@ -1,25 +1,31 @@
### Introduction
## Introduction
Lume is a nostr client
Lume is a Nostr client for desktop include Linux, Windows and macOS. It is free and open source, you can look at source code on Github. Lume is actively improving the app and adding new features, you can expect new update every month.
### Usage
## Usage
Download Lume for your platform here: [https://github.com/luminous-devs/lume/releases](https://github.com/luminous-devs/lume/releases)
Download Lume v4 for your platform here: [https://github.com/lumehq/lume/releases](https://github.com/lumehq/lume/releases)
Supported platform: macOS, Windows and Linux
Supported platform: macOS. Windows and Linux are coming soon.
### Prerequisites
Windows and Linux are availabel on v3 and below.
- PNPM or Bun (experiment)
## Prerequisites
- Tauri: https://tauri.app/v1/guides/getting-started/prerequisites#setting-up-macos
- Node.js >= 18: https://nodejs.org/en
### Develop
- Rust: https://rustup.rs/
- PNPM: https://pnpm.io
- Tauri v2: https://beta.tauri.app/guides/prerequisites/
## Develop
Clone project
```
git clone https://github.com/luminous-devs/lume.git && cd lume
git clone https://github.com/lumehq/lume.git && cd lume
```
Install packages
@@ -40,7 +46,7 @@ Generate production build
pnpm tauri build
```
#### Nix
## Nix
Requirements:
@@ -48,3 +54,13 @@ Requirements:
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.
## License
Copyright (C) 2023-2024 Ren Amamiya & other Lume contributors (see AUTHORS.md)
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/.

View File

@@ -1,13 +0,0 @@
<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 w-screen h-screen overflow-hidden font-sans antialiased cursor-default select-none text-neutral-950 dark:text-neutral-50"
>
<div id="root"></div>
<script type="module" src="./src/main.jsx"></script>
</body>
</html>

View File

@@ -1,86 +0,0 @@
{
"name": "lume",
"private": true,
"version": "3.0.0",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"dependencies": {
"@columns/antenas": "workspace:^",
"@columns/default": "workspace:^",
"@columns/group": "workspace:^",
"@columns/hashtag": "workspace:^",
"@columns/thread": "workspace:^",
"@columns/timeline": "workspace:^",
"@columns/user": "workspace:^",
"@getalby/sdk": "^3.2.3",
"@lume/ark": "workspace:^",
"@lume/icons": "workspace:^",
"@lume/storage": "workspace:^",
"@lume/ui": "workspace:^",
"@lume/utils": "workspace:^",
"@nostr-dev-kit/ndk": "^2.3.2",
"@radix-ui/react-accordion": "^1.1.2",
"@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.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-select": "^2.0.0",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-query": "^5.17.9",
"@tauri-apps/api": "2.0.0-alpha.13",
"@tauri-apps/plugin-autostart": "2.0.0-alpha.5",
"@tauri-apps/plugin-clipboard-manager": "2.0.0-alpha.5",
"@tauri-apps/plugin-dialog": "2.0.0-alpha.5",
"@tauri-apps/plugin-fs": "2.0.0-alpha.6",
"@tauri-apps/plugin-http": "2.0.0-alpha.6",
"@tauri-apps/plugin-notification": "2.0.0-alpha.5",
"@tauri-apps/plugin-os": "2.0.0-alpha.6",
"@tauri-apps/plugin-process": "2.0.0-alpha.5",
"@tauri-apps/plugin-shell": "2.0.0-alpha.5",
"@tauri-apps/plugin-sql": "2.0.0-alpha.5",
"@tauri-apps/plugin-updater": "2.0.0-alpha.5",
"@tauri-apps/plugin-upload": "2.0.0-alpha.5",
"@vidstack/react": "^1.9.8",
"framer-motion": "^10.18.0",
"jotai": "^2.6.1",
"minidenticons": "^4.2.0",
"nanoid": "^5.0.4",
"nostr-fetch": "^0.15.0",
"nostr-tools": "^1.17.0",
"qrcode.react": "^3.1.0",
"react": "^18.2.0",
"react-currency-input-field": "^3.6.14",
"react-dom": "^18.2.0",
"react-hook-form": "^7.49.3",
"react-router-dom": "^6.21.2",
"smol-toml": "^1.1.3",
"sonner": "^1.3.1",
"unique-names-generator": "^4.7.1",
"virtua": "^0.20.4"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/node": "^20.11.0",
"@types/react": "^18.2.47",
"@types/react-dom": "^18.2.18",
"@vitejs/plugin-react-swc": "^3.5.0",
"autoprefixer": "^10.4.16",
"cross-env": "^7.0.3",
"encoding": "^0.1.13",
"postcss": "^8.4.33",
"tailwind-merge": "^2.2.0",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3",
"vite": "^5.0.11",
"vite-plugin-top-level-await": "^1.4.1",
"vite-tsconfig-paths": "^4.2.3"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 416 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 986 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 473 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 KiB

View File

@@ -1,41 +0,0 @@
/* Vidstack */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer utilities {
.break-p {
word-break: break-word;
word-wrap: break-word;
overflow-wrap: break-word;
}
.prose :where(iframe):not(:where([class~='not-prose'] *)) {
@apply w-full h-auto mx-auto aspect-video;
}
}
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;
}

View File

@@ -1,28 +0,0 @@
import { ColumnProvider, LumeProvider } from "@lume/ark";
import { StorageProvider } from "@lume/storage";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Toaster } from "sonner";
import Router from "./router";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000), // 10 seconds
},
},
});
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Toaster position="top-center" theme="system" closeButton />
<StorageProvider>
<LumeProvider>
<ColumnProvider>
<Router />
</ColumnProvider>
</LumeProvider>
</StorageProvider>
</QueryClientProvider>
);
}

View File

@@ -1,8 +0,0 @@
import { createRoot } from "react-dom/client";
import App from "./app";
import "./app.css";
const container = document.getElementById("root");
const root = createRoot(container);
root.render(<App />);

View File

@@ -1,323 +0,0 @@
import { useArk } from "@lume/ark";
import { LoaderIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { AppLayout, AuthLayout, HomeLayout, SettingsLayout } from "@lume/ui";
import { fetch } from "@tauri-apps/plugin-http";
import {
RouterProvider,
createBrowserRouter,
defer,
redirect,
} from "react-router-dom";
import { ErrorScreen } from "./routes/error";
export default function Router() {
const ark = useArk();
const storage = useStorage();
const router = createBrowserRouter([
{
element: <AppLayout platform={storage.platform} />,
children: [
{
path: "/",
element: <HomeLayout />,
errorElement: <ErrorScreen />,
loader: async () => {
if (!ark.account) return redirect("auth");
return null;
},
children: [
{
index: true,
async lazy() {
const { HomeScreen } = await import("./routes/home");
return { Component: HomeScreen };
},
},
{
path: "settings",
element: <SettingsLayout />,
children: [
{
index: true,
async lazy() {
const { UserSettingScreen } = await import(
"./routes/settings"
);
return { Component: UserSettingScreen };
},
},
{
path: "edit-profile",
async lazy() {
const { EditProfileScreen } = await import(
"./routes/settings/editProfile"
);
return { Component: EditProfileScreen };
},
},
{
path: "edit-contact",
async lazy() {
const { EditContactScreen } = await import(
"./routes/settings/editContact"
);
return { Component: EditContactScreen };
},
},
{
path: "general",
async lazy() {
const { GeneralSettingScreen } = await import(
"./routes/settings/general"
);
return { Component: GeneralSettingScreen };
},
},
{
path: "backup",
async lazy() {
const { BackupSettingScreen } = await import(
"./routes/settings/backup"
);
return { Component: BackupSettingScreen };
},
},
{
path: "advanced",
async lazy() {
const { AdvancedSettingScreen } = await import(
"./routes/settings/advanced"
);
return { Component: AdvancedSettingScreen };
},
},
{
path: "nwc",
async lazy() {
const { NWCScreen } = await import("./routes/settings/nwc");
return { Component: NWCScreen };
},
},
{
path: "about",
async lazy() {
const { AboutScreen } = await import(
"./routes/settings/about"
);
return { Component: AboutScreen };
},
},
],
},
],
},
{
path: "activity",
async lazy() {
const { ActivityScreen } = await import("./routes/activty");
return { Component: ActivityScreen };
},
children: [
{
path: ":id",
async lazy() {
const { ActivityIdScreen } = await import(
"./routes/activty/id"
);
return { Component: ActivityIdScreen };
},
},
],
},
{
path: "relays",
async lazy() {
const { RelaysScreen } = await import("./routes/relays");
return { Component: RelaysScreen };
},
children: [
{
index: true,
async lazy() {
const { RelayGlobalScreen } = await import(
"./routes/relays/global"
);
return { Component: RelayGlobalScreen };
},
},
{
path: "follows",
async lazy() {
const { RelayFollowsScreen } = await import(
"./routes/relays/follows"
);
return { Component: RelayFollowsScreen };
},
},
{
path: ":url",
loader: async ({ request, params }) => {
return defer({
relay: fetch(`https://${params.url}`, {
method: "GET",
headers: {
Accept: "application/nostr+json",
},
signal: request.signal,
}).then((res) => res.json()),
});
},
async lazy() {
const { RelayUrlScreen } = await import("./routes/relays/url");
return { Component: RelayUrlScreen };
},
},
],
},
{
path: "depot",
children: [
{
index: true,
loader: () => {
const depot = storage.checkDepot();
if (!depot) return redirect("/depot/onboarding/");
return null;
},
async lazy() {
const { DepotScreen } = await import("./routes/depot");
return { Component: DepotScreen };
},
},
{
path: "onboarding",
async lazy() {
const { DepotOnboardingScreen } = await import(
"./routes/depot/onboarding"
);
return { Component: DepotOnboardingScreen };
},
},
],
},
],
},
{
path: "auth",
element: <AuthLayout platform={storage.platform} />,
errorElement: <ErrorScreen />,
children: [
{
index: true,
async lazy() {
const { WelcomeScreen } = await import("./routes/auth/welcome");
return { Component: WelcomeScreen };
},
},
{
path: "create",
loader: async () => {
return await ark.getOAuthServices();
},
async lazy() {
const { CreateAccountScreen } = await import(
"./routes/auth/create"
);
return { Component: CreateAccountScreen };
},
},
{
path: "login",
async lazy() {
const { LoginScreen } = await import("./routes/auth/login");
return { Component: LoginScreen };
},
},
{
path: "login-key",
async lazy() {
const { LoginWithKey } = await import("./routes/auth/login-key");
return { Component: LoginWithKey };
},
},
{
path: "login-nsecbunker",
async lazy() {
const { LoginWithNsecbunker } = await import(
"./routes/auth/login-nsecbunker"
);
return { Component: LoginWithNsecbunker };
},
},
{
path: "login-oauth",
async lazy() {
const { LoginWithOAuth } = await import(
"./routes/auth/login-oauth"
);
return { Component: LoginWithOAuth };
},
},
{
path: "onboarding",
async lazy() {
const { OnboardingScreen } = await import(
"./routes/auth/onboarding"
);
return { Component: OnboardingScreen };
},
},
{
path: "tutorials/note",
async lazy() {
const { TutorialNoteScreen } = await import(
"./routes/auth/tutorials/note"
);
return { Component: TutorialNoteScreen };
},
},
{
path: "tutorials/widget",
async lazy() {
const { TutorialWidgetScreen } = await import(
"./routes/auth/tutorials/widget"
);
return { Component: TutorialWidgetScreen };
},
},
{
path: "tutorials/posting",
async lazy() {
const { TutorialPostingScreen } = await import(
"./routes/auth/tutorials/posting"
);
return { Component: TutorialPostingScreen };
},
},
{
path: "tutorials/finish",
async lazy() {
const { TutorialFinishScreen } = await import(
"./routes/auth/tutorials/finish"
);
return { Component: TutorialFinishScreen };
},
},
],
},
]);
return (
<RouterProvider
router={router}
fallbackElement={
<div className="flex items-center justify-center w-full h-full">
<LoaderIcon className="w-6 h-6 animate-spin" />
</div>
}
future={{ v7_startTransition: true }}
/>
);
}

View File

@@ -1,28 +0,0 @@
import { User } from "@lume/ark";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { Link } from "react-router-dom";
export function ActivityRepost({ event }: { event: NDKEvent }) {
return (
<Link
to={`/activity/${event.id}`}
className="block px-5 py-4 border-b border-black/5 dark:border-white/5 hover:bg-black/10 dark:hover:bg-white/10"
>
<User.Provider pubkey={event.pubkey}>
<User.Root className="flex items-center justify-between">
<div className="flex items-center gap-2">
<User.Avatar className="size-8 rounded-lg shrink-0" />
<div className="inline-flex items-center gap-1.5">
<User.Name className="max-w-[8rem] font-semibold text-neutral-950 dark:text-neutral-50" />
<p className="shrink-0">reposted</p>
</div>
</div>
<User.Time
time={event.created_at}
className="text-neutral-500 dark:text-neutral-400"
/>
</User.Root>
</User.Provider>
</Link>
);
}

View File

@@ -1,28 +0,0 @@
import { User } from "@lume/ark";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { Link } from "react-router-dom";
export function ActivityText({ event }: { event: NDKEvent }) {
return (
<Link
to={`/activity/${event.id}`}
className="block px-5 py-4 border-b border-black/5 dark:border-white/5 hover:bg-black/10 dark:hover:bg-white/10"
>
<User.Provider pubkey={event.pubkey}>
<User.Root className="flex items-center justify-between">
<div className="flex items-center gap-2">
<User.Avatar className="size-8 rounded-lg shrink-0" />
<div className="inline-flex items-center gap-1.5">
<User.Name className="max-w-[8rem] font-semibold text-neutral-950 dark:text-neutral-50" />
<p className="shrink-0">mention you</p>
</div>
</div>
<User.Time
time={event.created_at}
className="text-neutral-500 dark:text-neutral-400"
/>
</User.Root>
</User.Provider>
</Link>
);
}

View File

@@ -1,33 +0,0 @@
import { User } from "@lume/ark";
import { compactNumber } from "@lume/utils";
import { NDKEvent, zapInvoiceFromEvent } from "@nostr-dev-kit/ndk";
import { Link } from "react-router-dom";
export function ActivityZap({ event }: { event: NDKEvent }) {
const invoice = zapInvoiceFromEvent(event);
return (
<Link
to={`/activity/${event.id}`}
className="block px-5 py-4 border-b border-black/5 dark:border-white/5 hover:bg-black/10 dark:hover:bg-white/10"
>
<User.Provider pubkey={event.pubkey}>
<User.Root className="flex items-center justify-between">
<div className="flex items-center gap-2">
<User.Avatar className="size-8 rounded-lg shrink-0" />
<div className="inline-flex items-center gap-1.5">
<User.Name className="max-w-[8rem] font-semibold text-neutral-950 dark:text-neutral-50" />
<p className="shrink-0">
zapped {compactNumber.format(invoice.amount)} sats
</p>
</div>
</div>
<User.Time
time={event.created_at}
className="text-neutral-500 dark:text-neutral-400"
/>
</User.Root>
</User.Provider>
</Link>
);
}

View File

@@ -1,115 +0,0 @@
import { useArk } from "@lume/ark";
import { ArrowRightCircleIcon, LoaderIcon } from "@lume/icons";
import { FETCH_LIMIT } from "@lume/utils";
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
import { useCallback, useMemo } from "react";
import { ActivityRepost } from "./activityRepost";
import { ActivityText } from "./activityText";
import { ActivityZap } from "./activityZap";
export function ActivityList() {
const ark = useArk();
const queryClient = useQueryClient();
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ["activity"],
initialPageParam: 0,
queryFn: async ({
signal,
pageParam,
}: {
signal: AbortSignal;
pageParam: number;
}) => {
const events = await ark.getInfiniteEvents({
filter: {
kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Zap],
"#p": [ark.account.pubkey],
},
limit: FETCH_LIMIT,
pageParam,
signal,
});
return events;
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage.at(-1);
if (!lastEvent) return;
return lastEvent.created_at - 1;
},
initialData: () => {
const queryCacheData = queryClient.getQueryState(["activity"])
?.data as NDKEvent[];
if (queryCacheData) {
return {
pageParams: [undefined, 1],
pages: [queryCacheData],
};
}
},
staleTime: 360 * 1000,
refetchOnWindowFocus: false,
refetchOnMount: false,
});
const allEvents = useMemo(
() => (data ? data.pages.flatMap((page) => page) : []),
[data],
);
const renderEvenKind = useCallback(
(event: NDKEvent) => {
if (event.pubkey === ark.account.pubkey) return null;
switch (event.kind) {
case NDKKind.Text:
return <ActivityText key={event.id} event={event} />;
case NDKKind.Repost:
return <ActivityRepost key={event.id} event={event} />;
case NDKKind.Zap:
return <ActivityZap key={event.id} event={event} />;
default:
return <ActivityText key={event.id} event={event} />;
}
},
[data],
);
return (
<div className="flex flex-col flex-1 min-h-0 overflow-y-auto">
{isLoading ? (
<div className="w-full h-full flex flex-col items-center justify-center">
<LoaderIcon className="size-5 animate-spin" />
</div>
) : !allEvents.length ? (
<div className="w-full h-full flex flex-col items-center justify-center">
<p className="mb-2 text-2xl">🎉</p>
<p className="text-center font-medium">Yo! Nothing new yet.</p>
</div>
) : (
allEvents.map((event) => renderEvenKind(event))
)}
<div className="flex items-center justify-center h-16 px-5">
{hasNextPage ? (
<button
type="button"
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex items-center justify-center w-full h-12 gap-2 font-medium bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20 rounded-xl focus:outline-none"
>
{isFetchingNextPage ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
<>
<ArrowRightCircleIcon className="size-5" />
Load more
</>
)}
</button>
) : null}
</div>
</div>
);
}

View File

@@ -1,40 +0,0 @@
import { Note, useEvent } from "@lume/ark";
export function ActivityRootNote({ eventId }: { eventId: string }) {
const { isLoading, isError, data } = useEvent(eventId);
if (isLoading) {
return (
<div className="relative flex gap-3">
<div className="relative flex-1 rounded-md bg-neutral-200 px-2 py-2 dark:bg-neutral-800">
<div className="h-4 w-full animate-pulse bg-neutral-300 dark:bg-neutral-700" />
</div>
</div>
);
}
if (isError) {
return (
<div className="relative flex gap-3">
<div className="relative flex-1 rounded-md bg-neutral-200 px-2 py-2 dark:bg-neutral-800">
Failed to fetch event
</div>
</div>
);
}
return (
<Note.Provider event={data}>
<Note.Root>
<div className="flex items-center justify-between px-3 h-14">
<Note.User className="flex-1 pr-1" />
</div>
<Note.Content className="min-w-0 px-3" />
<div className="flex items-center justify-between px-3 h-14">
<Note.Pin />
<div className="inline-flex items-center gap-10" />
</div>
</Note.Root>
</Note.Provider>
);
}

View File

@@ -1,33 +0,0 @@
import { User } from "@lume/ark";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { ActivityRootNote } from "./rootNote";
export function ActivitySingleRepost({ event }: { event: NDKEvent }) {
const repostId = event.tags.find((el) => el[0] === "e")[1];
return (
<div className="pb-3 flex flex-col">
<div className="h-14 shrink-0 border-b border-neutral-100 dark:border-neutral-900 flex flex-col items-center justify-center px-3">
<h3 className="text-center font-semibold leading-tight">Boost</h3>
<p className="text-sm text-blue-500 font-medium leading-tight">
@ Someone has reposted to your note
</p>
</div>
<div className="flex-1 min-h-0">
<div className="max-w-xl mx-auto py-6 flex flex-col items-center gap-6">
<User.Provider pubkey={event.pubkey}>
<User.Root>
<User.Avatar className="size-10 shrink-0 rounded-lg object-cover" />
</User.Root>
</User.Provider>
<div className="flex flex-col items-center gap-3">
<div className="h-4 w-px bg-blue-500" />
<h3 className="font-semibold">Reposted</h3>
<div className="h-4 w-px bg-blue-500" />
</div>
<ActivityRootNote eventId={repostId} />
</div>
</div>
</div>
);
}

View File

@@ -1,53 +0,0 @@
import { Note, useArk } from "@lume/ark";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { ActivityRootNote } from "./rootNote";
export function ActivitySingleText({ event }: { event: NDKEvent }) {
const ark = useArk();
const thread = ark.getEventThread({
content: event.content,
tags: event.tags,
});
return (
<div className="h-full w-full flex flex-col justify-between">
<div className="h-14 border-b border-neutral-100 dark:border-neutral-900 flex flex-col items-center justify-center px-3">
<h3 className="text-center font-semibold leading-tight">
Conversation
</h3>
<p className="text-sm text-blue-500 font-medium leading-tight">
@ Someone has replied to your note
</p>
</div>
<div className="overflow-y-auto">
<div className="max-w-xl mx-auto py-6">
{thread ? (
<div className="flex flex-col gap-3 mb-1">
<ActivityRootNote eventId={thread.rootEventId} />
<ActivityRootNote eventId={thread.replyEventId} />
</div>
) : null}
<div className="mt-3 flex flex-col gap-3">
<div className="flex items-center gap-3">
<p className="text-teal-500 font-medium">New reply</p>
<div className="flex-1 h-px bg-teal-300" />
<div className="w-4 shrink-0 h-px bg-teal-300" />
</div>
<Note.Provider event={event}>
<Note.Root>
<div className="flex items-center justify-between px-3 h-14">
<Note.User className="flex-1 pr-1" />
</div>
<Note.Content className="min-w-0 px-3" />
<div className="flex items-center justify-between px-3 h-14">
<Note.Pin />
<div className="inline-flex items-center gap-10" />
</div>
</Note.Root>
</Note.Provider>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,39 +0,0 @@
import { User } from "@lume/ark";
import { compactNumber } from "@lume/utils";
import { NDKEvent, zapInvoiceFromEvent } from "@nostr-dev-kit/ndk";
import { ActivityRootNote } from "./rootNote";
export function ActivitySingleZap({ event }: { event: NDKEvent }) {
const zapEventId = event.tags.find((el) => el[0] === "e")[1];
const invoice = zapInvoiceFromEvent(event);
return (
<div className="h-full w-full flex flex-col justify-between">
<div className="h-14 border-b border-neutral-100 dark:border-neutral-900 flex flex-col items-center justify-center px-3">
<h3 className="text-center font-semibold leading-tight">
Conversation
</h3>
<p className="text-sm text-blue-500 font-medium leading-tight">
@ Someone has replied to your note
</p>
</div>
<div className="flex-1 min-h-0">
<div className="max-w-xl mx-auto py-6 flex flex-col items-center gap-6">
<User.Provider pubkey={event.pubkey}>
<User.Root>
<User.Avatar className="size-10 shrink-0 rounded-lg object-cover" />
</User.Root>
</User.Provider>
<div className="flex flex-col items-center gap-3">
<div className="h-4 w-px bg-blue-500" />
<h3 className="font-semibold">
Zap you {compactNumber.format(invoice.amount)} sats for
</h3>
<div className="h-4 w-px bg-blue-500" />
</div>
<ActivityRootNote eventId={zapEventId} />
</div>
</div>
</div>
);
}

View File

@@ -1,27 +0,0 @@
import { useEvent } from "@lume/ark";
import { LoaderIcon } from "@lume/icons";
import { NDKKind } from "@nostr-dev-kit/ndk";
import { useParams } from "react-router-dom";
import { ActivitySingleRepost } from "./components/singleRepost";
import { ActivitySingleText } from "./components/singleText";
import { ActivitySingleZap } from "./components/singleZap";
export function ActivityIdScreen() {
const { id } = useParams();
const { isLoading, data } = useEvent(id);
if (isLoading || !data) {
return (
<div className="w-full h-full flex items-center justify-center">
<LoaderIcon className="size-5 animate-spin" />
</div>
);
}
if (data.kind === NDKKind.Text) return <ActivitySingleText event={data} />;
if (data.kind === NDKKind.Zap) return <ActivitySingleZap event={data} />;
if (data.kind === NDKKind.Repost)
return <ActivitySingleRepost event={data} />;
return <ActivitySingleText event={data} />;
}

View File

@@ -1,18 +0,0 @@
import { Outlet } from "react-router-dom";
import { ActivityList } from "./components/list";
export function ActivityScreen() {
return (
<div className="flex h-full w-full rounded-xl shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:shadow-none dark:ring-1 dark:ring-white/10">
<div className="h-full flex flex-col w-96 shrink-0 rounded-l-xl bg-white/50 backdrop-blur-xl dark:bg-black/50">
<div className="h-14 shrink-0 flex items-center px-5 text-lg font-semibold border-b border-black/10 dark:border-white/10">
Activity
</div>
<ActivityList />
</div>
<div className="flex-1 rounded-r-xl bg-white pb-20 dark:bg-black">
<Outlet />
</div>
</div>
);
}

View File

@@ -1,302 +0,0 @@
import { useArk } from "@lume/ark";
import { CheckIcon, ChevronDownIcon, LoaderIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { onboardingAtom } from "@lume/utils";
import NDK, {
NDKEvent,
NDKKind,
NDKNip46Signer,
NDKPrivateKeySigner,
} from "@nostr-dev-kit/ndk";
import * as Select from "@radix-ui/react-select";
import { downloadDir } from "@tauri-apps/api/path";
import { Window } from "@tauri-apps/api/window";
import { save } from "@tauri-apps/plugin-dialog";
import { writeTextFile } from "@tauri-apps/plugin-fs";
import { useSetAtom } from "jotai";
import { getPublicKey, nip19 } from "nostr-tools";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useLoaderData, useNavigate } from "react-router-dom";
import { toast } from "sonner";
const Item = ({ event }: { event: NDKEvent }) => {
const domain = JSON.parse(event.content).nip05.replace("_@", "");
return (
<Select.Item
value={event.id}
className="relative flex items-center pr-10 leading-none rounded-md select-none text-neutral-100 rounded-mg h-9 pl-7"
>
<Select.ItemText>@{domain}</Select.ItemText>
<Select.ItemIndicator className="absolute left-0 inline-flex items-center justify-center transform h-7">
<CheckIcon className="size-4" />
</Select.ItemIndicator>
</Select.Item>
);
};
export function CreateAccountScreen() {
const ark = useArk();
const storage = useStorage();
const navigate = useNavigate();
const services = useLoaderData() as NDKEvent[];
const setOnboarding = useSetAtom(onboardingAtom);
const [serviceId, setServiceId] = useState(services?.[0]?.id);
const [loading, setIsLoading] = useState(false);
const {
register,
handleSubmit,
formState: { isValid },
} = useForm();
const getDomainName = (id: string) => {
const event = services.find((ev) => ev.id === id);
return JSON.parse(event.content).nip05.replace("_@", "") as string;
};
const generateNostrKeys = async () => {
const signer = NDKPrivateKeySigner.generate();
const pubkey = getPublicKey(signer.privateKey);
const npub = nip19.npubEncode(pubkey);
const nsec = nip19.nsecEncode(signer.privateKey);
ark.updateNostrSigner({ signer });
const downloadPath = await downloadDir();
const fileName = `nostr_keys_${new Date().getTime().toString(36)}.txt`;
const filePath = await save({
defaultPath: `${downloadPath}/${fileName}`,
});
if (filePath) {
await writeTextFile(
filePath,
`Nostr account, generated by Lume (lume.nu)\nPublic key: ${npub}\nPrivate key: ${nsec}`,
);
} // else { user cancel action }
await storage.createAccount({
pubkey: pubkey,
privkey: signer.privateKey,
});
setOnboarding(true);
return navigate("/auth/onboarding", { replace: true });
};
const onSubmit = async (data: { username: string; email: string }) => {
try {
setIsLoading(true);
const domain = getDomainName(serviceId);
const service = services.find((ev) => ev.id === serviceId);
// generate ndk for nsecbunker
const localSigner = NDKPrivateKeySigner.generate();
const bunker = new NDK({
explicitRelayUrls: [
"wss://relay.nsecbunker.com/",
"wss://nostr.vulpem.com/",
],
});
await bunker.connect(2000);
// generate tmp remote singer for create account
const remoteSigner = new NDKNip46Signer(
bunker,
service.pubkey,
localSigner,
);
// handle auth url request
let authWindow: Window;
remoteSigner.addListener("authUrl", (authUrl: string) => {
authWindow = new Window(`auth-${serviceId}`, {
url: authUrl,
title: domain,
titleBarStyle: "overlay",
width: 415,
height: 600,
center: true,
closable: false,
});
});
// create new account
const account = await remoteSigner.createAccount(
data.username,
domain,
data.email,
);
if (!account) {
authWindow.close();
setIsLoading(false);
return toast.error("Failed to create new account, try again later");
}
authWindow.close();
// add account to storage
await storage.createSetting("nsecbunker", "1");
const dbAccount = await storage.createAccount({
pubkey: account,
privkey: localSigner.privateKey,
});
ark.account = dbAccount;
// get final signer with newly created account
const finalSigner = new NDKNip46Signer(bunker, account, localSigner);
await finalSigner.blockUntilReady();
// update main ndk instance signer
ark.updateNostrSigner({ signer: finalSigner });
// remove default nsecbunker profile and contact list
await ark.createEvent({ kind: NDKKind.Metadata, content: "", tags: [] });
await ark.createEvent({ kind: NDKKind.Contacts, content: "", tags: [] });
setOnboarding(true);
setIsLoading(false);
return navigate("/auth/onboarding", { replace: true });
} catch (e) {
setIsLoading(false);
toast.error(String(e));
}
};
return (
<div className="relative flex items-center justify-center w-full h-full">
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">
Let's get you set up on Nostr.
</h1>
<p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
With an account on Nostr, you'll be able to travel across all nostr
clients, all your data are synced.
</p>
</div>
{!services ? (
<div className="flex items-center justify-center w-full">
<LoaderIcon className="size-5 animate-spin" />
</div>
) : (
<div className="flex flex-col gap-6">
<form
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col gap-3 mb-0"
>
<div className="flex flex-col gap-6 p-5 bg-neutral-950 rounded-2xl">
<div className="flex flex-col gap-2">
<label
htmlFor="username"
className="text-sm font-semibold uppercase text-neutral-600"
>
Username *
</label>
<div className="flex items-center justify-between w-full gap-2 bg-neutral-900 rounded-xl">
<input
type={"text"}
{...register("username", {
required: true,
minLength: 1,
})}
spellCheck={false}
placeholder="satoshi"
className="flex-1 min-w-0 text-xl bg-transparent border-transparent outline-none focus:outline-none focus:ring-0 focus:border-none h-14 ring-0 placeholder:text-neutral-600"
/>
<Select.Root value={serviceId} onValueChange={setServiceId}>
<Select.Trigger className="inline-flex items-center justify-end gap-2 pr-3 text-xl font-semibold text-blue-500 w-max shrink-0">
<Select.Value>@{getDomainName(serviceId)}</Select.Value>
<Select.Icon>
<ChevronDownIcon className="size-5" />
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Content className="border rounded-lg bg-neutral-950 border-neutral-900">
<Select.Viewport className="p-3">
<Select.Group>
<Select.Label className="mb-2 text-sm font-medium uppercase px-7 text-neutral-600">
Public handles
</Select.Label>
{services.map((service) => (
<Item key={service.id} event={service} />
))}
</Select.Group>
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
</div>
</div>
<div className="flex flex-col gap-2">
<label
htmlFor="email"
className="text-sm font-semibold uppercase text-neutral-600"
>
Backup Email (Optional)
</label>
<input
type={"email"}
{...register("email", { required: false })}
spellCheck={false}
autoCapitalize="none"
autoCorrect="none"
className="px-3 text-xl border-transparent rounded-xl h-14 bg-neutral-900 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-800"
/>
</div>
</div>
<button
type="submit"
disabled={!isValid}
className="inline-flex items-center justify-center w-full text-lg h-12 font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600 disabled:opacity-50"
>
{loading ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
"Create Account"
)}
</button>
</form>
<div className="flex flex-col gap-6">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-neutral-900" />
</div>
<div className="relative flex justify-center">
<span className="px-2 font-medium bg-black text-neutral-600">
Or
</span>
</div>
</div>
<div>
<button
type="button"
onClick={generateNostrKeys}
className="mb-2 inline-flex items-center justify-center w-full h-12 text-lg font-medium text-neutral-50 rounded-xl bg-neutral-950 hover:bg-neutral-900"
>
Generate Nostr Keys
</button>
<p className="text-sm text-center text-neutral-500">
If you are using this option, please make sure keep your keys
in safe place. You{" "}
<span className="text-red-600">cannot recover</span> if it
lost, all your data will be{" "}
<span className="text-red-600">lost forever.</span>
</p>
</div>
</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -1,118 +0,0 @@
import { useArk } from "@lume/ark";
import { EyeOffIcon, EyeOnIcon, LoaderIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { 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";
export function LoginWithKey() {
const ark = useArk();
const storage = useStorage();
const navigate = useNavigate();
const [showKey, setShowKey] = useState(false);
const [loading, setLoading] = useState(false);
const {
register,
handleSubmit,
setError,
formState: { errors, isValid },
} = useForm();
const onSubmit = async (data: { nsec: string }) => {
try {
if (!data.nsec.startsWith("nsec1"))
return toast.error("You need to enter a private key start with nsec1");
setLoading(true);
const privkey = nip19.decode(data.nsec).data as string;
const pubkey = getPublicKey(privkey);
const account = await storage.createAccount({
pubkey: pubkey,
privkey: privkey,
});
ark.account = account;
return navigate("/auth/onboarding", { replace: true });
} catch (e) {
setLoading(false);
setError("nsec", {
type: "manual",
message: String(e),
});
}
};
return (
<div className="relative flex items-center justify-center w-full h-full">
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">Enter your Private Key</h1>
<p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
Lume will put your private key to{" "}
<span className="text-teal-500">
{storage.platform === "macos"
? "Apple Keychain"
: storage.platform === "windows"
? "Credential Manager"
: "Secret Service"}
</span>
.
<br />
It will be secured by your OS.
</p>
</div>
<div className="flex flex-col gap-6">
<form
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col gap-4 mb-0"
>
<div className="relative flex flex-col gap-1">
<input
type={showKey ? "text" : "password"}
{...register("nsec", { required: false })}
spellCheck={false}
autoCapitalize="none"
autoCorrect="none"
placeholder="nsec1..."
className="pl-3 pr-11 text-xl border-transparent rounded-xl h-14 bg-neutral-950 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-800"
/>
{errors.nsec && (
<p className="text-sm text-center text-red-600">
{errors.nsec.message as string}
</p>
)}
<button
type="button"
onClick={() => setShowKey((state) => !state)}
className="absolute right-2 top-2 size-10 inline-flex items-center justify-center rounded-lg text-white bg-neutral-900 hover:bg-neutral-800"
>
{showKey ? (
<EyeOnIcon className="size-5" />
) : (
<EyeOffIcon className="size-5" />
)}
</button>
</div>
<button
type="submit"
disabled={!isValid || loading}
className="inline-flex items-center justify-center w-full text-lg h-12 font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600 disabled:opacity-50"
>
{loading ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
"Continue"
)}
</button>
</form>
</div>
</div>
</div>
);
}

View File

@@ -1,112 +0,0 @@
import { useArk } from "@lume/ark";
import { LoaderIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import { nip19 } from "nostr-tools";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
export function LoginWithNsecbunker() {
const ark = useArk();
const storage = useStorage();
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const {
register,
handleSubmit,
setError,
formState: { errors, isValid },
} = useForm();
const onSubmit = async (data: { npub: string }) => {
try {
if (!data.npub.startsWith("npub1"))
return toast.info("You need to enter a token start with npub1");
if (!data.npub.includes("#"))
return toast.info("Token must include #secret");
setLoading(true);
const bunker = new NDK({
explicitRelayUrls: [
"wss://relay.nsecbunker.com",
"wss://nostr.vulpem.com",
],
});
await bunker.connect(2000);
const pubkey = nip19.decode(data.npub.split("#")[0]).data as string;
const localSigner = NDKPrivateKeySigner.generate();
const remoteSigner = new NDKNip46Signer(bunker, data.npub, localSigner);
await remoteSigner.blockUntilReady();
ark.updateNostrSigner({ signer: remoteSigner });
await storage.createSetting("nsecbunker", "1");
const account = await storage.createAccount({
pubkey: pubkey,
privkey: localSigner.privateKey,
});
ark.account = account;
return navigate("/auth/onboarding", { replace: true });
} catch (e) {
setLoading(false);
setError("npub", {
type: "manual",
message: String(e),
});
}
};
return (
<div className="relative flex items-center justify-center w-full h-full">
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">
Enter your nsecbunker token
</h1>
</div>
<div className="flex flex-col gap-6">
<form
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col gap-4 mb-0"
>
<div className="relative flex flex-col gap-1">
<input
type="text"
{...register("npub", { required: false })}
spellCheck={false}
autoCapitalize="none"
autoCorrect="none"
placeholder="npub1...#..."
className="px-3 text-xl border-transparent rounded-xl h-14 bg-neutral-950 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-800"
/>
{errors.npub && (
<p className="text-sm text-center text-red-600">
{errors.npub.message as string}
</p>
)}
</div>
<button
type="submit"
disabled={!isValid || loading}
className="inline-flex items-center justify-center w-full text-lg h-12 font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600 disabled:opacity-50"
>
{loading ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
"Continue"
)}
</button>
</form>
</div>
</div>
</div>
);
}

View File

@@ -1,172 +0,0 @@
import { useArk } from "@lume/ark";
import { LoaderIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { NIP05 } from "@lume/types";
import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import { Window } from "@tauri-apps/api/window";
import { fetch } from "@tauri-apps/plugin-http";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/;
export function LoginWithOAuth() {
const ark = useArk();
const storage = useStorage();
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const {
register,
handleSubmit,
setError,
formState: { errors, isValid },
} = useForm();
const onSubmit = async (data: { nip05: string }) => {
try {
setLoading(true);
if (!emailRegex.test(data.nip05)) {
setLoading(false);
return toast.error(
"Cannot verify your NIP-05 address, please try again later.",
);
}
const localPath = data.nip05.split("@")[0];
const service = data.nip05.split("@")[1];
const verifyURL = `https://${service}/.well-known/nostr.json?name=${localPath}`;
const req = await fetch(verifyURL, {
method: "GET",
headers: {
"Content-Type": "application/json; charset=utf-8",
},
});
if (!req.ok) {
setLoading(false);
return toast.error(
"Cannot verify your NIP-05 address, please try again later.",
);
}
const res: NIP05 = await req.json();
if (!res.names[localPath.toLowerCase()] || !res.names[localPath]) {
setLoading(false);
return toast.error(
"Cannot verify your NIP-05 address, please try again later.",
);
}
const pubkey =
(res.names[localPath] as string) ||
(res.names[localPath.toLowerCase()] as string);
if (!res.nip46[pubkey]) {
setLoading(false);
return toast.error("Cannot found NIP-46 with this address");
}
const nip46Relays = res.nip46[pubkey] as unknown as string[];
const bunker = new NDK({
explicitRelayUrls: nip46Relays || [
"wss://relay.nsecbunker.com",
"wss://nostr.vulpem.com",
],
});
await bunker.connect(2000);
const localSigner = NDKPrivateKeySigner.generate();
const remoteSigner = new NDKNip46Signer(bunker, pubkey, localSigner);
// handle auth url request
let authWindow: Window;
remoteSigner.addListener("authUrl", (authUrl: string) => {
authWindow = new Window(`auth-${pubkey}`, {
url: authUrl,
title: "Login",
titleBarStyle: "overlay",
width: 415,
height: 600,
center: true,
closable: false,
});
});
const remoteUser = await remoteSigner.blockUntilReady();
if (remoteUser) {
authWindow.close();
ark.updateNostrSigner({ signer: remoteSigner });
await storage.createSetting("nsecbunker", "1");
const account = await storage.createAccount({
pubkey,
privkey: localSigner.privateKey,
});
ark.account = account;
return navigate("/auth/onboarding", { replace: true });
}
} catch (e) {
setLoading(false);
setError("nip05", {
type: "manual",
message: String(e),
});
}
};
return (
<div className="relative flex items-center justify-center w-full h-full">
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">Enter your NIP-05 address</h1>
</div>
<div className="flex flex-col gap-6">
<form
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col gap-4 mb-0"
>
<div className="relative flex flex-col gap-1">
<input
type="email"
{...register("nip05", { required: false })}
spellCheck={false}
autoCapitalize="none"
autoCorrect="none"
placeholder="satoshi@nostr.me"
className="px-3 text-xl border-transparent rounded-xl h-14 bg-neutral-950 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-800"
/>
{errors.nip05 && (
<p className="text-sm text-center text-red-600">
{errors.nip05.message as string}
</p>
)}
</div>
<button
type="submit"
disabled={!isValid || loading}
className="inline-flex items-center justify-center w-full text-lg h-12 font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600 disabled:opacity-50"
>
{loading ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
"Continue"
)}
</button>
</form>
</div>
</div>
</div>
);
}

View File

@@ -1,55 +0,0 @@
import { Link } from "react-router-dom";
export function LoginScreen() {
return (
<div className="relative flex items-center justify-center w-full h-full">
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">
Continue your experience on Nostr
</h1>
</div>
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-2">
<Link
to="/auth/login-oauth"
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600"
>
Login with address
</Link>
<Link
to="/auth/login-nsecbunker"
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-neutral-50 rounded-xl bg-neutral-950 hover:bg-neutral-900"
>
Login with nsecbunker
</Link>
</div>
<div className="flex flex-col gap-6">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-neutral-900" />
</div>
<div className="relative flex justify-center">
<span className="px-2 font-medium bg-black text-neutral-600">
Or (Not recommend)
</span>
</div>
</div>
<div>
<Link
to="/auth/login-key"
className="mb-2 inline-flex items-center justify-center w-full h-12 text-lg font-medium text-neutral-50 rounded-xl bg-neutral-950 hover:bg-neutral-900"
>
Login with Private Key
</Link>
<p className="text-sm text-center text-neutral-500">
Lume will store your Private Key in{" "}
<span className="text-teal-600">OS Secure Storage</span>
</p>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,227 +0,0 @@
import { useArk } from "@lume/ark";
import { InfoIcon, LoaderIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { TranslateRegisterModal } from "@lume/ui";
import { FETCH_LIMIT } from "@lume/utils";
import { NDKKind } from "@nostr-dev-kit/ndk";
import * as Switch from "@radix-ui/react-switch";
import { useQueryClient } from "@tanstack/react-query";
import {
isPermissionGranted,
requestPermission,
} from "@tauri-apps/plugin-notification";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
export function OnboardingScreen() {
const ark = useArk();
const storage = useStorage();
const queryClient = useQueryClient();
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [apiKey, setAPIKey] = useState("");
const [settings, setSettings] = useState({
notification: false,
lowPower: false,
translation: false,
});
const toggleLowPower = async () => {
await storage.createSetting("lowPower", String(+!settings.lowPower));
setSettings((state) => ({ ...state, lowPower: !settings.lowPower }));
};
const toggleTranslation = async () => {
await storage.createSetting("translation", String(+!settings.translation));
setSettings((state) => ({ ...state, translation: !settings.translation }));
};
const toggleNofitication = async () => {
await requestPermission();
setSettings((state) => ({
...state,
notification: !settings.notification,
}));
};
const completeAuth = async () => {
if (settings.translation) {
if (!apiKey.length)
return toast.warning(
"You need to provide Translate API if enable translation",
);
await storage.createSetting("translateApiKey", apiKey);
}
setLoading(true);
// get account contacts
await ark.getUserContacts();
// refetch newsfeed
await queryClient.prefetchInfiniteQuery({
queryKey: ["timeline-9999"],
initialPageParam: 0,
queryFn: async ({
signal,
pageParam,
}: {
signal: AbortSignal;
pageParam: number;
}) => {
const events = await ark.getInfiniteEvents({
filter: {
kinds: [NDKKind.Text, NDKKind.Repost],
authors: ark.account.contacts,
},
limit: FETCH_LIMIT,
pageParam,
signal,
});
return events;
},
});
navigate("/");
};
useEffect(() => {
async function loadSettings() {
// get notification permission
const permissionGranted = await isPermissionGranted();
setSettings((prev) => ({ ...prev, notification: permissionGranted }));
// get other settings
const data = await storage.getAllSettings();
for (const item of data) {
if (item.key === "lowPower")
setSettings((prev) => ({
...prev,
lowPower: !!parseInt(item.value),
}));
if (item.key === "translation")
setSettings((prev) => ({
...prev,
translation: !!parseInt(item.value),
}));
}
}
loadSettings();
}, []);
return (
<div className="relative flex h-full w-full items-center justify-center">
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
<div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">
You&apos;re almost ready to use Lume.
</h1>
<p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
Let&apos;s start personalizing your experience.
</p>
</div>
<div className="flex flex-col gap-3">
<div className="flex w-full items-start justify-between gap-4 rounded-xl px-5 py-4 bg-neutral-950">
<Switch.Root
checked={settings.notification}
onClick={() => toggleNofitication()}
className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full outline-none data-[state=checked]:bg-blue-500 bg-neutral-800"
>
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-neutral-50 transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
</Switch.Root>
<div>
<h3 className="font-semibold text-lg">Push notification</h3>
<p className="text-neutral-500">
Enabling push notifications will allow you to receive
notifications from Lume.
</p>
</div>
</div>
<div className="flex w-full items-start justify-between gap-4 rounded-xl px-5 py-4 bg-neutral-950">
<Switch.Root
checked={settings.lowPower}
onClick={() => toggleLowPower()}
className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full outline-none data-[state=checked]:bg-blue-500 bg-neutral-800"
>
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-neutral-50 transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
</Switch.Root>
<div>
<h3 className="font-semibold text-lg">Low Power Mode</h3>
<p className="text-neutral-500">
Limited relay connection and hide all media, sustainable for low
network environment.
</p>
</div>
</div>
<div className="flex w-full items-start justify-between gap-4 rounded-xl px-5 py-4 bg-neutral-950">
<Switch.Root
checked={settings.translation}
onClick={() => toggleTranslation()}
className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full outline-none data-[state=checked]:bg-blue-500 bg-neutral-800"
>
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-neutral-50 transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
</Switch.Root>
<div>
<h3 className="font-semibold text-lg">
Translation (nostr.wine)
</h3>
<p className="text-neutral-500">
Translate text to your preferred language, powered by Nostr
Wine.
</p>
</div>
</div>
{settings.translation ? (
<div className="flex flex-col w-full items-start justify-between gap-2 rounded-xl px-5 py-4 bg-neutral-950">
<h3 className="font-semibold">Translate API Key</h3>
<input
type="password"
spellCheck={false}
value={apiKey}
onChange={(e) => setAPIKey(e.target.value)}
className="w-full text-xl border-transparent outline-none focus:outline-none focus:ring-0 focus:border-none h-11 rounded-lg ring-0 placeholder:text-neutral-600 bg-neutral-900"
/>
<div className="w-full mt-1">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-neutral-900" />
</div>
<div className="relative flex justify-center">
<span className="px-2 text-sm font-medium bg-neutral-950 text-neutral-600">
Not have API ?
</span>
</div>
</div>
<TranslateRegisterModal setAPIKey={setAPIKey} />
</div>
</div>
) : null}
<div className="flex items-center gap-2 rounded-xl px-5 py-3 text-sm bg-blue-950 text-blue-300">
<InfoIcon className="size-8" />
<p>
There are many more settings you can configure from the
&quot;Settings&quot; screen. Be sure to visit it later.
</p>
</div>
<button
type="button"
onClick={completeAuth}
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600 disabled:opacity-50"
>
{loading ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
"Continue"
)}
</button>
</div>
</div>
</div>
);
}

View File

@@ -1,46 +0,0 @@
import { Link } from "react-router-dom";
export function TutorialFinishScreen() {
return (
<div className="flex h-full w-full items-center justify-center">
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
<div className="text-center">
<img
src="/icon.png"
alt="Lume's logo"
className="mx-auto mb-1 h-auto w-16"
/>
<h1 className="text-2xl font-light">
Yo, you&apos;ve understood basic features 🎉
</h1>
</div>
<div className="flex flex-col gap-2">
<Link
to="/"
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600"
>
Start using Lume
</Link>
<Link
to="https://nostr.how/"
target="_blank"
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-neutral-100 font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
Learn more about Nostr
</Link>
<p className="text-center text-sm font-medium text-neutral-500 dark:text-neutral-600">
If you&apos;ve trouble when user Lume, you can report the issue{" "}
<a
href="github.com/luminous-devs/lume"
target="_blank"
className="text-blue-500 !underline"
rel="noreferrer"
>
here
</a>
</p>
</div>
</div>
</div>
);
}

View File

@@ -1,102 +0,0 @@
import { TextNote } from "@lume/ark";
import {
EditIcon,
ReactionIcon,
ReplyIcon,
RepostIcon,
ZapIcon,
} from "@lume/icons";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { Link } from "react-router-dom";
export function TutorialNoteScreen() {
const exampleEvent = new NDKEvent(undefined, {
id: "a3527670dd9b178bf7c2a9ea673b63bc8bfe774942b196691145343623c45821",
pubkey: "04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9",
created_at: 1701355223,
kind: 1,
tags: [],
content: "good morning nostr, stay humble and stack sats 🫡",
sig: "9e0bd67ec25598744f20bff0fe360fdf190c4240edb9eea260e50f77e07f94ea767ececcc6270819b7f64e5e7ca1fe20b4971f46dc120e6db43114557f3a6dae",
});
return (
<div className="flex h-full w-full select-text items-center justify-center">
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
<div className="flex flex-col items-center gap-3">
<div className="inline-flex h-11 w-11 items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
<EditIcon className="h-5 w-5" />
</div>
<h1 className="text-2xl font-light">
What is a <span className="font-bold">Note?</span>
</h1>
</div>
<div className="flex flex-col gap-2">
<p className="px-3">
Posts on Nostr based Social Network client are usually called
&apos;Notes.&apos; Notes are arranged chronologically on the
timeline and are updated in real-time.
</p>
<p className="px-3 font-semibold">Here is one example:</p>
<TextNote event={exampleEvent} />
<p className="px-3 font-semibold">
Here are how you can interact with a note:
</p>
<div className="flex flex-col gap-2 px-3">
<div className="inline-flex gap-3">
<div className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
<ReplyIcon className="h-5 w-5 text-blue-500" />
</div>
<p>
Reply - Click on this button to reply to a note. It&apos;s also
possible to reply to replies, continuing the conversation like a
thread.
</p>
</div>
<div className="inline-flex gap-3">
<div className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
<ReactionIcon className="h-5 w-5 text-red-500" />
</div>
<p>
Reaction - You can add reactions to the Note to express your
concern.
</p>
</div>
<div className="inline-flex gap-3">
<div className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
<RepostIcon className="h-5 w-5 text-teal-500" />
</div>
<p>
Repost - You can share that note to your own timeline. You can
also quote them with your comments.
</p>
</div>
<div className="inline-flex gap-3">
<div className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
<ZapIcon className="h-5 w-5 text-orange-500" />
</div>
<p>
Zap - You can send tip in Bitcoin to that note owner with
zero-fees
</p>
</div>
</div>
<div className="mt-5 flex gap-2 px-3">
<Link
to="/auth/finish"
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-neutral-100 font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
Back
</Link>
<Link
to="/auth/tutorials/widget"
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600"
>
Continue
</Link>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,3 +0,0 @@
export function TutorialPostingScreen() {
return <div></div>;
}

View File

@@ -1,67 +0,0 @@
import { BellIcon, HomeIcon, PlusIcon } from "@lume/icons";
import { Link } from "react-router-dom";
export function TutorialWidgetScreen() {
return (
<div className="flex h-full w-full select-text items-center justify-center">
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
<div className="flex flex-col items-center gap-3">
<div className="inline-flex h-11 w-11 items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
<HomeIcon className="h-5 w-5" />
</div>
<h1 className="text-2xl font-light">
The concept of <span className="font-bold">Widgets</span>
</h1>
</div>
<div className="flex flex-col gap-2 px-3">
<p>
Lume provides multiple widgets based on usage. You always can
control what you need to show on your Home.
</p>
<p className="font-semibold">Default widgets:</p>
<div className="inline-flex gap-3">
<div className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
<HomeIcon className="h-5 w-5" />
</div>
<p>Newsfeed - You can view notes from accounts you follow.</p>
</div>
<div className="inline-flex gap-3">
<div className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
<BellIcon className="h-5 w-5" />
</div>
<p>
Notification - You can view all notifications related to your
account.
</p>
</div>
<p>
If you want to add more widget, you can click to this button on Home
Screen.
</p>
<div className="flex h-24 w-full items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
<button
type="button"
className="inline-flex h-14 w-14 items-center justify-center rounded-full bg-neutral-200 text-neutral-900 hover:bg-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700"
>
<PlusIcon className="h-5 w-5" />
</button>
</div>
<div className="mt-5 flex gap-2">
<Link
to="/auth/tutorials/note"
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-neutral-100 font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
Back
</Link>
<Link
to="/auth/tutorials/finish"
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600"
>
Continue
</Link>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,65 +0,0 @@
import { LoaderIcon } from "@lume/icons";
import { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
export function WelcomeScreen() {
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const gotoCreateAccount = () => {
setLoading(true);
navigate("/auth/create");
};
return (
<div className="flex flex-col items-center justify-between w-full h-full">
<div />
<div className="flex flex-col items-center w-full max-w-4xl gap-10 mx-auto">
<div className="flex flex-col items-center text-center">
<img
src="/heading.png"
srcSet="/heading@2x.png 2x"
alt="lume"
className="w-2/3"
/>
<p className="mt-5 text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
Lume is your safe Nostr client to meet, explore and
<br />
freely sharing your though to everyone in nostrverse
</p>
</div>
<div className="flex flex-col w-full max-w-xs gap-2 mx-auto">
<button
type="button"
onClick={gotoCreateAccount}
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600"
>
{loading ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
"Create New Account"
)}
</button>
<Link
to="/auth/login"
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-neutral-50 rounded-xl bg-neutral-950 hover:bg-neutral-900"
>
Login
</Link>
</div>
</div>
<div className="flex items-center justify-center h-11">
<p className="text-neutral-800">
Before joining Nostr, you can take time to learn more about Nostr{" "}
<Link
to="https://nostr.com"
target="_blank"
className="text-blue-500"
>
here
</Link>
</p>
</div>
</div>
);
}

View File

@@ -1,72 +0,0 @@
import { useArk } from "@lume/ark";
import { LoaderIcon, RunIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { User } from "@lume/ui";
import { NDKKind } from "@nostr-dev-kit/ndk";
import { useState } from "react";
import { toast } from "sonner";
export function DepotContactCard() {
const ark = useArk();
const storage = useStorage();
const [status, setStatus] = useState(false);
const backupContact = async () => {
try {
setStatus(true);
const event = await ark.getEventByFilter({
filter: {
authors: [ark.account.pubkey],
kinds: [NDKKind.Contacts],
},
});
// broadcast to depot
const publish = await event.publish();
if (publish) {
setStatus(false);
toast.success("Backup contact list successfully.");
}
} catch (e) {
setStatus(false);
toast.error(e);
}
};
return (
<div className="flex h-56 w-full flex-col gap-2 overflow-hidden rounded-xl bg-neutral-100 p-2 dark:bg-neutral-900">
<div className="flex flex-1 items-center justify-center rounded-lg bg-neutral-200 dark:bg-neutral-800">
<div className="isolate flex -space-x-2">
{ark.account.contacts?.slice(0, 8).map((item) => (
<User key={item} pubkey={item} variant="ministacked" />
))}
{ark.account.contacts?.length > 8 ? (
<div className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-neutral-300 text-neutral-900 ring-1 ring-white dark:bg-neutral-700 dark:text-neutral-100 dark:ring-black">
<span className="text-[8px] font-medium">
+{ark.account.contacts?.length - 8}
</span>
</div>
) : null}
</div>
</div>
<div className="inline-flex shrink-0 items-center justify-between">
<div className="text-sm font-medium">Contacts</div>
<button
type="button"
onClick={backupContact}
className="inline-flex h-8 w-max items-center justify-center gap-2 rounded-md bg-blue-500 pl-2 pr-3 font-medium text-white shadow shadow-blue-500/50 hover:bg-blue-600"
>
{status ? (
<LoaderIcon className="size-4 animate-spin" />
) : (
<RunIcon className="size-4" />
)}
Backup
</button>
</div>
</div>
);
}

View File

@@ -1,150 +0,0 @@
import { CancelIcon, PlusIcon, UserAddIcon, UserRemoveIcon } from "@lume/icons";
import { User } from "@lume/ui";
import * as Dialog from "@radix-ui/react-dialog";
import { resolveResource } from "@tauri-apps/api/path";
import { readTextFile, writeTextFile } from "@tauri-apps/plugin-fs";
import { nip19 } from "nostr-tools";
import { useEffect, useState } from "react";
import { parse, stringify } from "smol-toml";
import { toast } from "sonner";
export function DepotMembers() {
const [members, setMembers] = useState<Set<string>>(null);
const [tmpMembers, setTmpMembers] = useState<Array<string>>([]);
const [newMember, setNewMember] = useState("");
const addMember = async () => {
if (!newMember.startsWith("npub1"))
return toast.error("You need to enter a valid npub");
try {
const pubkey = nip19.decode(newMember).data as string;
setTmpMembers((prev) => [...prev, pubkey]);
} catch (e) {
console.error(e);
}
};
const removeMember = (member: string) => {
setTmpMembers((prev) => prev.filter((item) => item !== member));
};
const updateMembers = async () => {
setMembers(new Set(tmpMembers));
const defaultConfig = await resolveResource("resources/config.toml");
const config = await readTextFile(defaultConfig);
const configContent = parse(config);
// biome-ignore lint/complexity/useLiteralKeys: <explanation>
configContent.authorization["pubkey_whitelist"] = [...members];
const newConfig = stringify(configContent);
return await writeTextFile(defaultConfig, newConfig);
};
useEffect(() => {
async function loadConfig() {
const defaultConfig = await resolveResource("resources/config.toml");
const config = await readTextFile(defaultConfig);
const configContent = parse(config);
setTmpMembers(
// biome-ignore lint/complexity/useLiteralKeys: <explanation>
Array.from(configContent.authorization["pubkey_whitelist"]),
);
}
loadConfig();
}, []);
return (
<Dialog.Root>
<div className="flex items-center justify-between rounded-lg bg-neutral-50 p-5 dark:bg-neutral-950">
<div className="flex flex-col items-start">
<h3 className="text-lg font-semibold">Members</h3>
<p className="text-neutral-700 dark:text-neutral-300">
Only allowed users can publish event to your Depot
</p>
</div>
<div className="inline-flex items-center gap-2">
<div className="isolate flex -space-x-2">
{tmpMembers.slice(0, 5).map((item) => (
<User key={item} pubkey={item} variant="stacked" />
))}
{tmpMembers.length > 5 ? (
<div className="inline-flex h-8 w-8 items-center justify-center rounded-full bg-neutral-200 text-neutral-900 ring-1 ring-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:ring-neutral-700">
<span className="text-xs font-medium">
+{tmpMembers.length}
</span>
</div>
) : null}
</div>
<Dialog.Trigger className="inline-flex h-8 w-max items-center justify-center gap-1 rounded-lg bg-blue-500 px-3 text-white hover:bg-blue-600">
<UserAddIcon className="size-4" />
Manage
</Dialog.Trigger>
</div>
</div>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/10 backdrop-blur-sm dark:bg-black/10" />
<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 overflow-hidden rounded-xl bg-white dark:bg-black">
<div className="inline-flex h-14 w-full shrink-0 items-center justify-between border-b border-neutral-100 px-5 dark:border-neutral-900">
<Dialog.Title className="text-center font-semibold">
Manage member
</Dialog.Title>
<div className="inline-flex items-center gap-2">
<button
type="button"
onClick={updateMembers}
className="inline-flex h-8 w-max items-center justify-center rounded-lg bg-blue-500 px-2.5 text-sm font-medium text-white hover:bg-blue-600"
>
Update
</button>
<Dialog.Close className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800">
<CancelIcon className="size-4" />
</Dialog.Close>
</div>
</div>
<div className="pb-3">
<div className="relative mb-2 mt-4 w-full px-5">
<input
type="text"
spellCheck={false}
value={newMember}
onChange={(e) => setNewMember(e.target.value)}
placeholder="npub1..."
className="h-11 w-full rounded-lg border-transparent bg-neutral-100 pl-3 pr-20 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
/>
<button
type="button"
onClick={addMember}
className="absolute right-7 top-1/2 inline-flex h-7 w-max -translate-y-1/2 transform items-center justify-center gap-1 rounded-md bg-neutral-200 px-2.5 text-sm font-medium text-blue-500 hover:bg-neutral-200 dark:bg-neutral-800 dark:hover:bg-neutral-800"
>
<PlusIcon className="size-4" />
Add
</button>
</div>
{tmpMembers.map((member) => (
<div
key={member}
className="group flex items-center justify-between px-5 py-2 hover:bg-neutral-100 dark:hover:bg-neutral-900"
>
<User pubkey={member} variant="simple" />
<button
type="button"
onClick={() => removeMember(member)}
className="hidden size-6 items-center justify-center rounded-md bg-neutral-200 group-hover:inline-flex hover:bg-red-200 dark:bg-neutral-800 dark:hover:bg-red-800 dark:hover:text-red-200"
>
<UserRemoveIcon className="size-4 text-red-500" />
</button>
</div>
))}
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}

View File

@@ -1,61 +0,0 @@
import { useArk } from "@lume/ark";
import { LoaderIcon, RunIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { User } from "@lume/ui";
import { NDKKind } from "@nostr-dev-kit/ndk";
import { useState } from "react";
import { toast } from "sonner";
export function DepotProfileCard() {
const ark = useArk();
const storage = useStorage();
const [status, setStatus] = useState(false);
const backupProfile = async () => {
try {
setStatus(true);
const event = await ark.getEventByFilter({
filter: {
authors: [ark.account.pubkey],
kinds: [NDKKind.Metadata],
},
});
// broadcast to depot
const publish = await event.publish();
if (publish) {
setStatus(false);
toast.success("Backup profile successfully.");
}
} catch (e) {
setStatus(false);
toast.error(JSON.stringify(e));
}
};
return (
<div className="flex h-56 w-full flex-col gap-2 overflow-hidden rounded-xl bg-neutral-100 p-2 dark:bg-neutral-900">
<div className="flex flex-1 items-center justify-center rounded-lg bg-neutral-200 dark:bg-neutral-800">
<User pubkey={ark.account.pubkey} variant="simple" />
</div>
<div className="inline-flex shrink-0 items-center justify-between">
<div className="text-sm font-medium">Profile</div>
<button
type="button"
onClick={backupProfile}
className="inline-flex h-8 w-max items-center justify-center gap-2 rounded-md bg-blue-500 pl-2 pr-3 font-medium text-white shadow shadow-blue-500/50 hover:bg-blue-600"
>
{status ? (
<LoaderIcon className="size-4 animate-spin" />
) : (
<RunIcon className="size-4" />
)}
Backup
</button>
</div>
</div>
);
}

View File

@@ -1,75 +0,0 @@
import { useArk } from "@lume/ark";
import { LoaderIcon, RunIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { NDKKind } from "@nostr-dev-kit/ndk";
import { useEffect, useState } from "react";
import { toast } from "sonner";
export function DepotRelaysCard() {
const ark = useArk();
const storage = useStorage();
const [status, setStatus] = useState(false);
const [relaySize, setRelaySize] = useState(0);
const backupRelays = async () => {
try {
setStatus(true);
const event = await ark.getEventByFilter({
filter: {
authors: [ark.account.pubkey],
kinds: [NDKKind.RelayList],
},
});
// broadcast to depot
const publish = await event.publish();
if (publish) {
setStatus(false);
toast.success("Backup profile successfully.");
}
} catch (e) {
setStatus(false);
toast.error(JSON.stringify(e));
}
};
useEffect(() => {
async function loadRelays() {
const event = await ark.getEventByFilter({
filter: {
authors: [ark.account.pubkey],
kinds: [NDKKind.RelayList],
},
});
if (event) setRelaySize(event.tags.length);
}
loadRelays();
}, []);
return (
<div className="flex h-56 w-full flex-col gap-2 overflow-hidden rounded-xl bg-neutral-100 p-2 dark:bg-neutral-900">
<div className="flex flex-1 items-center justify-center rounded-lg bg-neutral-200 dark:bg-neutral-800">
<p className="text-lg font-semibold">{relaySize} relays</p>
</div>
<div className="inline-flex shrink-0 items-center justify-between">
<div className="text-sm font-medium">Relay List</div>
<button
type="button"
onClick={backupRelays}
className="inline-flex h-8 w-max items-center justify-center gap-2 rounded-md bg-blue-500 pl-2 pr-3 font-medium text-white shadow shadow-blue-500/50 hover:bg-blue-600"
>
{status ? (
<LoaderIcon className="size-4 animate-spin" />
) : (
<RunIcon className="size-4" />
)}
Backup
</button>
</div>
</div>
);
}

View File

@@ -1,222 +0,0 @@
import { useArk } from "@lume/ark";
import { ChevronDownIcon, DepotIcon, GossipIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { NDKKind } from "@nostr-dev-kit/ndk";
import * as Collapsible from "@radix-ui/react-collapsible";
import { invoke } from "@tauri-apps/api/core";
import { appConfigDir } from "@tauri-apps/api/path";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { DepotContactCard } from "./components/contact";
import { DepotMembers } from "./components/members";
import { DepotProfileCard } from "./components/profile";
import { DepotRelaysCard } from "./components/relays";
export function DepotScreen() {
const ark = useArk();
const storage = useStorage();
const [dataPath, setDataPath] = useState("");
const [tunnelUrl, setTunnelUrl] = useState("");
const openFolder = async () => {
await invoke("show_in_folder", {
path: `${dataPath}/nostr.db`,
});
};
const updateRelayList = async () => {
try {
if (tunnelUrl.length < 1)
return toast.info("Please enter a valid relay url");
if (!tunnelUrl.startsWith("ws"))
return toast.info("Please enter a valid relay url");
const relayUrl = new URL(tunnelUrl.replace(/\s/g, ""));
if (!/^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/.test(relayUrl.host)) return;
const relayEvent = await ark.getEventByFilter({
filter: {
authors: [ark.account.pubkey],
kinds: [NDKKind.RelayList],
},
});
let publish: { id: string; seens: string[] };
if (!relayEvent) {
publish = await ark.createEvent({
kind: NDKKind.RelayList,
tags: [["r", tunnelUrl, ""]],
});
}
const newTags = relayEvent.tags ?? [];
newTags.push(["r", tunnelUrl, ""]);
publish = await ark.createEvent({
kind: NDKKind.RelayList,
tags: newTags,
});
if (publish) {
await storage.createSetting("tunnel_url", tunnelUrl);
toast.success("Update relay list successfully.");
setTunnelUrl("");
}
} catch (e) {
console.error(e);
toast.error("Error");
}
};
useEffect(() => {
async function loadConfig() {
const appDir = await appConfigDir();
setDataPath(appDir);
}
loadConfig();
}, []);
return (
<div className="flex h-full w-full rounded-xl shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:shadow-[inset_0_0_0.5px_1px_hsla(0,0%,100%,0.075),0_0_0_1px_hsla(0,0%,0%,0.05),0_0.3px_0.4px_hsla(0,0%,0%,0.02),0_0.9px_1.5px_hsla(0,0%,0%,0.045),0_3.5px_6px_hsla(0,0%,0%,0.09)]">
<div className="h-full w-72 shrink-0 rounded-l-xl bg-white/50 px-8 pt-8 backdrop-blur-xl dark:bg-black/50">
<div className="flex flex-col justify-center gap-4">
<div className="size-16 rounded-xl bg-gradient-to-bl from-teal-300 to-teal-600 p-1">
<div className="relative inline-flex h-full w-full items-center justify-center overflow-hidden rounded-lg bg-gradient-to-bl from-teal-400 to-teal-700 shadow-sm shadow-white/20">
<DepotIcon className="size-8 text-white" />
</div>
</div>
<h1 className="text-xl font-semibold">Depot is running</h1>
</div>
<div className="mt-8 flex flex-col gap-4">
<div className="flex flex-col gap-1.5">
<div className="text-sm font-medium">Relay URL</div>
<div className="inline-flex h-10 w-full select-text items-center rounded-lg bg-black/10 px-3 text-sm backdrop-blur-xl dark:bg-white/10">
ws://localhost:6090
</div>
</div>
<div className="flex flex-col gap-1.5">
<div className="text-sm font-medium">Database</div>
<div className="inline-flex h-10 w-full items-center gap-2 truncate rounded-lg bg-black/10 p-1 backdrop-blur-xl dark:bg-white/10">
<p className="shrink-0 pl-2 text-sm">nostr.db (SQLite)</p>
<button
type="button"
onClick={openFolder}
className="inline-flex h-full w-full items-center justify-center rounded-md bg-white text-sm font-medium shadow hover:bg-blue-500 hover:text-white dark:bg-black"
>
Open
</button>
</div>
</div>
</div>
</div>
<div className="flex-1 overflow-y-auto rounded-r-xl bg-white pb-20 dark:bg-black">
<div className="mb-5 flex h-12 items-center border-b border-neutral-100 px-5 dark:border-neutral-900">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
Actions
</h3>
</div>
<div className="flex flex-col gap-5 px-5">
<Collapsible.Root
defaultOpen
className="flex flex-col overflow-hidden rounded-xl border border-transparent bg-neutral-50 data-[state=open]:border-blue-500 dark:bg-neutral-950"
>
<Collapsible.Trigger className="flex h-20 items-center justify-between px-5 hover:bg-neutral-100 dark:hover:bg-neutral-900">
<div className="flex flex-col items-start">
<h3 className="text-lg font-semibold">Expose</h3>
<p className="text-neutral-700 dark:text-neutral-300">
Make your Depot visible in the Internet, everyone can connect
into it.
</p>
</div>
<ChevronDownIcon className="size-5 shrink-0" />
</Collapsible.Trigger>
<Collapsible.Content>
<div className="flex w-full flex-col gap-4 p-5">
<div>
<p className="mb-1 font-medium">ngrok</p>
<input
readOnly
value="ngrok http --domain=<your_domain> 6090"
className="h-11 w-full rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
/>
</div>
<div>
<p className="mb-1 font-medium">Cloudflare Tunnel</p>
<input
readOnly
value="cloudflared tunnel --url localhost:6090"
className="h-11 w-full rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
/>
</div>
<div>
<p className="mb-1 font-medium">Local Tunnel</p>
<input
readOnly
value="lt --port 6090"
className="h-11 w-full rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
/>
</div>
<div className="mt-4 border-t border-neutral-100 pt-4 dark:border-neutral-900">
<div className="inline-flex items-center gap-2">
<GossipIcon className="size-5 text-blue-500" />
<h3 className="mb-1 font-semibold">
Support Gossip Model (Recommended)
</h3>
</div>
<div className="w-full max-w-xl">
<p className=" text-balance">
By adding to Relay List, other Nostr Client which support
Gossip Model will automatically connect to your Depot and
improve the discoverability.
</p>
<div className="mt-2 inline-flex w-full items-center gap-2">
<input
type="text"
value={tunnelUrl}
onChange={(e) => setTunnelUrl(e.target.value)}
spellCheck={false}
placeholder="wss://"
className="h-10 flex-1 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
/>
<button
type="button"
onClick={updateRelayList}
className="inline-flex h-10 w-max shrink-0 items-center justify-center rounded-lg bg-blue-500 px-4 font-medium text-white hover:bg-blue-600"
>
Update
</button>
</div>
</div>
</div>
</div>
</Collapsible.Content>
</Collapsible.Root>
<Collapsible.Root className="flex flex-col overflow-hidden rounded-xl border border-transparent bg-neutral-50 data-[state=open]:border-blue-500 dark:bg-neutral-950">
<Collapsible.Trigger className="flex h-20 items-center justify-between px-5 hover:bg-neutral-100 dark:hover:bg-neutral-900">
<div className="flex flex-col items-start">
<h3 className="text-lg font-semibold">Backup (Recommended)</h3>
<p className="text-neutral-700 dark:text-neutral-300">
Backup all your data to Depot, it always live on your machine.
</p>
</div>
<ChevronDownIcon className="size-5 shrink-0" />
</Collapsible.Trigger>
<Collapsible.Content>
<div className="grid grid-cols-3 gap-4 px-5 py-5">
<DepotProfileCard />
<DepotContactCard />
<DepotRelaysCard />
</div>
</Collapsible.Content>
</Collapsible.Root>
<DepotMembers />
</div>
</div>
</div>
);
}

View File

@@ -1,98 +0,0 @@
import { useArk } from "@lume/ark";
import { LoaderIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { delay } from "@lume/utils";
import { resolveResource } from "@tauri-apps/api/path";
import { readTextFile, writeTextFile } from "@tauri-apps/plugin-fs";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { parse, stringify } from "smol-toml";
import { toast } from "sonner";
export function DepotOnboardingScreen() {
const ark = useArk();
const storage = useStorage();
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const launchDepot = async () => {
try {
setLoading(true);
// get default config
const defaultConfig = await resolveResource("resources/config.toml");
const config = await readTextFile(defaultConfig);
const parsedConfig = parse(config);
// add current user to whitelist
// biome-ignore lint/complexity/useLiteralKeys: <explanation>
parsedConfig.authorization["pubkey_whitelist"].push(ark.account.pubkey);
// update new config
const newConfig = stringify(parsedConfig);
await writeTextFile(defaultConfig, newConfig);
// launch depot
await storage.launchDepot();
await storage.createSetting("depot", "1");
await delay(2000); // delay 2s to make sure depot is running
// default depot url: ws://localhost:6090
// #TODO: user can custom depot url
const connect = await ark.connectDepot();
if (connect) {
toast.success("Your Depot is successfully launch.");
setLoading(false);
navigate("/depot/");
}
} catch (e) {
toast.error(e);
}
};
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-10 rounded-xl bg-white shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-[inset_0_0_0.5px_1px_hsla(0,0%,100%,0.075),0_0_0_1px_hsla(0,0%,0%,0.05),0_0.3px_0.4px_hsla(0,0%,0%,0.02),0_0.9px_1.5px_hsla(0,0%,0%,0.045),0_3.5px_6px_hsla(0,0%,0%,0.09)]">
<div className="flex flex-col items-center gap-8">
<div className="text-center">
<h1 className="mb-1 text-3xl font-semibold text-neutral-400 dark:text-neutral-600">
Run your Personal Nostr Relay inside Lume
</h1>
<h2 className="text-4xl font-semibold">Your Relay, Your Control.</h2>
</div>
<div className="rounded-xl bg-blue-100 p-1.5 dark:bg-blue-900">
<button
type="button"
onClick={launchDepot}
className="inline-flex h-11 w-36 transform items-center justify-center gap-2 rounded-lg bg-blue-500 font-medium text-white active:translate-y-1"
>
{loading ? (
<>
<LoaderIcon className="h-5 w-5 animate-spin" />
Launching...
</>
) : (
<>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="h-5 w-5"
>
<path
fillRule="evenodd"
d="M12 2.25a.75.75 0 0 1 .75.75v9a.75.75 0 0 1-1.5 0V3a.75.75 0 0 1 .75-.75ZM6.166 5.106a.75.75 0 0 1 0 1.06 8.25 8.25 0 1 0 11.668 0 .75.75 0 1 1 1.06-1.06c3.808 3.807 3.808 9.98 0 13.788-3.807 3.808-9.98 3.808-13.788 0-3.808-3.807-3.808-9.98 0-13.788a.75.75 0 0 1 1.06 0Z"
clipRule="evenodd"
/>
</svg>
Launch
</>
)}
</button>
</div>
</div>
</div>
);
}

View File

@@ -1,157 +0,0 @@
import { useStorage } from "@lume/storage";
import { downloadDir } from "@tauri-apps/api/path";
import { message, save } from "@tauri-apps/plugin-dialog";
import { writeTextFile } from "@tauri-apps/plugin-fs";
import { relaunch } from "@tauri-apps/plugin-process";
import { useRouteError } from "react-router-dom";
interface RouteError {
statusText: string;
message: string;
}
export function ErrorScreen() {
const storage = useStorage();
const error = useRouteError() as RouteError;
const restart = async () => {
await relaunch();
};
const download = async () => {
try {
const downloadPath = await downloadDir();
const fileName = `nostr_keys_${new Date().toISOString()}.txt`;
const filePath = await save({
defaultPath: `${downloadPath}/${fileName}`,
});
const nsec = await storage.loadPrivkey(ark.account.pubkey);
if (filePath) {
if (nsec) {
await writeTextFile(
filePath,
`Nostr account, generated by Lume (lume.nu)\nPublic key: ${ark.account.id}\nPrivate key: ${nsec}`,
);
} else {
await writeTextFile(
filePath,
`Nostr account, generated by Lume (lume.nu)\nPublic key: ${ark.account.id}`,
);
}
} // else { user cancel action }
} catch (e) {
await message(e, {
title: "Cannot download account keys",
type: "error",
});
}
};
return (
<div
data-tauri-drag-region
className="relative flex h-screen w-screen items-center justify-center bg-blue-600 overflow-hidden rounded-t-xl"
>
<div className="flex w-full max-w-2xl flex-col items-start gap-8">
<div className="flex flex-col">
<h1 className="mb-3 text-4xl font-semibold text-blue-400">
Sorry, an unexpected error has occurred.
</h1>
<h3 className="text-3xl font-semibold leading-snug text-white">
Don&apos;t panic, your account is safe.
<br />
Here are what things you can do:
</h3>
</div>
<div className="flex w-full flex-col gap-3">
<div className="flex items-center justify-between rounded-xl bg-blue-700 px-3 py-4">
<div className="text-xl font-semibold text-white">
1. Try to close and re-open the app
</div>
<button
type="button"
onClick={() => restart()}
className="h-9 w-28 rounded-lg bg-blue-800 px-3 font-medium text-white hover:bg-blue-900"
>
Restart
</button>
</div>
<div className="flex items-center justify-between rounded-xl bg-blue-700 px-3 py-4">
<div className="text-xl font-semibold text-white">
2. Backup Nostr account
</div>
<button
type="button"
onClick={() => download()}
className="h-9 w-28 rounded-lg bg-blue-800 px-3 font-medium text-white hover:bg-blue-900"
>
Download
</button>
</div>
<div className="rounded-xl bg-blue-700 px-3 py-4">
<div className="flex w-full flex-col gap-2">
<div className="flex w-full items-center justify-between">
<div className="text-xl font-semibold text-white">
3. Report this issue to Lume&apos;s Devs
</div>
<a
href="https://github.com/luminous-devs/lume/issues/new"
target="_blank"
rel="noreferrer"
className="inline-flex h-9 w-28 items-center justify-center rounded-lg bg-blue-800 px-3 font-medium text-white hover:bg-blue-900"
>
Report
</a>
</div>
<div className="inline-flex h-16 items-center justify-center overflow-y-auto rounded-lg border border-dashed border-red-300 bg-blue-800 px-5">
<p className="select-text break-all text-red-400">
{error.statusText || error.message}
</p>
</div>
</div>
</div>
<div className="rounded-xl bg-blue-700 px-3 py-4">
<div className="flex w-full flex-col gap-1.5">
<div className="text-xl font-semibold text-white">
4. Use another Nostr client
</div>
<div className="select-text text-lg font-medium text-blue-300">
<p>
While waiting for Lume&apos;s Devs to release the bug fixes,
you always can use other Nostr clients with your account:
</p>
<div className="mt-2 flex flex-col gap-1 text-white">
<a
className="hover:!underline"
href="https://snort.social"
target="_blank"
rel="noreferrer"
>
snort.social
</a>
<a
className="hover:!underline"
href="https://primal.net"
target="_blank"
rel="noreferrer"
>
primal.net
</a>
<a
className="hover:!underline"
href="https://nostrudel.ninja"
target="_blank"
rel="noreferrer"
>
nostrudel.ninja
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,133 +0,0 @@
import { Antenas } from "@columns/antenas";
import { Default } from "@columns/default";
import { Group } from "@columns/group";
import { Hashtag } from "@columns/hashtag";
import { Thread } from "@columns/thread";
import { Timeline } from "@columns/timeline";
import { User } from "@columns/user";
import { useColumnContext } from "@lume/ark";
import { ArrowLeftIcon, ArrowRightIcon, PlusSquareIcon } from "@lume/icons";
import { IColumn } from "@lume/types";
import { TutorialModal } from "@lume/ui/src/tutorial/modal";
import { COL_TYPES } from "@lume/utils";
import { useRef, useState } from "react";
import { VList, VListHandle } from "virtua";
export function HomeScreen() {
const ref = useRef<VListHandle>(null);
const { addColumn, columns } = useColumnContext();
const [selectedIndex, setSelectedIndex] = useState(-1);
const renderItem = (column: IColumn) => {
switch (column.kind) {
case COL_TYPES.default:
return <Default key={column.id} column={column} />;
case COL_TYPES.newsfeed:
return <Timeline key={column.id} column={column} />;
case COL_TYPES.thread:
return <Thread key={column.id} column={column} />;
case COL_TYPES.user:
return <User key={column.id} column={column} />;
case COL_TYPES.hashtag:
return <Hashtag key={column.id} column={column} />;
case COL_TYPES.group:
return <Group key={column.id} column={column} />;
case COL_TYPES.antenas:
return <Antenas key={column.id} column={column} />;
default:
return <Default key={column.id} column={column} />;
}
};
return (
<div className="relative w-full h-full">
<VList
id="timeline"
ref={ref}
className="h-full w-full flex-nowrap overflow-x-auto !overflow-y-hidden scrollbar-none focus:outline-none"
itemSize={420}
tabIndex={0}
horizontal
onKeyDown={(e) => {
if (!ref.current) return;
switch (e.code) {
case "ArrowUp":
case "ArrowLeft": {
e.preventDefault();
const prevIndex = Math.max(selectedIndex - 1, 0);
setSelectedIndex(prevIndex);
ref.current.scrollToIndex(prevIndex, {
align: "center",
smooth: true,
});
break;
}
case "ArrowDown":
case "ArrowRight": {
e.preventDefault();
const nextIndex = Math.min(selectedIndex + 1, columns.length - 1);
setSelectedIndex(nextIndex);
ref.current.scrollToIndex(nextIndex, {
align: "center",
smooth: true,
});
break;
}
default:
break;
}
}}
>
{columns.map((column) => renderItem(column))}
<div className="w-[420px]" />
</VList>
<div className="absolute bottom-3 right-3">
<div className="flex items-center gap-1 p-1 bg-black/30 dark:bg-white/30 backdrop-blur-xl rounded-xl">
<button
type="button"
onClick={() => {
const prevIndex = Math.max(selectedIndex - 1, 0);
setSelectedIndex(prevIndex);
ref.current.scrollToIndex(prevIndex, {
align: "center",
smooth: true,
});
}}
className="inline-flex items-center justify-center rounded-lg text-white/70 hover:text-white hover:bg-black/30 size-10"
>
<ArrowLeftIcon className="size-5" />
</button>
<button
type="button"
onClick={() => {
const nextIndex = Math.min(selectedIndex + 1, columns.length - 1);
setSelectedIndex(nextIndex);
ref.current.scrollToIndex(nextIndex, {
align: "center",
smooth: true,
});
}}
className="inline-flex items-center justify-center rounded-lg text-white/70 hover:text-white hover:bg-black/30 size-10"
>
<ArrowRightIcon className="size-5" />
</button>
<button
type="button"
onClick={async () =>
await addColumn({
kind: COL_TYPES.default,
title: "",
content: "",
})
}
className="inline-flex items-center justify-center rounded-lg text-white/70 hover:text-white hover:bg-black/30 size-10"
>
<PlusSquareIcon className="size-5" />
</button>
<div className="w-px h-6 bg-white/10" />
<TutorialModal />
</div>
</div>
</div>
);
}

View File

@@ -1,90 +0,0 @@
import { NoteSkeleton, RepostNote, TextNote, useArk } from "@lume/ark";
import { ArrowRightCircleIcon, LoaderIcon } from "@lume/icons";
import { FETCH_LIMIT } from "@lume/utils";
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useCallback, useMemo } from "react";
import { VList } from "virtua";
export function RelayEventList({ relayUrl }: { relayUrl: string }) {
const ark = useArk();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ["relay-events", relayUrl],
initialPageParam: 0,
queryFn: async ({
signal,
pageParam,
}: {
signal: AbortSignal;
pageParam: number;
}) => {
const url = `wss://${relayUrl}`;
const events = await ark.getRelayEvents({
relayUrl: url,
filter: {
kinds: [NDKKind.Text, NDKKind.Repost],
},
limit: FETCH_LIMIT,
pageParam,
signal,
});
return events;
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage.at(-1);
if (!lastEvent) return;
return lastEvent.created_at - 1;
},
refetchOnWindowFocus: false,
});
const allEvents = useMemo(
() => (data ? data.pages.flatMap((page) => page) : []),
[data],
);
const renderItem = useCallback(
(event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return <TextNote key={event.id} event={event} className="mt-3" />;
case NDKKind.Repost:
return <RepostNote key={event.id} event={event} className="mt-3" />;
default:
return <TextNote key={event.id} event={event} className="mt-3" />;
}
},
[data],
);
return (
<VList className="mx-auto h-full w-full max-w-[500px] px-3 scrollbar-none">
{status === "pending" ? (
<NoteSkeleton />
) : (
allEvents.map((item) => renderItem(item))
)}
<div className="flex h-16 items-center justify-center px-3 pb-3">
{hasNextPage ? (
<button
type="button"
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-10 w-max items-center justify-center gap-2 rounded-full bg-blue-500 px-6 font-medium text-white hover:bg-blue-600 focus:outline-none"
>
{isFetchingNextPage ? (
<LoaderIcon className="h-4 w-4 animate-spin" />
) : (
<>
<ArrowRightCircleIcon className="h-5 w-5" />
Load more
</>
)}
</button>
) : null}
</div>
</VList>
);
}

View File

@@ -1,59 +0,0 @@
import { useRelaylist } from "@lume/ark";
import { PlusIcon } from "@lume/icons";
import { NDKRelayUrl } from "@nostr-dev-kit/ndk";
import { normalizeRelayUrl } from "nostr-fetch";
import { useState } from "react";
import { toast } from "sonner";
const domainRegex = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/;
export function RelayForm() {
const { connectRelay } = useRelaylist();
const [relay, setRelay] = useState<{
url: NDKRelayUrl;
purpose: "read" | "write" | undefined;
}>({ url: "", purpose: undefined });
const create = () => {
if (relay.url.length < 1) return toast.info("Please enter relay url");
try {
const relayUrl = new URL(relay.url.replace(/\s/g, ""));
if (
domainRegex.test(relayUrl.host) &&
(relayUrl.protocol === "wss:" || relayUrl.protocol === "ws:")
) {
connectRelay.mutate(normalizeRelayUrl(relay.url));
setRelay({ url: "", purpose: undefined });
} else {
return toast.error(
"URL is invalid, a relay must use websocket protocol (start with wss:// or ws://). Please check again",
);
}
} catch {
return toast.error("Relay URL is not valid. Please check again");
}
};
return (
<div className="flex gap-2">
<input
className="h-11 w-full rounded-lg border-transparent px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 bg-white/50 dark:bg-black/50 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
placeholder="wss://"
spellCheck={false}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
value={relay.url}
onChange={(e) => setRelay((prev) => ({ ...prev, url: e.target.value }))}
/>
<button
type="button"
onClick={() => create()}
className="inline-flex size-11 shrink-0 items-center justify-center rounded-lg bg-blue-500 text-white hover:bg-blue-600"
>
<PlusIcon className="size-5" />
</button>
</div>
);
}

View File

@@ -1,38 +0,0 @@
import { useRelaylist } from "@lume/ark";
import { PlusIcon, ShareIcon } from "@lume/icons";
import { normalizeRelayUrl } from "nostr-fetch";
import { Link } from "react-router-dom";
export function RelayItem({ url }: { url: string }) {
const domain = new URL(url).hostname;
const { connectRelay } = useRelaylist();
return (
<div className="flex h-14 w-full items-center justify-between border-b border-neutral-100 px-5 dark:border-neutral-950">
<div className="inline-flex items-center gap-2">
<span className="text-sm font-semibold text-neutral-500 dark:text-neutral-400">
Relay:{" "}
</span>
<span className="max-w-[200px] truncate text-sm font-medium text-neutral-900 dark:text-neutral-100">
{url}
</span>
</div>
<div className="inline-flex items-center gap-2">
<Link
to={`/relays/${domain}/`}
className="inline-flex h-6 items-center justify-center gap-1 rounded bg-neutral-100 px-1.5 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
<ShareIcon className="h-3 w-3" />
Inspect
</Link>
<button
type="button"
onClick={() => connectRelay.mutate(normalizeRelayUrl(url))}
className="inline-flex h-6 w-6 items-center justify-center rounded bg-blue-100 text-blue-500 hover:bg-blue-200 dark:bg-blue-900 hover:dark:bg-blue-800"
>
<PlusIcon className="size-4" />
</button>
</div>
</div>
);
}

View File

@@ -1,91 +0,0 @@
import { useArk, useRelay } from "@lume/ark";
import { LoaderIcon, PlusIcon, ShareIcon } from "@lume/icons";
import { User } from "@lume/ui";
import { useQuery } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
import { VList } from "virtua";
export function RelayList() {
const ark = useArk();
const { connectRelay } = useRelay();
const { status, data } = useQuery({
queryKey: ["relays"],
queryFn: async () => {
return await ark.getAllRelaysFromContacts();
},
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
staleTime: Infinity,
});
const navigate = useNavigate();
const inspectRelay = (relayUrl: string) => {
const url = new URL(relayUrl);
navigate(`/relays/${url.hostname}`);
};
return (
<div className="col-span-2 bg-white">
{status === "pending" ? (
<div className="flex h-full w-full items-center justify-center pb-10">
<div className="inline-flex flex-col items-center justify-center gap-2">
<LoaderIcon className="h-5 w-5 animate-spin text-neutral-900 dark:text-neutral-100" />
<p>Loading relay...</p>
</div>
</div>
) : (
<VList className="h-full">
<div className="inline-flex h-16 w-full items-center border-b border-neutral-100 px-3 dark:border-neutral-900">
<h3 className="font-semibold">Relay discovery</h3>
</div>
{[...data].map(([key, value]) => (
<div
key={key}
className="flex h-14 w-full items-center justify-between border-b border-neutral-100 px-3 dark:border-neutral-900"
>
<div className="inline-flex items-center gap-2 divide-x divide-neutral-100 dark:divide-neutral-900">
<div className="inline-flex items-center gap-2">
<button
type="button"
onClick={() => inspectRelay(key)}
className="inline-flex h-6 items-center justify-center gap-1 rounded bg-neutral-200 px-1.5 text-sm font-medium text-neutral-900 hover:bg-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700"
>
<ShareIcon className="h-3 w-3" />
Inspect
</button>
<button
type="button"
onClick={() => connectRelay.mutate(key)}
className="inline-flex h-6 w-6 items-center justify-center rounded text-neutral-900 hover:bg-neutral-200 dark:text-neutral-100 dark:hover:bg-neutral-800"
>
<PlusIcon className="h-3 w-3" />
</button>
</div>
<div className="inline-flex items-center gap-2 pl-3">
<span className="text-sm font-semibold text-neutral-500 dark:text-neutral-400">
Relay:{" "}
</span>
<span className="max-w-[200px] truncate text-sm font-medium text-neutral-900 dark:text-neutral-100">
{key}
</span>
</div>
</div>
<div className="isolate flex -space-x-2">
{value.slice(0, 4).map((item) => (
<User key={item} pubkey={item} variant="stacked" />
))}
{value.length > 4 ? (
<div className="inline-flex h-8 w-8 items-center justify-center rounded-full bg-neutral-200 text-neutral-900 ring-1 ring-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:ring-neutral-700">
<span className="text-xs font-medium">+{value.length}</span>
</div>
) : null}
</div>
</div>
))}
</VList>
)}
</div>
);
}

View File

@@ -1,99 +0,0 @@
import { useArk } from "@lume/ark";
import { CancelIcon, RefreshIcon } from "@lume/icons";
import { cn } from "@lume/utils";
import { NDKKind } from "@nostr-dev-kit/ndk";
import { useQuery } from "@tanstack/react-query";
import { RelayForm } from "./relayForm";
export function RelaySidebar({ className }: { className?: string }) {
const ark = useArk();
const { status, data, refetch } = useQuery({
queryKey: ["relay-personal"],
queryFn: async () => {
const event = await ark.getEventByFilter({
filter: {
kinds: [NDKKind.RelayList],
authors: [ark.account.pubkey],
},
});
if (!event) return [];
return event.tags.filter((tag) => tag[0] === "r");
},
refetchOnWindowFocus: false,
});
const currentRelays = new Set(
ark.ndk.pool.connectedRelays().map((item) => item.url),
);
return (
<div
className={cn(
"rounded-l-xl bg-white/50 backdrop-blur-xl dark:bg-black/50",
className,
)}
>
<div className="inline-flex items-center justify-between w-full h-14 px-3 border-b border-black/10 dark:border-white/10">
<h3 className="font-semibold">Connected relays</h3>
<button
type="button"
onClick={() => refetch()}
className="inline-flex items-center justify-center w-6 h-6 rounded-md shrink-0 hover:bg-neutral-100 dark:hover:bg-neutral-900"
>
<RefreshIcon className="w-4 h-4" />
</button>
</div>
<div className="flex flex-col gap-2 px-3 mt-3">
{status === "pending" ? (
<p>Loading...</p>
) : !data.length ? (
<div className="flex items-center justify-center w-full h-20 rounded-xl bg-neutral-50 dark:bg-neutral-950">
<p className="text-sm font-medium">
You not have personal relay list yet
</p>
</div>
) : (
data.map((item) => (
<div
key={item[1]}
className="flex items-center justify-between px-3 rounded-lg group h-11 bg-white/50 dark:bg-black/50"
>
<div className="inline-flex items-baseline gap-2">
{currentRelays.has(item[1]) ? (
<span className="relative flex w-2 h-2">
<span className="absolute inline-flex w-full h-full bg-green-400 rounded-full opacity-75 animate-ping" />
<span className="relative inline-flex w-2 h-2 bg-teal-500 rounded-full" />
</span>
) : (
<span className="relative flex w-2 h-2">
<span className="absolute inline-flex w-full h-full bg-red-400 rounded-full opacity-75 animate-ping" />
<span className="relative inline-flex w-2 h-2 bg-red-500 rounded-full" />
</span>
)}
<p className="max-w-[20rem] truncate text-sm font-medium text-neutral-900 dark:text-neutral-100">
{item[1].replace("wss://", "").replace("ws://", "")}
</p>
</div>
<div className="inline-flex items-center gap-2">
{item[2]?.length ? (
<div className="inline-flex items-center justify-center h-6 px-2 text-xs font-medium capitalize rounded w-max bg-neutral-200 dark:bg-neutral-800">
{item[2]}
</div>
) : null}
<button
type="button"
className="items-center justify-center hidden w-6 h-6 rounded group-hover:inline-flex hover:bg-neutral-300 dark:hover:bg-neutral-700"
>
<CancelIcon className="w-4 h-4 text-neutral-900 dark:text-neutral-100" />
</button>
</div>
</div>
))
)}
<RelayForm />
</div>
</div>
);
}

View File

@@ -1,33 +0,0 @@
import { useArk } from "@lume/ark";
import { LoaderIcon, PlusIcon, ShareIcon } from "@lume/icons";
import { useQuery } from "@tanstack/react-query";
import { VList } from "virtua";
import { RelayItem } from "./components/relayItem";
export function RelayFollowsScreen() {
const ark = useArk();
const { isLoading, data: relays } = useQuery({
queryKey: ["relay-follows"],
queryFn: async ({ signal }: { signal: AbortSignal }) => {
return await ark.getAllRelaysFromContacts();
},
refetchOnMount: false,
refetchOnWindowFocus: false,
});
if (isLoading) {
return (
<div className="flex h-full w-full items-center justify-center">
<LoaderIcon className="size-5 animate-spin" />
</div>
);
}
return (
<VList itemSize={49}>
{relays.map((item: string) => (
<RelayItem key={item} url={item} />
))}
</VList>
);
}

View File

@@ -1,34 +0,0 @@
import { LoaderIcon } from "@lume/icons";
import { useQuery } from "@tanstack/react-query";
import { fetch } from "@tauri-apps/plugin-http";
import { VList } from "virtua";
import { RelayItem } from "./components/relayItem";
export function RelayGlobalScreen() {
const { isLoading, data: relays } = useQuery({
queryKey: ["relay-global"],
queryFn: async ({ signal }: { signal: AbortSignal }) => {
const res = await fetch("https://api.nostr.watch/v1/online", { signal });
if (!res.ok) throw new Error("Failed to get online relays");
return (await res.json()) as string[];
},
refetchOnMount: false,
refetchOnWindowFocus: false,
});
if (isLoading) {
return (
<div className="flex h-full w-full items-center justify-center">
<LoaderIcon className="size-5 animate-spin" />
</div>
);
}
return (
<VList itemSize={49}>
{relays.map((item: string) => (
<RelayItem key={item} url={item} />
))}
</VList>
);
}

View File

@@ -1,45 +0,0 @@
import { cn } from "@lume/utils";
import { NavLink, Outlet } from "react-router-dom";
import { RelaySidebar } from "./components/sidebar";
export function RelaysScreen() {
return (
<div className="grid h-full w-full lg:grid-cols-4 xl:grid-cols-5 rounded-xl shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:shadow-none dark:ring-1 dark:ring-white/10">
<RelaySidebar className="col-span-1" />
<div className="col-span-3 xl:col-span-4 flex flex-col rounded-r-xl bg-white dark:bg-black">
<div className="h-14 shrink-0 flex px-5 items-center gap-6 border-b border-neutral-100 dark:border-neutral-950">
<NavLink
end
to={"/relays/"}
className={({ isActive }) =>
cn(
"h-9 w-24 rounded-lg inline-flex items-center justify-center font-medium",
isActive
? "bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-950 dark:hover:bg-neutral-900"
: "",
)
}
>
Global
</NavLink>
<NavLink
to={"/relays/follows/"}
className={({ isActive }) =>
cn(
"h-9 w-24 rounded-lg inline-flex items-center justify-center font-medium",
isActive
? "bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-950 dark:hover:bg-neutral-900"
: "",
)
}
>
Follows
</NavLink>
</div>
<div className="flex flex-col flex-1 min-h-0 overflow-y-auto">
<Outlet />
</div>
</div>
</div>
);
}

View File

@@ -1,161 +0,0 @@
import { ArrowLeftIcon, LoaderIcon } from "@lume/icons";
import { NIP11 } from "@lume/types";
import { User } from "@lume/ui";
import { Suspense } from "react";
import { Await, useLoaderData, useNavigate, useParams } from "react-router-dom";
import { RelayEventList } from "./components/relayEventList";
export function RelayUrlScreen() {
const { url } = useParams();
const data: { relay?: { [key: string]: string } } = useLoaderData();
const navigate = useNavigate();
const getSoftwareName = (url: string) => {
const filename = url.substring(url.lastIndexOf("/") + 1);
return filename.replace(".git", "");
};
const titleCase = (s: string) => {
return s
.replace(/^[-_]*(.)/, (_, c) => c.toUpperCase())
.replace(/[-_]+(.)/g, (_, c) => ` ${c.toUpperCase()}`);
};
return (
<div className="grid h-full w-full grid-cols-3">
<div className="col-span-2 border-r border-neutral-100 dark:border-neutral-900">
<RelayEventList relayUrl={url} />
</div>
<div className="col-span-1 px-3 py-3">
<Suspense
fallback={
<div className="flex items-center gap-2 text-sm font-medium text-neutral-900 dark:text-neutral-100">
<LoaderIcon className="h-4 w-4 animate-spin" />
Loading...
</div>
}
>
<Await
resolve={data.relay}
errorElement={
<div className="text-sm font-medium">
<p>Could not load relay information 😬</p>
</div>
}
>
{(resolvedRelay: NIP11) => (
<div className="flex flex-col gap-5">
<div>
<h3 className="font-semibold">{resolvedRelay.name}</h3>
<p className="text-sm text-neutral-600 dark:text-neutral-500">
{resolvedRelay.description}
</p>
</div>
{resolvedRelay.pubkey ? (
<div className="flex flex-col gap-1">
<h5 className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
Owner:
</h5>
<div className="w-full rounded-lg bg-neutral-100 px-2 py-2 dark:bg-neutral-900">
<User pubkey={resolvedRelay.pubkey} variant="simple" />
</div>
</div>
) : null}
{resolvedRelay.contact ? (
<div>
<h5 className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
Contact:
</h5>
<a
href={`mailto:${resolvedRelay.contact}`}
target="_blank"
className="truncate underline after:content-['_↗'] hover:text-blue-500"
rel="noreferrer"
>
{resolvedRelay.contact}
</a>
</div>
) : null}
<div>
<h5 className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
Software:
</h5>
<a
href={resolvedRelay.software}
target="_blank"
rel="noreferrer"
className="underline after:content-['_↗'] hover:text-blue-500"
>
{`${getSoftwareName(resolvedRelay.software)} - ${
resolvedRelay.version
}`}
</a>
</div>
<div>
<h5 className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
Supported NIPs:
</h5>
<div className="mt-2 grid grid-cols-7 gap-2">
{resolvedRelay.supported_nips.map((item) => (
<a
key={item}
href={`https://nips.be/${item}`}
target="_blank"
rel="noreferrer"
className="inline-flex aspect-square h-auto w-full items-center justify-center rounded bg-neutral-100 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-900"
>
{item}
</a>
))}
</div>
</div>
{resolvedRelay.limitation ? (
<div>
<h5 className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
Limitation
</h5>
<div className="flex flex-col gap-2 divide-y divide-white/5">
{Object.keys(resolvedRelay.limitation).map(
(key, index) => {
return (
<div
key={key + index}
className="flex items-baseline justify-between pt-2"
>
<p className="text-sm font-medium text-neutral-900 dark:text-neutral-100">
{titleCase(key)}:
</p>
<p className="text-sm font-medium text-neutral-600 dark:text-neutral-400">
{resolvedRelay.limitation[key].toString()}
</p>
</div>
);
},
)}
</div>
</div>
) : null}
{resolvedRelay.payments_url ? (
<div className="flex flex-col gap-1">
<a
href={resolvedRelay.payments_url}
target="_blank"
rel="noreferrer"
className="inline-flex h-10 w-full items-center justify-center rounded-lg bg-blue-500 text-sm font-medium hover:bg-blue-600"
>
Open payment website
</a>
<span className="text-center text-xs text-neutral-600 dark:text-neutral-400">
You need to make a payment to connect this relay
</span>
</div>
) : null}
</div>
)}
</Await>
</Suspense>
</div>
</div>
);
}

View File

@@ -1,76 +0,0 @@
import { getVersion } from '@tauri-apps/api/app';
import { relaunch } from '@tauri-apps/plugin-process';
import { Update, check } from '@tauri-apps/plugin-updater';
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { toast } from 'sonner';
export function AboutScreen() {
const [version, setVersion] = useState('');
const [newUpdate, setNewUpdate] = useState<Update>(null);
const checkUpdate = async () => {
const update = await check();
if (!update) toast.info('There is no update available');
setNewUpdate(update);
};
const installUpdate = async () => {
await newUpdate.downloadAndInstall();
await relaunch();
};
useEffect(() => {
async function loadVersion() {
const appVersion = await getVersion();
setVersion(appVersion);
}
loadVersion();
}, []);
return (
<div className="mx-auto w-full max-w-lg">
<div className="flex items-center justify-center gap-2">
<img src="/icon.png" alt="Lume's logo" className="w-16 shrink-0" />
<div>
<h1 className="text-xl font-semibold">Lume</h1>
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
Version {version}
</p>
</div>
</div>
<div className="mx-auto mt-4 flex w-full max-w-xs flex-col gap-2">
{!newUpdate ? (
<button
type="button"
onClick={() => checkUpdate()}
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-blue-500 text-sm font-medium text-white hover:bg-blue-600"
>
Check for update
</button>
) : (
<button
type="button"
onClick={() => installUpdate()}
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-blue-500 text-sm font-medium text-white hover:bg-blue-600"
>
Install {newUpdate.version}
</button>
)}
<Link
to="https://lume.nu"
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-neutral-100 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
Website
</Link>
<Link
to="https://github.com/luminous-devs/lume/issues"
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-neutral-100 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
Report a issue
</Link>
</div>
</div>
);
}

View File

@@ -1,31 +0,0 @@
import { useStorage } from "@lume/storage";
export function AdvancedSettingScreen() {
const storage = useStorage();
const clearCache = async () => {
await storage.clearCache();
};
return (
<div className="mx-auto w-full max-w-lg">
<div className="flex flex-col gap-6">
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-8">
<div className="w-24 shrink-0 text-end text-sm font-semibold">
Cache
</div>
<div className="text-sm">Use for boost up nostr connection</div>
</div>
<button
type="button"
onClick={() => clearCache()}
className="h-8 w-max rounded-lg px-3 text-sm font-semibold text-blue-500 bg-blue-100 hover:bg-blue-200"
>
Clear
</button>
</div>
</div>
</div>
);
}

View File

@@ -1,62 +0,0 @@
import { EyeOffIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { nip19 } from "nostr-tools";
import { useEffect, useState } from "react";
export function BackupSettingScreen() {
const storage = useStorage();
const [privkey, setPrivkey] = useState(null);
const [showPassword, setShowPassword] = useState(false);
const removePrivkey = async () => {
await storage.removePrivkey(ark.account.pubkey);
};
useEffect(() => {
async function loadPrivkey() {
const key = await storage.loadPrivkey(ark.account.pubkey);
if (key) setPrivkey(key);
}
loadPrivkey();
}, []);
return (
<div className="mx-auto w-full max-w-lg">
<div className="mb-2 text-sm font-semibold">Private key</div>
<div>
{!privkey ? (
<div className="inline-flex h-24 w-full items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
You&apos;ve stored private key on Lume
</div>
) : (
<>
<div className="relative">
<input
readOnly
type={showPassword ? "text" : "password"}
value={nip19.nsecEncode(privkey)}
className="relative h-11 w-full resize-none rounded-lg border-none bg-neutral-200 py-1 pl-3 pr-11 text-neutral-900 !outline-none placeholder:text-neutral-600 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder:text-neutral-400"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-1.5 top-1/2 inline-flex h-8 w-8 -translate-y-1/2 transform items-center justify-center rounded-lg bg-neutral-50 dark:bg-neutral-950"
>
<EyeOffIcon className="h-4 w-4" />
</button>
</div>
<button
type="button"
onClick={() => removePrivkey()}
className="mt-2 inline-flex h-9 w-full items-center justify-center gap-2 rounded-lg bg-red-200 px-6 font-medium text-red-500 hover:bg-red-500 hover:text-white focus:outline-none dark:hover:text-white"
>
Remove private key
</button>
</>
)}
</div>
</div>
);
}

View File

@@ -1,45 +0,0 @@
import { useArk } from "@lume/ark";
import { EditIcon, LoaderIcon } from "@lume/icons";
import { compactNumber } from "@lume/utils";
import { useQuery } from "@tanstack/react-query";
import { Link } from "react-router-dom";
export function ContactCard() {
const ark = useArk();
const { status, data } = useQuery({
queryKey: ["contacts"],
queryFn: async () => {
const contacts = await ark.getUserContacts();
return contacts;
},
refetchOnWindowFocus: false,
});
return (
<div className="col-span-1 h-44 rounded-2xl bg-neutral-100 transition-all duration-150 ease-smooth hover:scale-105 dark:bg-neutral-900">
{status === "pending" ? (
<div className="flex h-full w-full items-center justify-center">
<LoaderIcon className="h-4 w-4 animate-spin" />
</div>
) : (
<div className="flex h-full w-full flex-col justify-between p-4">
<h3 className="pt-1 text-5xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-100">
{compactNumber.format(data.length)}
</h3>
<div className="mt-auto flex h-6 w-full items-center justify-between">
<p className="text-xl font-medium leading-none text-neutral-600 dark:text-neutral-400">
Contacts
</p>
<Link
to="/settings/edit-contact"
className="inline-flex h-6 w-max items-center gap-1 rounded-full bg-neutral-200 px-2.5 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700"
>
<EditIcon className="h-3 w-3" />
Edit
</Link>
</div>
</div>
)}
</div>
);
}

View File

@@ -1,60 +0,0 @@
import { useArk } from "@lume/ark";
import { LoaderIcon } from "@lume/icons";
import { compactNumber } from "@lume/utils";
import { useQuery } from "@tanstack/react-query";
import { fetch } from "@tauri-apps/plugin-http";
import { Link } from "react-router-dom";
export function PostCard() {
const ark = useArk();
const { status, data } = useQuery({
queryKey: ["user-stats", ark.account.pubkey],
queryFn: async ({ signal }: { signal: AbortSignal }) => {
const res = await fetch(
`https://api.nostr.band/v0/stats/profile/${ark.account.pubkey}`,
{
signal,
},
);
if (!res.ok) {
throw new Error("Error");
}
return await res.json();
},
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
staleTime: Infinity,
});
return (
<div className="col-span-1 h-44 rounded-2xl bg-neutral-100 transition-all duration-150 ease-smooth hover:scale-105 dark:bg-neutral-900">
{status === "pending" ? (
<div className="flex h-full w-full items-center justify-center">
<LoaderIcon className="h-4 w-4 animate-spin" />
</div>
) : (
<div className="flex h-full w-full flex-col justify-between p-4">
<h3 className="pt-1 text-5xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-100">
{compactNumber.format(
data.stats[ark.account.pubkey].pub_note_count,
)}
</h3>
<div className="mt-auto flex h-6 w-full items-center justify-between">
<p className="text-xl font-medium leading-none text-neutral-600 dark:text-neutral-400">
Posts
</p>
<Link
to={`/users/${ark.account.pubkey}`}
className="inline-flex h-6 w-max items-center gap-1 rounded-full bg-neutral-200 px-2.5 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700"
>
View
</Link>
</div>
</div>
)}
</div>
);
}

View File

@@ -1,77 +0,0 @@
import { useArk, useProfile } from "@lume/ark";
import { EditIcon, LoaderIcon } from "@lume/icons";
import { displayNpub } from "@lume/utils";
import * as Avatar from "@radix-ui/react-avatar";
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import { minidenticon } from "minidenticons";
import { nip19 } from "nostr-tools";
import { Link } from "react-router-dom";
export function ProfileCard() {
const ark = useArk();
const svgURI = `data:image/svg+xml;utf8,${encodeURIComponent(
minidenticon(ark.account.pubkey, 90, 50),
)}`;
const { isLoading, user } = useProfile(ark.account.pubkey);
const copyNpub = async () => {
return await writeText(nip19.npubEncode(ark.account.pubkey));
};
return (
<div className="mb-4 h-56 w-full rounded-2xl bg-neutral-100 transition-all duration-150 ease-smooth hover:scale-105 dark:bg-neutral-900">
{isLoading ? (
<div className="flex h-full w-full items-center justify-center">
<LoaderIcon className="h-4 w-4 animate-spin" />
</div>
) : (
<div className="flex h-full w-full flex-col justify-between p-4">
<div className="flex h-10 w-full justify-end gap-3">
<button
type="button"
onClick={copyNpub}
className="inline-flex h-8 w-28 transform items-center justify-center gap-1.5 rounded-full bg-neutral-200 text-sm font-medium hover:bg-neutral-400 active:translate-y-1 dark:bg-neutral-800 dark:hover:bg-neutral-600"
>
Copy NPUB
</button>
<Link
to="/settings/edit-profile"
className="inline-flex h-8 w-20 items-center justify-center gap-1.5 rounded-full bg-neutral-200 text-sm font-medium hover:bg-neutral-400 dark:bg-neutral-800 dark:hover:bg-neutral-600"
>
<EditIcon className="h-4 w-4" />
Edit
</Link>
</div>
<div className="flex flex-col gap-2.5">
<Avatar.Root className="shrink-0">
<Avatar.Image
src={user?.picture || user?.image}
alt={ark.account.pubkey}
loading="lazy"
decoding="async"
style={{ contentVisibility: "auto" }}
className="h-16 w-16 rounded-xl border border-neutral-200/50 shadow-[rgba(17,_17,_26,_0.1)_0px_0px_16px] dark:border-neutral-800/50"
/>
<Avatar.Fallback delayMs={300}>
<img
src={svgURI}
alt={ark.account.pubkey}
className="h-16 w-16 rounded-xl bg-black dark:bg-white"
/>
</Avatar.Fallback>
</Avatar.Root>
<div>
<h3 className="text-3xl font-semibold leading-8 text-neutral-900 dark:text-neutral-100">
{user?.display_name || user?.name}
</h3>
<p className="text-lg text-neutral-700 dark:text-neutral-300">
{user?.nip05 || displayNpub(ark.account.pubkey, 16)}
</p>
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -1,46 +0,0 @@
import { useArk } from "@lume/ark";
import { EditIcon, LoaderIcon } from "@lume/icons";
import { compactNumber } from "@lume/utils";
import { useQuery } from "@tanstack/react-query";
import { Link } from "react-router-dom";
export function RelayCard() {
const ark = useArk();
const { status, data } = useQuery({
queryKey: ["relays", ark.account.pubkey],
queryFn: async () => {
const relays = await ark.getUserRelays({});
return relays;
},
refetchOnWindowFocus: false,
});
return (
<div className="col-span-1 h-44 rounded-2xl bg-neutral-100 transition-all duration-150 ease-smooth hover:scale-105 dark:bg-neutral-900">
{status === "pending" ? (
<div className="flex h-full w-full items-center justify-center">
<LoaderIcon className="h-4 w-4 animate-spin" />
</div>
) : (
<div className="flex h-full w-full flex-col justify-between p-4">
<h3 className="pt-1 text-5xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-100">
{compactNumber.format(data?.relays?.length || 0)}
</h3>
<div className="mt-auto flex h-6 w-full items-center justify-between">
<p className="text-xl font-medium leading-none text-neutral-600 dark:text-neutral-400">
Relays
</p>
<Link
to="/relays"
className="inline-flex h-6 w-max items-center gap-1 rounded-full bg-neutral-200 px-2.5 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700"
>
<EditIcon className="h-3 w-3" />
Edit
</Link>
</div>
</div>
)}
</div>
);
}

View File

@@ -1,62 +0,0 @@
import { useArk } from "@lume/ark";
import { LoaderIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { useState } from "react";
import { toast } from "sonner";
export function NWCForm({ setWalletConnectURL }) {
const ark = useArk();
const storage = useStorage();
const [uri, setUri] = useState("");
const [loading, setLoading] = useState(false);
const submit = async () => {
try {
setLoading(true);
if (!uri.startsWith("nostr+walletconnect:")) {
toast.error(
"Connect URI is required and must start with format nostr+walletconnect:, please check again",
);
setLoading(false);
return;
}
const uriObj = new URL(uri);
const params = new URLSearchParams(uriObj.search);
if (params.has("relay") && params.has("secret")) {
await storage.createPrivkey(`${ark.account.pubkey}-nwc`, uri);
setWalletConnectURL(uri);
setLoading(false);
} else {
setLoading(false);
toast.error("Connect URI is not valid, please check again");
return;
}
} catch (e) {
setLoading(false);
toast.error(String(e));
}
};
return (
<div className="flex flex-col gap-3 rounded-xl bg-neutral-100 p-3 dark:bg-neutral-900">
<textarea
name="walletConnectURL"
value={uri}
onChange={(e) => setUri(e.target.value)}
placeholder="nostr+walletconnect://"
className="h-40 w-full resize-none rounded-lg border-transparent bg-neutral-200 px-3 py-3 text-neutral-900 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:focus:ring-blue-800 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder:text-neutral-400"
/>
<button
type="button"
onClick={submit}
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600"
>
{loading ? <LoaderIcon className="h-4 w-4 animate-spin" /> : "Connect"}
</button>
</div>
);
}

View File

@@ -1,51 +0,0 @@
import { useArk } from "@lume/ark";
import { LoaderIcon } from "@lume/icons";
import { compactNumber } from "@lume/utils";
import { useQuery } from "@tanstack/react-query";
import { fetch } from "@tauri-apps/plugin-http";
export function ZapCard() {
const ark = useArk();
const { status, data } = useQuery({
queryKey: ["user-stats", ark.account.pubkey],
queryFn: async ({ signal }: { signal: AbortSignal }) => {
const res = await fetch(
`https://api.nostr.band/v0/stats/profile/${ark.account.pubkey}`,
{
signal,
},
);
if (!res.ok) {
throw new Error("Error");
}
return await res.json();
},
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
staleTime: Infinity,
});
return (
<div className="col-span-1 h-44 rounded-2xl bg-neutral-100 transition-all duration-150 ease-smooth hover:scale-105 dark:bg-neutral-900">
{status === "pending" ? (
<div className="flex h-full w-full items-center justify-center">
<LoaderIcon className="h-4 w-4 animate-spin" />
</div>
) : (
<div className="flex h-full w-full flex-col justify-between p-4">
<h3 className="pt-1 text-5xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-100">
{compactNumber.format(
data?.stats[ark.account.pubkey]?.zaps_received?.msats / 1000 || 0,
)}
</h3>
<div className="mt-auto flex h-6 items-center text-xl font-medium leading-none text-neutral-600 dark:text-neutral-400">
Sats received
</div>
</div>
)}
</div>
);
}

View File

@@ -1,34 +0,0 @@
import { useArk } from "@lume/ark";
import { LoaderIcon } from "@lume/icons";
import { User } from "@lume/ui";
import { useQuery } from "@tanstack/react-query";
export function EditContactScreen() {
const ark = useArk();
const { status, data } = useQuery({
queryKey: ["contacts"],
queryFn: async () => {
return await ark.getUserContacts();
},
refetchOnWindowFocus: false,
});
return (
<div className="mx-auto flex w-full max-w-xl flex-col gap-3">
{status === "pending" ? (
<div className="flex h-10 w-full items-center justify-center">
<LoaderIcon className="h-4 w-4 animate-spin" />
</div>
) : (
data.map((item) => (
<div
key={item}
className="flex h-16 w-full items-center justify-between rounded-xl bg-neutral-100 px-2.5 dark:bg-neutral-900"
>
<User pubkey={item} variant="simple" />
</div>
))
)}
</div>
);
}

View File

@@ -1,307 +0,0 @@
import { useArk } from "@lume/ark";
import {
CheckCircleIcon,
LoaderIcon,
PlusIcon,
UnverifiedIcon,
} from "@lume/icons";
import { useStorage } from "@lume/storage";
import { NDKKind, NDKUserProfile } from "@nostr-dev-kit/ndk";
import { useQueryClient } from "@tanstack/react-query";
import { message } from "@tauri-apps/plugin-dialog";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
export function EditProfileScreen() {
const [loading, setLoading] = useState(false);
const [picture, setPicture] = useState("");
const [banner, setBanner] = useState("");
const [nip05, setNIP05] = useState({ verified: true, text: "" });
const ark = useArk();
const storage = useStorage();
const {
register,
handleSubmit,
reset,
setError,
formState: { isValid, errors },
} = useForm({
defaultValues: async () => {
const res: NDKUserProfile = queryClient.getQueryData([
"user",
ark.account.pubkey,
]);
if (res.image) {
setPicture(res.image);
}
if (res.banner) {
setBanner(res.banner);
}
if (res.nip05) {
setNIP05((prev) => ({ ...prev, text: res.nip05 }));
}
return res;
},
});
const queryClient = useQueryClient();
const navigate = useNavigate();
const uploadAvatar = async () => {
try {
if (!ark.ndk.signer) return navigate("/new/privkey");
setLoading(true);
const image = await ark.upload({});
if (image) {
setPicture(image);
setLoading(false);
}
} catch (e) {
setLoading(false);
await message(`Upload failed, error: ${e}`, {
title: "Lume",
type: "error",
});
}
};
const uploadBanner = async () => {
try {
setLoading(true);
const image = await ark.upload({});
if (image) {
setBanner(image);
setLoading(false);
}
} catch (e) {
setLoading(false);
await message(`Upload failed, error: ${e}`, {
title: "Lume",
type: "error",
});
}
};
const onSubmit = async (data: NDKUserProfile) => {
// start loading
setLoading(true);
let content = {
...data,
username: data.name,
display_name: data.name,
bio: data.about,
image: data.picture,
};
if (data.nip05) {
const verify = ark.validateNIP05({
pubkey: ark.account.pubkey,
nip05: data.nip05,
});
if (verify) {
content = { ...content, nip05: data.nip05 };
} else {
setNIP05((prev) => ({ ...prev, verified: false }));
setError("nip05", {
type: "manual",
message: "Can't verify your Lume ID / NIP-05, please check again",
});
}
}
const publish = await ark.createEvent({
kind: NDKKind.Metadata,
tags: [],
content: JSON.stringify(content),
});
if (publish) {
// invalid cache
queryClient.invalidateQueries({
queryKey: ["user", ark.account.pubkey],
});
// reset form
reset();
// reset state
setLoading(false);
setPicture(null);
setBanner(null);
} else {
setLoading(false);
}
};
return (
<div className="mx-auto w-full max-w-md">
<form onSubmit={handleSubmit(onSubmit)} className="mb-0">
<input type={"hidden"} {...register("picture")} value={picture} />
<input type={"hidden"} {...register("banner")} value={banner} />
<div className="flex flex-col items-center justify-center">
<div className="relative h-36 w-full">
{banner ? (
<img
src={banner}
alt="user's banner"
className="h-full w-full rounded-xl object-cover"
/>
) : (
<div className="h-full w-full rounded-xl bg-neutral-200 dark:bg-neutral-900" />
)}
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform overflow-hidden rounded-xl">
<button
type="button"
onClick={() => uploadBanner()}
className="inline-flex h-full w-full items-center justify-center bg-black/20 text-white"
>
<PlusIcon className="h-5 w-5" />
</button>
</div>
</div>
<div className="mb-5 px-4">
<div className="relative z-10 -mt-7 h-14 w-14 overflow-hidden rounded-xl ring-2 ring-white dark:ring-black">
<img
src={picture}
alt="user's avatar"
className="h-14 w-14 rounded-xl object-cover"
/>
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
<button
type="button"
onClick={() => uploadAvatar()}
className="inline-flex h-full w-full items-center justify-center rounded-xl bg-black/50 text-white"
>
<PlusIcon className="h-5 w-5" />
</button>
</div>
</div>
</div>
</div>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<label
htmlFor="display_name"
className="text-sm font-semibold uppercase tracking-wider"
>
Display Name
</label>
<input
type={"text"}
{...register("display_name")}
spellCheck={false}
className="relative h-11 w-full rounded-lg border-transparent bg-neutral-100 px-3 py-1 text-neutral-900 !outline-none backdrop-blur-xl placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:text-neutral-100"
/>
</div>
<div className="flex flex-col gap-1">
<label
htmlFor="name"
className="text-sm font-semibold uppercase tracking-wider"
>
Name
</label>
<input
type={"text"}
{...register("name")}
spellCheck={false}
className="relative h-11 w-full rounded-lg border-transparent bg-neutral-100 px-3 py-1 text-neutral-900 !outline-none backdrop-blur-xl placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:text-neutral-100"
/>
</div>
<div className="flex flex-col gap-1">
<label
htmlFor="nip05"
className="text-sm font-semibold uppercase tracking-wider"
>
NIP-05
</label>
<div className="relative">
<input
{...register("nip05")}
spellCheck={false}
className="relative h-11 w-full rounded-lg border-transparent bg-neutral-100 px-3 py-1 text-neutral-900 !outline-none backdrop-blur-xl placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:text-neutral-100"
/>
<div className="absolute right-2 top-1/2 -translate-y-1/2 transform">
{nip05.verified ? (
<span className="inline-flex h-6 items-center gap-1 rounded-full bg-teal-500 px-1 pr-1.5 text-xs font-medium text-white">
<CheckCircleIcon className="h-4 w-4" />
Verified
</span>
) : (
<span className="inline-flex h-6 items-center gap-1 rounded bg-red-500 pl-1 pr-1.5 text-xs font-medium text-white">
<UnverifiedIcon className="h-4 w-4" />
Unverified
</span>
)}
</div>
{errors.nip05 && (
<p className="mt-1 text-sm text-red-400">
{errors.nip05.message.toString()}
</p>
)}
</div>
</div>
<div className="flex flex-col gap-1">
<label
htmlFor="website"
className="text-sm font-semibold uppercase tracking-wider"
>
Website
</label>
<input
type={"text"}
{...register("website", { required: false })}
spellCheck={false}
className="relative h-11 w-full rounded-lg border-transparent bg-neutral-100 px-3 py-1 text-neutral-900 !outline-none backdrop-blur-xl placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:text-neutral-100"
/>
</div>
<div className="flex flex-col gap-1">
<label
htmlFor="website"
className="text-sm font-semibold uppercase tracking-wider"
>
Lightning address
</label>
<input
type={"text"}
{...register("lud16", { required: false })}
spellCheck={false}
className="relative h-11 w-full rounded-lg border-transparent bg-neutral-100 px-3 py-1 text-neutral-900 !outline-none backdrop-blur-xl placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:text-neutral-100"
/>
</div>
<div className="flex flex-col gap-1">
<label
htmlFor="about"
className="text-sm font-semibold uppercase tracking-wider"
>
Bio
</label>
<textarea
{...register("about")}
spellCheck={false}
className="relative h-20 w-full resize-none rounded-lg border-transparent bg-neutral-100 px-3 py-2 text-neutral-900 !outline-none backdrop-blur-xl placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:text-neutral-100"
/>
</div>
<div>
<button
type="submit"
disabled={!isValid}
className="mx-auto inline-flex h-11 w-full transform items-center justify-center gap-1 rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600 focus:outline-none active:translate-y-1 disabled:pointer-events-none disabled:opacity-50"
>
{loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-white" />
) : (
"Update"
)}
</button>
</div>
</div>
</form>
</div>
);
}

View File

@@ -1,352 +0,0 @@
import { CheckIcon, DarkIcon, LightIcon, SystemModeIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import * as Switch from "@radix-ui/react-switch";
import { invoke } from "@tauri-apps/api/core";
import { getCurrent } from "@tauri-apps/api/window";
import { disable, enable, isEnabled } from "@tauri-apps/plugin-autostart";
import {
isPermissionGranted,
requestPermission,
} from "@tauri-apps/plugin-notification";
import { useEffect, useState } from "react";
import { twMerge } from "tailwind-merge";
export function GeneralSettingScreen() {
const storage = useStorage();
const [apiKey, setAPIKey] = useState("");
const [settings, setSettings] = useState({
lowPower: false,
autoupdate: false,
autolaunch: false,
outbox: false,
media: true,
hashtag: true,
notification: true,
translation: false,
appearance: "system",
});
const changeTheme = async (theme: "light" | "dark" | "auto") => {
await invoke("plugin:theme|set_theme", { theme });
// update state
setSettings((prev) => ({ ...prev, appearance: theme }));
};
const toggleLowPower = async () => {
await storage.createSetting("lowPower", String(+!settings.lowPower));
setSettings((state) => ({ ...state, lowPower: !settings.lowPower }));
};
const toggleAutolaunch = async () => {
if (!settings.autolaunch) {
await enable();
// update state
setSettings((prev) => ({ ...prev, autolaunch: true }));
} else {
await disable();
// update state
setSettings((prev) => ({ ...prev, autolaunch: false }));
}
};
const toggleMedia = async () => {
await storage.createSetting("media", String(+!settings.media));
storage.settings.media = !settings.media;
// update state
setSettings((prev) => ({ ...prev, media: !settings.media }));
};
const toggleHashtag = async () => {
await storage.createSetting("hashtag", String(+!settings.hashtag));
storage.settings.hashtag = !settings.hashtag;
// update state
setSettings((prev) => ({ ...prev, hashtag: !settings.hashtag }));
};
const toggleAutoupdate = async () => {
await storage.createSetting("autoupdate", String(+!settings.autoupdate));
storage.settings.autoupdate = !settings.autoupdate;
// update state
setSettings((prev) => ({ ...prev, autoupdate: !settings.autoupdate }));
};
const toggleNofitication = async () => {
if (settings.notification) return;
await requestPermission();
// update state
setSettings((prev) => ({ ...prev, notification: !settings.notification }));
};
const toggleTranslation = async () => {
await storage.createSetting("translation", String(+!settings.translation));
storage.settings.translation = !settings.translation;
// update state
setSettings((prev) => ({ ...prev, translation: !settings.translation }));
};
const saveApi = async () => {
await storage.createSetting("translateApiKey", apiKey);
};
useEffect(() => {
async function loadSettings() {
const theme = await getCurrent().theme();
setSettings((prev) => ({ ...prev, appearance: theme }));
const autostart = await isEnabled();
setSettings((prev) => ({ ...prev, autolaunch: autostart }));
const permissionGranted = await isPermissionGranted();
setSettings((prev) => ({ ...prev, notification: permissionGranted }));
const data = await storage.getAllSettings();
if (!data) return;
for (const item of data) {
if (item.key === "autoupdate")
setSettings((prev) => ({
...prev,
autoupdate: !!parseInt(item.value),
}));
if (item.key === "lowPower")
setSettings((prev) => ({
...prev,
lowPower: !!parseInt(item.value),
}));
if (item.key === "outbox")
setSettings((prev) => ({
...prev,
outbox: !!parseInt(item.value),
}));
if (item.key === "media")
setSettings((prev) => ({
...prev,
media: !!parseInt(item.value),
}));
if (item.key === "hashtag")
setSettings((prev) => ({
...prev,
hashtag: !!parseInt(item.value),
}));
if (item.key === "translation")
setSettings((prev) => ({
...prev,
translation: !!parseInt(item.value),
}));
}
}
loadSettings();
}, []);
return (
<div className="mx-auto w-full max-w-lg">
<div className="flex flex-col gap-6">
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-8">
<div className="w-24 shrink-0 text-end text-sm font-semibold">
Update
</div>
<div className="text-sm">Automatically download new update</div>
</div>
<Switch.Root
checked={settings.autoupdate}
onClick={() => toggleAutoupdate()}
className="relative h-7 w-12 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
>
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
</Switch.Root>
</div>
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-8">
<div className="w-24 shrink-0 text-end text-sm font-semibold">
Low Power
</div>
<div className="text-sm">
Sustainable for low network environment.
</div>
</div>
<Switch.Root
checked={settings.lowPower}
onClick={() => toggleLowPower()}
className="relative h-7 w-12 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
>
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
</Switch.Root>
</div>
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-8">
<div className="w-24 shrink-0 text-end text-sm font-semibold">
Startup
</div>
<div className="text-sm">Launch Lume at Login</div>
</div>
<Switch.Root
checked={settings.autolaunch}
onClick={() => toggleAutolaunch()}
className="relative h-7 w-12 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
>
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
</Switch.Root>
</div>
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-8">
<div className="w-24 shrink-0 text-end text-sm font-semibold">
Media
</div>
<div className="text-sm">Automatically load media</div>
</div>
<Switch.Root
checked={settings.media}
onClick={() => toggleMedia()}
className="relative h-7 w-12 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
>
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
</Switch.Root>
</div>
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-8">
<div className="w-24 shrink-0 text-end text-sm font-semibold">
Hashtag
</div>
<div className="text-sm">Show all hashtags in content</div>
</div>
<Switch.Root
checked={settings.hashtag}
onClick={() => toggleHashtag()}
className="relative h-7 w-12 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
>
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
</Switch.Root>
</div>
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-8">
<div className="w-24 shrink-0 text-end text-sm font-semibold">
Notification
</div>
<div className="text-sm">Automatically send notification</div>
</div>
<Switch.Root
checked={settings.notification}
disabled={settings.notification}
onClick={() => toggleNofitication()}
className="relative h-7 w-12 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
>
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
</Switch.Root>
</div>
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-8">
<div className="w-24 shrink-0 text-end text-sm font-semibold">
Translation
</div>
<div className="text-sm">Translate text to your language</div>
</div>
<Switch.Root
checked={settings.translation}
onClick={() => toggleTranslation()}
className="relative h-7 w-12 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
>
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
</Switch.Root>
</div>
{settings.translation ? (
<div className="flex w-full items-center gap-8">
<div className="w-24 shrink-0 text-end text-sm font-semibold">
API Key
</div>
<div className="relative w-full">
<input
type="password"
spellCheck={false}
value={apiKey}
onChange={(e) => setAPIKey(e.target.value)}
className="w-full border-transparent outline-none focus:outline-none focus:ring-0 focus:border-none h-9 rounded-lg ring-0 placeholder:text-neutral-600 bg-neutral-100 dark:bg-neutral-900"
/>
<div className="h-9 absolute right-0 top-0 inline-flex items-center justify-center">
<button
type="button"
onClick={saveApi}
className="mr-1 h-7 w-16 text-sm font-medium shrink-0 inline-flex items-center justify-center rounded-md bg-neutral-200 dark:bg-neutral-800 hover:bg-neutral-300 dark:hover:bg-neutral-700"
>
Save
</button>
</div>
</div>
</div>
) : null}
<div className="flex w-full items-start gap-8">
<div className="w-24 shrink-0 text-end text-sm font-semibold">
Appearance
</div>
<div className="flex flex-1 gap-6">
<button
type="button"
onClick={() => changeTheme("light")}
className="flex flex-col items-center justify-center gap-0.5"
>
<div
className={twMerge(
"inline-flex h-11 w-11 items-center justify-center rounded-lg",
settings.appearance === "light"
? "bg-blue-500 text-white"
: "bg-neutral-100 dark:bg-neutral-900",
)}
>
<LightIcon className="h-5 w-5" />
</div>
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
Light
</p>
</button>
<button
type="button"
onClick={() => changeTheme("dark")}
className="flex flex-col items-center justify-center gap-0.5"
>
<div
className={twMerge(
"inline-flex h-11 w-11 items-center justify-center rounded-lg",
settings.appearance === "dark"
? "bg-blue-500 text-white"
: "bg-neutral-100 dark:bg-neutral-900",
)}
>
<DarkIcon className="h-5 w-5" />
</div>
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
Dark
</p>
</button>
<button
type="button"
onClick={() => changeTheme("auto")}
className="flex flex-col items-center justify-center gap-0.5"
>
<div
className={twMerge(
"inline-flex h-11 w-11 items-center justify-center rounded-lg",
settings.appearance === "auto"
? "bg-blue-500 text-white"
: "bg-neutral-100 dark:bg-neutral-900",
)}
>
<SystemModeIcon className="h-5 w-5" />
</div>
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
System
</p>
</button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,19 +0,0 @@
import { ContactCard } from "./components/contactCard";
import { PostCard } from "./components/postCard";
import { ProfileCard } from "./components/profileCard";
import { RelayCard } from "./components/relayCard";
import { ZapCard } from "./components/zapCard";
export function UserSettingScreen() {
return (
<div className="mx-auto w-full max-w-xl">
<ProfileCard />
<div className="grid grid-cols-2 gap-4">
<ContactCard />
<RelayCard />
<PostCard />
<ZapCard />
</div>
</div>
);
}

View File

@@ -1,154 +0,0 @@
import { webln } from "@getalby/sdk";
import { useArk } from "@lume/ark";
import { CheckCircleIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { useEffect, useState } from "react";
import { NWCForm } from "./components/walletConnectForm";
export function NWCScreen() {
const ark = useArk();
const storage = useStorage();
const [walletConnectURL, setWalletConnectURL] = useState<null | string>(null);
const [balance, setBalance] = useState(0);
const remove = async () => {
await storage.removePrivkey(`${ark.account.pubkey}-nwc`);
setWalletConnectURL(null);
};
const loadBalance = async () => {
const nwc = new webln.NostrWebLNProvider({
nostrWalletConnectUrl: walletConnectURL,
});
await nwc.enable();
const balanceResponse = await nwc.getBalance();
setBalance(balanceResponse.balance);
nwc.close();
};
useEffect(() => {
if (walletConnectURL) loadBalance();
}, [walletConnectURL]);
useEffect(() => {
async function getNWC() {
const nwc = await storage.loadPrivkey(`${ark.account.pubkey}-nwc`);
if (nwc) setWalletConnectURL(nwc);
}
getNWC();
}, []);
return (
<div>
<div className="flex w-full flex-col gap-5">
<div className="text-center">
<h3 className="text-2xl font-semibold leading-tight">
Nostr Wallet Connect
</h3>
<p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
Sending zap easily via Bitcoin Lightning.
</p>
</div>
<div className="mx-auto w-full max-w-lg">
{!walletConnectURL ? (
<NWCForm setWalletConnectURL={setWalletConnectURL} />
) : (
<div className="flex w-full flex-col gap-3 rounded-xl bg-neutral-100 p-3 dark:bg-neutral-900">
<div className="flex items-center justify-center gap-1.5 text-sm text-teal-500">
<CheckCircleIcon className="h-4 w-4" />
<div>You&apos;re using nostr wallet connect</div>
</div>
<div className="flex flex-col gap-3">
<textarea
readOnly
value={`${walletConnectURL.substring(0, 120)}****`}
className="h-40 w-full resize-none rounded-lg border-transparent bg-neutral-200 px-3 py-3 text-neutral-900 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:focus:ring-blue-800 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder:text-neutral-400"
/>
<button
type="button"
onClick={() => remove()}
className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg bg-neutral-200 px-6 font-medium text-red-500 hover:bg-red-500 hover:text-white focus:outline-none dark:bg-neutral-800 dark:text-neutral-100"
>
Remove connection
</button>
</div>
</div>
)}
{walletConnectURL ? (
<div className="mt-5 flex flex-col">
<h3 className="font-medium text-neutral-600 dark:text-neutral-400">
Current balance
</h3>
<p className="text-2xl font-semibold">
{new Intl.NumberFormat().format(balance)} sats
</p>
</div>
) : (
<div className="mt-5 flex flex-col gap-4">
<div className="flex flex-col gap-1.5">
<h5 className="font-semibold text-neutral-900 dark:text-neutral-100">
Introduction
</h5>
<p className="text-neutral-600 dark:text-neutral-400">
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-neutral-600 dark:text-neutral-400">
To learn more about the details have a look at{" "}
<a
href="https://github.com/nostr-protocol/nips/blob/master/47.md"
target="_blank"
className="text-blue-500"
rel="noreferrer"
>
the specs (NIP47)
</a>
</p>
</div>
<div className="flex flex-col gap-1.5">
<h5 className="font-semibold text-neutral-900 dark:text-neutral-100">
About zapping
</h5>
<p className="text-neutral-600 dark:text-neutral-400">
Lume doesn&apos;t take any commission or platform fees when
you zap someone. Lume doesn&apos;t hold your Bitcoin
</p>
</div>
<div className="flex flex-col gap-1.5">
<h5 className="font-semibold text-neutral-900 dark:text-neutral-100">
Recommend wallet that support NWC
</h5>
<p className="text-neutral-600 dark:text-neutral-400">
- Mutiny Wallet:{" "}
<a
href="https://www.mutinywallet.com/"
target="_blank"
rel="noreferrer"
className="text-blue-500"
>
website
</a>
</p>
<p className="text-neutral-600 dark:text-neutral-400">
- Self hosted NWC on Umbrel :{" "}
<a
href="https://apps.umbrel.com/app/alby-nostr-wallet-connect"
target="_blank"
rel="noreferrer"
className="text-blue-500"
>
website
</a>
</p>
</div>
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -1,153 +0,0 @@
import { useArk, useProfile } from "@lume/ark";
import { useStorage } from "@lume/storage";
import { NIP05 } from "@lume/ui";
import { displayNpub } from "@lume/utils";
import * as Avatar from "@radix-ui/react-avatar";
import { minidenticon } from "minidenticons";
import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { toast } from "sonner";
import { UserStats } from "./stats";
export function UserProfile({ pubkey }: { pubkey: string }) {
const ark = useArk();
const storage = useStorage();
const { user } = useProfile(pubkey);
const [followed, setFollowed] = useState(false);
const navigate = useNavigate();
const svgURI = `data:image/svg+xml;utf8,${encodeURIComponent(
minidenticon(pubkey, 90, 50),
)}`;
const follow = async () => {
try {
if (!ark.ndk.signer) return navigate("/new/privkey");
setFollowed(true);
const add = await ark.createContact({ pubkey });
if (!add) {
toast.success("You already follow this user");
setFollowed(false);
}
} catch (e) {
toast.error(e);
setFollowed(false);
}
};
const unfollow = async () => {
try {
if (!ark.ndk.signer) return navigate("/new/privkey");
setFollowed(false);
await ark.deleteContact({ pubkey });
} catch (e) {
toast.error(e);
}
};
useEffect(() => {
if (ark.account.contacts.includes(pubkey)) {
setFollowed(true);
}
}, []);
if (!user) return <p>Loading...</p>;
return (
<>
<div className="h-56 w-full overflow-hidden rounded-tl-lg">
{user?.banner ? (
<img
src={user?.banner}
alt="user banner"
className="h-full w-full rounded-tl-lg object-cover"
/>
) : (
<div className="h-full w-full rounded-tl-lg bg-neutral-100 dark:bg-neutral-900" />
)}
</div>
<div className="-mt-7 flex w-full flex-col items-center px-5">
<Avatar.Root className="shrink-0">
<Avatar.Image
src={user?.picture || user?.image}
alt={pubkey}
loading="lazy"
decoding="async"
style={{ contentVisibility: "auto" }}
className="h-14 w-14 rounded-lg bg-white object-cover ring-2 ring-neutral-100 dark:ring-neutral-900"
/>
<Avatar.Fallback delayMs={300}>
<img
src={svgURI}
alt={pubkey}
className="h-14 w-14 rounded-lg bg-black dark:bg-white"
/>
</Avatar.Fallback>
</Avatar.Root>
<div className="mt-2 flex flex-1 flex-col gap-6">
<div className="flex flex-col items-center gap-1">
<div className="inline-flex flex-col items-center">
<h5 className="text-center text-xl font-semibold text-neutral-900 dark:text-neutral-100">
{user?.name ||
user?.display_name ||
user?.displayName ||
"No name"}
</h5>
{user?.nip05 ? (
<NIP05
pubkey={pubkey}
nip05={user.nip05}
className="text-neutral-600 dark:text-neutral-400"
/>
) : (
<span className="max-w-[15rem] truncate text-sm text-neutral-500 dark:text-neutral-400">
{displayNpub(pubkey, 16)}
</span>
)}
</div>
<div className="flex flex-col gap-6">
{user?.about || user?.bio ? (
<p className="mt-2 max-w-[500px] select-text break-words text-center text-neutral-900 dark:text-neutral-100">
{user.about || user.bio}
</p>
) : (
<div />
)}
<UserStats pubkey={pubkey} />
</div>
</div>
<div className="inline-flex items-center justify-center gap-2">
{followed ? (
<button
type="button"
onClick={unfollow}
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-neutral-200 text-sm font-medium text-neutral-900 backdrop-blur-xl hover:bg-blue-500 hover:text-neutral-100 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-blue-600 dark:hover:text-neutral-100"
>
Unfollow
</button>
) : (
<button
type="button"
onClick={follow}
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-neutral-200 text-sm font-medium text-neutral-900 backdrop-blur-xl hover:bg-blue-500 hover:text-neutral-100 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-blue-600 dark:hover:text-neutral-100"
>
Follow
</button>
)}
<Link
to={`/chats/${pubkey}`}
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-neutral-200 text-sm font-medium text-neutral-900 backdrop-blur-xl hover:bg-blue-500 hover:text-neutral-100 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-blue-600 dark:hover:text-neutral-100"
>
Message
</Link>
</div>
</div>
</div>
</>
);
}

View File

@@ -1,84 +0,0 @@
import { LoaderIcon } from "@lume/icons";
import { compactNumber } from "@lume/utils";
import { useQuery } from "@tanstack/react-query";
import { fetch } from "@tauri-apps/plugin-http";
export function UserStats({ pubkey }: { pubkey: string }) {
const { status, data } = useQuery({
queryKey: ["user-stats", pubkey],
queryFn: async ({ signal }: { signal: AbortSignal }) => {
const res = await fetch(
`https://api.nostr.band/v0/stats/profile/${pubkey}`,
{
signal,
},
);
if (!res.ok) {
throw new Error("Error");
}
return await res.json();
},
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
staleTime: Infinity,
});
if (status === "pending") {
return (
<div className="flex w-full items-center justify-center">
<LoaderIcon className="h-5 w-5 animate-spin text-neutral-900 dark:text-neutral-100" />
</div>
);
}
if (status === "error") {
return <div className="flex w-full items-center justify-center" />;
}
return (
<div className="flex w-full items-center justify-center gap-10">
<div className="inline-flex flex-col items-center gap-1">
<span className="font-semibold leading-none text-neutral-900 dark:text-neutral-100">
{compactNumber.format(data?.stats[pubkey]?.followers_pubkey_count) ??
0}
</span>
<span className="text-sm leading-none text-neutral-500 dark:text-neutral-400">
Followers
</span>
</div>
<div className="inline-flex flex-col items-center gap-1">
<span className="font-semibold leading-none text-neutral-900 dark:text-neutral-100">
{compactNumber.format(
data?.stats[pubkey]?.pub_following_pubkey_count,
) ?? 0}
</span>
<span className="text-sm leading-none text-neutral-500 dark:text-neutral-400">
Following
</span>
</div>
<div className="inline-flex flex-col items-center gap-1">
<span className="font-semibold leading-none text-neutral-900 dark:text-neutral-100">
{compactNumber.format(
data?.stats[pubkey]?.zaps_received?.msats / 1000 ?? 0,
)}
</span>
<span className="text-sm leading-none text-neutral-500 dark:text-neutral-400">
Zaps received
</span>
</div>
<div className="inline-flex flex-col items-center gap-1">
<span className="font-semibold leading-none text-neutral-900 dark:text-neutral-100">
{compactNumber.format(
data?.stats[pubkey]?.zaps_sent?.msats / 1000 ?? 0,
)}
</span>
<span className="text-sm leading-none text-neutral-500 dark:text-neutral-400">
Zaps sent
</span>
</div>
</div>
);
}

View File

@@ -1,97 +0,0 @@
import { NoteSkeleton, RepostNote, TextNote, useArk } from "@lume/ark";
import { ArrowRightCircleIcon, LoaderIcon } from "@lume/icons";
import { FETCH_LIMIT } from "@lume/utils";
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useMemo } from "react";
import { useParams } from "react-router-dom";
import { UserProfile } from "./components/profile";
export function UserScreen() {
const { pubkey } = useParams();
const ark = useArk();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ["user-posts", pubkey],
initialPageParam: 0,
queryFn: async ({
signal,
pageParam,
}: {
signal: AbortSignal;
pageParam: number;
}) => {
const events = await ark.getInfiniteEvents({
filter: {
kinds: [NDKKind.Text, NDKKind.Repost],
authors: [pubkey],
},
limit: FETCH_LIMIT,
pageParam,
signal,
});
return events;
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage.at(-1);
if (!lastEvent) return;
return lastEvent.created_at - 1;
},
refetchOnWindowFocus: false,
});
const allEvents = useMemo(
() => (data ? data.pages.flatMap((page) => page) : []),
[data],
);
// render event match event kind
const renderItem = (event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return <TextNote key={event.id} event={event} />;
case NDKKind.Repost:
return <RepostNote key={event.id} event={event} />;
default:
return <TextNote key={event.id} event={event} />;
}
};
return (
<div className="relative h-full w-full overflow-y-auto">
<UserProfile pubkey={pubkey} />
<div className="mt-6 h-full w-full border-t border-neutral-100 px-1.5 dark:border-neutral-900">
<h3 className="mb-2 pt-4 text-center text-lg font-semibold leading-none text-neutral-900 dark:text-neutral-100">
Latest posts
</h3>
<div className="mx-auto flex h-full max-w-[500px] flex-col justify-between gap-1.5 pb-4 pt-1.5">
{status === "pending" ? (
<NoteSkeleton />
) : (
allEvents.map((item) => renderItem(item))
)}
<div className="flex h-16 items-center justify-center px-3 pb-3">
{hasNextPage ? (
<button
type="button"
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex items-center justify-center w-full h-12 gap-2 font-medium bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800 rounded-xl focus:outline-none"
>
{isFetchingNextPage ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
<>
<ArrowRightCircleIcon className="size-5" />
Load more
</>
)}
</button>
) : null}
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,36 +0,0 @@
import react from "@vitejs/plugin-react-swc";
import million from "million/compiler";
import { defineConfig } from "vite";
import topLevelAwait from "vite-plugin-top-level-await";
import viteTsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [
million.vite({
auto: {
threshold: 0.05,
},
mute: true,
}),
react(),
viteTsconfigPaths(),
topLevelAwait({
// The export name of top-level await promise for each chunk module
promiseExportName: "__tla",
// The function to generate import names of top-level await promise in each chunk module
promiseImportName: (i) => `__tla_${i}`,
}),
],
envPrefix: ["VITE_", "TAURI_"],
build: {
target: process.env.TAURI_PLATFORM === "windows" ? "chrome105" : "safari13",
minify: !process.env.TAURI_DEBUG ? "esbuild" : false,
sourcemap: !!process.env.TAURI_DEBUG,
outDir: "../../dist",
},
server: {
strictPort: true,
port: 3000,
},
clearScreen: false,
});

26
apps/desktop2/.gitignore vendored Normal file
View File

@@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
src/router.gen.ts

14
apps/desktop2/index.html Normal file
View File

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

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