Compare commits

...
574 Commits
Author SHA1 Message Date
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
reya 7744a5e17c feat: update translation 2024-01-15 20:01:06 +07:00
reya 3301af5cbb feat: move nwc to settings 2024-01-15 15:33:05 +07:00
reya 3f1218e7bc feat: add tutorial 2024-01-15 14:06:11 +07:00
reya fbcb3ae6dc feat: add register translate api 2024-01-15 09:51:49 +07:00
reya e93aedb703 feat: update default column list 2024-01-15 08:36:23 +07:00
reya dae4b1d52b feat: redesign relay screen 2024-01-14 18:05:36 +07:00
reya f908c46a19 feat: polish 2024-01-14 09:39:56 +07:00
reya ab27bd5f44 feat: polish 2024-01-13 20:24:52 +07:00
reya 72870bb131 feat: add activity screen 2024-01-13 17:12:44 +07:00
reya 1822eac488 feat(ark): add user component 2024-01-13 08:21:49 +07:00
reya 0487b8a801 feat: refactor 2024-01-12 20:32:45 +07:00
reya 67c6177291 chore: update 2024-01-12 15:14:49 +07:00
reya ad6ae6745d chore: fixed circular import 2024-01-12 14:46:50 +07:00
reya a9d10ff93b feat: polish note component 2024-01-12 13:56:20 +07:00
reya e0d4c53098 feat: update onboarding flow 2024-01-12 10:12:06 +07:00
reya 2c8571ecc7 wip: new activity sidebar 2024-01-11 21:00:42 +07:00
reya a8cd34d998 chore: small fixes 2024-01-11 07:56:28 +07:00
reya a5ad4fe05c feat: redesign navigation bar 2024-01-10 13:57:44 +07:00
reya f2504071cd feat: update login flow 2024-01-10 09:22:13 +07:00
reya 73f90ebaf9 chore: minor fixes and updates 2024-01-09 08:35:30 +07:00
reya c172c0f80f feat: add onboarding modal 2024-01-08 20:18:07 +07:00
reya aa80301778 refactor: add event and user routes to default ui 2024-01-08 09:30:04 +07:00
reya c04ca3a1ab fix: migarate to virtua 0.2.0 2024-01-08 08:03:19 +07:00
reya 3eae38e1cb chore: update dependencies 2024-01-07 20:47:48 +07:00
reya 87099c6388 feat: update create account flow 2024-01-07 16:39:05 +07:00
reya 7554a35c31 chore: update config 2024-01-07 07:44:37 +07:00
reya 70707f69c8 feat: update create account screen 2024-01-07 07:42:08 +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
reya 8e8e6fe244 chore: restructure 2024-01-05 07:31:08 +07:00
reya 2726bfd595 feat: support nip 89 2024-01-04 15:10:52 +07:00
reya 542b6033c2 refactor(note): only support kind 1 2024-01-04 12:35:21 +07:00
reya fcde669685 feat(editor): add hot key and update function 2024-01-04 10:44:00 +07:00
reya f4cbcee8b4 feat: add editor 2024-01-04 08:52:45 +07:00
reya ba13ac7535 chore: restructure packages 2024-01-03 11:12:36 +07:00
reya 9f27d68533 chore: clean up 2024-01-03 11:03:56 +07:00
reya 698f5a5d6d feat(columns): add default column 2024-01-02 12:28:48 +07:00
reya 7856d6d49d feat(columns): add antenas column 2024-01-02 09:02:11 +07:00
reya a52fb3c437 feat(columns): add group column 2024-01-01 17:32:57 +07:00
reya 499765c10a chore: update dependencies 2024-01-01 08:19:43 +07:00
reya 56fab1dda6 feat: the last commit of year 2023-12-31 20:53:51 +07:00
reya b1d2496f8e feat(column): add hashtag column 2023-12-30 17:33:04 +07:00
reya ddbbcf41b5 chore: polish some components 2023-12-30 09:02:39 +07:00
reya 55d6318614 refactor(ark): update note component 2023-12-29 14:14:39 +07:00
reya be333260f2 refactor(column): use context for manage column 2023-12-29 13:12:37 +07:00
reya e1edba8a78 chore: update dependencies 2023-12-29 08:26:13 +07:00
reya 4fc3cc8a80 feat(column): add thread and user columns 2023-12-28 11:31:47 +07:00
reya 893f3f7181 feat(icon): update navigation icons 2023-12-28 09:38:59 +07:00
reya 4103b509d4 feat(parser): improve media parser 2023-12-28 08:44:55 +07:00
reya ed538c91c6 feat(columns): update timeline column 2023-12-27 15:01:40 +07:00
reya b4dac2d477 refactor(ark): add note provider 2023-12-27 10:52:13 +07:00
reya 3956ed622d chore: fix build 2023-12-26 15:21:51 +07:00
reya e1db873bd5 refactor(ark): rename widget to column 2023-12-26 13:44:38 +07:00
reya 227c2ddefa chore: monorepo 2023-12-25 14:28:39 +07:00
reya a6da07cd3f refactor: everything 2023-12-24 19:14:46 +07:00
reya 9591d8626d chore: update dependencies 2023-12-23 09:05:52 +07:00
reya ee4e6b1ee6 chore: remove signal 2023-12-22 14:49:00 +07:00
reya a882ead649 chore: update icon 2023-12-22 14:10:23 +07:00
reya 0522611669 feat(icon): update app and tray icons 2023-12-22 10:13:21 +07:00
reya 2536630ff7 update dependencies 2023-12-22 07:52:24 +07:00
reya 4670778181 feat(depot): update screens 2023-12-21 15:29:07 +07:00
reya a6ca2589ab feat(depot): update onboarding screen 2023-12-19 15:43:32 +07:00
reya d9e8d05db7 feat(ui): update ui consistent 2023-12-19 10:13:52 +07:00
reya ec2ac2dce3 feat(ark): add note component to ark 2023-12-19 08:06:10 +07:00
reya 55298515af refactor(widget): migrate widget component to ark lib 2023-12-18 13:39:03 +07:00
reya 344bdc0c66 feat(depot): add setting run depot at launch 2023-12-17 08:07:44 +07:00
reya ba88a4e0f2 feat(ark): update screen 2023-12-16 10:53:16 +07:00
reya 17c64ee357 feat(ark): refactor 2023-12-16 07:47:00 +07:00
reya ba93bdbb91 feat(depot): initial work for depot 2023-12-15 09:15:30 +07:00
reya 591373fd52 fix(layout): fix flickering on home layout 2023-12-14 14:30:49 +07:00
reya 2fcc4dead1 feat(router): restructure 2023-12-14 13:22:03 +07:00
reya d9ab7893e0 feat: update format 2023-12-14 13:11:21 +07:00
Ren AmamiyaandGitHub a93ebd3861 Merge pull request #132 from luminous-devs/feat/universal-titlebar
feat: add window titlebar
2023-12-11 08:57:31 +07:00
reya 7c4ec71089 feat: add window titlebar 2023-12-11 08:56:00 +07:00
Ren AmamiyaandGitHub e9d845cf25 Merge pull request #131 from luminous-devs/wip/cleanup
Clean up
2023-12-11 07:34:24 +07:00
reya 8883be7ed6 upgrade to vite 5 2023-12-10 17:00:04 +07:00
reya 132ea7f887 clean up 2023-12-10 16:53:07 +07:00
Ren AmamiyaandGitHub f9402f5c4f Merge pull request #130 from luminous-devs/feat/ark
Introduction Ark
2023-12-10 11:19:04 +07:00
reya 72a38e3aa7 polish 2023-12-10 08:39:40 +07:00
reya 38e82a4feb prefetch data 2023-12-09 18:11:02 +07:00
reya 6440680898 fix ark 2023-12-09 09:34:20 +07:00
reya e507187044 wip: fully migrate to ark 2023-12-08 12:39:15 +07:00
Ren AmamiyaandGitHub feeb92b6ef Merge pull request #129 from vivganes/patch-2
Grammar touch-up + 1 more tip 🚀
2023-12-08 11:20:04 +07:00
Vivek GanesanandGitHub 2d4a77e8ed Grammar touch-up + 1 more tip 🚀 2023-12-08 08:47:54 +05:30
reya 6f5ea1229d Merge branch 'main' into feat/ark 2023-12-08 09:43:35 +07:00
reya 68886ad584 wip: migrate to ark 2023-12-08 09:32:48 +07:00
reya 5f90bd0d22 update dependencies 2023-12-08 08:18:47 +07:00
reya 8b434d577f fix nip-05 verification 2023-12-08 08:01:06 +07:00
reya f2b1458bd2 bump version & fix using nsecbunker with token 2023-12-07 18:49:55 +07:00
reya 7507cd9ba1 wip: migrate to ark 2023-12-07 18:09:00 +07:00
reya 0d43c13928 bump version 2023-12-07 11:55:49 +07:00
reya 95124e5ded wip: ark 2023-12-07 11:50:25 +07:00
reya a42a2788ea fix nip-05 2023-12-06 10:15:41 +07:00
reya e30274dab3 fix typo 2023-12-06 09:07:32 +07:00
Ren AmamiyaandGitHub 740b7588bc Merge pull request #127 from luminous-devs/hotfix/2.2.1
v2.2.1
2023-12-06 08:09:44 +07:00
reya 24c2ed4eb2 update 2023-12-06 08:08:11 +07:00
reya 4006c0010e polish 2023-12-05 15:31:45 +07:00
reya 7decf264d7 polish nsecbunker 2023-12-05 14:25:44 +07:00
reya 482b218f74 improve error handling for useevent hook 2023-12-05 09:42:08 +07:00
reya e06b760e41 bump version 2023-12-04 14:14:50 +07:00
reya 7efc35f622 replace media chrome with vidstack 2023-12-04 14:02:07 +07:00
reya 8795923443 update 2023-12-04 13:36:16 +07:00
reya 4093821fd0 clean up 2023-12-04 12:47:29 +07:00
reya b19637bdb7 remove misconfigure in react query 2023-12-04 12:17:16 +07:00
reya 21e758ec13 update user component 2023-12-04 11:49:52 +07:00
reya 48ab123850 improve relay screen 2023-12-04 09:26:19 +07:00
reya a401070031 update dependencies 2023-12-04 08:46:42 +07:00
reya e5e4109e79 bump version 2023-12-03 08:38:32 +07:00
Ren AmamiyaandGitHub d62c814f33 Merge pull request #125 from luminous-devs/next
Lume v2.2.0
2023-12-03 08:37:37 +07:00
reya 2a92b7c202 polish 2023-12-03 08:34:44 +07:00
reya 255dcb43fe improve relay form 2023-12-02 17:53:45 +07:00
reya a528b646e3 add finish step to tutorial 2023-12-02 08:33:23 +07:00
reya fc35745c0d wip: tutorial 2023-12-01 15:45:43 +07:00
reya 9ddf3471ce fix nsecbunker 2023-12-01 08:23:46 +07:00
reya 8355ad6863 auto connect user relays 2023-11-30 20:15:54 +07:00
reya 217ac490b1 fix outbox 2023-11-30 19:22:00 +07:00
reya 092cf49227 improve relay connection 2023-11-30 18:19:24 +07:00
reya 5318f6c4cb clean up 2023-11-30 17:24:07 +07:00
reya 80f675cb54 improve notification and performance 2023-11-30 16:02:28 +07:00
reya 6f68c2762b add prefetch data 2023-11-30 10:35:08 +07:00
reya f4390b29e2 revamp onboarding and launching process 2023-11-30 09:38:58 +07:00
reya 00e4f9d357 clean up dependencies 2023-11-28 15:36:57 +07:00
reya d28d183620 Merge branch 'main' into next 2023-11-28 09:07:14 +07:00
reya 3c6c9c86d1 2.1.7 2023-11-28 09:00:46 +07:00
reya bcd079c88e update dependencies 2023-11-28 08:34:21 +07:00
Ren AmamiyaandGitHub d989d6ffad Merge pull request #123 from luminous-devs/feat/v2.1.6
Feat/v2.1.6
2023-11-27 12:01:25 +07:00
reya 5229458746 bump version 2023-11-27 09:56:43 +07:00
reya 2bfa1db816 update 2023-11-27 09:48:51 +07:00
reya 8439428ce1 fix crash on settings screen 2023-11-26 15:01:13 +07:00
reya 34dceef4a3 fix mention popup 2023-11-26 07:48:28 +07:00
Ren AmamiyaandGitHub 619bfb8dff Merge pull request #122 from luminous-devs/v2.1.4
v2.1.4
2023-11-26 07:22:13 +07:00
reya 7759851541 clean up 2023-11-26 07:21:24 +07:00
reya 9112c1c24a improve connection 2023-11-25 17:56:45 +07:00
reya 24b21a9451 update 2023-11-25 16:03:05 +07:00
reya 31a53b9c48 add @ suggestion popup 2023-11-25 15:41:18 +07:00
reya dc229f40cb fix new article layout 2023-11-25 11:07:31 +07:00
reya 54ad1e6e1d fix new post layout 2023-11-25 09:22:15 +07:00
Ren AmamiyaandGitHub 065ccbbea4 Merge pull request #121 from luminous-devs/fix/nsecbunker
Fix stuck issue for connect with nsecbunker
2023-11-24 13:53:26 +07:00
reya 74738c36cd disable blockUntilReady 2023-11-23 15:12:46 +07:00
Ren AmamiyaandGitHub 2fdf437789 Merge pull request #120 from luminous-devs/fix/logout
Fix logout function and other issues
2023-11-23 08:54:24 +07:00
reya 731c72535c bump version 2023-11-23 08:52:47 +07:00
reya 628102087e fix total account count function 2023-11-23 08:52:04 +07:00
reya 536ea30ed2 fix logout function 2023-11-23 08:49:05 +07:00
reya 8ee38cdb42 temp disable single-instance plugin 2023-11-22 17:27:09 +07:00
Ren AmamiyaandGitHub a896300f23 Merge pull request #118 from luminous-devs/v2.1.2
v2.1.2
2023-11-22 16:18:02 +07:00
reya d3cf1200ba bump version 2023-11-22 16:13:06 +07:00
reya b5ac3df090 fix package 2023-11-22 16:10:20 +07:00
reya 3b40dd6903 update dependencies 2023-11-22 15:27:19 +07:00
Ren AmamiyaandGitHub efba6b20ea Merge pull request #117 from luminous-devs/feat/optional-updater
Make auto update is optional
2023-11-22 10:34:49 +07:00
Ren AmamiyaandGitHub 05fb56e5fc Merge pull request #116 from vivganes/patch-1
Little grammar corrections
2023-11-22 10:32:47 +07:00
Vivek GanesanandGitHub 59d9646e9f Little grammar corrections 2023-11-22 08:43:31 +05:30
reya b73d84fccb update storage provider 2023-11-22 09:05:10 +07:00
reya 1929ceb72d add toast message 2023-11-22 08:31:58 +07:00
reya a1d22c1daf make auto update is optional 2023-11-22 08:30:43 +07:00
reya cf7af1ba64 fix ci again again 2023-11-20 08:47:16 +07:00
reya 933ca758ee fix ci again 2023-11-20 08:27:37 +07:00
reya f537209b92 fix ci 2023-11-20 08:16:39 +07:00
reya 6777610b07 update dependencies 2023-11-20 08:02:09 +07:00
reya 88803cd3cd bump version 2023-11-19 19:51:00 +07:00
Ren AmamiyaandGitHub 6adf5933b0 Merge pull request #113 from luminous-devs/hotfix/themes
Add support dark mode for toaster
2023-11-19 19:49:59 +07:00
reya 9521a49fff support dark mode for toaster 2023-11-19 19:49:26 +07:00
Ren AmamiyaandGitHub 5789a105f5 Merge pull request #112 from luminous-devs/hotfix/settings
Fix settings screen
2023-11-19 15:15:11 +07:00
reya b7a18bea34 respect user settings 2023-11-19 14:50:59 +07:00
reya 7117ed05a9 update settings screen 2023-11-19 08:48:01 +07:00
reya c53bdb68e5 add change theme function 2023-11-18 21:17:37 +07:00
reya 6725dca807 fix all dms bugs 2023-11-17 16:03:12 +07:00
Ren AmamiyaandGitHub 077712cf43 Merge pull request #105 from luminous-devs/feat/v2.1.0
v2.1.0
2023-11-17 10:06:58 +07:00
reya 2794c78ee1 add new private key screen 2023-11-17 09:24:21 +07:00
reya 21574023db update dark mode 2023-11-17 08:16:25 +07:00
reya 954b729dc9 wip: settings screen 2023-11-16 17:48:29 +07:00
reya 6dc4e1cde6 bump version 2023-11-16 08:38:23 +07:00
reya efbdf26706 add window state plugin 2023-11-16 08:25:08 +07:00
reya b41ec353c6 improve relay connection 2023-11-16 07:59:29 +07:00
reya 875225591a wip: settings screen 2023-11-15 14:20:45 +07:00
reya 04c1223f2e update article screen 2023-11-15 13:32:23 +07:00
reya 773e49afa2 wip 2023-11-15 13:13:11 +07:00
reya 025d7a623b polish 2023-11-15 10:27:47 +07:00
reya b6caab15e1 update tauri controls 2023-11-15 09:26:59 +07:00
reya 1d3d0a17dc fix updater 2023-11-15 08:32:40 +07:00
reya 1cbe781698 update error screen 2023-11-14 16:14:40 +07:00
reya dc5b4f8ac1 refactor publish event 2023-11-14 15:15:13 +07:00
reya fee4ad7b98 smal fixes and update article layout 2023-11-13 15:44:58 +07:00
reya d5647d7452 redesign loading screen 2023-11-13 08:16:36 +07:00
reya 0a5076f06c polish 2023-11-12 15:43:14 +07:00
reya a3632571ff polish 2023-11-12 08:41:47 +07:00
reya 5c48ebe103 improve spacing 2023-11-11 16:12:50 +07:00
reya 1c3119577f update widget list 2023-11-11 09:14:23 +07:00
reya 0710996a0d wip: update widget list 2023-11-10 16:05:20 +07:00
reya 0cdf199cb5 wip: rework widget 2023-11-09 18:02:25 +07:00
reya cb9006abb2 add topic widget 2023-11-09 09:12:42 +07:00
reya 108ecafab7 update widgets 2023-11-08 16:17:47 +07:00
reya 6b030f2902 polish 2023-11-08 08:21:52 +07:00
reya ce864c8990 wip 2023-11-07 16:23:01 +07:00
reya ee3e8eb105 wip 2023-11-07 09:35:13 +07:00
reya 701712e7b8 polish 2023-11-05 15:19:51 +07:00
reya dad388c6ab new parser 2023-11-04 14:18:29 +07:00
Ren AmamiyaandGitHub 912c740c51 Merge pull request #103 from luminous-devs/feat/personal-dashboard
Add personal dashboard
2023-11-03 15:41:28 +07:00
reya da0b48c5df update personal dashboard screen 2023-11-03 15:38:58 +07:00
reya 64b4745993 update personal screen 2023-11-03 08:45:25 +07:00
reya 8aa2ef39c5 update nip-05 and user profile component styles 2023-11-02 13:47:44 +07:00
reya a945f04959 update dependencies 2023-11-02 13:09:52 +07:00
reya c93989237a fix app crash issue 2023-11-01 16:59:13 +07:00
reya 95cf3f60f3 readd updater pubkey 2023-11-01 16:42:20 +07:00
Ren AmamiyaandGitHub dd21633624 Merge pull request #102 from luminous-devs/feat/v2.0.1
v2.0.1
2023-11-01 15:38:39 +07:00
reya f01074ea9f fix article 2023-11-01 15:37:50 +07:00
reya c8d04f4695 support tiff image 2023-11-01 15:24:46 +07:00
reya 2f8aa66ff6 bump version 2023-11-01 15:08:22 +07:00
reya 3ad6830bfb update title bar 2023-11-01 14:58:20 +07:00
reya f2dddf97f5 polish 2023-11-01 13:01:52 +07:00
reya e218ebee89 refactor text parser 2023-11-01 10:05:08 +07:00
reya fd5ecc18a9 refactor all widgets 2023-11-01 08:07:49 +07:00
reya e7738fb128 refactor newsfeed widget 2023-10-30 16:29:49 +07:00
reya 0b25a4a04b add ndk cache tauri 2023-10-29 11:07:05 +07:00
reya ace58ecdd5 refactor text parser 2023-10-28 14:36:12 +07:00
reya 6685d9af38 update user component 2023-10-28 08:29:38 +07:00
reya 60b803f419 unify upload function 2023-10-28 07:51:42 +07:00
reya 555c8ec08a migrate to tanstack query v5 2023-10-28 07:35:39 +07:00
reya 42eb882f52 update dependencies 2023-10-27 17:45:08 +07:00
reya b6784aa979 add missing deps to gh ci 2023-10-26 13:39:11 +07:00
reya 08a7bdc6d5 fix ci again 2023-10-26 13:30:43 +07:00
reya 6fac37a0ca improve follow/unfollow, support petname 2023-10-26 13:29:13 +07:00
reya 37ed0f4892 fix ci 2023-10-26 13:11:06 +07:00
reya 1cb2d8cb41 small fixes 2023-10-26 12:32:03 +07:00
reya 3abce5e6d6 update appicon and tauri config 2023-10-26 12:24:32 +07:00
reya 842f9e14e0 add updater v2 2023-10-26 10:33:27 +07:00
reya 0c8dcef937 ready for alpha 2023-10-26 09:29:33 +07:00
Ren AmamiyaandGitHub 50f81a7d0b Merge pull request #98 from sectore/feat/flake-nix
Add Nix dev environment
2023-10-25 09:23:45 +07:00
reya dcacf23625 add notification widget 2023-10-25 09:23:20 +07:00
jk b01af39445 typo + cleanup 2023-10-24 17:26:50 +02:00
reya 507628bcaa update ui for consistent in light and dark mode 2023-10-24 21:15:59 +07:00
jk 4dfea49f71 Nix dev environment 2023-10-24 14:57:15 +02:00
reya 854a47f266 wip 2023-10-24 13:11:10 +07:00
reya b1a44f2cbf wip: new composer 2023-10-22 15:48:06 +07:00
reya cade8c8b4c wip: multi-type composer 2023-10-21 15:58:39 +07:00
reya de88ca51fe update 2023-10-20 15:15:30 +07:00
reya 7c8d8a09fd wip: nsecbunker 2023-10-20 09:36:49 +07:00
reya e1e54c1a98 update thread widget 2023-10-19 14:45:41 +07:00
reya 0de72eb009 polish 2023-10-19 08:59:50 +07:00
reya 823fb0f239 bump version 2023-10-18 14:59:33 +07:00
Ren AmamiyaandGitHub 3660db4887 Merge pull request #94 from luminous-devs/feat/v2
v2.0.0
2023-10-18 14:55:05 +07:00
reya b18ae56c36 Merge branch 'main' into feat/v2 2023-10-18 14:53:11 +07:00
reya 939a72f945 ok fine 2023-10-18 14:49:20 +07:00
reya 489ab6bd0b wip: polish 2023-10-18 08:43:31 +07:00
reya 7fa1e89dc8 wip: complete new onboarding 2023-10-17 16:33:41 +07:00
reya 3aa4f294f9 wip: new onboarding 2023-10-16 14:42:19 +07:00
reya cd3b9ada5a wip: new import account 2023-10-15 16:10:16 +07:00
reya 620e763380 wip: new onboarding 2023-10-14 15:19:49 +07:00
reya 0777c483e5 wip: fix ui for macos 2023-10-13 09:12:30 +07:00
reya 893663561d wip: fix light mode 2023-10-13 08:35:07 +07:00
reya 35650a40f2 wip 2023-10-12 09:13:06 +07:00
Phong 3b46e71525 wip: native secure store 2023-10-11 14:46:35 +07:00
Phong 2fcbf1987b rework macos version 2023-10-11 13:45:56 +07:00
Ren Amamiya c3f399ea0b rollback to virtua v0.9.1 2023-10-11 08:04:20 +07:00
Ren Amamiya 770a63de63 wip 2023-10-10 15:49:23 +07:00
Ren Amamiya bc4c3b9803 wip 2023-10-10 11:51:01 +07:00
Ren Amamiya 043c1b1220 wip: update color palette 2023-10-10 08:25:31 +07:00
Ren Amamiya d20ee26e22 wip: add harmony color palette 2023-10-09 19:05:50 +07:00
Ren Amamiya 8930300fb5 wip 2023-10-09 15:17:15 +07:00
Ren Amamiya 140b8a47bf wip: ui 2023-10-09 11:30:52 +07:00
Ren Amamiya ced23341d2 wip: new sidebar 2023-10-08 18:01:19 +07:00
Ren Amamiya 0946e9125e wip: ui 2023-10-08 14:14:39 +07:00
Ren Amamiya bce76bd41c wip: ui 2023-10-08 09:31:11 +07:00
Ren Amamiya cb91373d33 wip: dark mode - light mode 2023-10-07 09:06:33 +07:00
Ren Amamiya c71bfb3f6d wip: fully migrate to tauri v2 2023-10-06 09:08:37 +07:00
reya 9627c40d75 wip: tauri v2 2023-10-06 08:30:59 +07:00
Ren Amamiya 1240353e30 wip 2023-10-06 07:40:50 +07:00
Ren Amamiya cef6b9aca9 wip: new chat layout 2023-10-05 14:55:12 +07:00
Ren Amamiya 508a746578 wip 2023-10-05 07:28:35 +07:00
Ren Amamiya 222ef2ca32 fix: vidstack bug 2023-10-04 20:30:41 +07:00
Ren Amamiya f80dd78a8e wip 2023-10-04 14:11:45 +07:00
Ren Amamiya 480580890e wip: new chat screen 2023-10-04 11:15:10 +07:00
Ren Amamiya ca57ef1760 Merge branch 'main' into feat/nip28 2023-10-04 07:48:52 +07:00
Ren Amamiya 428d52f175 wip 2023-10-03 16:24:09 +07:00
688 changed files with 50162 additions and 23962 deletions
+44
View File
@@ -0,0 +1,44 @@
# Dependencies
**/node_modules
.pnp
.pnp.js
# Local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Testing
coverage
# Turbo
.turbo
# Vercel
.vercel
# Build Outputs
**/.next/
**/out/
**/build
**/dist
**/target
# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Misc
.DS_Store
*.pem
# Unnecessary files
**/.git/
.github/
flatpak/*.xml
flatpak/*.desktop
flatpak/*.yml
+1
View File
@@ -0,0 +1 @@
use flake
-3
View File
@@ -1,3 +0,0 @@
/**/node_modules/*
node_modules/
dist/
-49
View File
@@ -1,49 +0,0 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
settings: {
react: {
version: 'detect',
},
'import/resolver': {
node: {
paths: ['src'],
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
},
},
env: {
browser: true,
amd: true,
node: true,
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:jsx-a11y/recommended',
'prettier'
],
plugins: [],
rules: {
'react/react-in-jsx-scope': 'off',
'jsx-a11y/accessible-emoji': 'off',
'react/prop-types': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'jsx-a11y/anchor-is-valid': [
'error',
{
components: ['Link'],
specialLink: ['hrefLeft', 'hrefRight'],
aspects: ['invalidHref', 'preferButton'],
},
],
},
};
+72
View File
@@ -0,0 +1,72 @@
name: Flatpak
on: workflow_dispatch
jobs:
prepare-repo:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: 'recursive'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: cache of container
id: cache-container
uses: actions/cache@v3
with:
path: prepare-dist
key: ${{ runner.os }}-container-${{ hashFiles('prepare-dist') }}
- name: Run latest-tag
id: latest-tag
uses: oprypin/find-latest-tag@v1
with:
repository:
lumehq/lume
#FIXME: lumehq after merged fix, now it just won't find tags
# repository: ${{ github.repository }}
- name: Build container
# if: steps.cache-container.outputs.cache-hit != 'true'
run: |
docker buildx build -t flatpak-prepare-lume --build-arg=${{steps.latest-tag.outputs.tag}} --rm --output=. --target=final -f flatpak/Containerfile .
- name: Copy flatpak files content
run: |
cp -r flatpak/*.xml flatpak/*.desktop flatpak/*.yml prepare-dist
- uses: actions/upload-artifact@v4
with:
name: repo-dist
path: prepare-dist
flatpak:
name: flatpak-bundle
needs: prepare-repo
runs-on: ubuntu-latest
container:
image: bilelmoussaoui/flatpak-github-actions:gnome-45
options: --privileged
steps:
- uses: actions/download-artifact@v4
with:
name: repo-dist
- uses: actions/checkout@v4
with:
repository: flathub/shared-modules
path: shared-modules
- uses: flatpak/flatpak-github-actions/flatpak-builder@v6
with:
bundle: lume.flatpak
manifest-path: nu.lume.Lume.yml
restore-cache: false
# cache-key: flatpak-builder-${{ github.sha }}
- name: Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
append_body: true
files: lume.flatpak
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: geekyeggo/delete-artifact@v4
with:
name: repo-dist
token: ${{ secrets.GITHUB_TOKEN }}
+41 -38
View File
@@ -1,58 +1,61 @@
name: 'Publish'
name: "Publish"
on: workflow_dispatch
env:
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
RUSTFLAGS: '-W unreachable-pub -W rust-2021-compatibility'
RUSTFLAGS: "-W unreachable-pub -W rust-2021-compatibility"
jobs:
publish-tauri:
strategy:
fail-fast: false
matrix:
settings:
- platform: 'macos-latest'
args: '--target universal-apple-darwin'
- platform: 'ubuntu-22.04'
args: ''
- platform: 'windows-latest'
args: '--target x86_64-pc-windows-msvc'
runs-on: ${{ matrix.settings.platform }}
include:
- platform: "macos-latest" # for Arm based macs (M1 and above).
args: "--target aarch64-apple-darwin"
- platform: "macos-latest" # for Intel based macs.
args: "--target x86_64-apple-darwin"
- platform: "macos-latest" # for Intel based macs.
args: "--target universal-apple-darwin"
#- platform: 'ubuntu-22.04'
# args: ''
#- platform: 'windows-latest'
# args: '--target x86_64-pc-windows-msvc'
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v3
- name: setup node
uses: actions/setup-node@v3
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 18
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin
- name: install dependencies (ubuntu only)
if: matrix.settings.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y build-essential libssl-dev libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev 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 }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
@@ -62,9 +65,9 @@ jobs:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with:
tagName: v__VERSION__
releaseName: 'App 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
+34 -27
View File
@@ -1,31 +1,38 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Dependencies
node_modules
dist
dist-ssr
out
*.local
.next
.vscode
*.db
*.db-journal
bun.lockb
.pnp
.pnp.js
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
# 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
*.log.*
# Misc
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
/.gtm/
*.pem
.vscode/
ndb/
-4
View File
@@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm exec lint-staged
+5
View File
@@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
+5
View File
@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>
+46
View File
@@ -0,0 +1,46 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="CheckEmptyScriptTag" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="CheckValidXmlInScriptTagBody" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="GithubFunctionSignatureValidation" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="GrazieInspection" enabled="false" level="GRAMMAR_ERROR" enabled_by_default="false" />
<inspection_tool class="HtmlExtraClosingTag" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HtmlMissingClosingTag" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="HtmlUnknownAnchorTarget" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HtmlUnknownAttribute" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HtmlUnknownBooleanAttribute" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HtmlUnknownTag" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HtmlUnknownTarget" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HtmlWrongAttributeValue" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HttpClientInappropriateProtocolUsageInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="HttpClientUnresolvedAuthId" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="HttpClientUnresolvedVariable" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HttpRequestContentLengthIsIgnored" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HttpRequestEnvironmentAuthConfigurationValidationInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HttpRequestPlaceholder" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HttpRequestRequestSeparatorJsonBodyInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="HttpRequestRequestSeparatorXmlBodyInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="HttpRequestRequestSeparatorYamlBodyInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="HttpRequestWhitespaceInsideRequestTargetPath" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="IncorrectHttpHeaderInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="LanguageDetectionInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="MandatoryParamsAbsent" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="MarkdownIncorrectTableFormatting" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="MarkdownIncorrectlyNumberedListItem" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="MarkdownLinkDestinationWithSpaces" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="MarkdownNoTableBorders" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="MarkdownOutdatedTableOfContents" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="MarkdownUnresolvedFileReference" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="MarkdownUnresolvedHeaderReference" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="MarkdownUnresolvedLinkLabel" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="RequiredAttributes" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
<inspection_tool class="UndefinedAction" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UndefinedParamsPresent" enabled="false" level="WARNING" enabled_by_default="false" />
</profile>
</component>
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src-tauri/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/.github" />
<excludeFolder url="file://$MODULE_DIR$/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/apps" />
<excludeFolder url="file://$MODULE_DIR$/flatpak" />
<excludeFolder url="file://$MODULE_DIR$/node_modules" />
<excludeFolder url="file://$MODULE_DIR$/packages" />
<excludeFolder url="file://$MODULE_DIR$/src-tauri/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/lume.iml" filepath="$PROJECT_DIR$/.idea/lume.iml" />
</modules>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
-9
View File
@@ -1,9 +0,0 @@
.tmp
.cache/
coverage/
.nyc_output/
**/.yarn/**
**/.pnp.*
/dist*/
node_modules/
src-tauri/
-22
View File
@@ -1,22 +0,0 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"tabWidth": 2,
"printWidth": 90,
"useTabs": false,
"bracketSpacing": true,
"bracketSameLine": false,
"importOrder": [
"^@app/(.*)$",
"^@libs/(.*)$",
"^@shared/(.*)$",
"^@stores/(.*)$",
"^@utils/(.*)$",
"^[./]"
],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"plugins": ["@trivago/prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"],
"pluginSearchDirs": false
}
+35 -10
View File
@@ -1,25 +1,31 @@
### Introduction
## Introduction
Lume is a nostr client
Lume is a Nostr client for desktop include Linux, Windows and macOS. It is free and open source, you can look at source code on Github. Lume is actively improving the app and adding new features, you can expect new update every month.
### Usage
## Usage
Download Lume for your platform here: [https://github.com/luminous-devs/lume/releases](https://github.com/luminous-devs/lume/releases)
Download Lume v4 for your platform here: [https://github.com/lumehq/lume/releases](https://github.com/lumehq/lume/releases)
Supported platform: macOS, Windows and Linux
Supported platform: macOS. Windows and Linux are coming soon.
### Prerequisites
Windows and Linux are availabel on v3 and below.
- PNPM or Bun (experiment)
## Prerequisites
- Tauri: https://tauri.app/v1/guides/getting-started/prerequisites#setting-up-macos
- Node.js >= 18: https://nodejs.org/en
### Develop
- Rust: https://rustup.rs/
- PNPM: https://pnpm.io
- Tauri v2: https://beta.tauri.app/guides/prerequisites/
## Develop
Clone project
```
git clone https://github.com/luminous-devs/lume.git && cd lume
git clone https://github.com/lumehq/lume.git && cd lume
```
Install packages
@@ -39,3 +45,22 @@ 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.
## License
Copyright (C) 2023-2024 Ren Amamiya & other Lume contributors (see AUTHORS.md)
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/.
+26
View File
@@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
src/router.gen.ts
+14
View File
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lume Desktop</title>
</head>
<body
class="relative h-screen w-screen cursor-default select-none overflow-hidden font-sans text-black antialiased dark:text-white"
>
<div id="root" class="h-full w-full"></div>
<script type="module" src="/src/app.tsx"></script>
</body>
</html>
+66
View File
@@ -0,0 +1,66 @@
{
"name": "@lume/desktop2",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@lume/icons": "workspace:^",
"@lume/system": "workspace:^",
"@lume/ui": "workspace:^",
"@lume/utils": "workspace:^",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^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-scroll-area": "^1.1.0",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/query-persist-client-core": "^5.45.0",
"@tanstack/react-query": "^5.45.0",
"@tanstack/react-router": "^1.38.1",
"embla-carousel-react": "^8.1.5",
"i18next": "^23.11.5",
"i18next-resources-to-backend": "^1.2.1",
"minidenticons": "^4.2.1",
"nanoid": "^5.0.7",
"nostr-tools": "^2.7.0",
"react": "^18.3.1",
"react-currency-input-field": "^3.8.0",
"react-dom": "^18.3.1",
"react-hook-form": "^7.52.0",
"react-hotkeys-hook": "^4.5.0",
"react-i18next": "^14.1.2",
"react-string-replace": "^1.1.1",
"slate": "^0.103.0",
"slate-react": "^0.105.0",
"sonner": "^1.5.0",
"use-debounce": "^10.0.1",
"virtua": "^0.31.0"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@tanstack/router-devtools": "^1.38.1",
"@tanstack/router-vite-plugin": "^1.38.0",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.7.0",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.4",
"typescript": "^5.4.5",
"vite": "^5.3.1",
"vite-plugin-top-level-await": "^1.4.1",
"vite-tsconfig-paths": "^4.3.2"
}
}
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

+110
View File
@@ -0,0 +1,110 @@
@tailwind base;
@tailwind utilities;
@tailwind components;
@layer utilities {
.content-break {
word-break: break-word;
word-wrap: break-word;
overflow-wrap: break-word;
}
.shadow-toolbar {
box-shadow:
0 0 #0000,
0 0 #0000,
0 8px 24px 0 rgba(0, 0, 0, 0.2),
0 2px 8px 0 rgba(0, 0, 0, 0.08),
inset 0 0 0 1px rgba(0, 0, 0, 0.2),
inset 0 0 0 2px hsla(0, 0%, 100%, 0.14);
}
.shadow-primary {
box-shadow: 0px 0px 4px rgba(66, 65, 73, 0.14);
}
}
/*
Overide some default styles
*/
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;
}
.spinner-leaf {
position: absolute;
top: 0;
left: calc(50% - 12.5% / 2);
width: 12.5%;
height: 100%;
animation: spinner-leaf-fade 800ms linear infinite;
&::before {
content: "";
display: block;
width: 100%;
height: 30%;
background-color: currentColor;
@apply rounded;
}
&:where(:nth-child(1)) {
transform: rotate(0deg);
animation-delay: -800ms;
}
&:where(:nth-child(2)) {
transform: rotate(45deg);
animation-delay: -700ms;
}
&:where(:nth-child(3)) {
transform: rotate(90deg);
animation-delay: -600ms;
}
&:where(:nth-child(4)) {
transform: rotate(135deg);
animation-delay: -500ms;
}
&:where(:nth-child(5)) {
transform: rotate(180deg);
animation-delay: -400ms;
}
&:where(:nth-child(6)) {
transform: rotate(225deg);
animation-delay: -300ms;
}
&:where(:nth-child(7)) {
transform: rotate(270deg);
animation-delay: -200ms;
}
&:where(:nth-child(8)) {
transform: rotate(315deg);
animation-delay: -100ms;
}
}
@keyframes spinner-leaf-fade {
from {
opacity: 1;
}
to {
opacity: 0.25;
}
}
+53
View File
@@ -0,0 +1,53 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { RouterProvider, createRouter } from "@tanstack/react-router";
import React, { StrictMode } from "react";
import ReactDOM from "react-dom/client";
import { I18nextProvider } from "react-i18next";
import "./app.css";
import { type } from "@tauri-apps/plugin-os";
import i18n from "./locale";
import { routeTree } from "./router.gen"; // auto generated file
const queryClient = new QueryClient();
const os = await type();
// Set up a Router instance
const router = createRouter({
routeTree,
context: {
queryClient,
platform: os,
},
Wrap: ({ children }) => {
return (
<I18nextProvider i18n={i18n} defaultNS={"translation"}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</I18nextProvider>
);
},
});
// Register things for typesafety
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
function App() {
return <RouterProvider router={router} />;
}
// biome-ignore lint/style/noNonNullAssertion: idk
const rootElement = document.getElementById("root")!;
if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement);
root.render(
<StrictMode>
<App />
</StrictMode>,
);
}
@@ -0,0 +1,43 @@
import { NostrQuery } from "@lume/system";
import { Spinner } from "@lume/ui";
import { cn } from "@lume/utils";
import {
type Dispatch,
type ReactNode,
type SetStateAction,
useState,
} from "react";
import { toast } from "sonner";
export function AvatarUploader({
setPicture,
children,
className,
}: {
setPicture: Dispatch<SetStateAction<string>>;
children: ReactNode;
className?: string;
}) {
const [loading, setLoading] = useState(false);
const uploadAvatar = async () => {
try {
setLoading(true);
const image = await NostrQuery.upload();
setPicture(image);
} catch (e) {
setLoading(false);
toast.error(String(e));
}
};
return (
<button
type="button"
onClick={() => uploadAvatar()}
className={cn("size-4", className)}
>
{loading ? <Spinner className="size-4" /> : children}
</button>
);
}
+41
View File
@@ -0,0 +1,41 @@
import { User } from "@/components/user";
import { NostrAccount } from "@lume/system";
import { getBitcoinDisplayValues } from "@lume/utils";
import { useEffect, useMemo, useState } from "react";
export function Balance({ account }: { account: string }) {
const [balance, setBalance] = useState(0);
const value = useMemo(() => getBitcoinDisplayValues(balance), [balance]);
useEffect(() => {
async function getBalance() {
const val = await NostrAccount.getBalance();
setBalance(val);
}
getBalance();
}, []);
return (
<div
data-tauri-drag-region
className="flex h-16 items-center justify-end px-3"
>
<div className="flex items-center gap-2">
<div className="text-end">
<div className="text-sm leading-tight text-neutral-700 dark:text-neutral-300">
Your balance
</div>
<div className="font-medium leading-tight">
{value.bitcoinFormatted}
</div>
</div>
<User.Provider pubkey={account}>
<User.Root>
<User.Avatar className="size-9 rounded-full" />
</User.Root>
</User.Provider>
</div>
</div>
);
}
+159
View File
@@ -0,0 +1,159 @@
import { CancelIcon, CheckIcon } from "@lume/icons";
import type { LumeColumn } from "@lume/types";
import { cn } from "@lume/utils";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { getCurrent } from "@tauri-apps/api/webviewWindow";
import { useCallback, useEffect, useRef, useState } from "react";
type WindowEvent = {
scroll: boolean;
resize: boolean;
};
export function Column({
column,
account,
}: {
column: LumeColumn;
account: string;
}) {
const container = useRef<HTMLDivElement>(null);
const webviewLabel = `column-${account}_${column.label}`;
const [isCreated, setIsCreated] = useState(false);
const repositionWebview = useCallback(async () => {
const newRect = container.current.getBoundingClientRect();
await invoke("reposition_column", {
label: webviewLabel,
x: newRect.x,
y: newRect.y,
});
}, []);
const resizeWebview = useCallback(async () => {
const newRect = container.current.getBoundingClientRect();
await invoke("resize_column", {
label: webviewLabel,
width: newRect.width,
height: newRect.height,
});
}, []);
useEffect(() => {
if (!isCreated) return;
const unlisten = listen<WindowEvent>("child-webview", (data) => {
if (data.payload.scroll) repositionWebview();
if (data.payload.resize) repositionWebview().then(() => resizeWebview());
});
return () => {
unlisten.then((f) => f());
};
}, [isCreated]);
useEffect(() => {
if (!container?.current) return;
const rect = container.current.getBoundingClientRect();
const url = `${column.content}?account=${account}&label=${column.label}&name=${column.name}`;
// create new webview
invoke("create_column", {
label: webviewLabel,
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
url,
}).then(() => {
console.log("created: ", webviewLabel);
setIsCreated(true);
});
// close webview when unmounted
return () => {
invoke("close_column", { label: webviewLabel }).then(() => {
console.log("closed: ", webviewLabel);
});
};
}, [account]);
return (
<div className="h-full w-[500px] shrink-0 p-2">
<div
className={cn(
"flex flex-col w-full h-full rounded-xl",
column.label !== "open"
? "bg-black/5 dark:bg-white/5 backdrop-blur-sm"
: "",
)}
>
<Header label={column.label} name={column.name} />
<div ref={container} className="flex-1 w-full h-full" />
</div>
</div>
);
}
function Header({ label, name }: { label: string; name: string }) {
const [title, setTitle] = useState(name);
const [isChanged, setIsChanged] = useState(false);
const saveNewTitle = async () => {
const mainWindow = getCurrent();
await mainWindow.emit("columns", { type: "set_title", label, title });
// update search params
// @ts-ignore, hahaha
search.name = title;
// reset state
setIsChanged(false);
};
const close = async () => {
const mainWindow = getCurrent();
await mainWindow.emit("columns", { type: "remove", label });
};
useEffect(() => {
if (title.length !== name.length) setIsChanged(true);
}, [title]);
return (
<div className="flex items-center justify-between w-full px-1 h-9 shrink-0">
<div className="size-7" />
<div className="flex items-center justify-center shrink-0 h-9">
<div className="relative flex items-center gap-2">
<div
contentEditable
suppressContentEditableWarning={true}
onBlur={(e) => setTitle(e.currentTarget.textContent)}
className="text-sm font-medium focus:outline-none"
>
{name}
</div>
{isChanged ? (
<button
type="button"
onClick={() => saveNewTitle()}
className="text-teal-500 hover:text-teal-600"
>
<CheckIcon className="size-4" />
</button>
) : null}
</div>
</div>
<button
type="button"
onClick={() => close()}
className="inline-flex items-center justify-center rounded-lg size-7 hover:bg-black/10 dark:hover:bg-white/10 text-neutral-600 dark:text-neutral-400 hover:text-neutral-800 dark:hover:text-neutral-200"
>
<CancelIcon className="size-4" />
</button>
</div>
);
}
@@ -0,0 +1,47 @@
import { ThreadIcon } from "@lume/icons";
import { Note } from "@/components/note";
import { cn } from "@lume/utils";
import type { LumeEvent } from "@lume/system";
import { useMemo } from "react";
export function Conversation({
event,
className,
}: {
event: LumeEvent;
className?: string;
}) {
const thread = useMemo(() => event.thread, [event]);
return (
<Note.Provider event={event}>
<Note.Root
className={cn(
"bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl flex flex-col gap-3 shadow-primary dark:ring-1 ring-neutral-800/50",
className,
)}
>
<div className="flex flex-col gap-3">
{thread?.root?.id ? <Note.Child event={thread?.root} isRoot /> : null}
<div className="flex items-center gap-2 px-3">
<div className="inline-flex items-center gap-1.5 shrink-0 text-sm font-medium text-neutral-600 dark:text-neutral-400">
<ThreadIcon className="size-4" />
Thread
</div>
<div className="flex-1 h-px bg-neutral-100 dark:bg-white/5" />
</div>
{thread?.reply?.id ? <Note.Child event={thread?.reply} /> : null}
<div>
<div className="flex items-center justify-between px-3 h-14">
<Note.User />
</div>
<Note.Content className="px-3" />
</div>
</div>
<div className="flex items-center px-3 h-14">
<Note.Open />
</div>
</Note.Root>
</Note.Provider>
);
}
@@ -0,0 +1,30 @@
import { VisitIcon } from "@lume/icons";
import * as Tooltip from "@radix-ui/react-tooltip";
import { useNoteContext } from "../provider";
import { LumeWindow } from "@lume/system";
export function NoteOpenThread() {
const event = useNoteContext();
return (
<Tooltip.Provider>
<Tooltip.Root delayDuration={150}>
<Tooltip.Trigger asChild>
<button
type="button"
onClick={() => LumeWindow.openEvent(event)}
className="group inline-flex h-7 w-14 bg-neutral-100 dark:bg-white/10 rounded-full items-center justify-center text-sm font-medium text-neutral-800 dark:text-neutral-200 hover:text-blue-500 hover:bg-neutral-200 dark:hover:bg-white/20"
>
<VisitIcon className="shrink-0 size-4" />
</button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
Open
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
);
}
@@ -0,0 +1,37 @@
import { ReplyIcon } from "@lume/icons";
import * as Tooltip from "@radix-ui/react-tooltip";
import { useNoteContext } from "../provider";
import { cn } from "@lume/utils";
import { LumeWindow } from "@lume/system";
export function NoteReply({ large = false }: { large?: boolean }) {
const event = useNoteContext();
return (
<Tooltip.Provider>
<Tooltip.Root delayDuration={150}>
<Tooltip.Trigger asChild>
<button
type="button"
onClick={() => LumeWindow.openEditor(event.id)}
className={cn(
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
large
? "rounded-full bg-neutral-100 dark:bg-white/10 h-7 gap-1.5 w-24 text-sm font-medium hover:text-blue-500 hover:bg-neutral-200 dark:hover:bg-white/20"
: "size-7",
)}
>
<ReplyIcon className="shrink-0 size-4" />
{large ? "Reply" : null}
</button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="inline-flex h-7 select-none items-center justify-center rounded-md bg-neutral-950 px-3.5 text-sm text-neutral-50 will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade dark:bg-neutral-50 dark:text-neutral-950">
Reply
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
);
}
@@ -0,0 +1,81 @@
import { RepostIcon } from "@lume/icons";
import { LumeWindow } from "@lume/system";
import { Spinner } from "@lume/ui";
import { cn } from "@lume/utils";
import { useRouteContext } from "@tanstack/react-router";
import { Menu, MenuItem } from "@tauri-apps/api/menu";
import { useCallback, useState } from "react";
import { toast } from "sonner";
import { useNoteContext } from "../provider";
export function NoteRepost({ large = false }: { large?: boolean }) {
const event = useNoteContext();
const { settings } = useRouteContext({ strict: false });
const [loading, setLoading] = useState(false);
const [isRepost, setIsRepost] = useState(false);
const repost = async () => {
if (isRepost) return;
try {
setLoading(true);
// repost
await event.repost();
// update state
setLoading(false);
setIsRepost(true);
// notify
toast.success("You've reposted this post successfully");
} catch {
setLoading(false);
toast.error("Repost failed, try again later");
}
};
const showContextMenu = useCallback(async (e: React.MouseEvent) => {
e.preventDefault();
const menuItems = await Promise.all([
MenuItem.new({
text: "Quote",
action: async () => repost(),
}),
MenuItem.new({
text: "Repost",
action: () => LumeWindow.openEditor(null, event.id),
}),
]);
const menu = await Menu.new({
items: menuItems,
});
await menu.popup().catch((e) => console.error(e));
}, []);
if (!settings.display_repost_button) return null;
return (
<button
type="button"
onClick={(e) => showContextMenu(e)}
className={cn(
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200 rounded-full",
large
? "bg-neutral-100 dark:bg-white/10 h-7 gap-1.5 w-24 text-sm font-medium hover:text-blue-500 hover:bg-neutral-200 dark:hover:bg-white/20"
: "size-7",
)}
>
{loading ? (
<Spinner className="size-4" />
) : (
<RepostIcon className={cn("size-4", isRepost ? "text-blue-500" : "")} />
)}
{large ? "Repost" : null}
</button>
);
}
@@ -0,0 +1,28 @@
import { ZapIcon } from "@lume/icons";
import { LumeWindow } from "@lume/system";
import { cn } from "@lume/utils";
import { useRouteContext } from "@tanstack/react-router";
import { useNoteContext } from "../provider";
export function NoteZap({ large = false }: { large?: boolean }) {
const event = useNoteContext();
const { settings } = useRouteContext({ strict: false });
if (!settings.display_zap_button) return null;
return (
<button
type="button"
onClick={() => LumeWindow.openZap(event.id, event.pubkey)}
className={cn(
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
large
? "rounded-full bg-neutral-100 dark:bg-white/10 h-7 gap-1.5 w-24 text-sm font-medium hover:text-blue-500 hover:bg-neutral-200 dark:hover:bg-white/20"
: "size-7",
)}
>
<ZapIcon className="size-4" />
{large ? "Zap" : null}
</button>
);
}
@@ -0,0 +1,48 @@
import { useEvent } from "@lume/system";
import { cn } from "@lume/utils";
import { Note } from ".";
import { InfoIcon } from "@lume/icons";
import type { EventTag } from "@lume/types";
export function NoteChild({
event,
isRoot,
}: {
event: EventTag;
isRoot?: boolean;
}) {
const { isLoading, isError, data } = useEvent(event.id, event.relayHint);
if (isLoading) {
return (
<div className="flex items-center gap-2 px-3 pt-3">
<div className="rounded-full size-8 shrink-0 bg-neutral-200 dark:bg-neutral-800 animate-pulse" />
<div className="w-2/3 h-4 rounded-md animate-pulse bg-neutral-200 dark:bg-neutral-800" />
</div>
);
}
if (isError || !data) {
return (
<div className="flex items-center gap-2 px-3 pt-3">
<div className="inline-flex items-center justify-center text-white bg-red-500 rounded-full size-8 shrink-0">
<InfoIcon className="size-5" />
</div>
<p className="text-sm text-red-500">
Event not found with your current relay set
</p>
</div>
);
}
return (
<Note.Provider event={data}>
<Note.Root className={cn(isRoot ? "mb-3" : "")}>
<div className="flex items-center justify-between px-3 h-14">
<Note.User />
</div>
<Note.Content className="px-3" />
</Note.Root>
</Note.Provider>
);
}
@@ -0,0 +1,115 @@
import { cn } from "@lume/utils";
import { useRouteContext } from "@tanstack/react-router";
import { nanoid } from "nanoid";
import { type ReactNode, useMemo } from "react";
import reactStringReplace from "react-string-replace";
import { Hashtag } from "./mentions/hashtag";
import { MentionNote } from "./mentions/note";
import { MentionUser } from "./mentions/user";
import { Images } from "./preview/images";
import { Videos } from "./preview/videos";
import { useNoteContext } from "./provider";
export function NoteContent({
quote = true,
mention = true,
clean,
className,
}: {
quote?: boolean;
mention?: boolean;
clean?: boolean;
className?: string;
}) {
const { settings } = useRouteContext({ strict: false });
const event = useNoteContext();
const content = useMemo(() => {
try {
// Get parsed meta
const { content, hashtags, events, mentions } = event.meta;
// Define rich content
let richContent: ReactNode[] | string = settings.display_media
? content
: event.content;
for (const hashtag of hashtags) {
const regex = new RegExp(`(|^)${hashtag}\\b`, "g");
richContent = reactStringReplace(richContent, regex, (_, index) => {
return <Hashtag key={hashtag + index} tag={hashtag} />;
});
}
for (const event of events) {
if (quote) {
richContent = reactStringReplace(richContent, event, (_, index) => (
<MentionNote key={event + index} eventId={event} />
));
}
if (!quote && clean) {
richContent = reactStringReplace(richContent, event, () => null);
}
}
for (const user of mentions) {
if (mention) {
richContent = reactStringReplace(richContent, user, (_, index) => (
<MentionUser key={user + index} pubkey={user} />
));
}
if (!mention && clean) {
richContent = reactStringReplace(richContent, user, () => null);
}
}
richContent = reactStringReplace(
richContent,
/(https?:\/\/\S+)/gi,
(match, index) => (
<a
key={match + index}
href={match}
target="_blank"
rel="noreferrer"
className="inline text-blue-500 hover:text-blue-600"
>
{match}
</a>
),
);
richContent = reactStringReplace(richContent, /(\r\n|\r|\n)+/g, () => (
<div key={nanoid()} className="h-3" />
));
return richContent;
} catch (e) {
console.log("[parser]: ", e);
return event.content;
}
}, [event.content]);
return (
<div className="flex flex-col gap-2">
<div
className={cn(
"select-text text-pretty content-break overflow-hidden",
event.meta?.content.length > 400
? "max-h-[250px] gradient-mask-b-0"
: "",
className,
)}
>
{content}
</div>
{settings.display_media && event.meta?.images.length ? (
<Images urls={event.meta.images} />
) : null}
{settings.display_media && event.meta?.videos.length ? (
<Videos urls={event.meta.videos} />
) : null}
</div>
);
}
@@ -0,0 +1,94 @@
import { cn } from "@lume/utils";
import { nanoid } from "nanoid";
import { type ReactNode, useMemo } from "react";
import reactStringReplace from "react-string-replace";
import { Hashtag } from "./mentions/hashtag";
import { MentionNote } from "./mentions/note";
import { MentionUser } from "./mentions/user";
import { ImagePreview } from "./preview/image";
import { VideoPreview } from "./preview/video";
import { useNoteContext } from "./provider";
export function NoteContentLarge({
className,
}: {
className?: string;
}) {
const event = useNoteContext();
const content = useMemo(() => {
try {
// Get parsed meta
const { images, videos, hashtags, events, mentions } = event.meta;
// Define rich content
let richContent: ReactNode[] | string = event.content;
for (const hashtag of hashtags) {
const regex = new RegExp(`(|^)${hashtag}\\b`, "g");
richContent = reactStringReplace(richContent, regex, () => (
<Hashtag key={nanoid()} tag={hashtag} />
));
}
for (const event of events) {
richContent = reactStringReplace(richContent, event, (match, i) => (
<MentionNote key={match + i} eventId={event} />
));
}
for (const mention of mentions) {
richContent = reactStringReplace(richContent, mention, (match, i) => (
<MentionUser key={match + i} pubkey={mention} />
));
}
for (const image of images) {
richContent = reactStringReplace(richContent, image, (match, i) => (
<ImagePreview key={match + i} url={match} />
));
}
for (const video of videos) {
richContent = reactStringReplace(richContent, video, (match, i) => (
<VideoPreview key={match + i} url={match} />
));
}
richContent = reactStringReplace(
richContent,
/(https?:\/\/\S+)/gi,
(match, i) => (
<a
key={match + i}
href={match}
target="_blank"
rel="noreferrer"
className="text-blue-500 line-clamp-1 hover:text-blue-600"
>
{match}
</a>
),
);
richContent = reactStringReplace(richContent, /(\r\n|\r|\n)+/g, () => (
<div key={nanoid()} className="h-3" />
));
return richContent;
} catch (e) {
console.log("[parser]: ", e);
return event.content;
}
}, [event.content]);
return (
<div
className={cn(
"select-text leading-normal text-pretty content-break",
className,
)}
>
{content}
</div>
);
}
@@ -0,0 +1,25 @@
import { NoteOpenThread } from "./buttons/open";
import { NoteReply } from "./buttons/reply";
import { NoteRepost } from "./buttons/repost";
import { NoteZap } from "./buttons/zap";
import { NoteChild } from "./child";
import { NoteContent } from "./content";
import { NoteContentLarge } from "./contentLarge";
import { NoteMenu } from "./menu";
import { NoteProvider } from "./provider";
import { NoteRoot } from "./root";
import { NoteUser } from "./user";
export const Note = {
Provider: NoteProvider,
Root: NoteRoot,
User: NoteUser,
Menu: NoteMenu,
Reply: NoteReply,
Repost: NoteRepost,
Content: NoteContent,
ContentLarge: NoteContentLarge,
Zap: NoteZap,
Open: NoteOpenThread,
Child: NoteChild,
};
@@ -0,0 +1,10 @@
export function Hashtag({ tag }: { tag: string }) {
return (
<span className="leading-normal break-all cursor-default group text-start">
<span className="text-blue-500">#</span>
<span className="underline underline-offset-1 decoration-2 decoration-blue-200 dark:decoration-blue-800 group-hover:decoration-blue-500">
{tag.replace("#", "")}
</span>
</span>
);
}
@@ -0,0 +1,76 @@
import { LumeWindow, useEvent } from "@lume/system";
import { LinkIcon } from "@lume/icons";
import { useTranslation } from "react-i18next";
import { cn } from "@lume/utils";
import { User } from "@/components/user";
import { Spinner } from "@lume/ui";
export function MentionNote({
eventId,
openable = true,
}: {
eventId: string;
openable?: boolean;
}) {
const { t } = useTranslation();
const { isLoading, isError, data } = useEvent(eventId);
if (isLoading) {
return (
<div className="flex items-center justify-center w-full h-20 mt-2 border rounded-xl border-black/10 dark:border-white/10">
<Spinner className="size-5" />
</div>
);
}
if (isError || !data) {
return (
<div className="w-full p-3 mt-2 border rounded-xl border-black/10 dark:border-white/10">
{t("note.error")}
</div>
);
}
return (
<div className="flex flex-col w-full border rounded-lg cursor-default border-black/10 dark:border-white/10">
<User.Provider pubkey={data.pubkey}>
<User.Root className="flex items-center gap-2 px-3 h-11">
<User.Avatar className="object-cover rounded-full size-6 shrink-0" />
<div className="inline-flex items-center flex-1 gap-2">
<User.Name className="font-semibold text-neutral-900 dark:text-neutral-100" />
<span className="text-neutral-600 dark:text-neutral-400">·</span>
<User.Time
time={data.created_at}
className="text-neutral-600 dark:text-neutral-400"
/>
</div>
</User.Root>
</User.Provider>
<div
className={cn(
"px-3 select-text whitespace-normal text-pretty content-break leading-normal",
data.content.length > 400 ? "max-h-[150px] gradient-mask-b-0" : "",
)}
>
{data.content}
</div>
{openable ? (
<div className="flex items-center justify-end px-2 h-11">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
LumeWindow.openEvent(data);
}}
className="z-10 inline-flex items-center justify-center gap-1 text-sm rounded-full h-7 w-28 bg-black/10 dark:bg-white/10 text-neutral-600 hover:text-blue-500 dark:text-neutral-400"
>
View post
<LinkIcon className="size-4" />
</button>
</div>
) : (
<div className="h-3" />
)}
</div>
);
}
@@ -0,0 +1,20 @@
import { LumeWindow, useProfile } from "@lume/system";
import { displayNpub } from "@lume/utils";
export function MentionUser({ pubkey }: { pubkey: string }) {
const { isLoading, isError, profile } = useProfile(pubkey);
return (
<button
type="button"
onClick={() => LumeWindow.openProfile(pubkey)}
className="break-words text-start text-blue-500 hover:text-blue-600"
>
{isLoading
? "@anon"
: isError
? displayNpub(pubkey, 16)
: `@${profile?.name || profile?.display_name || "anon"}`}
</button>
);
}
@@ -0,0 +1,62 @@
import { HorizontalDotsIcon } from "@lume/icons";
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import { useNoteContext } from "./provider";
import { useCallback } from "react";
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
export function NoteMenu() {
const event = useNoteContext();
const showContextMenu = useCallback(async (e: React.MouseEvent) => {
e.preventDefault();
const menuItems = await Promise.all([
MenuItem.new({
text: "Copy Sharable Link",
action: async () => {
const eventId = await event.idAsBech32();
await writeText(`https://njump.me/${eventId}`);
},
}),
MenuItem.new({
text: "Copy Event ID",
action: async () => {
const eventId = await event.idAsBech32();
await writeText(eventId);
},
}),
MenuItem.new({
text: "Copy Public Key",
action: async () => {
const pubkey = await event.pubkeyAsBech32();
await writeText(pubkey);
},
}),
PredefinedMenuItem.new({ item: "Separator" }),
MenuItem.new({
text: "Copy Raw Event",
action: async () => {
event.meta = undefined;
const raw = JSON.stringify(event);
await writeText(raw);
},
}),
]);
const menu = await Menu.new({
items: menuItems,
});
await menu.popup().catch((e) => console.error(e));
}, []);
return (
<button
type="button"
onClick={(e) => showContextMenu(e)}
className="inline-flex items-center justify-center group size-7 text-neutral-600 dark:text-neutral-400"
>
<HorizontalDotsIcon className="size-5" />
</button>
);
}
@@ -0,0 +1,48 @@
import { useRouteContext } from "@tanstack/react-router";
import { open } from "@tauri-apps/plugin-shell";
import { useMemo } from "react";
export function ImagePreview({ url }: { url: string }) {
const { settings } = useRouteContext({ strict: false });
const imageUrl = useMemo(() => {
if (settings.image_resize_service.length) {
const newUrl = `${settings.image_resize_service}?url=${url}&ll&af&default=1&n=-1`;
return newUrl;
} else {
return url;
}
}, [settings.image_resize_service]);
if (!settings.display_media) {
return (
<a
href={url}
target="_blank"
rel="noreferrer"
className="inline text-blue-500 hover:text-blue-600"
>
{url}
</a>
);
}
return (
<div className="my-1">
<img
src={imageUrl}
alt={url}
loading="lazy"
decoding="async"
style={{ contentVisibility: "auto" }}
className="max-h-[600px] w-auto object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
onClick={() => open(url)}
onKeyDown={() => open(url)}
onError={({ currentTarget }) => {
currentTarget.onerror = null;
currentTarget.src = "/404.jpg";
}}
/>
</div>
);
}
@@ -0,0 +1,65 @@
import { Carousel, CarouselItem } from "@lume/ui";
import { useRouteContext } from "@tanstack/react-router";
import { open } from "@tauri-apps/plugin-shell";
import { useMemo } from "react";
export function Images({ urls }: { urls: string[] }) {
const { settings } = useRouteContext({ strict: false });
const imageUrls = useMemo(() => {
if (settings.image_resize_service.length) {
const newUrls = urls.map(
(url) =>
`${settings.image_resize_service}?url=${url}&ll&af&default=1&n=-1`,
);
return newUrls;
} else {
return urls;
}
}, [settings.image_resize_service]);
if (urls.length === 1) {
return (
<div className="px-3 group">
<img
src={imageUrls[0]}
alt={urls[0]}
loading="lazy"
decoding="async"
style={{ contentVisibility: "auto" }}
className="max-h-[400px] w-auto object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
onClick={() => urls[0]}
onKeyDown={() => urls[0]}
onError={({ currentTarget }) => {
currentTarget.onerror = null;
currentTarget.src = "/404.jpg";
}}
/>
</div>
);
}
return (
<Carousel
items={imageUrls}
renderItem={({ item, index, isSnapPoint }) => (
<CarouselItem key={item + index} isSnapPoint={isSnapPoint}>
<img
src={item}
alt={item}
loading="lazy"
decoding="async"
style={{ contentVisibility: "auto" }}
className="object-cover w-full h-full rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
onClick={() => open(item)}
onKeyDown={() => open(item)}
onError={({ currentTarget }) => {
currentTarget.onerror = null;
currentTarget.src = "/404.jpg";
}}
/>
</CarouselItem>
)}
/>
);
}
@@ -0,0 +1,31 @@
import { useRouteContext } from "@tanstack/react-router";
export function VideoPreview({ url }: { url: string }) {
const { settings } = useRouteContext({ strict: false });
if (settings.display_media) {
return (
<a
href={url}
target="_blank"
rel="noreferrer"
className="inline text-blue-500 hover:text-blue-600"
>
{url}
</a>
);
}
return (
<div className="my-1">
<video
className="max-h-[600px] w-auto object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
controls
muted
>
<source src={url} type="video/mp4" />
Your browser does not support the video tag.
</video>
</div>
);
}
@@ -0,0 +1,36 @@
import { Carousel, CarouselItem } from "@lume/ui";
export function Videos({ urls }: { urls: string[] }) {
if (urls.length === 1) {
return (
<div className="group px-3">
<video
className="w-full h-auto object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
controls
muted
>
<source src={urls[0]} type="video/mp4" />
Your browser does not support the video tag.
</video>
</div>
);
}
return (
<Carousel
items={urls}
renderItem={({ item, isSnapPoint }) => (
<CarouselItem key={item} isSnapPoint={isSnapPoint}>
<video
className="w-full h-full object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
controls={false}
muted
>
<source src={item} type="video/mp4" />
Your browser does not support the video tag.
</video>
</CarouselItem>
)}
/>
);
}
@@ -0,0 +1,22 @@
import type { LumeEvent } from "@lume/system";
import { type ReactNode, createContext, useContext } from "react";
const NoteContext = createContext<LumeEvent>(null);
export function NoteProvider({
event,
children,
}: {
event: LumeEvent;
children: ReactNode;
}) {
return <NoteContext.Provider value={event}>{children}</NoteContext.Provider>;
}
export function useNoteContext() {
const context = useContext(NoteContext);
if (!context) {
throw new Error("Please import Note Provider to use useNoteContext() hook");
}
return context;
}
@@ -0,0 +1,16 @@
import { cn } from "@lume/utils";
import type { ReactNode } from "react";
export function NoteRoot({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return (
<div className={cn("h-min w-full", className)} contentEditable={false}>
{children}
</div>
);
}
@@ -0,0 +1,62 @@
import { cn } from "@lume/utils";
import * as HoverCard from "@radix-ui/react-hover-card";
import { User } from "../user";
import { useNoteContext } from "./provider";
import { LumeWindow } from "@lume/system";
export function NoteUser({ className }: { className?: string }) {
const event = useNoteContext();
return (
<User.Provider pubkey={event.pubkey}>
<HoverCard.Root>
<User.Root
className={cn("flex items-start justify-between", className)}
>
<div className="flex w-full gap-2">
<HoverCard.Trigger className="shrink-0">
<User.Avatar className="object-cover rounded-full size-8 outline outline-1 -outline-offset-1 outline-black/15" />
</HoverCard.Trigger>
<div className="flex items-center w-full gap-3">
<div className="flex items-center gap-1">
<User.Name className="font-semibold text-neutral-950 dark:text-neutral-50" />
<User.NIP05 />
</div>
<div className="text-neutral-600 dark:text-neutral-400">·</div>
<User.Time
time={event.created_at}
className="text-neutral-600 dark:text-neutral-400"
/>
</div>
</div>
</User.Root>
<HoverCard.Portal>
<HoverCard.Content
className="w-[300px] rounded-xl bg-black p-3 data-[side=bottom]:animate-slideUpAndFade data-[state=open]:transition-all dark:bg-white dark:shadow-none"
sideOffset={5}
side="right"
>
<div className="flex flex-col gap-2">
<User.Avatar className="object-cover rounded-lg size-11" />
<div className="flex flex-col gap-2">
<div className="inline-flex items-center gap-1">
<User.Name className="font-semibold leading-tight text-white dark:text-neutral-900" />
<User.NIP05 />
</div>
<User.About className="text-sm text-white line-clamp-3 dark:text-neutral-900" />
<button
type="button"
onClick={() => LumeWindow.openProfile(event.pubkey)}
className="inline-flex items-center justify-center w-full mt-2 text-sm font-medium bg-white rounded-lg h-9 hover:bg-neutral-200 dark:bg-neutral-100 dark:text-neutral-900 dark:hover:bg-neutral-200"
>
View profile
</button>
</div>
</div>
<HoverCard.Arrow className="fill-black dark:fill-white" />
</HoverCard.Content>
</HoverCard.Portal>
</HoverCard.Root>
</User.Provider>
);
}
+43
View File
@@ -0,0 +1,43 @@
import { QuoteIcon } from "@lume/icons";
import { Note } from "@/components/note";
import { cn } from "@lume/utils";
import type { LumeEvent } from "@lume/system";
export function Quote({
event,
className,
}: {
event: LumeEvent;
className?: string;
}) {
return (
<Note.Provider event={event}>
<Note.Root
className={cn(
"bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl flex flex-col gap-3 shadow-primary dark:ring-1 ring-neutral-800/50",
className,
)}
>
<div className="flex flex-col gap-3">
<Note.Child event={event.quote} isRoot />
<div className="flex items-center gap-2 px-3">
<div className="inline-flex items-center gap-1.5 shrink-0 text-sm font-medium text-neutral-600 dark:text-neutral-400">
<QuoteIcon className="size-4" />
Quote
</div>
<div className="flex-1 h-px bg-neutral-100 dark:bg-white/5" />
</div>
<div>
<div className="flex items-center justify-between px-3 h-14">
<Note.User />
</div>
<Note.Content className="px-3" quote={false} clean />
</div>
</div>
<div className="flex items-center px-3 h-14">
<Note.Open />
</div>
</Note.Root>
</Note.Provider>
);
}
+81
View File
@@ -0,0 +1,81 @@
import { Spinner } from "@lume/ui";
import { Note } from "@/components/note";
import { User } from "@/components/user";
import { cn } from "@lume/utils";
import { useQuery } from "@tanstack/react-query";
import { type LumeEvent, NostrQuery } from "@lume/system";
export function RepostNote({
event,
className,
}: {
event: LumeEvent;
className?: string;
}) {
const { isLoading, isError, data } = useQuery({
queryKey: ["event", event.repostId],
queryFn: async () => {
try {
const data = await NostrQuery.getRepostEvent(event);
return data;
} catch (e) {
throw new Error(e);
}
},
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
staleTime: Number.POSITIVE_INFINITY,
retry: 2,
});
return (
<Note.Root
className={cn(
"bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50",
className,
)}
>
{isLoading ? (
<div className="flex items-center justify-center h-20 gap-2">
<Spinner />
<span className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
Loading event...
</span>
</div>
) : isError || !data ? (
<div className="flex items-center justify-center h-20">
Event not found within your current relay set
</div>
) : (
<Note.Provider event={data}>
<Note.Root>
<div className="flex items-center justify-between px-3 h-14">
<Note.User />
<Note.Menu />
</div>
<Note.Content className="px-3" />
<div className="flex items-center justify-between px-3 mt-3 h-14">
<div className="inline-flex items-center gap-3">
<Note.Open />
<Note.Reply />
<Note.Repost />
<Note.Zap />
</div>
<div>
<User.Provider pubkey={event.pubkey}>
<User.Root className="flex items-center gap-2">
<div className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
Reposted by
</div>
<User.Avatar className="object-cover rounded-full size-6 shrink-0 ring-1 ring-neutral-200/50 dark:ring-neutral-800/50" />
</User.Root>
</User.Provider>
</div>
</div>
</Note.Root>
</Note.Provider>
)}
</Note.Root>
);
}
+34
View File
@@ -0,0 +1,34 @@
import { cn } from "@lume/utils";
import { Note } from "@/components/note";
import type { LumeEvent } from "@lume/system";
export function TextNote({
event,
className,
}: {
event: LumeEvent;
className?: string;
}) {
return (
<Note.Provider event={event}>
<Note.Root
className={cn(
"bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50",
className,
)}
>
<div className="flex items-center justify-between px-3 h-14">
<Note.User />
<Note.Menu />
</div>
<Note.Content className="px-3" />
<div className="flex items-center gap-4 px-3 mt-3 h-14">
<Note.Open />
<Note.Reply />
<Note.Repost />
<Note.Zap />
</div>
</Note.Root>
</Note.Provider>
);
}
+15
View File
@@ -0,0 +1,15 @@
import type { ReactNode } from "@tanstack/react-router";
import { useLayoutEffect, useState } from "react";
import { createPortal } from "react-dom";
export function Toolbar({ children }: { children: ReactNode }) {
const [domReady, setDomReady] = useState(false);
useLayoutEffect(() => {
setDomReady(true);
}, []);
return domReady
? createPortal(children, document.getElementById("toolbar"))
: null;
}
@@ -0,0 +1,12 @@
import { cn } from "@lume/utils";
import { useUserContext } from "./provider";
export function UserAbout({ className }: { className?: string }) {
const user = useUserContext();
return (
<div className={cn("content-break select-text", className)}>
{user.profile?.about?.trim() || "No bio"}
</div>
);
}
@@ -0,0 +1,65 @@
import { cn } from "@lume/utils";
import * as Avatar from "@radix-ui/react-avatar";
import { useRouteContext } from "@tanstack/react-router";
import { minidenticon } from "minidenticons";
import { nanoid } from "nanoid";
import { useMemo } from "react";
import { useUserContext } from "./provider";
export function UserAvatar({ className }: { className?: string }) {
const user = useUserContext();
const { settings } = useRouteContext({ strict: false });
const picture = useMemo(() => {
if (
settings?.image_resize_service?.length &&
user.profile?.picture?.length
) {
const url = `${settings.image_resize_service}?url=${user.profile?.picture}&w=100&h=100&default=1&n=-1`;
return url;
} else {
return user.profile?.picture;
}
}, [user.profile?.picture]);
const fallbackAvatar = useMemo(
() =>
`data:image/svg+xml;utf8,${encodeURIComponent(
minidenticon(user.pubkey || nanoid(), 90, 50),
)}`,
[user.pubkey],
);
if (settings && !settings.display_avatar) {
return (
<Avatar.Root className="shrink-0">
<Avatar.Fallback delayMs={120}>
<img
src={fallbackAvatar}
alt={user.pubkey}
className={cn("bg-black dark:bg-white", className)}
/>
</Avatar.Fallback>
</Avatar.Root>
);
}
return (
<Avatar.Root className="shrink-0">
<Avatar.Image
src={picture}
alt={user.pubkey}
loading="eager"
decoding="async"
className={cn("outline-[.5px] outline-black/5 object-cover", className)}
/>
<Avatar.Fallback delayMs={120}>
<img
src={fallbackAvatar}
alt={user.pubkey}
className={cn("bg-black dark:bg-white", className)}
/>
</Avatar.Fallback>
</Avatar.Root>
);
}
@@ -0,0 +1,36 @@
import { cn } from "@lume/utils";
import { useUserContext } from "./provider";
export function UserCover({ className }: { className?: string }) {
const user = useUserContext();
if (!user) {
return (
<div
className={cn(
"animate-pulse bg-neutral-300 dark:bg-neutral-700",
className,
)}
/>
);
}
if (user && !user.profile?.banner) {
return (
<div
className={cn("bg-gradient-to-b from-blue-400 to-teal-200", className)}
/>
);
}
return (
<img
src={user?.profile?.banner}
alt="banner"
loading="lazy"
decoding="async"
style={{ contentVisibility: "auto" }}
className={cn("object-cover", className)}
/>
);
}
@@ -0,0 +1,60 @@
import { cn } from "@lume/utils";
import { useEffect, useState } from "react";
import { Spinner } from "@lume/ui";
import { useUserContext } from "./provider";
import { NostrAccount } from "@lume/system";
export function UserFollowButton({
simple = false,
className,
}: {
simple?: boolean;
className?: string;
}) {
const user = useUserContext();
const [loading, setLoading] = useState(false);
const [followed, setFollowed] = useState(false);
const toggleFollow = async () => {
setLoading(true);
const toggle = await NostrAccount.toggleContact(user.pubkey);
if (toggle) {
setFollowed((prev) => !prev);
setLoading(false);
}
};
useEffect(() => {
let mounted = true;
NostrAccount.checkContact(user.pubkey).then((status) => {
if (mounted) setFollowed(status);
});
return () => {
mounted = false;
};
}, []);
return (
<button
type="button"
disabled={loading}
onClick={() => toggleFollow()}
className={cn("w-max", className)}
>
{loading ? (
<Spinner className="size-4" />
) : followed ? (
!simple ? (
"Unfollow"
) : null
) : (
"Follow"
)}
</button>
);
}
@@ -0,0 +1,21 @@
import { UserAbout } from "./about";
import { UserAvatar } from "./avatar";
import { UserCover } from "./cover";
import { UserFollowButton } from "./followButton";
import { UserName } from "./name";
import { UserNip05 } from "./nip05";
import { UserProvider } from "./provider";
import { UserRoot } from "./root";
import { UserTime } from "./time";
export const User = {
Provider: UserProvider,
Root: UserRoot,
Avatar: UserAvatar,
Cover: UserCover,
Name: UserName,
NIP05: UserNip05,
Time: UserTime,
About: UserAbout,
Button: UserFollowButton,
};
@@ -0,0 +1,21 @@
import { cn, displayNpub } from "@lume/utils";
import { useUserContext } from "./provider";
export function UserName({
className,
prefix,
}: {
className?: string;
prefix?: string;
}) {
const user = useUserContext();
return (
<div className={cn("max-w-[12rem] truncate", className)}>
{prefix}
{user.profile?.display_name ||
user.profile?.name ||
displayNpub(user.pubkey, 16)}
</div>
);
}
@@ -0,0 +1,58 @@
import { VerifiedIcon } from "@lume/icons";
import { displayLongHandle, displayNpub } from "@lume/utils";
import * as Tooltip from "@radix-ui/react-tooltip";
import { useQuery } from "@tanstack/react-query";
import { useUserContext } from "./provider";
import { NostrQuery } from "@lume/system";
import { experimental_createPersister } from "@tanstack/query-persist-client-core";
export function UserNip05() {
const user = useUserContext();
const { isLoading, data: verified } = useQuery({
queryKey: ["nip05", user?.pubkey],
queryFn: async () => {
if (!user.profile?.nip05?.length) return false;
const verify = await NostrQuery.verifyNip05(
user.pubkey,
user.profile?.nip05,
);
return verify;
},
enabled: !!user.profile?.nip05,
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
staleTime: Number.POSITIVE_INFINITY,
retry: false,
persister: experimental_createPersister({
storage: localStorage,
maxAge: 1000 * 60 * 60 * 72, // 72 hours
}),
});
if (!user.profile?.nip05?.length) return;
return (
<Tooltip.Provider>
<Tooltip.Root delayDuration={150}>
<Tooltip.Trigger>
{!isLoading && verified ? (
<VerifiedIcon className="text-teal-500 size-4" />
) : null}
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="inline-flex h-7 select-none items-center justify-center rounded-md bg-neutral-950 px-3.5 text-sm font-medium text-neutral-50 will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade dark:bg-neutral-50 dark:text-neutral-950">
{!user.profile?.nip05
? displayNpub(user.pubkey, 16)
: user.profile?.nip05.length > 50
? displayLongHandle(user.profile?.nip05)
: user.profile.nip05?.replace("_@", "")}
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
);
}
@@ -0,0 +1,33 @@
import { useProfile } from "@lume/system";
import type { Metadata } from "@lume/types";
import { type ReactNode, createContext, useContext } from "react";
const UserContext = createContext<{
pubkey: string;
profile: Metadata;
isError: boolean;
isLoading: boolean;
}>(null);
export function UserProvider({
pubkey,
children,
embedProfile,
}: {
pubkey: string;
children: ReactNode;
embedProfile?: string;
}) {
const { isLoading, isError, profile } = useProfile(pubkey, embedProfile);
return (
<UserContext.Provider value={{ pubkey, profile, isError, isLoading }}>
{children}
</UserContext.Provider>
);
}
export function useUserContext() {
const context = useContext(UserContext);
return context;
}
@@ -0,0 +1,12 @@
import { cn } from "@lume/utils";
import type { ReactNode } from "react";
export function UserRoot({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return <div className={cn(className)}>{children}</div>;
}
@@ -0,0 +1,18 @@
import { cn, formatCreatedAt } from "@lume/utils";
import { useMemo } from "react";
export function UserTime({
time,
className,
}: {
time: number;
className?: string;
}) {
const createdAt = useMemo(() => formatCreatedAt(time), [time]);
return (
<div className={cn("text-neutral-600 dark:text-neutral-400", className)}>
{createdAt}
</div>
);
}
+23
View File
@@ -0,0 +1,23 @@
import { resolveResource } from "@tauri-apps/api/path";
import { readTextFile } from "@tauri-apps/plugin-fs";
import i18n from "i18next";
import resourcesToBackend from "i18next-resources-to-backend";
import { initReactI18next } from "react-i18next";
i18n
.use(
resourcesToBackend(async (language: string) => {
const file_path = await resolveResource(`locales/${language}.json`);
return JSON.parse(await readTextFile(file_path));
}),
)
.use(initReactI18next)
.init({
lng: "en",
fallbackLng: "en",
interpolation: {
escapeValue: false,
},
});
export default i18n;
+187
View File
@@ -0,0 +1,187 @@
import { Column } from "@/components/column";
import { Toolbar } from "@/components/toolbar";
import { ArrowLeftIcon, ArrowRightIcon, PlusSquareIcon } from "@lume/icons";
import { NostrQuery } from "@lume/system";
import type { ColumnEvent, LumeColumn } from "@lume/types";
import { createFileRoute } from "@tanstack/react-router";
import { listen } from "@tauri-apps/api/event";
import { getCurrent } from "@tauri-apps/api/window";
import useEmblaCarousel from "embla-carousel-react";
import { nanoid } from "nanoid";
import { useCallback, useEffect, useState } from "react";
import { useDebouncedCallback } from "use-debounce";
export const Route = createFileRoute("/$account/home")({
loader: async () => {
const columns = await NostrQuery.getColumns();
return columns;
},
component: Screen,
});
function Screen() {
const { account } = Route.useParams();
const initialColumnList = Route.useLoaderData();
const [columns, setColumns] = useState<LumeColumn[]>([]);
const [emblaRef, emblaApi] = useEmblaCarousel({
watchDrag: false,
loop: false,
});
const scrollPrev = useCallback(() => {
if (emblaApi) emblaApi.scrollPrev(true);
}, [emblaApi]);
const scrollNext = useCallback(() => {
if (emblaApi) emblaApi.scrollNext(true);
}, [emblaApi]);
const emitScrollEvent = useCallback(() => {
getCurrent().emit("child-webview", { scroll: true });
}, []);
const emitResizeEvent = useCallback(() => {
getCurrent().emit("child-webview", { resize: true, direction: "x" });
}, []);
const openLumeStore = useDebouncedCallback(async () => {
await getCurrent().emit("columns", {
type: "add",
column: {
label: "store",
name: "Store",
content: "/store/official",
},
});
}, 150);
const add = useDebouncedCallback((column: LumeColumn) => {
column.label = `${column.label}-${nanoid()}`; // update col label
setColumns((prev) => [column, ...prev]);
}, 150);
const remove = useDebouncedCallback((label: string) => {
setColumns((prev) => prev.filter((t) => t.label !== label));
}, 150);
const updateName = useDebouncedCallback((label: string, title: string) => {
const currentColIndex = columns.findIndex((col) => col.label === label);
const updatedCol = Object.assign({}, columns[currentColIndex]);
updatedCol.name = title;
const newCols = columns.slice();
newCols[currentColIndex] = updatedCol;
setColumns(newCols);
}, 150);
const reset = useDebouncedCallback(() => setColumns([]), 150);
const handleKeyDown = useDebouncedCallback((event) => {
if (event.defaultPrevented) return;
switch (event.code) {
case "ArrowLeft":
if (emblaApi) emblaApi.scrollPrev(true);
break;
case "ArrowRight":
if (emblaApi) emblaApi.scrollNext(true);
break;
default:
break;
}
event.preventDefault();
}, 150);
useEffect(() => {
if (emblaApi) {
emblaApi.on("scroll", emitScrollEvent);
emblaApi.on("resize", emitResizeEvent);
emblaApi.on("slidesChanged", emitScrollEvent);
}
return () => {
emblaApi?.off("scroll", emitScrollEvent);
emblaApi?.off("resize", emitResizeEvent);
emblaApi?.off("slidesChanged", emitScrollEvent);
};
}, [emblaApi, emitScrollEvent, emitResizeEvent]);
useEffect(() => {
if (columns?.length) {
NostrQuery.setColumns(columns).then(() => console.log("saved"));
}
}, [columns]);
useEffect(() => {
setColumns(initialColumnList);
}, [initialColumnList]);
// Listen for keyboard event
useEffect(() => {
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [handleKeyDown]);
// Listen for columns event
useEffect(() => {
const unlisten = listen<ColumnEvent>("columns", (data) => {
if (data.payload.type === "reset") reset();
if (data.payload.type === "add") add(data.payload.column);
if (data.payload.type === "remove") remove(data.payload.label);
if (data.payload.type === "set_title")
updateName(data.payload.label, data.payload.title);
});
return () => {
unlisten.then((f) => f());
};
}, []);
return (
<div className="size-full">
<div ref={emblaRef} className="overflow-hidden size-full">
<div className="flex size-full">
{columns?.map((column) => (
<Column
key={account + column.label}
column={column}
account={account}
/>
))}
</div>
</div>
<Toolbar>
<div className="flex items-center h-8 gap-1 p-[2px] rounded-full bg-black/5 dark:bg-white/5">
<button
type="button"
onClick={() => scrollPrev()}
className="inline-flex items-center justify-center rounded-full size-7 text-neutral-800 hover:bg-black/10 dark:text-neutral-200 dark:hover:bg-white/10"
>
<ArrowLeftIcon className="size-4" />
</button>
<button
type="button"
onClick={() => openLumeStore()}
className="inline-flex items-center justify-center rounded-full size-7 text-neutral-800 hover:bg-black/10 dark:text-neutral-200 dark:hover:bg-white/10"
>
<PlusSquareIcon className="size-4" />
</button>
<button
type="button"
onClick={() => scrollNext()}
className="inline-flex items-center justify-center rounded-full size-7 text-neutral-800 hover:bg-black/10 dark:text-neutral-200 dark:hover:bg-white/10"
>
<ArrowRightIcon className="size-4" />
</button>
</div>
</Toolbar>
</div>
);
}
+186
View File
@@ -0,0 +1,186 @@
import { User } from "@/components/user";
import {
ComposeFilledIcon,
HorizontalDotsIcon,
PlusIcon,
SearchIcon,
} from "@lume/icons";
import { LumeWindow, NostrAccount } from "@lume/system";
import { cn } from "@lume/utils";
import * as Popover from "@radix-ui/react-popover";
import { Outlet, createFileRoute } from "@tanstack/react-router";
import { Link } from "@tanstack/react-router";
import { getCurrent } from "@tauri-apps/api/window";
import { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
export const Route = createFileRoute("/$account")({
beforeLoad: async () => {
const accounts = await NostrAccount.getAccounts();
return { accounts };
},
component: Screen,
});
function Screen() {
const { platform } = Route.useRouteContext();
return (
<div className="flex flex-col w-screen h-screen">
<div
data-tauri-drag-region
className={cn(
"flex h-11 shrink-0 items-center justify-between pr-2",
platform === "macos" ? "ml-2 pl-20" : "pl-4",
)}
>
<div className="flex items-center gap-3">
<Accounts />
<Link
to="/landing"
className="inline-flex items-center justify-center rounded-full size-8 shrink-0 bg-black/10 text-neutral-800 hover:bg-black/20 dark:bg-white/10 dark:text-neutral-200 dark:hover:bg-white/20"
>
<PlusIcon className="size-5" />
</Link>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => LumeWindow.openEditor()}
className="inline-flex items-center justify-center h-8 gap-1 px-3 text-sm font-medium text-white bg-blue-500 rounded-full w-max hover:bg-blue-600"
>
<ComposeFilledIcon className="size-4" />
New Post
</button>
<div id="toolbar" />
</div>
</div>
<div className="flex-1">
<Outlet />
</div>
</div>
);
}
function Accounts() {
const { accounts } = Route.useRouteContext();
const { account } = Route.useParams();
const [windowWidth, setWindowWidth] = useState<number>(null);
const navigate = Route.useNavigate();
const sortedList = useMemo(() => {
const list = accounts;
for (const [i, item] of list.entries()) {
if (item === account) {
list.splice(i, 1);
list.unshift(item);
}
}
return list;
}, [accounts]);
const changeAccount = async (npub: string) => {
if (npub === account) {
return await LumeWindow.openProfile(account);
}
// Change current account and update signer
const select = await NostrAccount.loadAccount(npub);
if (select) {
// Reset current columns
await getCurrent().emit("columns", { type: "reset" });
// Redirect to new account
return navigate({
to: "/$account/home",
params: { account: npub },
resetScroll: true,
replace: true,
});
} else {
toast.warning("Something wrong.");
}
};
const getWindowDimensions = () => {
const { innerWidth: width, innerHeight: height } = window;
return {
width,
height,
};
};
useEffect(() => {
function handleResize() {
setWindowWidth(getWindowDimensions().width);
}
if (!windowWidth) {
setWindowWidth(getWindowDimensions().width);
}
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
return (
<div data-tauri-drag-region className="flex items-center gap-3">
{sortedList
.slice(0, windowWidth > 500 ? account.length : 2)
.map((user) => (
<button key={user} type="button" onClick={() => changeAccount(user)}>
<User.Provider pubkey={user}>
<User.Root
className={cn(
"shrink-0 rounded-full transition-all ease-in-out duration-150 will-change-auto",
user === account
? "ring-1 ring-teal-500 ring-offset-2 ring-offset-neutral-200 dark:ring-offset-neutral-950"
: "",
)}
>
<User.Avatar
className={cn(
"aspect-square h-auto rounded-full object-cover transition-all ease-in-out duration-150 will-change-auto",
user === account ? "w-7" : "w-8",
)}
/>
</User.Root>
</User.Provider>
</button>
))}
{accounts.length >= 3 && windowWidth <= 700 ? (
<Popover.Root>
<Popover.Trigger className="inline-flex items-center justify-center rounded-full size-8 shrink-0 bg-black/10 text-neutral-800 hover:bg-black/20 dark:bg-white/10 dark:text-neutral-200 dark:hover:bg-white/20">
<HorizontalDotsIcon className="size-5" />
</Popover.Trigger>
<Popover.Portal>
<Popover.Content className="flex h-11 select-none items-center justify-center rounded-md bg-neutral-950 p-1 text-sm text-neutral-50 will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade dark:bg-neutral-50 dark:text-neutral-950">
{sortedList.slice(2).map((user) => (
<button
key={user}
type="button"
onClick={() => changeAccount(user)}
className="inline-flex items-center justify-center rounded-md size-9 hover:bg-white/10"
>
<User.Provider pubkey={user}>
<User.Root className="rounded-full ring-1 ring-white/10">
<User.Avatar className="object-cover h-auto rounded-full size-7 aspect-square" />
</User.Root>
</User.Provider>
</button>
))}
<Popover.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Popover.Content>
</Popover.Portal>
</Popover.Root>
) : null}
</div>
);
}
+47
View File
@@ -0,0 +1,47 @@
import { CancelCircleIcon, CheckCircleIcon, InfoCircleIcon } from "@lume/icons";
import type { Settings } from "@lume/system";
import { Spinner } from "@lume/ui";
import type { QueryClient } from "@tanstack/react-query";
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
import type { Platform } from "@tauri-apps/plugin-os";
import { Toaster } from "sonner";
interface RouterContext {
// System
queryClient: QueryClient;
// App info
platform?: Platform;
locale?: string;
// Settings
settings?: Settings;
// Accounts
accounts?: string[];
}
export const Route = createRootRouteWithContext<RouterContext>()({
component: () => (
<>
<Toaster
position="bottom-right"
icons={{
success: <CheckCircleIcon className="size-5" />,
info: <InfoCircleIcon className="size-5" />,
error: <CancelCircleIcon className="size-5" />,
}}
closeButton
theme="system"
/>
<Outlet />
</>
),
pendingComponent: Pending,
wrapInSuspense: true,
});
function Pending() {
return (
<div className="flex flex-col items-center justify-center w-screen h-screen">
<Spinner className="size-5" />
</div>
);
}
+16
View File
@@ -0,0 +1,16 @@
import { Box, Container } from "@lume/ui";
import { Outlet, createLazyFileRoute } from "@tanstack/react-router";
export const Route = createLazyFileRoute("/auth")({
component: Screen,
});
function Screen() {
return (
<Container withDrag>
<Box className="px-3 pt-3">
<Outlet />
</Box>
</Container>
);
}
@@ -0,0 +1,191 @@
import { CheckIcon } from "@lume/icons";
import { Spinner } from "@lume/ui";
import { displayNsec } from "@lume/utils";
import * as Checkbox from "@radix-ui/react-checkbox";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { invoke } from "@tauri-apps/api/core";
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
export const Route = createFileRoute("/auth/$account/backup")({
component: Screen,
});
function Screen() {
const { account } = Route.useParams();
const { t } = useTranslation();
const [key, setKey] = useState(null);
const [passphase, setPassphase] = useState("");
const [copied, setCopied] = useState(false);
const [loading, setLoading] = useState(false);
const [confirm, setConfirm] = useState({ c1: false, c2: false, c3: false });
const navigate = useNavigate();
const submit = async () => {
try {
if (key) {
if (!confirm.c1 || !confirm.c2 || !confirm.c3) {
return toast.warning("You need to confirm before continue");
}
navigate({ to: "/", replace: true });
}
// start loading
setLoading(true);
invoke("get_encrypted_key", {
npub: account,
password: passphase,
}).then((encrypted: string) => {
// update state
setKey(encrypted);
setLoading(false);
});
} catch (e) {
setLoading(false);
toast.error(String(e));
}
};
const copyKey = async () => {
try {
await writeText(key);
setCopied(true);
} catch (e) {
toast.error(e);
}
};
return (
<div className="flex flex-col items-center justify-center w-full h-full gap-6 px-5 mx-auto xl:max-w-xl">
<div className="flex flex-col text-center">
<h3 className="text-xl font-semibold">Backup your sign in keys</h3>
<p className="text-neutral-700 dark:text-neutral-300">
It's use for login to Lume or other Nostr clients. You will lost
access to your account if you lose this key.
</p>
</div>
<div className="flex flex-col w-full gap-5">
<div className="flex flex-col gap-2">
<label htmlFor="passphase" className="font-medium">
Set a passphase to secure your key
</label>
<div className="relative">
<input
name="passphase"
type="password"
value={passphase}
onChange={(e) => setPassphase(e.target.value)}
className="w-full px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
/>
</div>
</div>
{key ? (
<>
<div className="flex flex-col gap-2">
<label htmlFor="nsec" className="font-medium">
Copy this key and keep it in safe place
</label>
<div className="flex items-center gap-2">
<input
name="nsec"
type="text"
value={displayNsec(key, 36)}
readOnly
className="w-full px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
/>
<button
type="button"
onClick={() => copyKey()}
className="inline-flex items-center justify-center w-24 rounded-lg h-11 bg-neutral-200 hover:bg-neutral-300 dark:bg-white/20 dark:hover:bg-white/30"
>
{copied ? "Copied" : "Copy"}
</button>
</div>
</div>
<div className="flex flex-col gap-2">
<div className="font-medium">Before you continue:</div>
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<Checkbox.Root
checked={confirm.c1}
onCheckedChange={() =>
setConfirm((state) => ({ ...state, c1: !state.c1 }))
}
className="flex items-center justify-center rounded-md outline-none appearance-none size-6 bg-neutral-100 dark:bg-white/10 dark:hover:bg-white/20"
id="confirm1"
>
<Checkbox.Indicator className="text-blue-500">
<CheckIcon className="size-4" />
</Checkbox.Indicator>
</Checkbox.Root>
<label
className="text-sm leading-none text-neutral-800 dark:text-neutral-200"
htmlFor="confirm1"
>
{t("backup.confirm1")}
</label>
</div>
<div className="flex items-center gap-2">
<Checkbox.Root
checked={confirm.c2}
onCheckedChange={() =>
setConfirm((state) => ({ ...state, c2: !state.c2 }))
}
className="flex items-center justify-center rounded-md outline-none appearance-none size-6 bg-neutral-100 dark:bg-white/10 dark:hover:bg-white/20"
id="confirm2"
>
<Checkbox.Indicator className="text-blue-500">
<CheckIcon className="size-4" />
</Checkbox.Indicator>
</Checkbox.Root>
<label
className="text-sm leading-none text-neutral-800 dark:text-neutral-200"
htmlFor="confirm2"
>
{t("backup.confirm2")}
</label>
</div>
<div className="flex items-center gap-2">
<Checkbox.Root
checked={confirm.c3}
onCheckedChange={() =>
setConfirm((state) => ({ ...state, c3: !state.c3 }))
}
className="flex items-center justify-center rounded-md outline-none appearance-none size-6 bg-neutral-100 dark:bg-white/10 dark:hover:bg-white/20"
id="confirm3"
>
<Checkbox.Indicator className="text-blue-500">
<CheckIcon className="size-4" />
</Checkbox.Indicator>
</Checkbox.Root>
<label
className="text-sm leading-none text-neutral-800 dark:text-neutral-200"
htmlFor="confirm3"
>
{t("backup.confirm3")}
</label>
</div>
</div>
</div>
</>
) : null}
<div>
<button
type="button"
onClick={() => submit()}
disabled={loading}
className="inline-flex items-center justify-center w-full font-semibold text-white bg-blue-500 rounded-lg h-11 shrink-0 hover:bg-blue-600 disabled:opacity-50"
>
{loading ? <Spinner /> : t("global.continue")}
</button>
</div>
</div>
</div>
);
}
@@ -0,0 +1,144 @@
import { AvatarUploader } from "@/components/avatarUploader";
import { PlusIcon } from "@lume/icons";
import { NostrAccount } from "@lume/system";
import type { Metadata } from "@lume/types";
import { Spinner } from "@lume/ui";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
export const Route = createFileRoute("/auth/create-profile")({
component: Screen,
loader: async () => {
const account = await NostrAccount.createAccount();
return account;
},
});
function Screen() {
const account = Route.useLoaderData();
const navigate = useNavigate();
const { t } = useTranslation();
const { register, handleSubmit } = useForm();
const [picture, setPicture] = useState<string>("");
const [loading, setLoading] = useState(false);
const onSubmit = async (data: {
name: string;
about: string;
website: string;
}) => {
setLoading(true);
try {
// Save account keys
const save = await NostrAccount.saveAccount(account.nsec);
// Then create profile
if (save) {
const profile: Metadata = { ...data, picture };
const eventId = await NostrAccount.createProfile(profile);
if (eventId) {
navigate({
to: "/auth/$account/backup",
params: { account: account.npub },
replace: true,
});
}
}
} catch (e) {
setLoading(false);
toast.error(String(e));
}
};
return (
<div className="flex flex-col items-center justify-center w-full h-full gap-6 px-5 mx-auto xl:max-w-xl">
<div className="text-center">
<h3 className="text-xl font-semibold">Let's set up your profile.</h3>
</div>
<div>
<div className="relative rounded-full size-24 bg-gradient-to-tr from-orange-100 via-red-50 to-blue-200">
{picture ? (
<img
src={picture}
alt="avatar"
loading="lazy"
decoding="async"
className="absolute inset-0 z-10 object-cover w-full h-full rounded-full"
/>
) : null}
<AvatarUploader
setPicture={setPicture}
className="absolute inset-0 z-20 flex items-center justify-center w-full h-full text-white rounded-full dark:text-black bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
>
<PlusIcon className="size-8" />
</AvatarUploader>
</div>
</div>
<form
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col w-full gap-3"
>
<div className="flex flex-col gap-1">
<label htmlFor="display_name" className="font-medium">
{t("user.displayName")} *
</label>
<input
type={"text"}
{...register("display_name", { required: true, minLength: 1 })}
placeholder="e.g. Alice in Nostrland"
spellCheck={false}
className="px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
/>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="name" className="font-medium">
{t("user.name")}
</label>
<input
type={"text"}
{...register("name")}
placeholder="e.g. alice"
spellCheck={false}
className="px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
/>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="about" className="font-medium">
{t("user.bio")}
</label>
<textarea
{...register("about")}
placeholder="e.g. Artist, anime-lover, and k-pop fan"
spellCheck={false}
className="relative h-24 w-full resize-none rounded-lg border-transparent bg-neutral-100 px-3 py-2 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
/>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="website" className="font-medium">
{t("user.website")}
</label>
<input
type="url"
{...register("website")}
placeholder="e.g. https://alice.me"
spellCheck={false}
className="px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-500 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
/>
</div>
<button
type="submit"
className="inline-flex items-center justify-center w-full mt-3 font-semibold text-white bg-blue-500 rounded-lg h-11 shrink-0 hover:bg-blue-600 disabled:opacity-50"
>
{loading ? <Spinner /> : t("global.continue")}
</button>
</form>
</div>
);
}
@@ -0,0 +1,86 @@
import { NostrAccount } from "@lume/system";
import { Spinner } from "@lume/ui";
import { createLazyFileRoute } from "@tanstack/react-router";
import { useState } from "react";
import { toast } from "sonner";
export const Route = createLazyFileRoute("/auth/import")({
component: Screen,
});
function Screen() {
const navigate = Route.useNavigate();
const [key, setKey] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const submit = async () => {
if (!key.startsWith("nsec1"))
return toast.warning(
"You need to enter a valid private key starts with nsec or ncryptsec",
);
try {
setLoading(true);
const npub = await NostrAccount.saveAccount(key, password);
if (npub) {
navigate({ to: "/", replace: true });
}
} catch (e) {
setLoading(false);
toast.error(e);
}
};
return (
<div className="flex flex-col items-center justify-center w-full h-full gap-6 px-5 mx-auto xl:max-w-xl">
<div className="text-center">
<h3 className="text-xl font-semibold">Continue with Private Key</h3>
</div>
<div className="flex flex-col w-full gap-3">
<div className="flex flex-col gap-1">
<label
htmlFor="key"
className="font-medium text-neutral-900 dark:text-neutral-100"
>
Private Key
</label>
<input
name="key"
type="text"
placeholder="nsec or ncryptsec..."
value={key}
onChange={(e) => setKey(e.target.value)}
className="px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
/>
</div>
<div className="flex flex-col gap-1">
<label
htmlFor="password"
className="font-medium text-neutral-900 dark:text-neutral-100"
>
Password (Optional)
</label>
<input
name="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
/>
</div>
<button
type="button"
onClick={() => submit()}
disabled={loading}
className="inline-flex items-center justify-center w-full mt-3 font-semibold text-white bg-blue-500 rounded-lg h-11 shrink-0 hover:bg-blue-600 disabled:opacity-50"
>
{loading ? <Spinner /> : "Login"}
</button>
</div>
</div>
);
}
@@ -0,0 +1,77 @@
import { NostrAccount } from "@lume/system";
import { Spinner } from "@lume/ui";
import { createLazyFileRoute } from "@tanstack/react-router";
import { useState } from "react";
import { toast } from "sonner";
export const Route = createLazyFileRoute("/auth/remote")({
component: Screen,
});
function Screen() {
const navigate = Route.useNavigate();
const [uri, setUri] = useState("");
const [loading, setLoading] = useState(false);
const submit = async () => {
if (!uri.startsWith("bunker://"))
return toast.warning(
"You need to enter a valid Connect URI starts with bunker://",
);
try {
setLoading(true);
const remoteAccount = await NostrAccount.connectRemoteAccount(uri);
if (remoteAccount?.length) {
navigate({ to: "/", replace: true });
}
} catch (e) {
setLoading(false);
toast.error(e);
}
};
return (
<div className="flex flex-col items-center justify-center w-full h-full gap-6 px-5 mx-auto xl:max-w-xl">
<div className="text-center">
<h3 className="text-xl font-semibold">Continue with Nostr Connect</h3>
</div>
<div className="flex flex-col w-full gap-3">
<div className="flex flex-col gap-1">
<label
htmlFor="uri"
className="font-medium text-neutral-900 dark:text-neutral-100"
>
Connect URI
</label>
<input
name="uri"
type="text"
placeholder="bunker://..."
value={uri}
onChange={(e) => setUri(e.target.value)}
className="px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
/>
</div>
<div className="flex flex-col items-center gap-1">
<button
type="button"
onClick={() => submit()}
disabled={loading}
className="inline-flex items-center justify-center w-full mt-3 font-semibold text-white bg-blue-500 rounded-lg h-11 shrink-0 hover:bg-blue-600 disabled:opacity-50"
>
{loading ? <Spinner /> : "Login"}
</button>
{loading ? (
<p className="text-sm text-center text-neutral-600 dark:text-neutral-400">
Waiting confirmation...
</p>
) : null}
</div>
</div>
</div>
);
}
@@ -0,0 +1,132 @@
import { CancelIcon, PlusIcon } from "@lume/icons";
import { NostrQuery } from "@lume/system";
import type { Relay } from "@lume/types";
import { Spinner } from "@lume/ui";
import { createFileRoute } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
export const Route = createFileRoute("/bootstrap-relays")({
loader: async () => {
const bootstrapRelays = await NostrQuery.getBootstrapRelays();
return bootstrapRelays;
},
component: Screen,
});
function Screen() {
const bootstrapRelays = Route.useLoaderData();
const { register, reset, handleSubmit } = useForm();
const [relays, setRelays] = useState<Relay[]>([]);
const [isLoading, setIsLoading] = useState(false);
const removeRelay = (url: string) => {
setRelays((prev) => prev.filter((relay) => relay.url !== url));
};
const onSubmit = async (data: { url: string; purpose: string }) => {
try {
const relay: Relay = { url: data.url, purpose: data.purpose };
setRelays((prev) => [...prev, relay]);
reset();
} catch (e) {
toast.error(String(e));
}
};
const save = async () => {
try {
setIsLoading(true);
await NostrQuery.saveBootstrapRelays(relays);
} catch (e) {
setIsLoading(false);
toast.error(String(e));
}
};
useEffect(() => {
setRelays(bootstrapRelays);
}, [bootstrapRelays]);
return (
<div className="flex flex-col items-center justify-center w-screen h-screen">
<div className="w-full max-w-sm mx-auto lg:max-w-lg">
<div className="text-center h-11">
<h1 className="font-semibold">Customize Bootstrap Relays</h1>
</div>
<div className="flex flex-col w-full px-2 bg-white rounded-xl shadow-primary backdrop-blur-lg dark:bg-white/20 dark:ring-1 ring-neutral-800/50">
{relays.map((relay) => (
<div
key={relay.url}
className="flex items-center justify-between h-11"
>
<div className="inline-flex items-center gap-2 text-sm font-medium">
{relay.url}
</div>
<div className="flex items-center gap-2">
{relay.purpose?.length ? (
<button
type="button"
className="inline-flex items-center justify-center px-2 text-xs font-medium uppercase rounded-md h-7 w-max hover:bg-black/10 dark:hover:bg-white/10"
>
{relay.purpose}
</button>
) : null}
<button
type="button"
onClick={() => removeRelay(relay.url)}
className="inline-flex items-center justify-center rounded-md size-7 text-neutral-700 dark:text-white/20 hover:bg-black/10 dark:hover:bg-white/10"
>
<CancelIcon className="size-3" />
</button>
</div>
</div>
))}
<div className="flex items-center border-t h-14 border-neutral-100 dark:border-white/5">
<form
onSubmit={handleSubmit(onSubmit)}
className="flex items-center w-full gap-2 mb-0"
>
<div className="flex items-center flex-1 gap-2 border rounded-lg border-neutral-300 dark:border-white/20">
<input
{...register("url", {
required: true,
minLength: 1,
})}
name="url"
placeholder="wss://..."
spellCheck={false}
className="flex-1 px-3 bg-transparent border-none rounded-l-lg h-9 placeholder:text-neutral-500 dark:placeholder:text-neutral-400"
/>
<select
{...register("purpose")}
className="flex-1 p-0 m-0 text-sm bg-transparent border-none outline-none h-9 ring-0 focus:outline-none focus:ring-0"
>
<option value="read">Read</option>
<option value="write">Write</option>
<option value="">Both</option>
</select>
</div>
<button
type="submit"
className="inline-flex items-center justify-center px-2 text-sm font-medium text-white rounded-lg shrink-0 h-9 w-14 bg-black/20 dark:bg-white/20 hover:bg-blue-500 disabled:opacity-50"
>
<PlusIcon className="size-7" />
</button>
</form>
</div>
</div>
<button
type="button"
onClick={() => save()}
disabled={isLoading}
className="inline-flex items-center justify-center w-full h-10 mt-4 text-sm font-semibold text-white bg-blue-500 rounded-lg shrink-0 hover:bg-blue-600 disabled:opacity-50"
>
{isLoading ? <Spinner /> : "Save & Relaunch"}
</button>
</div>
</div>
);
}
+198
View File
@@ -0,0 +1,198 @@
import { CancelIcon, PlusIcon } from "@lume/icons";
import type { ColumnRouteSearch } from "@lume/types";
import { Spinner } from "@lume/ui";
import { User } from "@/components/user";
import { createFileRoute } from "@tanstack/react-router";
import { useState } from "react";
import { toast } from "sonner";
import { NostrAccount, NostrQuery } from "@lume/system";
export const Route = createFileRoute("/create-group")({
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
return {
account: search.account,
label: search.label,
name: search.name,
};
},
loader: async () => {
const contacts = await NostrAccount.getContactList();
return contacts;
},
component: Screen,
});
function Screen() {
const [title, setTitle] = useState("");
const [npub, setNpub] = useState("");
const [users, setUsers] = useState<string[]>([
"npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445", // reya
]);
const [isLoading, setIsLoading] = useState(false);
const contacts = Route.useLoaderData();
const search = Route.useSearch();
const navigate = Route.useNavigate();
const toggleUser = (pubkey: string) => {
setUsers((prev) =>
prev.includes(pubkey)
? prev.filter((i) => i !== pubkey)
: [...prev, pubkey],
);
};
const addUser = () => {
if (!npub.startsWith("npub1")) return;
if (users.includes(npub)) return;
setUsers((prev) => [...prev, npub]);
setNpub("");
};
const submit = async () => {
try {
setIsLoading(true);
const key = `lume_group_${search.label}`;
const createGroup = await NostrQuery.setNstore(
key,
JSON.stringify(users),
);
if (createGroup) {
return navigate({ to: search.redirect, search: { ...search } });
}
} catch (e) {
setIsLoading(false);
toast.error(e);
}
};
return (
<div className="w-full h-full flex flex-col items-center justify-center gap-4">
<div className="text-center flex flex-col items-center justify-center">
<h1 className="text-2xl font-serif font-medium">
Focus feeds for people you like
</h1>
<p className="leading-tight text-neutral-700 dark:text-neutral-300">
Add some people for custom feeds.
</p>
</div>
<div className="w-4/5 max-w-full flex flex-col gap-3">
<div className="w-full h-9 shrink-0 flex items-center bg-black/5 dark:bg-white/5 rounded-lg">
<label
htmlFor="name"
className="w-16 border-r border-black/10 dark:border-white/10 shrink-0 text-center text-sm font-semibold"
>
Name
</label>
<input
name="name"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Enter a name for this group"
className="h-full bg-transparent border-none text-sm px-3 placeholder:text-neutral-600 focus:border-neutral-500 focus:ring-0 dark:placeholder:text-neutral-400"
/>
</div>
<div className="w-full flex flex-col items-center gap-3">
<div className="overflow-y-auto scrollbar-none p-2 w-full h-[450px] flex flex-col gap-3 bg-black/5 dark:bg-white/5 backdrop-blur-lg rounded-xl">
<div className="flex gap-2">
<input
name="npub"
value={npub}
onChange={(e) => setNpub(e.target.value)}
placeholder="npub1..."
className="h-9 w-full rounded-lg bg-black/10 dark:bg-white/10 border-none text-sm px-3 placeholder:text-neutral-600 focus:border-neutral-500 focus:ring-0 dark:placeholder:text-neutral-400"
/>
<button
type="button"
onClick={() => addUser()}
className="inline-flex size-9 rounded-lg items-center justify-center bg-black/20 dark:bg-white/20 shrink-0 text-white hover:bg-blue-500"
>
<PlusIcon className="size-6" />
</button>
</div>
<div className="flex flex-col gap-2">
<span className="text-sm font-semibold">Added</span>
<div className="flex flex-col gap-2">
{users.length ? (
users.map((item: string) => (
<button
key={item}
type="button"
onClick={() => toggleUser(item)}
className="inline-flex items-center justify-between px-3 py-2 rounded-lg bg-white dark:bg-black/20 backdrop-blur-lg shadow-primary dark:ring-1 ring-neutral-800/50"
>
<User.Provider pubkey={item}>
<User.Root className="flex items-center gap-2.5">
<User.Avatar className="size-8 rounded-full object-cover" />
<div className="flex items-center gap-1">
<User.Name className="text-sm font-medium" />
</div>
</User.Root>
</User.Provider>
<div>
<CancelIcon className="size-4" />
</div>
</button>
))
) : (
<div className="bg-black/5 dark:bg-white/5 text-sm flex items-center justify-center h-14 rounded-lg">
Empty.
</div>
)}
</div>
</div>
<div className="flex flex-col gap-2">
<span className="text-sm font-semibold">Contacts</span>
<div className="flex flex-col gap-2">
{contacts.length ? (
contacts.map((item: string) => (
<button
key={item}
type="button"
onClick={() => toggleUser(item)}
className="inline-flex items-center justify-between px-3 py-2 rounded-lg bg-white dark:bg-black/20 backdrop-blur-lg shadow-primary dark:ring-1 ring-neutral-800/50"
>
<User.Provider pubkey={item}>
<User.Root className="flex items-center gap-2.5">
<User.Avatar className="size-8 rounded-full object-cover" />
<div className="flex items-center gap-1">
<User.Name className="text-sm font-medium" />
</div>
</User.Root>
</User.Provider>
</button>
))
) : (
<div className="bg-black/5 dark:bg-white/5 text-sm flex items-center justify-center h-14 rounded-lg">
<p>
Find more user at{" "}
<a
href="https://www.nostr.directory/"
target="_blank"
className="text-blue-600 after:content-['_↗']"
rel="noreferrer"
>
Nostr Directory
</a>
</p>
</div>
)}
</div>
</div>
</div>
<button
type="button"
onClick={() => submit()}
disabled={isLoading || users.length < 1}
className="inline-flex items-center justify-center w-36 rounded-full h-9 bg-blue-500 text-white text-sm font-medium hover:bg-blue-600 disabled:opacity-50"
>
{isLoading ? <Spinner /> : "Confirm"}
</button>
</div>
</div>
</div>
);
}
@@ -0,0 +1,86 @@
import { NostrAccount } from "@lume/system";
import type { ColumnRouteSearch } from "@lume/types";
import { Spinner } from "@lume/ui";
import { createFileRoute } from "@tanstack/react-router";
import { useState } from "react";
import { toast } from "sonner";
export const Route = createFileRoute("/create-newsfeed/f2f")({
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
return {
account: search.account,
label: search.label,
name: search.name,
};
},
component: Screen,
});
function Screen() {
const navigate = Route.useNavigate();
const { redirect } = Route.useSearch();
const [npub, setNpub] = useState("");
const [isLoading, setIsLoading] = useState(false);
const submit = async () => {
if (!npub.startsWith("npub1"))
return toast.warning("You must enter a valid npub.");
try {
setIsLoading(true);
const sync = await NostrAccount.f2f(npub);
if (sync) {
return navigate({ to: redirect });
}
} catch (e) {
setIsLoading(false);
toast.error(String(e));
}
};
return (
<div className="overflow-y-auto scrollbar-none p-2 shrink-0 h-[450px] bg-white dark:bg-white/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50">
<div className="h-full flex flex-col justify-between">
<div className="flex-1 flex flex-col gap-1.5 justify-center px-5">
<p className="font-semibold text-neutral-500">
You already have a friend on Nostr?
</p>
<p>Instead of building the timeline by yourself.</p>
<p className="font-semibold text-neutral-500">
Just enter your friend's{" "}
<span className="text-blue-500">npub.</span>
</p>
<p>
You will have the same experience as your friend. Of course, you
always can edit your network later.
</p>
</div>
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-1">
<label htmlFor="npub" className="font-medium text-sm">
NPUB
</label>
<input
name="npub"
placeholder="npub1..."
value={npub}
onChange={(e) => setNpub(e.target.value)}
spellCheck={false}
className="h-11 rounded-lg bg-transparent border border-neutral-200 dark:border-neutral-800 px-3 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:placeholder:text-neutral-400"
/>
</div>
<button
type="button"
onClick={() => submit()}
className="inline-flex items-center justify-center w-full rounded-lg h-9 bg-blue-500 text-white text-sm font-medium hover:bg-blue-600"
>
{isLoading ? <Spinner /> : "Confirm"}
</button>
</div>
</div>
</div>
);
}
@@ -0,0 +1,70 @@
import type { ColumnRouteSearch } from "@lume/types";
import { cn } from "@lume/utils";
import { Link, Outlet } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/create-newsfeed")({
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
return {
account: search.account,
label: search.label,
name: search.name,
};
},
component: Screen,
});
function Screen() {
const search = Route.useSearch();
return (
<div className="flex flex-col items-center justify-center w-full h-full gap-4">
<div className="flex flex-col items-center justify-center text-center">
<h1 className="font-serif text-2xl font-medium">
Build up your timeline.
</h1>
<p className="leading-tight text-neutral-700 dark:text-neutral-300">
Follow some people to keep up to date with them.
</p>
</div>
<div className="flex flex-col w-4/5 max-w-full gap-3">
<div className="w-full h-9 shrink-0 flex items-center justify-between bg-black/5 dark:bg-white/5 rounded-lg px-0.5">
<Link
to="/create-newsfeed/users"
search={search}
className="flex-1 h-8"
>
{({ isActive }) => (
<div
className={cn(
"text-sm font-medium rounded-md h-full flex items-center justify-center",
isActive
? "bg-white dark:bg-white/20 shadow"
: "bg-transparent",
)}
>
Users
</div>
)}
</Link>
<Link
to="/create-newsfeed/f2f"
search={search}
className="flex-1 h-8"
>
{({ isActive }) => (
<div
className={cn(
"rounded-md h-full flex items-center justify-center",
isActive ? "bg-white dark:bg-white/20" : "bg-transparent",
)}
>
Friend to Friend
</div>
)}
</Link>
</div>
<Outlet />
</div>
</div>
);
}
@@ -0,0 +1,128 @@
import { createFileRoute } from "@tanstack/react-router";
import { Suspense, useState } from "react";
import { Await, defer } from "@tanstack/react-router";
import { User } from "@/components/user";
import { Spinner } from "@lume/ui";
import { toast } from "sonner";
import type { ColumnRouteSearch } from "@lume/types";
import { NostrAccount } from "@lume/system";
export const Route = createFileRoute("/create-newsfeed/users")({
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
return {
account: search.account,
label: search.label,
name: search.name,
};
},
loader: async ({ abortController }) => {
try {
return {
data: defer(
fetch("https://api.nostr.band/v0/trending/profiles", {
signal: abortController.signal,
}).then((res) => res.json()),
),
};
} catch (e) {
throw new Error(String(e));
}
},
component: Screen,
});
function Screen() {
const { data } = Route.useLoaderData();
const { redirect } = Route.useSearch();
const [isLoading, setIsLoading] = useState(false);
const [follows, setFollows] = useState<string[]>([]);
const navigate = Route.useNavigate();
const toggleFollow = (pubkey: string) => {
setFollows((prev) =>
prev.includes(pubkey)
? prev.filter((i) => i !== pubkey)
: [...prev, pubkey],
);
};
const submit = async () => {
try {
setIsLoading(true);
const newContactList = await NostrAccount.setContactList(follows);
if (newContactList) {
return navigate({ to: redirect });
}
} catch (e) {
setIsLoading(false);
toast.error(String(e));
}
};
return (
<div className="w-full flex flex-col items-center gap-3">
<div className="overflow-y-auto scrollbar-none p-2 w-full h-[450px] bg-black/5 dark:bg-white/5 backdrop-blur-lg rounded-xl">
<Suspense
fallback={
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
<button
type="button"
className="inline-flex items-center gap-2 text-sm font-medium"
disabled
>
<Spinner className="size-5" />
Loading...
</button>
</div>
}
>
<Await promise={data}>
{(users) =>
users.profiles.map((item: { pubkey: string }) => (
<div
key={item.pubkey}
className="h-max w-full overflow-hidden mb-2 p-2 bg-white dark:bg-black/20 backdrop-blur-lg rounded-lg shadow-primary dark:ring-1 ring-neutral-800/50"
>
<User.Provider pubkey={item.pubkey}>
<User.Root>
<div className="flex h-full w-full flex-col gap-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<User.Avatar className="size-7 shrink-0 rounded-full object-cover" />
<User.Name className="text-sm leadning-tight max-w-[15rem] truncate font-semibold" />
</div>
<button
type="button"
onClick={() => toggleFollow(item.pubkey)}
className="inline-flex h-7 w-20 items-center justify-center rounded-lg bg-black/10 text-sm font-medium hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
>
{follows.includes(item.pubkey)
? "Unfollow"
: "Follow"}
</button>
</div>
<User.About className="line-clamp-3 max-w-none select-text text-neutral-800 dark:text-neutral-400" />
</div>
</User.Root>
</User.Provider>
</div>
))
}
</Await>
</Suspense>
</div>
<button
type="button"
onClick={() => submit()}
disabled={isLoading || follows.length < 1}
className="inline-flex items-center justify-center w-36 rounded-full h-9 bg-blue-500 text-white text-sm font-medium hover:bg-blue-600 disabled:opacity-50"
>
{isLoading ? <Spinner /> : "Confirm"}
</button>
</div>
);
}
+112
View File
@@ -0,0 +1,112 @@
import { CheckCircleIcon } from "@lume/icons";
import { NostrQuery } from "@lume/system";
import type { ColumnRouteSearch } from "@lume/types";
import { Spinner } from "@lume/ui";
import { TOPICS } from "@lume/utils";
import { createFileRoute } from "@tanstack/react-router";
import { useState } from "react";
import { toast } from "sonner";
type Topic = {
title: string;
content: string[];
};
export const Route = createFileRoute("/create-topic")({
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
return {
account: search.account,
label: search.label,
name: search.name,
};
},
component: Screen,
});
function Screen() {
const [topics, setTopics] = useState<Topic[]>([]);
const [isLoading, setIsLoading] = useState(false);
const search = Route.useSearch();
const navigate = Route.useNavigate();
const toggleTopic = (topic: Topic) => {
setTopics((prev) =>
prev.find((item) => item.title === topic.title)
? prev.filter((i) => i.title !== topic.title)
: [...prev, topic],
);
};
const submit = async () => {
try {
setIsLoading(true);
const key = `lume_topic_${search.label}`;
const createTopic = await NostrQuery.setNstore(
key,
JSON.stringify(topics),
);
if (createTopic) {
return navigate({ to: search.redirect, search: { ...search } });
}
} catch (e) {
setIsLoading(false);
toast.error(String(e));
}
};
return (
<div className="flex flex-col items-center justify-center w-full h-full gap-4">
<div className="flex flex-col items-center justify-center text-center">
<h1 className="font-serif text-2xl font-medium">
What are your interests?
</h1>
<p className="leading-tight text-neutral-700 dark:text-neutral-300">
Add some topics you want to focus on.
</p>
</div>
<div className="flex flex-col w-4/5 max-w-full gap-3">
<div className="flex items-center justify-between w-full px-3 rounded-lg h-9 shrink-0 bg-black/5 dark:bg-white/5">
<span className="text-sm font-medium">Added: {topics.length}</span>
</div>
<div className="flex flex-col items-center w-full gap-3">
<div className="overflow-y-auto scrollbar-none p-2 w-full h-[450px] bg-black/5 dark:bg-white/5 backdrop-blur-lg rounded-xl">
<div className="flex flex-col gap-3">
{TOPICS.map((topic) => (
<button
key={topic.title}
type="button"
onClick={() => toggleTopic(topic)}
className="flex items-center justify-between px-3 bg-white border border-transparent rounded-lg h-11 dark:bg-black/20 backdrop-blur-lg hover:border-blue-500 shadow-primary dark:ring-1 ring-neutral-800/50"
>
<div className="inline-flex items-center gap-1">
<div>{topic.icon}</div>
<div className="text-sm font-medium">
<span>{topic.title}</span>
<span className="ml-1 italic font-normal text-neutral-400 dark:text-neutral-600">
{topic.content.length} hashtags
</span>
</div>
</div>
{topics.find((item) => item.title === topic.title) ? (
<CheckCircleIcon className="text-teal-500 size-4" />
) : null}
</button>
))}
</div>
</div>
<button
type="button"
onClick={() => submit()}
disabled={isLoading || topics.length < 1}
className="inline-flex items-center justify-center text-sm font-medium text-white bg-blue-500 rounded-full w-36 h-9 hover:bg-blue-600 disabled:opacity-50"
>
{isLoading ? <Spinner /> : "Confirm"}
</button>
</div>
</div>
</div>
);
}
@@ -0,0 +1,77 @@
import { AddMediaIcon } from "@lume/icons";
import { NostrQuery } from "@lume/system";
import { Spinner } from "@lume/ui";
import { insertImage, isImagePath } from "@lume/utils";
import type { UnlistenFn } from "@tauri-apps/api/event";
import { getCurrent } from "@tauri-apps/api/window";
import { useEffect, useState } from "react";
import { useSlateStatic } from "slate-react";
import { toast } from "sonner";
export function MediaButton() {
const editor = useSlateStatic();
const [loading, setLoading] = useState(false);
const upload = async () => {
try {
// start loading
setLoading(true);
const image = await NostrQuery.upload();
insertImage(editor, image);
// reset loading
setLoading(false);
} catch (e) {
setLoading(false);
toast.error(`Upload failed, error: ${e}`);
}
};
useEffect(() => {
let unlisten: UnlistenFn = undefined;
async function listenFileDrop() {
const window = getCurrent();
if (!unlisten) {
unlisten = await window.listen("tauri://file-drop", async (event) => {
// @ts-ignore, lfg !!!
const items: string[] = event.payload.paths;
// start loading
setLoading(true);
// upload all images
for (const item of items) {
if (isImagePath(item)) {
const image = await NostrQuery.upload(item);
insertImage(editor, image);
}
}
// stop loading
setLoading(false);
});
}
}
listenFileDrop();
return () => {
if (unlisten) unlisten();
};
}, []);
return (
<button
type="button"
onClick={() => upload()}
disabled={loading}
className="inline-flex items-center h-8 gap-2 px-2.5 text-sm rounded-lg text-black/70 dark:text-white/70 w-max hover:bg-black/10 dark:hover:bg-white/10"
>
{loading ? (
<Spinner className="size-4" />
) : (
<AddMediaIcon className="size-4" />
)}
Add media
</button>
);
}
@@ -0,0 +1,21 @@
import { PowIcon } from "@lume/icons";
import type { Dispatch, SetStateAction } from "react";
export function PowButton({
setDifficulty,
}: {
setDifficulty: Dispatch<SetStateAction<{ enable: boolean; num: number }>>;
}) {
return (
<button
type="button"
onClick={() =>
setDifficulty((prev) => ({ ...prev, enable: !prev.enable }))
}
className="inline-flex items-center h-8 gap-2 px-2.5 text-sm rounded-lg text-black/70 dark:text-white/70 w-max hover:bg-black/10 dark:hover:bg-white/10"
>
<PowIcon className="size-4" />
PoW
</button>
);
}
@@ -0,0 +1,19 @@
import { NsfwIcon } from "@lume/icons";
import type { Dispatch, SetStateAction } from "react";
export function WarningButton({
setWarning,
}: {
setWarning: Dispatch<SetStateAction<{ enable: boolean; reason: string }>>;
}) {
return (
<button
type="button"
onClick={() => setWarning((prev) => ({ ...prev, enable: !prev.enable }))}
className="inline-flex items-center h-8 gap-2 px-2.5 text-sm rounded-lg text-black/70 dark:text-white/70 w-max hover:bg-black/10 dark:hover:bg-white/10"
>
<NsfwIcon className="size-4" />
Mark as sensitive
</button>
);
}
+399
View File
@@ -0,0 +1,399 @@
import { ComposeFilledIcon } from "@lume/icons";
import { Spinner } from "@lume/ui";
import { cn, insertImage, insertNostrEvent, isImageUrl } from "@lume/utils";
import { createFileRoute } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { type Descendant, Node, Transforms, createEditor } from "slate";
import {
Editable,
ReactEditor,
Slate,
useFocused,
useSelected,
useSlateStatic,
withReact,
} from "slate-react";
import { MediaButton } from "./-components/media";
import { LumeEvent, useEvent } from "@lume/system";
import { WarningButton } from "./-components/warning";
import { MentionNote } from "@/components/note/mentions/note";
import { PowButton } from "./-components/pow";
import { User } from "@/components/user";
import { Note } from "@/components/note";
import { nip19 } from "nostr-tools";
type EditorSearch = {
reply_to: string;
quote: string;
};
type EditorElement = {
type: string;
children: Descendant[];
eventId?: string;
};
export const Route = createFileRoute("/editor/")({
validateSearch: (search: Record<string, string>): EditorSearch => {
return {
reply_to: search.reply_to,
quote: search.quote,
};
},
beforeLoad: ({ search }) => {
let initialValue: EditorElement[];
if (search?.quote?.length) {
const eventId = nip19.noteEncode(search.quote);
initialValue = [
{
type: "paragraph",
children: [{ text: "" }],
},
{
type: "event",
eventId: `nostr:${eventId}`,
children: [{ text: "" }],
},
];
} else {
initialValue = [
{
type: "paragraph",
children: [{ text: "" }],
},
];
}
return { initialValue };
},
component: Screen,
});
function Screen() {
const { reply_to } = Route.useSearch();
const { initialValue } = Route.useRouteContext();
const [editorValue, setEditorValue] = useState<EditorElement[]>(null);
const [loading, setLoading] = useState(false);
const [warning, setWarning] = useState({ enable: false, reason: "" });
const [difficulty, setDifficulty] = useState({ enable: false, num: 21 });
const [editor] = useState(() =>
withMentions(withNostrEvent(withImages(withReact(createEditor())))),
);
const reset = () => {
// @ts-expect-error, backlog
editor.children = [{ type: "paragraph", children: [{ text: "" }] }];
setEditorValue([{ type: "paragraph", children: [{ text: "" }] }]);
};
const serialize = (nodes: Descendant[]) => {
return nodes
.map((n) => {
// @ts-expect-error, backlog
if (n.type === "image") return n.url;
// @ts-expect-error, backlog
if (n.type === "event") return n.eventId;
// @ts-expect-error, backlog
if (n.children.length) {
// @ts-expect-error, backlog
return n.children
.map((n) => {
if (n.type === "mention") return n.npub;
return Node.string(n).trim();
})
.join(" ");
}
return Node.string(n);
})
.join("\n");
};
const publish = async () => {
try {
// start loading
setLoading(true);
const content = serialize(editor.children);
const eventId = await LumeEvent.publish(
content,
warning.enable && warning.reason.length ? warning.reason : null,
difficulty.enable && difficulty.num > 0 ? difficulty.num : null,
reply_to,
);
if (eventId) {
// stop loading
setLoading(false);
// reset form
reset();
}
} catch (e) {
setLoading(false);
}
};
useEffect(() => {
setEditorValue(initialValue);
}, [initialValue]);
if (!editorValue) return null;
return (
<div className="flex flex-col w-full h-full">
<Slate editor={editor} initialValue={editorValue}>
<div data-tauri-drag-region className="h-9 shrink-0" />
<div className="flex flex-col flex-1 overflow-y-auto">
{reply_to?.length ? (
<div className="flex items-center gap-3 px-2.5 pb-3 border-b border-black/5 dark:border-white/5">
<div className="text-sm font-semibold shrink-0">Reply to:</div>
<ChildNote id={reply_to} />
</div>
) : null}
<div className="px-4 py-4 overflow-y-auto">
<Editable
key={JSON.stringify(editorValue)}
autoFocus={true}
autoCapitalize="none"
autoCorrect="none"
spellCheck={false}
renderElement={(props) => <Element {...props} />}
placeholder={
reply_to ? "Type your reply..." : "What're you up to?"
}
className="focus:outline-none"
/>
</div>
</div>
{warning.enable ? (
<div className="flex items-center w-full px-4 border-t h-11 shrink-0 border-black/5 dark:border-white/5">
<span className="text-sm shrink-0 text-black/50 dark:text-white/50">
Reason:
</span>
<input
type="text"
placeholder="NSFW..."
value={warning.reason}
onChange={(e) =>
setWarning((prev) => ({ ...prev, reason: e.target.value }))
}
className="flex-1 text-sm bg-transparent border-none focus:outline-none focus:ring-0 placeholder:text-black/50 dark:placeholder:text-white/50"
/>
</div>
) : null}
{difficulty.enable ? (
<div className="flex items-center w-full px-4 border-t h-11 shrink-0 border-black/5 dark:border-white/5">
<span className="text-sm shrink-0 text-black/50 dark:text-white/50">
Difficulty:
</span>
<input
type="text"
inputMode="numeric"
pattern="[0-9]"
onKeyDown={(event) => {
if (!/[0-9]/.test(event.key)) {
event.preventDefault();
}
}}
placeholder="21"
defaultValue={difficulty.num}
onChange={(e) =>
setWarning((prev) => ({ ...prev, num: Number(e.target.value) }))
}
className="flex-1 text-sm bg-transparent border-none focus:outline-none focus:ring-0 placeholder:text-black/50 dark:placeholder:text-white/50"
/>
</div>
) : null}
<div
data-tauri-drag-region
className="flex items-center w-full h-16 gap-4 px-4 border-t divide-x divide-black/5 dark:divide-white/5 shrink-0 border-black/5 dark:border-white/5"
>
<button
type="button"
onClick={() => publish()}
className="inline-flex items-center justify-center h-8 gap-1 px-2.5 text-sm font-medium rounded-lg bg-black/10 w-max hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
>
{loading ? (
<Spinner className="size-4" />
) : (
<ComposeFilledIcon className="size-4" />
)}
Publish
</button>
<div className="inline-flex items-center flex-1 gap-2 pl-4">
<MediaButton />
<WarningButton setWarning={setWarning} />
<PowButton setDifficulty={setDifficulty} />
</div>
</div>
</Slate>
</div>
);
}
function ChildNote({ id }: { id: string }) {
const { isLoading, isError, data } = useEvent(id);
if (isLoading) {
return <Spinner className="size-5" />;
}
if (isError || !data) {
return <div>Event not found with your current relay set.</div>;
}
return (
<Note.Provider event={data}>
<Note.Root className="flex items-center gap-2">
<User.Provider pubkey={data.pubkey}>
<User.Root className="shrink-0">
<User.Avatar className="rounded-full size-8 shrink-0" />
</User.Root>
</User.Provider>
<div className="content-break line-clamp-1">{data.content}</div>
</Note.Root>
</Note.Provider>
);
}
const withNostrEvent = (editor: ReactEditor) => {
const { insertData, isVoid } = editor;
editor.isVoid = (element) => {
// @ts-expect-error, wtf
return element.type === "event" ? true : isVoid(element);
};
editor.insertData = (data) => {
const text = data.getData("text/plain");
if (text.startsWith("nevent") || text.startsWith("note")) {
insertNostrEvent(editor, text);
} else {
insertData(data);
}
};
return editor;
};
const withMentions = (editor: ReactEditor) => {
const { isInline, isVoid, markableVoid } = editor;
editor.isInline = (element) => {
// @ts-expect-error, wtf
return element.type === "mention" ? true : isInline(element);
};
editor.isVoid = (element) => {
// @ts-expect-error, wtf
return element.type === "mention" ? true : isVoid(element);
};
editor.markableVoid = (element) => {
// @ts-expect-error, wtf
return element.type === "mention" || markableVoid(element);
};
return editor;
};
const withImages = (editor: ReactEditor) => {
const { insertData, isVoid } = editor;
editor.isVoid = (element) => {
// @ts-expect-error, wtf
return element.type === "image" ? true : isVoid(element);
};
editor.insertData = (data) => {
const text = data.getData("text/plain");
if (isImageUrl(text)) {
insertImage(editor, text);
} else {
insertData(data);
}
};
return editor;
};
const Image = ({ attributes, element, children }) => {
const editor = useSlateStatic();
const selected = useSelected();
const focused = useFocused();
const path = ReactEditor.findPath(editor as ReactEditor, element);
return (
<div {...attributes}>
{children}
<img
src={element.url}
alt={element.url}
className={cn(
"my-2 h-auto w-1/2 rounded-lg object-cover ring-2 outline outline-1 -outline-offset-1 outline-black/15",
selected && focused ? "ring-blue-500" : "ring-transparent",
)}
onClick={() => Transforms.removeNodes(editor, { at: path })}
onKeyDown={() => Transforms.removeNodes(editor, { at: path })}
/>
</div>
);
};
const Mention = ({ attributes, element }) => {
const editor = useSlateStatic();
const path = ReactEditor.findPath(editor as ReactEditor, element);
return (
<span
{...attributes}
type="button"
contentEditable={false}
onClick={() => Transforms.removeNodes(editor, { at: path })}
className="inline-block text-blue-500 align-baseline hover:text-blue-600"
>{`@${element.name}`}</span>
);
};
const Event = ({ attributes, element, children }) => {
const editor = useSlateStatic();
const path = ReactEditor.findPath(editor as ReactEditor, element);
return (
<div {...attributes}>
{children}
<div
contentEditable={false}
className="relative my-2 user-select-none"
onClick={() => Transforms.removeNodes(editor, { at: path })}
onKeyDown={() => Transforms.removeNodes(editor, { at: path })}
>
<MentionNote eventId={element.eventId} openable={false} />
</div>
</div>
);
};
const Element = (props) => {
const { attributes, children, element } = props;
switch (element.type) {
case "image":
return <Image {...props} />;
case "mention":
return <Mention {...props} />;
case "event":
return <Event {...props} />;
default:
return (
<p {...attributes} className="text-[15px]">
{children}
</p>
);
}
};
@@ -0,0 +1,82 @@
import { Note } from "@/components/note";
import { type LumeEvent, NostrQuery } from "@lume/system";
import { Box, Container, Spinner } from "@lume/ui";
import { createFileRoute } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { WindowVirtualizer } from "virtua";
import { Reply } from "./-components/reply";
export const Route = createFileRoute("/events/$eventId")({
beforeLoad: async () => {
const settings = await NostrQuery.getUserSettings();
return { settings };
},
loader: async ({ params }) => {
const event = await NostrQuery.getEvent(params.eventId);
return event;
},
component: Screen,
});
function Screen() {
const event = Route.useLoaderData();
const [reload, setReload] = useState(false);
const [replies, setReplies] = useState<LumeEvent[]>(null);
useEffect(() => {
let mounted = true;
if (event) {
event.getAllReplies().then((data) => {
if (mounted) setReplies(data);
});
}
return () => {
mounted = false;
};
}, [event]);
return (
<Container withDrag>
<Box className="scrollbar-none">
<WindowVirtualizer>
<Note.Provider event={event}>
<Note.Root>
<div className="flex items-center justify-between px-3 h-14">
<Note.User />
<Note.Menu />
</div>
<Note.ContentLarge className="px-3" />
<div className="flex items-center justify-end gap-2 px-3 mt-4 h-11">
<Note.Reply large />
<Note.Repost large />
<Note.Zap large />
</div>
</Note.Root>
</Note.Provider>
<div className="flex flex-col">
<div className="flex items-center px-3 text-sm font-semibold border-t h-11 text-neutral-700 dark:text-neutral-300 border-neutral-100 dark:border-neutral-900">
Replies ({replies?.length ?? 0})
</div>
{!replies ? (
<Spinner />
) : !replies.length ? (
<div className="flex items-center justify-center w-full">
<div className="flex flex-col items-center justify-center gap-2 py-6">
<h3 className="text-3xl">👋</h3>
<p className="leading-none text-neutral-600 dark:text-neutral-400">
Be the first to Reply!
</p>
</div>
</div>
) : (
replies.map((event) => <Reply key={event.id} event={event} />)
)}
</div>
</WindowVirtualizer>
</Box>
</Container>
);
}
@@ -0,0 +1,36 @@
import { Note } from "@/components/note";
import type { LumeEvent } from "@lume/system";
import { cn } from "@lume/utils";
import { SubReply } from "./subReply";
export function Reply({ event }: { event: LumeEvent }) {
return (
<Note.Provider event={event}>
<Note.Root className="border-t border-neutral-100 dark:border-neutral-900">
<div className="flex items-center justify-between px-3 h-14">
<Note.User />
<Note.Menu />
</div>
<Note.ContentLarge className="px-3" />
<div className="flex items-center gap-4 px-3 mt-3 h-14">
<Note.Reply />
<Note.Repost />
<Note.Zap />
</div>
<div
className={cn(
event.replies?.length > 0
? "py-2 pl-3 flex flex-col gap-3 divide-y divide-neutral-100 bg-neutral-50 dark:bg-white/5 border-l-2 border-blue-500 dark:divide-neutral-900"
: "",
)}
>
{event.replies?.length > 0
? event.replies?.map((childEvent) => (
<SubReply key={childEvent.id} event={childEvent} />
))
: null}
</div>
</Note.Root>
</Note.Provider>
);
}

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