Compare commits

..
451 Commits
Author SHA1 Message Date
reya fab34de1ee chore: update readme 2025-04-01 17:52:13 +07:00
reya 9730837e00 chore: update deps 2025-01-11 20:18:30 +07:00
reya ec34255df1 chore: update deps 2024-11-30 07:39:23 +07:00
reya a262217ab2 chore: release 2024-11-11 10:02:44 +07:00
reya c5d06a2492 feat: add negentropy sync to settings 2024-11-11 10:02:19 +07:00
reya c93edde7d2 chore: use upstream rust nostr 2024-11-11 07:56:31 +07:00
reya 5103126001 fix: build on windows 2024-11-10 14:28:14 +07:00
reya b0c49c5141 chore: release 2024-11-10 13:23:57 +07:00
reya 2ed2cb3afd chore: update dependencies 2024-11-10 10:43:38 +07:00
reya 2bcda1f2ef feat: support NIP22 for comment 2024-11-10 08:51:07 +07:00
reya ca20bbd298 chore: release 2024-11-08 09:19:51 +07:00
reya c6da06cd4d chore: clean up 2024-11-08 09:02:57 +07:00
reya 50bf6c04c1 chore: use upstream nostr-sdk instead of fork 2024-11-08 07:51:48 +07:00
reya 490417771c chore: release 2024-11-07 15:02:05 +07:00
reya 0cb491eaf9 feat: auto open browser if receive auth_url 2024-11-07 14:26:13 +07:00
reya ece6bcc125 feat: add DVM feeds 2024-11-07 09:26:28 +07:00
reya 4b79e559d2 chore: clean up 2024-11-06 09:43:31 +07:00
reya 322e510db2 fix: settings 2024-11-06 09:16:22 +07:00
reya 4e279f127d chore: release 2024-11-06 07:45:00 +07:00
reya 5655a8136d feat: improve dark mode and some copy 2024-11-05 19:33:47 +07:00
reya d80534c51f feat: allow user enter custom relay for relayfeeds 2024-11-05 15:04:39 +07:00
reya 0b97248fb8 chore: release 2024-11-04 10:42:46 +07:00
reya f54f448ecb feat: add tray 2024-11-04 10:40:50 +07:00
reya bd1f2b899d refactor: sync based on interval not window event 2024-11-04 09:40:33 +07:00
reya efd3c83193 fix: missing check for update 2024-11-03 13:50:52 +07:00
reya 85fa1e2359 fix: missing check for update 2024-11-03 13:50:31 +07:00
reya cd6ba5884f chore: release 2024-11-03 13:18:36 +07:00
reya bfed56ba13 fix: crash on windows 2024-11-03 13:05:24 +07:00
reya d1018ba8d1 feat: remove mica effect and support windows 10 2024-11-02 13:43:13 +07:00
reya a42542c16e fix: performance 2024-11-02 13:33:48 +07:00
reya 26a6ec954c fix: thread not reload after reply 2024-11-02 08:26:46 +07:00
reya f346282c3c fix: calver 2024-11-01 18:33:03 +07:00
reya f3e875eeea fix: some issues with publish new note 2024-11-01 17:57:01 +07:00
reya 6ad8ffddf0 chore: clean up 2024-11-01 13:14:47 +07:00
reya d37e2a3c80 chore: update nostr-sdk and nostr-connect 2024-11-01 09:57:46 +07:00
reya 18e1ac0e6c feat: add relay feeds 2024-11-01 09:25:12 +07:00
reya aa4f21a869 fix: dark mode 2024-10-31 16:25:16 +07:00
reya bbc052eebc chore: switch to calver 2024-10-31 13:46:41 +07:00
reya c201b5816c feat: add discover newsfeeds and interests 2024-10-31 13:34:25 +07:00
reya 043cabfd4e fix: overload 2024-10-31 10:25:00 +07:00
reya 9b02ab5842 feat: re-enable gossip and optimize 2024-10-30 16:02:09 +07:00
reya 618a45d349 refactor: app settings 2024-10-30 10:57:43 +07:00
reya 11dcef4e87 feat: group metadata query 2024-10-29 15:05:03 +07:00
reya d87371aec4 chore: use latest nostr sdk 2024-10-28 16:19:50 +07:00
reya cfb017f70b chore: update deps 2024-10-28 09:00:28 +07:00
reya 9ba95301db update 2024-10-28 08:04:19 +07:00
reya 0518389f50 update 2024-10-27 15:57:32 +07:00
reya eb6e3e52df update 2024-10-27 09:57:56 +07:00
reya 1e95a2fd95 update 2024-10-27 08:14:15 +07:00
reya 83d24351cd update 2024-10-26 17:24:39 +07:00
reya 470dc1c759 fix: command 2024-10-26 13:11:07 +07:00
reya 42b780ce6a update 2024-10-26 09:08:50 +07:00
reya 5ab2b1ae31 feat: negentropy progress 2024-10-25 14:57:12 +07:00
reya 055d73c829 feat: rework multi account 2024-10-24 15:50:45 +07:00
reya 469296790e wip: rework multi account 2024-10-24 07:59:41 +07:00
reya c032dbea1a chore: clean up 2024-10-23 10:43:39 +07:00
reya 172566028b feat: use latest nostr sdk 2024-10-23 10:16:56 +07:00
雨宮蓮andGitHub cc7de41bfd feat: Multi Accounts (#237)
* wip: new sync

* wip: restructure routes

* update

* feat: improve sync

* feat: repost with multi-account

* feat: improve sync

* feat: publish with multi account

* fix: settings screen

* feat: add zap for multi accounts
2024-10-22 16:00:06 +07:00
reya ba9c81a10a feat: use upstream rust nostr 2024-10-15 10:37:07 +07:00
reya e158f2e4d7 feat: improve column carousel 2024-10-15 09:59:26 +07:00
reya 62bd689031 fix: async mutex lock forever 2024-10-14 14:59:42 +07:00
reya cb6006f596 feat: handle error when publish note 2024-10-11 14:09:15 +07:00
reya adad048873 feat: improve negentropy sync 2024-10-11 08:56:43 +07:00
reya 790fee6c05 chore: bump version 2024-10-09 14:51:20 +07:00
reya 9f5b14956a feat: save state when close app 2024-10-09 14:02:13 +07:00
reya e04497d841 chore: update README 2024-10-09 09:42:49 +07:00
reya 106c627ec4 feat: only query from local database and other improvements 2024-10-09 09:15:49 +07:00
reya c40762cc04 feat: add support for NIP-09 2024-10-08 16:30:26 +07:00
reya d2b5ae0507 feat: use negentropy as much as possible 2024-10-08 10:36:31 +07:00
reya 8c6aea8050 chore: follow-up #236 2024-10-07 15:12:30 +07:00
雨宮蓮andGitHub 090a815f99 feat: Add support for NIP-51 (#236)
* feat: and follow and interest sets

* feat: improve query

* feat: improve
2024-10-07 14:33:20 +07:00
reya d841163ba7 revamp 2024-10-05 08:49:09 +07:00
reya 8398ae80d3 chore: bump version 2024-10-03 10:43:16 +07:00
reya fe60f75e96 chore: upgrade to tauri v2 stable 2024-10-03 09:00:48 +07:00
雨宮蓮andGitHub e098743d43 feat: new flow (#235)
* feat: redesign initial screen

* feat: improve login process
2024-10-02 14:56:26 +07:00
reya 0c19ada1ab chore: bump version 2024-09-30 05:57:42 +07:00
reya 09db39fce1 chore: add some small improvements 2024-09-30 04:49:17 +07:00
雨宮蓮andGitHub f0fc89724d feat: improve performance (#234)
* feat: use negentropy as much as possible

* update

* update
2024-09-29 16:53:39 +07:00
reya afa9327bb7 feat: add content parser to reply 2024-09-27 09:58:02 +07:00
reya 5c3644f977 feat: update general settings 2024-09-27 09:12:38 +07:00
reya 0a8eed9a46 feat: new post editor 2024-09-26 10:57:58 +07:00
reya bacfaed48a wip: local relay 2024-09-25 09:59:54 +07:00
reya 3d5085785b feat: pow by default 2024-09-23 17:18:41 +07:00
reya 9152c3e122 feat: add basic web of trust 2024-09-23 13:24:33 +07:00
reya a5574bef6c feat: add notification for nip42 2024-09-22 09:40:07 +07:00
reya 2c7f3685b6 fix: build on macos 2024-09-20 09:26:48 +07:00
reya be0abc4075 chore: update deps 2024-09-20 08:42:08 +07:00
reya dafe35cd1f feat: add client tag 2024-09-20 08:05:32 +07:00
reya b23903240b fix: wrong menu item in repost button 2024-09-20 08:05:22 +07:00
reya 872a6cee36 fix: missing context menu in user's avatar 2024-09-20 07:25:07 +07:00
雨宮蓮andGitHub ac7ce726c5 feat: Add gossip model (#232)
* feat: enable gossip

* chore: remove deprecated functions

* chore: use upstream rust nostr

* fix
2024-09-11 11:10:11 +07:00
AndrewandGitHub e5e290c0c3 fix: Improved text legibility on login/create account screens (#230)
* style: standardized placeholder text to be color neutral-400

* fix: changed dark:text-neutral-600 to dark:text-neutral-200 as in dark mode text was not visible
2024-08-30 20:04:44 +07:00
reya 2eab6f04c7 chore: bump version 2024-08-30 13:30:29 +07:00
reya e06b0334a5 chore: bump version 2024-08-29 08:00:58 +07:00
reya 74d8bf2ead fix: cannot import ncryptsec 2024-08-29 07:48:26 +07:00
reya d128af1db8 chore: clean up 2024-08-28 08:48:17 +07:00
reya f6eb5eea44 chore: update ci 2024-08-27 19:43:51 +07:00
reya bca2e0b7b7 chore: bump version 2024-08-27 19:42:35 +07:00
雨宮蓮andGitHub 61ad96ca63 Release v4.1 (#229)
* refactor: remove custom icon packs

* fix: command not work on windows

* fix: make open_window command async

* feat: improve commands

* feat: improve

* refactor: column

* feat: improve thread column

* feat: improve

* feat: add stories column

* feat: improve

* feat: add search column

* feat: add reset password

* feat: add subscription

* refactor: settings

* chore: improve commands

* fix: crash on production

* feat: use tauri store plugin for cache

* feat: new icon

* chore: update icon for windows

* chore: improve some columns

* chore: polish code
2024-08-27 19:37:30 +07:00
reya 26ae473521 fix: disable some default webview behaviors 2024-08-19 13:46:22 +07:00
reya bcc5e18082 fix: adjust window controls position 2024-08-19 10:48:29 +07:00
reya 307fff7a53 fix: titlebar on windows 2024-08-19 10:28:38 +07:00
reya ce7828310b chore: add some improvements and remove linux support 2024-08-19 09:58:29 +07:00
reya beac1a189e chore: update deps 2024-08-18 15:51:34 +07:00
reya 4cb49d44c7 feat: improve account management 2024-08-13 10:33:21 +07:00
reya be16d5c21d chore: upgrade to react 19 rc 2024-08-12 10:32:20 +07:00
reya da8162069b refactor: remove turborepo 2024-08-12 09:07:11 +07:00
reya e2103ae23a feat: upgrade to tauri v2 rc 2024-08-10 16:45:56 +07:00
XIAO YUandGitHub 4c6d1c768a Handle errors when adding and connecting relays in init_nip65 (#227) 2024-08-03 08:56:38 +07:00
reya 9b75a04f91 chore: update deps 2024-07-31 13:03:27 +07:00
reya a5255fa503 chore: bump version 2024-07-31 12:51:41 +07:00
reya 954a17b541 fix: build on linux 2024-07-31 12:51:17 +07:00
reya a55b31b0e6 feat: add keyring support for linux and windows 2024-07-31 10:59:54 +07:00
reya bdf3ffd7bf fix: tray panel is missing 2024-07-19 13:58:32 +07:00
reya 07ce253f5b fix: child webview is not reposition after scroll 2024-07-19 13:10:29 +07:00
reya f3db010c74 chore: update tauri config 2024-07-19 10:01:20 +07:00
reya dcf2791fe5 fix: build 2024-07-19 09:28:03 +07:00
reya 8fcf3551d8 chore: bump version 2024-07-19 08:33:46 +07:00
reya 2d987849d8 feat: use native border in macos 2024-07-19 08:33:16 +07:00
reya 3b99926f3b feat: adapt latest changes in tauri v2 2024-07-19 08:25:36 +07:00
reya 113d69a4df chore: update deps 2024-07-18 18:57:28 +07:00
reya 5d12ba7216 fix: some screen too large 2024-07-03 14:36:54 +07:00
reya 72b59020b4 fix: build on linux and windows 2024-07-03 14:09:07 +07:00
XIAO YUandGitHub 4c323b9daa chore: Refactor code to use HashSet for account search results (#222) 2024-07-03 07:33:16 +07:00
reya 72da83d648 feat: add force quit command 2024-07-02 17:24:55 +07:00
reya 783a4538a4 Revert "chore: update ci"
This reverts commit 04706a6d7c.
2024-07-02 15:54:00 +07:00
reya 15e62cad11 fix: temporary include devtools in release build 2024-07-02 14:58:00 +07:00
reya c52b20ca80 chore: bump version 2024-07-02 14:36:16 +07:00
reya 04706a6d7c chore: update ci 2024-07-02 14:07:12 +07:00
reya 0755cbeb6c chore: bump version 2024-07-02 13:03:45 +07:00
雨宮蓮andGitHub 8eb01c8bbf Improve column management (#221)
* wip: redesign store

* feat: update trending column

* feat: add more functions
2024-07-02 12:51:50 +07:00
reya ed4f89ff66 feat: add option to toggle window transparent 2024-07-02 08:49:52 +07:00
reya d9fe647f8e feat: few improvements 2024-07-01 14:41:33 +07:00
reya 843c2d52e7 refactor: tray panel 2024-07-01 13:04:32 +07:00
reya 017a3676a4 feat: optimize spawn thread 2024-06-30 21:06:04 +07:00
reya fcb70c0e9a chore: update deps 2024-06-30 14:37:26 +07:00
雨宮蓮andGitHub 0fec21b9ce Some improments and Negentropy (#219)
* feat: adjust default window size

* feat: save window state

* feat: add window state plugin

* feat: add search

* feat: use negentropy for newsfeed

* feat: live feeds

* feat: add search user
2024-06-30 14:26:02 +07:00
XIAO YUandGitHub 968b1ada94 refactor: improve relay management code structure (#220) 2024-06-29 07:41:16 +07:00
reya 5c9b599b1e feat: use memo for some components 2024-06-26 17:49:36 +07:00
雨宮蓮andGitHub 717c3e17df Event Subscriptions (#218)
* feat: improve create column command

* refactor: thread

* feat: add window virtualized to event screen

* chore: update deps

* fix: window decoration

* feat: improve mention ntoe

* feat: add subscription to event screen
2024-06-26 14:51:50 +07:00
XIAO YUandGitHub a4540a0802 refactor: improve error handling in event.rs (#217) 2024-06-26 07:57:53 +07:00
reya 31bacc2646 chore: remove unused code 2024-06-24 11:09:32 +07:00
reya 6e5d0f0e76 chore: bump version 2024-06-24 10:06:12 +07:00
XIAO YUandGitHub f0712e5961 refactor: improve error handling (#216) 2024-06-24 07:19:36 +07:00
雨宮蓮andGitHub 3fbd66dece Add bitcoin connect (#215)
* feat: add bitcoin connect

* feat: improve zap screen
2024-06-21 14:56:10 +07:00
reya 1283432632 feat: use native feature instead of react 2024-06-21 10:24:09 +07:00
reya 59eaaec903 feat: support content warning 2024-06-21 08:57:49 +07:00
雨宮蓮andGitHub 4f0f210076 Add lazy carousel (#214)
* refactor: carousel

* feat: improve image carousel
2024-06-21 07:51:11 +07:00
reya e4a317f038 chore: bump version 2024-06-20 13:23:05 +07:00
reya 9779d020c7 feat: improve list virtualization 2024-06-20 13:22:28 +07:00
雨宮蓮andGitHub f8280ec8ee fix: get replies function (#213) 2024-06-19 21:02:33 +07:00
XIAO YUandGitHub 6c26f8967b chore: Refactor code for better performance and reliability (#212) 2024-06-19 14:36:59 +07:00
reya e1424b851c fix: routes 2024-06-19 14:22:45 +07:00
reya d14e609579 chore: bump version 2024-06-19 14:14:34 +07:00
reya 8c0627ff27 fix: crash on startup 2024-06-19 14:14:03 +07:00
雨宮蓮andGitHub 18c133d096 Settings Manager (#211)
* refactor: landing screen

* fix: code debt

* feat: add settings screen

* chore: clean up

* feat: settings

* feat: small updates
2024-06-19 14:00:58 +07:00
reya 0061ecea78 feat: use native context menu in tray panel 2024-06-18 09:07:58 +07:00
reya d01cf8319d feat: native context menu 2024-06-17 15:31:59 +07:00
雨宮蓮andGitHub 843895d876 Make Lume Faster (#208)
* chore: fix some lint issues

* feat: refactor contact list

* feat: refactor relay hint

* feat: add missing commands

* feat: use new cache layer for react query

* feat: refactor column

* feat: improve relay hint

* fix: replace break with continue in parser

* refactor: publish function

* feat: add reply command

* feat: improve editor

* fix: quote

* chore: update deps

* refactor: note component

* feat: improve repost

* feat: improve cache

* fix: backup screen

* refactor: column manager
2024-06-17 13:52:06 +07:00
XIAO YUandGitHub 7c99ed39e4 chore: handle unwrap err (#210) 2024-06-16 08:11:48 +07:00
71be59b2e9 Move the event parser and dedup functions to Rust (#206)
* feat: improve js parser

* feat: move parser and dedup to rust

* fix: parser

* fix: get event function

* feat: improve parser performance (#207)

* feat: improve parser performance

* feat: add test for video parsing

* feat: finish new parser

---------

Co-authored-by: XIAO YU <xyzmhx@gmail.com>
2024-06-12 08:27:53 +07:00
reya 1c20512ecc chore: update deps 2024-06-10 13:15:57 +07:00
雨宮蓮andGitHub 90342c552f Customize Bootstrap Relays (#205)
* feat: add bootstrap relays file

* feat: add save bootstrap relays command

* feat: add customize bootstrap relays screen
2024-06-10 10:48:39 +07:00
reya b396c8a695 feat: upgrade to rust-nostr 0.32 2024-06-08 08:00:02 +07:00
reya 6996e30889 chore: update github ci 2024-06-07 11:26:20 +07:00
reya 7ba793fad8 chore: bump version 2024-06-07 10:03:04 +07:00
reya f11f836518 chore: update tray icon 2024-06-07 09:56:57 +07:00
reya 04fe0fcec8 feat: respect the relay hint 2024-06-07 09:07:33 +07:00
799835a629 Notification Panel (#200)
* feat: add tauri nspanel

* feat: add notification panel

* feat: move notification service to backend

* feat: add sync notification job

* feat: enable panel to join all spaces including fullscreen (#203)

* feat: fetch notification

* feat: listen for new notification

* feat: finish panel

---------

Co-authored-by: Victor Aremu <me@victorare.mu>
2024-06-06 14:32:30 +07:00
XIAO YUandGitHub 4e7da4108b feat: Add get user following function (#202)
* feat: Add get user following function

* refactor: Refactor get_following function to use state and string public key

* feat: Update get_following function to use timeout duration

* feat: Fix connect_remote_account function to return remote_npub without conversion

* feat: Refactor get_following function to handle public key parsing errors

* Refactor get_followers function to handle public key parsing errors and use timeout duration
2024-06-05 13:24:32 +07:00
reya 7c7b082b3a fix: memory leak in image component 2024-06-03 07:32:34 +07:00
reya 38d6c51921 feat: use nostrdb for unix and rocksdb for windows 2024-06-02 08:16:59 +07:00
reya 1738cbdd97 chore: upgrade tauri 2024-06-01 14:54:35 +07:00
reya 2e885b76a1 feat: improve text wrap 2024-06-01 08:27:22 +07:00
reya f94680e487 fix: column overlapped after change account 2024-05-31 15:25:47 +07:00
XIAO YUandGitHub c682a58842 chore: Remove unused modules and update metadata.rs (#199) 2024-05-31 12:53:35 +07:00
reya 921cf871ee chore: bump version 2024-05-31 08:54:53 +07:00
reya d5b1593aca feat: improve nostr connect flow 2024-05-31 08:54:17 +07:00
reya 6676b4e2a4 Revert "chore: Update dependencies and add thiserror crate (#196)"
This reverts commit e254ee3203.
2024-05-30 15:40:44 +07:00
reya 5f30ddcfca Merge branch 'main' of github.com:lumehq/lume 2024-05-30 15:23:11 +07:00
reya 41d0de539d feat: revamp nostr connect flow 2024-05-30 15:21:33 +07:00
XIAO YUandGitHub e254ee3203 chore: Update dependencies and add thiserror crate (#196) 2024-05-30 07:12:46 +07:00
reya 6d42360549 chore: bump version 2024-05-29 14:47:22 +07:00
reya 70c5143445 feat: redirect to index after add account 2024-05-29 14:41:33 +07:00
reya 41b66b18f5 feat: lazy load onboarding videos 2024-05-29 14:11:30 +07:00
雨宮蓮andGitHub dda0720ed4 chore: update deps (#195) 2024-05-29 12:54:58 +07:00
reya 4b60b39119 chore: bump version 2024-05-29 10:34:32 +07:00
reya d2e5122d5a feat: improve splash screen and notification service 2024-05-29 10:33:38 +07:00
reya 32f3315344 fix: editor crash on open 2024-05-28 14:43:34 +07:00
reya 5ca9444358 feat: add theme switcher 2024-05-26 16:11:27 +07:00
reya 4dc13385a5 feat: add option for relay hint 2024-05-26 08:23:29 +07:00
XIAO YUandGitHub b90ad1421f chore: update keys.rs and relay.rs (#193) 2024-05-26 07:28:16 +07:00
雨宮蓮andGitHub bba324ea53 refactor: use specta for commands (#192)
* feat: add tauri-specta

* refactor: system library

* chore: format
2024-05-25 15:21:40 +07:00
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 YUandGitHub 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
雨宮蓮andGitHub 407fe40b67 refactor: account managements (#190)
* feat: add keyring-search

* feat: improve nostr connect
2024-05-22 13:24:58 +07:00
雨宮蓮andGitHub 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
雨宮蓮andGitHub 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
雨宮蓮andGitHub 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
雨宮蓮andGitHub 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
雨宮蓮andGitHub 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
雨宮蓮andGitHub f027eae52d feat: use negentropy (#182)
* feat: use negentropy

* chore: polish
2024-04-24 10:18:51 +07:00
雨宮蓮andGitHub 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
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
reya 17766d29d6 chore: update deps 2024-04-22 13:18:34 +07:00
reya 3b13dfeed8 chore: update github action 2024-04-20 18:13:36 +07:00
reya 17ba79e01b chore: update app icon 2024-04-20 08:58:00 +07:00
reya bafad544e9 chore: update deps and bump version 2024-04-19 14:21:12 +07:00
reya 89c36423ae feat: add nip46 2024-04-18 15:09:33 +07:00
reya cd31b99559 feat: improve official columns 2024-04-18 07:50:46 +07:00
reya f3c52237fa fix: privacy setting is not working 2024-04-17 13:50:50 +07:00
reya 413d8d82df feat: finish settings screen 2024-04-16 15:28:38 +07:00
reya 2eb2010d43 chore: upgrade deps 2024-04-16 08:55:36 +07:00
reya 94d400cab2 feat: support nip-36 2024-04-16 07:49:44 +07:00
reya 09b143cb08 feat: auto resize mini webview when main webview resized 2024-04-15 13:30:55 +07:00
reya e3ede34108 feat: revert to sqlite 2024-04-14 09:05:46 +07:00
reya ed6aca41ea feat: add new spinner component 2024-04-13 15:01:27 +07:00
reya 89f577fbef feat: upgrade to rust nostr 0.30.0 and migrate to nostrdb 2024-04-13 08:53:31 +07:00
reya 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
reya 8a7b246315 chore: update deps 2024-04-11 07:47:37 +07:00
Ren AmamiyaandGitHub d98c6d0709 Merge pull request #175 from lumehq/v4/beta
Prepare for v4 - beta
2024-04-11 07:45:43 +07:00
reya bda20e8fe8 chore: small updates 2024-04-11 07:44:41 +07:00
reya c342daecc8 feat: improve 2024-04-10 14:11:05 +07:00
reya 5e6692cd6d feat: re add group column 2024-04-09 14:05:50 +07:00
reya 420be77b5c feat: add nstore 2024-04-07 15:11:20 +07:00
reya 999073f84c feat: readd for you column 2024-04-04 13:47:15 +07:00
reya 174b28f1a7 chore: update deps 2024-04-03 08:01:40 +07:00
reya 763cb10e85 chore: clean up 2024-04-03 07:29:46 +07:00
reya 89bb8d88f6 feat: settings screens 2024-04-02 13:19:26 +07:00
reya 09aa2ecafc feat: re-add trending column 2024-03-30 10:50:45 +07:00
reya 7271e9ea87 feat: add custom traffic light inset for macos 2024-03-29 13:26:35 +07:00
reya a02e496b29 chore: update tauri 2024-03-29 07:45:37 +07:00
reya cbbf5eaf50 feat: add create account flow 2024-03-28 15:12:43 +07:00
reya d3fa59d2b1 chore: polish 2024-03-27 08:36:34 +07:00
reya aa23e39334 feat: update new icons 2024-03-26 14:58:06 +07:00
reya c8e2018fd0 feat: update tray icon 2024-03-26 13:08:03 +07:00
reya 6e489f1c49 wip: onboarding 2024-03-25 14:22:06 +07:00
reya a49b88ab35 Merge branch 'main' into v4/beta 2024-03-23 07:24:58 +07:00
reya 31839531ea feat: improve open store column 2024-03-22 15:25:53 +07:00
reya ec0f3fabc0 feat: improve column management 2024-03-22 11:28:54 +07:00
reya dd7155a3a6 feat: update default column informations 2024-03-21 14:53:08 +07:00
reya cb565ff35b feat: readd default columns 2024-03-20 15:12:54 +07:00
reya 5d59040224 chore: follow up 2024-03-19 15:36:20 +07:00
reya 7fabf949c6 feat: move column manager to rust 2024-03-19 14:56:24 +07:00
reya ea5120e2f0 chore: upgrade tauri and bump version 2024-03-19 12:46:19 +07:00
reya 14f07dfea8 feat: add lume store screen 2024-03-19 08:21:16 +07:00
Ren AmamiyaandGitHub 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
reya 05b52564e0 feat: column manager 2024-03-18 10:50:08 +07:00
reya c8e014f33e chore: clean up 2024-03-16 18:45:54 +07:00
reya 46cc01e0ee feat: child webview 2024-03-16 15:05:06 +07:00
reya 16e6d234e5 feat: space 2024-03-14 14:22:41 +07:00
reya 3005d27403 feat: update fetch opg function 2024-03-13 10:24:45 +07:00
reya 1be84f3139 chore: release v4.0.0-alpha+2 2024-03-13 09:40:30 +07:00
Ren AmamiyaandGitHub 04bde7dd43 Merge pull request #172 from lumehq/v4/functions
Some polish for v4
2024-03-13 09:39:57 +07:00
reya f1504d99ac feat: add mock tray menu 2024-03-13 09:19:25 +07:00
reya e928f2ee37 feat: polish 2024-03-13 08:40:18 +07:00
Ren AmamiyaandGitHub ccab78ca11 Merge pull request #171 from lumehq/v4/zap
Add zap to v4
2024-03-11 15:39:35 +07:00
reya 5b13c3e45c feat: auto close window if zapp successfully 2024-03-11 15:38:54 +07:00
reya e6b97ab9ae feat: add zap 2024-03-11 15:36:17 +07:00
reya bb6badfed6 feat: add zap setup screen 2024-03-10 16:39:23 +07:00
Ren AmamiyaandGitHub 0c4b309a11 Merge pull request #170 from lumehq/v4/home-screen
v4/home-screen
2024-03-09 10:09:29 +07:00
reya f4400d711f feat: improve home screen 2024-03-09 10:08:50 +07:00
reya a3e46aa96b fix: crash when database is not exist 2024-03-08 14:47:36 +07:00
reya a4fdcfdf0b feat: add local and global newsfeeds 2024-03-07 15:39:43 +07:00
reya 25d07303a3 fix: build error in windows and linux 2024-03-06 15:32:51 +07:00
reya 0e1e7524c9 chore: bump version 2024-03-06 15:05:17 +07:00
Ren AmamiyaandGitHub 32d770eb91 Merge pull request #169 from lumehq/v4/onboarding
Add basic onboarding dialog to v4
2024-03-06 15:00:07 +07:00
reya e93cbf78d5 feat: add npub to file associations 2024-03-06 14:40:55 +07:00
reya 71a2290b8f feat: add backup dialog 2024-03-06 14:40:00 +07:00
reya 95294a80cb feat: simplify account management 2024-03-06 11:11:24 +07:00
reya 8eaf47f6d2 feat: add login dialog 2024-03-06 09:42:44 +07:00
reya 86183d799a fix: package version in windows 2024-03-02 17:59:36 +07:00
reya 2dbfff3c17 chore: release v4.0.0-alpha.0 2024-03-02 16:08:01 +07:00
Ren AmamiyaandGitHub d1f5c372ae Merge pull request #166 from lumehq/feat/rust-nostr
Rust Nostr
2024-03-02 15:32:13 +07:00
reya 13281455ed chore: refer old branch in readme 2024-03-02 15:31:41 +07:00
reya 9cfb32be4d feat: update readme 2024-03-02 15:29:45 +07:00
reya cb0672c22a Merge branch 'main' into feat/rust-nostr 2024-03-02 15:22:11 +07:00
reya ca0e041731 feat: improve 2024-03-02 15:21:28 +07:00
reya cfcb9bc6ed feat: improve ui 2024-02-29 13:02:16 +07:00
reya 09df8672d0 feat: add zap command 2024-02-27 15:37:49 +07:00
reya 2403231ac4 feat: add user screen 2024-02-27 13:42:59 +07:00
reya 98ef1927f2 feat: editor 2024-02-26 15:10:42 +07:00
reya 63db8b1423 chore: clean up 2024-02-25 15:52:47 +07:00
reya 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
reya 88a6c3c81f feat: account switcher 2024-02-24 14:41:57 +07:00
reya 84584a4d1f feat: add editor screen 2024-02-23 14:56:24 +07:00
reya 64286aa354 wip 2024-02-22 14:01:29 +07:00
reya e9ce932646 chore: remove storage layer 2024-02-22 08:58:45 +07:00
Ren AmamiyaandGitHub 4c28b4879c chore: update readme 2024-02-21 15:04:30 +07:00
reya 9127d5c5ea feat: add more metadata commands 2024-02-21 14:29:54 +07:00
reya 090e54ec5a feat: add contact list commands 2024-02-20 14:46:21 +07:00
reya d0c9f93ebb wip: small updates 2024-02-20 10:26:19 +07:00
reya e2cdc5b576 feat: account selection screen 2024-02-19 14:13:33 +07:00
reya bfe35ad885 wip: multi accounts 2024-02-19 09:58:27 +07:00
reya 0f06522c28 wip: refactor 2024-02-17 16:01:53 +07:00
reya f47eba5af7 feat: upgrade to rust nostr 0.28 2024-02-17 09:29:37 +07:00
reya f28a7ae82f wip: update design 2024-02-16 14:11:49 +07:00
reya 296b11b7b8 wip: basic support multi windows 2024-02-16 09:58:07 +07:00
reya a0d9a729dd feat: add tauri devtools 2024-02-15 14:41:32 +07:00
reya 1de8c7240d wip: desktop2 2024-02-15 14:32:40 +07:00
reya 70126ef1b3 Merge branch 'main' into feat/rust-nostr 2024-02-15 12:48:20 +07:00
reya cdf29f8a54 wip: update 2024-02-15 12:47:15 +07:00
reya 6171b9bed1 wip: tired... 2024-02-14 15:51:06 +07:00
reya 60fd09000b wip: new design 2024-02-14 08:29:24 +07:00
reya 4292def206 feat: add more nostr commands 2024-02-13 09:36:11 +07:00
reya 90f149b09c chore: clean up 2024-02-13 08:57:24 +07:00
reya ed52105c02 wip: migrate to desktop2 2024-02-12 20:04:45 +07:00
reya 1950cb59a2 wip: migrate to desktop2 2024-02-12 09:08:35 +07:00
Ren AmamiyaandGitHub 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
reya c809ab6b4e feat: add desktop2 2024-02-11 09:30:18 +07:00
Ren AmamiyaandGitHub c6674c4a2d Merge pull request #165 from higedamc/main
Update ja.json
2024-02-11 08:10:36 +07:00
reya 35c5b5fb78 chore: fix build 2024-02-10 11:19:18 +07:00
reya 739ba63e6c wip: migrate frontend to new backend 2024-02-09 15:33:27 +07:00
reya ec78cf8bf7 feat: migrate frontend to new backend 2024-02-08 21:24:08 +07:00
reya 17052aeeaa feat: add new account management 2024-02-08 17:17:45 +07:00
reya d7bbda6e7b fix: remove native db 2024-02-07 14:26:58 +07:00
reya 6a08c1de10 feat: add all nostr events command 2024-02-07 14:04:57 +07:00
reya 3c4bd39384 feat: update rust nostr 2024-02-06 19:28:46 +07:00
reya 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
reya a21da11a91 feat: add desktop2 2024-02-05 14:18:27 +07:00
reya 08fa7de01d feat: add check active account before init client 2024-02-05 13:36:10 +07:00
reya 2a58326cd1 chore: update dependencies 2024-02-05 08:30:06 +07:00
reya 8a17c36a5b feat: upgrade to tauri beta 2024-02-05 08:14:58 +07:00
reya 7bd6f6a8db fix: remove surrealdb and clean up 2024-02-05 07:46:41 +07:00
reya 3ba870be4b feat: add verify signer 2024-02-04 10:46:50 +07:00
reya bd2b6a3759 feat: add surreal db 2024-02-03 11:24:08 +07:00
reya a3a8f57bfc feat: add more nostr commands 2024-02-03 09:13:17 +07:00
reya fd393a4d30 feat: add basic nostr connection 2024-02-02 09:09:14 +07:00
reya e8bd48e51b feat: add rust nostr 2024-02-01 12:54:25 +07:00
reya 0191180f31 chore: rename 2024-02-01 09:15:25 +07:00
reya 60ed56b1b9 chore: bump version 2024-02-01 08:40:27 +07:00
Ren AmamiyaandGitHub da722afed3 Merge pull request #154 from luminous-devs/next
v3.0.1
2024-02-01 08:36:21 +07:00
reya d8eb51e49c Revert "chore: update dependencies"
This reverts commit c700a45ab6.
2024-02-01 08:18:51 +07:00
reya c700a45ab6 chore: update dependencies 2024-02-01 07:59:25 +07:00
reya b806a34edb chore: remove submodule and clean up flatpak 2024-01-31 14:31:16 +07:00
reya 21989e6fa5 fix: minor improves 2024-01-31 14:20:03 +07:00
reya 0539c5649d feat: add waifu column and fix wrong package 2024-01-31 11:04:47 +07:00
reya ad488ff72d feat: add global column 2024-01-31 09:12:26 +07:00
reya 02e0309a41 feat: refactor rust commands 2024-01-31 08:20:39 +07:00
reya b7f4af7883 fix: respect NIP spec 2024-01-31 07:29:47 +07:00
reya cc48a4f36b chore: update dependencies 2024-01-30 15:33:24 +07:00
reya 46ed3330fc feat: add hover card to user 2024-01-30 14:45:46 +07:00
reya 1fa1872ca6 feat: add trending notes column 2024-01-30 14:22:56 +07:00
Ren AmamiyaandGitHub 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
reya eaf9bda077 feat: add new update notify to navigation 2024-01-30 13:16:39 +07:00
reya 84a248a5a9 chore: fix typos 2024-01-30 10:01:45 +07:00
Ren AmamiyaandGitHub 711c1d561a Merge pull request #151 from luminous-devs/feat/multi-lang
Add support for multi-languages
2024-01-30 09:12:06 +07:00
reya 21210b4336 feat: migrate activity screen to i18n 2024-01-30 09:10:26 +07:00
reya 3bd480b75e feat: optimize locale loader 2024-01-30 08:55:49 +07:00
reya 2b19650e46 feat: migrate onboarding to i18n 2024-01-30 08:13:12 +07:00
reya 23482531c5 feat: migrate settings screen to i18n 2024-01-29 14:57:00 +07:00
reya cfda9ba899 feat: migrate ui components to i18n 2024-01-29 13:38:22 +07:00
reya 698bd78684 feat: migrate note component to i18n 2024-01-29 10:22:55 +07:00
reya b97676dd3e feat: add translation to relay screen 2024-01-29 09:30:33 +07:00
reya 25ae4f2201 feat: add multi-lang 2024-01-29 09:12:44 +07:00
Ren AmamiyaandGitHub 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
reya af1b4e60d3 web: update download link 2024-01-28 08:45:04 +07:00
reya 648cbf6f80 chore: fix ci 2024-01-28 08:05:36 +07:00
reya 7b06a82ee7 chore: bump version and fix gh action 2024-01-28 07:40:08 +07:00
Ren AmamiyaandGitHub d18de93c60 Merge pull request #148 from luminous-devs/feat/improve-perf
Improve overall performance
2024-01-27 20:01:55 +07:00
reya df15eb7a03 Merge branch 'main' into feat/improve-perf 2024-01-27 17:56:50 +07:00
Ren AmamiyaandGitHub 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
reya b11e2a4291 fix: update mention popup style 2024-01-27 11:16:17 +07:00
reya 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 AmamiyaandGitHub bc48391a1a Merge pull request #147 from luminous-devs/feat/improve-design
Improve overal design
2024-01-26 14:21:27 +07:00
reya b0a443c002 feat: polish 2024-01-26 14:15:25 +07:00
reya bef1f136ad chore: bump version 2024-01-26 12:43:19 +07:00
reya 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
reya 74426e13c8 wip: improve onboarding 2024-01-25 15:25:40 +07:00
reya bd45c36072 feat: fix typos and other stuffs 2024-01-25 09:49:04 +07:00
reya c13aefcd15 feat: update dependencies 2024-01-25 08:35:17 +07:00
reya 167caee8bc feat: improve ui contrast 2024-01-25 08:14:25 +07:00
reya d527078d5c feat: redesign column header 2024-01-24 15:08:55 +07:00
Ren AmamiyaandGitHub 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
reya 057c57b70f feat: add empty state to search dialog 2024-01-24 13:34:27 +07:00
reya cb71786ac1 feat: add basic search dialog 2024-01-23 13:07:24 +07:00
Ren AmamiyaandGitHub 67afeac198 Merge pull request #143 from luminous-devs/apps/web
Add landing page
2024-01-22 14:41:24 +07:00
reya f4ee25de8e feat: add landing page 2024-01-22 14:40:39 +07:00
reya 445a218a9e Merge branch 'main' into next 2024-01-21 09:54:47 +07:00
reya f09139ffbe final 2024-01-21 09:43:46 +07:00
reya 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
reya 9fcdac4edb feat: add suggest screen 2024-01-20 14:51:13 +07:00
reya b726ae3c7c feat: add for you column 2024-01-20 09:06:00 +07:00
reya a3460418f6 feat: add interest screen to onboarding 2024-01-19 14:53:26 +07:00
reya f65175f11e feat: polish 2024-01-19 08:24:13 +07:00
reya 16efd495a0 chore: clean up 2024-01-19 07:45:28 +07:00
reya ed6423e4aa feat: polish 2024-01-18 15:09:16 +07:00
reya 0e9418949b feat: add instant zap 2024-01-18 13:56:35 +07:00
reya 240fe8bc7c feat: fix relay manager 2024-01-18 09:41:53 +07:00
reya c3482cddd8 feat: improve zap 2024-01-18 08:22:35 +07:00
reya d13e7b3ef6 feat: polish 2024-01-18 07:37:40 +07:00
reya 47800bd2ff chore: upgrade tauri 2024-01-17 13:07:01 +07:00
reya c0305db5fc chore: update and fix dependencies 2024-01-17 12:39:04 +07:00
reya 0b745cb40e feat: add reply form 2024-01-17 12:24:04 +07:00
reya a20f5ca15d feat: polish 2024-01-17 09:30:32 +07:00
reya c29b4e173e feat: polish 2024-01-17 08:50:43 +07:00
reya 33dd8b1d8a feat: better error handler 2024-01-16 19:56:07 +07:00
reya 1503d90bd5 fix: tauri cache 2024-01-16 14:51:06 +07:00
reya 6581ffb92b feat: polish 2024-01-16 14:49:00 +07:00
reya 939dfd9cc1 feat: fix errros 2024-01-16 08:37:42 +07:00
Ren AmamiyaandGitHub 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
591 changed files with 36531 additions and 27217 deletions
-1
View File
@@ -1 +0,0 @@
use flake
+35 -34
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: ''
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: "macos-latest" # for Intel & Arm based macs.
args: "--target universal-apple-darwin"
- platform: 'windows-latest'
args: '--target x86_64-pc-windows-msvc'
runs-on: ${{ matrix.settings.platform }}
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
+22 -33
View File
@@ -1,38 +1,27 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# 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
# Debug
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Misc
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.pem
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
src/routes.gen.ts
src/commands.gen.ts
-3
View File
@@ -1,3 +0,0 @@
[submodule "modules/depot"]
path = modules/depot
url = https://github.com/luminous-devs/depot.git
+1 -50
View File
@@ -1,50 +1 @@
### Introduction
Lume is a nostr client
### Usage
Download Lume for your platform here: [https://github.com/luminous-devs/lume/releases](https://github.com/luminous-devs/lume/releases)
Supported platform: macOS, Windows and Linux
### Prerequisites
- PNPM or Bun (experiment)
- Tauri: https://tauri.app/v1/guides/getting-started/prerequisites#setting-up-macos
### Develop
Clone project
```
git clone https://github.com/luminous-devs/lume.git && cd lume
```
Install packages
```
pnpm install
```
Run dev build
```
pnpm tauri dev
```
Generate production build
```
pnpm tauri build
```
#### Nix
Requirements:
1. [Install Nix](https://zero-to-flakes.com/install)
1. [Setup `direnv`](https://zero-to-flakes.com/direnv)
`cd` into the root folder of the project to enter `nix develop` shell. Run `direnv allow` (only once). Then run `pnpm` or `bun` (experimental) commands as described above.
### Lume will be rewritten in Rust
-13
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>
-86
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

-41
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;
}
-28
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>
);
}
-8
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 />);
-323
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 }}
/>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
-27
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} />;
}
-18
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>
);
}
-302
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>
);
}
-118
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>
);
}
@@ -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>
);
}
@@ -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>
);
}
-55
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>
);
}
-227
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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -1,3 +0,0 @@
export function TutorialPostingScreen() {
return <div></div>;
}
@@ -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>
);
}
-65
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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
-222
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>
);
}
@@ -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>
);
}
-157
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>
);
}
-133
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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
-34
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>
);
}
-45
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>
);
}
-161
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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
-154
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>
);
}
@@ -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>
</>
);
}
@@ -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>
);
}
-97
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>
);
}
-14
View File
@@ -1,14 +0,0 @@
import sharedConfig from "@lume/tailwindcss";
const config = {
content: [
"./src/**/*.{js,ts,jsx,tsx}",
"../../packages/@columns/**/*{.js,.ts,.jsx,.tsx}",
"../../packages/ark/**/*{.js,.ts,.jsx,.tsx}",
"../../packages/ui/**/*{.js,.ts,.jsx,.tsx}",
"index.html",
],
presets: [sharedConfig],
};
export default config;
-8
View File
@@ -1,8 +0,0 @@
{
"extends": "@lume/tsconfig/base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
-36
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,
});
+8 -1
View File
@@ -3,18 +3,25 @@
"organizeImports": {
"enabled": true
},
"files": {
"ignore": ["./src/routes.gen.ts", "./src/commands.gen.ts"]
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"style": {
"noNonNullAssertion": "warn"
"noNonNullAssertion": "warn",
"noUselessElse": "off"
},
"correctness": {
"useExhaustiveDependencies": "off"
},
"a11y": {
"noSvgWithoutTitle": "off"
},
"complexity": {
"noStaticOnlyClass": "off"
}
}
}

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