Compare commits

...
515 Commits
Author SHA1 Message Date
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 32843018aa chore(release): v1.2.7 2023-10-04 14:27:48 +07:00
Ren Amamiya f80dd78a8e wip 2023-10-04 14:11:45 +07:00
Ren Amamiya 9df4835be3 fix ci 2023-10-04 11:17:04 +07:00
Ren Amamiya 480580890e wip: new chat screen 2023-10-04 11:15:10 +07:00
Ren Amamiya ca57ef1760 Merge branch 'main' into feat/nip28 2023-10-04 07:48:52 +07:00
Ren Amamiya 8e39bca57c fix build 2023-10-04 07:48:20 +07:00
Ren Amamiya 8d9ec0dcfd update config 2023-10-03 18:43:39 +07:00
Ren Amamiya 428d52f175 wip 2023-10-03 16:24:09 +07:00
Ren Amamiya cdeb5afd28 upgrade to tauri v1.5.1 2023-10-03 07:38:23 +07:00
Ren Amamiya 1f3ba09cec new app icon 2023-10-02 15:36:20 +07:00
Ren Amamiya 4915b833e7 small fixed 2023-10-01 15:00:38 +07:00
Ren Amamiya 674e5f0339 finish relay manegament screen 2023-10-01 09:02:14 +07:00
Ren Amamiya 11ed618a7f wip: relay manegament screen 2023-09-30 19:07:17 +07:00
Ren Amamiya a2e3247432 wip 2023-09-30 15:12:33 +07:00
Ren Amamiya 09b3eeda99 small fixes and bump version 2023-09-29 12:40:02 +07:00
Ren Amamiya 700f3eb85f upgrade to tauri v1.5.0 2023-09-29 09:19:40 +07:00
Ren Amamiya 2f87ed8949 polish widget code 2023-09-29 09:11:38 +07:00
Ren Amamiya cb3c95b133 clean up and improve perf 2023-09-28 16:18:04 +07:00
Ren Amamiya 4f4e2f5ccd wip: multi account 2023-09-28 14:00:52 +07:00
Ren AmamiyaandGitHub 0e6fc65b08 Merge pull request #91 from luminous-devs/feat/ui-patch
v1.2.6
2023-09-28 08:28:34 +07:00
Ren Amamiya 876d351358 update dependencies and polish 2023-09-28 08:22:38 +07:00
Ren Amamiya c80414a72d small fixes and support $ boost sign 2023-09-28 07:29:05 +07:00
Ren Amamiya 7cef6efa6f fix crash on windows 2023-09-27 18:24:58 +07:00
Ren Amamiya 74ff49b8db fix app window is not resizable 2023-09-27 16:11:33 +07:00
Ren Amamiya 2b50fc438f improve startup time 2023-09-27 14:53:01 +07:00
Ren Amamiya b339e842ca perf improve 2023-09-27 08:32:19 +07:00
Ren Amamiya 1d93f8cf6a clean up 2023-09-26 09:40:02 +07:00
Ren Amamiya 236131087a polish 2023-09-26 09:05:39 +07:00
Ren Amamiya a66770989b wip: finish browse users 2023-09-25 14:35:47 +07:00
Ren Amamiya 9ff74599eb customize traffic light on macOS 2023-09-25 07:43:13 +07:00
Ren Amamiya c049fa8865 wip: update browse user screen 2023-09-24 15:42:49 +07:00
Ren Amamiya 41b12746a7 wip 2023-09-24 09:13:42 +07:00
Ren Amamiya 50f90ddcc2 wip 2023-09-24 07:55:27 +07:00
Ren Amamiya c9bc7b81dd wip: browse user 2023-09-22 14:13:55 +07:00
Ren Amamiya 18a9ba3fb0 wip: replace ndk cache with metadata table 2023-09-21 16:11:35 +07:00
Ren Amamiya 413571ee7f wip 2023-09-21 15:28:01 +07:00
Ren Amamiya 17fe3bb1f6 wip: timeline 2023-09-21 09:11:45 +07:00
Ren Amamiya 0e5adb246f resizable widget 2023-09-20 14:31:14 +07:00
Ren Amamiya 296136203a update dependencies 2023-09-20 08:22:02 +07:00
Ren Amamiya 1bbfebc2b8 fix ci again 2023-09-19 16:31:38 +07:00
Ren Amamiya d84e97b0d4 fix ci 2023-09-19 15:59:50 +07:00
Ren Amamiya 824aa8fa28 back to pnpm, bun is fun but cannot generate tauri build 2023-09-19 15:56:33 +07:00
Ren Amamiya 2b34ef3b7a fix build error 2023-09-19 15:29:26 +07:00
Ren Amamiya 4fa8f40e6a improve nwc 2023-09-19 15:06:12 +07:00
Ren Amamiya c1bddeb6ed bump version 2023-09-19 11:20:15 +07:00
Ren Amamiya 5c2bfa0ea3 improve notification 2023-09-19 11:15:35 +07:00
Ren Amamiya 60e93965ea respect user's relay list (kind 10002) 2023-09-19 08:01:57 +07:00
Ren Amamiya 380d1fb930 temporary using default relays 2023-09-18 15:42:17 +07:00
Ren Amamiya 53aa13c8aa clean up messy code 2023-09-18 09:50:15 +07:00
Ren Amamiya 13f5190ba1 update dependencies and better handle repost 2023-09-17 16:14:04 +07:00
Ren AmamiyaandGitHub c590e290e0 Merge pull request #87 from luminous-devs/feat/improve-onboarding
merge now, improve later
2023-09-17 08:44:16 +07:00
Ren Amamiya cdf86a2613 polish 2023-09-17 08:43:42 +07:00
Ren Amamiya 8726e22b38 replace nostr.com with njump.me 2023-09-17 08:03:29 +07:00
Ren Amamiya 1206486016 partial support replaceable event 2023-09-16 16:06:01 +07:00
Ren Amamiya 11ad281d72 wip 2023-09-16 08:57:24 +07:00
Ren Amamiya fe4bfa1699 wip: learn nostr widget 2023-09-16 07:47:44 +07:00
Ren Amamiya c6a0636e8c add complete screen 2023-09-15 10:29:39 +07:00
Ren Amamiya d3db6492d9 update onboarding 2023-09-15 08:58:09 +07:00
Ren Amamiya 8f8617d8f9 update import key flow 2023-09-14 16:51:38 +07:00
Ren Amamiya 8e513404c3 update create account flow 2023-09-14 09:20:36 +07:00
Ren Amamiya 5a6dd172b1 small fixes 2023-09-13 11:10:24 +07:00
Ren Amamiya fa0d7cac31 hide nwc secret in frontend 2023-09-12 16:16:57 +07:00
Ren Amamiya 432b2ae185 polish nwc connection flow 2023-09-12 16:00:41 +07:00
Ren Amamiya fb8a6581dd replace pnpm with bun 2023-09-12 08:43:12 +07:00
Ren Amamiya a4f221f868 wip: redesign nwc 2023-09-12 08:27:29 +07:00
Ren Amamiya 602d012efe fix alby connection 2023-09-11 07:52:43 +07:00
Ren Amamiya 5bf816eba2 fully suport alby nostr-wallet-connect 2023-09-10 16:25:35 +07:00
Ren Amamiya a33c9d3517 wip: integrate alby 2023-09-10 07:19:36 +07:00
Ren Amamiya 1553f5ced2 bump version 2023-09-09 07:21:02 +07:00
Ren Amamiya 41901b2174 fix hashtag step in onboarding 2023-09-08 17:02:01 +07:00
Ren Amamiya 177e4c1ff7 fix some bugs 2023-09-08 15:29:41 +07:00
Ren Amamiya 10036500cb small fixes 2023-09-08 09:22:09 +07:00
Ren Amamiya a1fa777f8c fix logout function, prepare for multi-account support 2023-09-08 08:36:15 +07:00
Ren Amamiya 472925bb05 small fixes 2023-09-07 12:19:28 +07:00
Ren Amamiya 8eb11efb34 update notification 2023-09-07 11:34:26 +07:00
Ren Amamiya 48066a4018 bump version 2023-09-06 16:53:05 +07:00
Ren Amamiya 5c8850ea8f redesign widget list 2023-09-06 14:30:57 +07:00
Ren Amamiya 09aea3cff5 update widgets 2023-09-06 08:58:02 +07:00
Ren Amamiya 45c5a890b9 update tauri config 2023-09-06 07:46:31 +07:00
Ren Amamiya 69a3e85cb3 small updates and bump version 2023-09-05 17:25:00 +07:00
Ren Amamiya 224439f62b support drag and drop upload in composer 2023-09-05 13:07:21 +07:00
Ren Amamiya 2389ad5fdc replace void.cat with nostr.build 2023-09-05 12:46:00 +07:00
Ren Amamiya 4019623d99 small updates 2023-09-05 08:50:13 +07:00
Ren Amamiya 57c17ffbf9 use nostr.com to display unfound event 2023-09-04 18:09:41 +07:00
Ren Amamiya 98d2ccfc86 small fixes 2023-09-04 17:18:28 +07:00
Ren Amamiya c74a81cfdb re-add link preview 2023-09-04 14:35:57 +07:00
Ren Amamiya 3ebcf4a981 new parser, faster than before 50% 2023-09-04 14:05:04 +07:00
Ren Amamiya 5d45027776 fix build issue 2023-09-04 07:44:57 +07:00
Ren Amamiya 21ea8309c7 fix ci (final) 2023-09-03 12:02:59 +07:00
Ren Amamiya 431331174a fix ci again 2023-09-03 11:30:45 +07:00
Ren Amamiya c26cfc038d fix ci 2023-09-03 11:08:13 +07:00
Ren Amamiya 39b7b34bb7 new mention popup in composer 2023-09-03 08:43:08 +07:00
Ren Amamiya a4cf65e7c2 update ui consistent for cross platform 2023-09-03 07:43:38 +07:00
Ren Amamiya 37668393f1 expt: disable note metadata 2023-09-02 17:28:05 +07:00
Ren AmamiyaandGitHub 4309f734b6 Merge pull request #80 from luminous-devs/revert-to-tauri-v1
Revert to Tauri v1.4.0
2023-09-02 12:49:51 +07:00
Ren Amamiya b4957bae1f native fetch and shadow 2023-09-02 12:49:04 +07:00
Ren Amamiya 7a3b19bf7b revert to tauri v1 2023-09-02 12:15:48 +07:00
Ren Amamiya 1931373515 update composer 2023-09-02 08:50:54 +07:00
Ren Amamiya 28939d1733 improve hashtag parser 2023-09-01 18:26:22 +07:00
Ren Amamiya e6d35bc635 fix mention in composer and improve error handling 2023-09-01 15:57:31 +07:00
Ren Amamiya cc315a190a fully support nip05 2023-09-01 08:58:33 +07:00
629 changed files with 44604 additions and 19520 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 }}
+7 -5
View File
@@ -1,4 +1,4 @@
name: 'publish'
name: 'Publish'
on: workflow_dispatch
env:
@@ -32,7 +32,7 @@ jobs:
if: matrix.settings.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y libglib2.0-dev libsoup-3.0 libgtk-3-dev webkit2gtk-4.1 javascriptcoregtk-4.1 libappindicator3-dev librsvg2-dev patchelf
sudo apt-get install -y build-essential libssl-dev javascriptcoregtk-4.1 libayatana-appindicator3-dev libsoup-3.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev webkit2gtk-4.1 librsvg2-dev patchelf
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
@@ -51,18 +51,20 @@ jobs:
- 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 }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with:
tagName: v__VERSION__
releaseName: 'App v__VERSION__'
releaseName: 'v__VERSION__'
releaseBody: 'See the assets to download this version and install.'
releaseDraft: true
prerelease: false
args: ${{ matrix.settings.args }}
includeDebug: false
+33 -27
View File
@@ -1,31 +1,37 @@
# 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
pnpm-lock.yaml
*.db
*.db-journal
.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/
-4
View File
@@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm exec lint-staged
-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
}
+41 -12
View File
@@ -1,19 +1,35 @@
### Introduction
_Note_: Lume is under rewrite to using Rust Nostr as back-end and more lightweight front-end. If you need stable version, you can download v3 and below.
Lume is a nostr client
Source code for v3 is stored here: https://github.com/lumehq/lume/tree/old
### Usage
--
Download Lume for your platform here: [https://github.com/luminous-devs/lume/releases](https://github.com/luminous-devs/lume/releases)
## Introduction
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
Download Lume v3 (v3.0.1-stable) for your platform here: [https://github.com/lumehq/lume/releases](https://github.com/lumehq/lume/releases)
Supported platform: macOS, Windows and Linux
### Develop
## Prerequisites
- Node.js >= 18: https://nodejs.org/en
- 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
@@ -22,20 +38,33 @@ Install packages
pnpm install
```
Run dev
Run dev build
```
pnpm tauri dev
```
Build
Generate production build
```
pnpm tauri build
```
(Advance) - Generate SQLite migration
## Nix
```
pnpm add-migrate <migrate_name>
```
Requirements:
1. [Install Nix](https://zero-to-flakes.com/install)
1. [Setup `direnv`](https://zero-to-flakes.com/direnv)
`cd` into the root folder of the project to enter `nix develop` shell. Run `direnv allow` (only once). Then run `pnpm` or `bun` (experimental) commands as described above.
## 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 bg-white font-sans text-black antialiased dark:bg-black dark:text-white"
>
<div id="root" class="h-full w-full"></div>
<script type="module" src="/src/app.tsx"></script>
</body>
</html>
+53
View File
@@ -0,0 +1,53 @@
{
"name": "@lume/desktop2",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@lume/ark": "workspace:^",
"@lume/icons": "workspace:^",
"@lume/ui": "workspace:^",
"@lume/utils": "workspace:^",
"@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-popover": "^1.0.7",
"@tanstack/query-sync-storage-persister": "^5.24.1",
"@tanstack/react-query": "^5.24.1",
"@tanstack/react-query-persist-client": "^5.24.1",
"@tanstack/react-router": "^1.18.1",
"i18next": "^23.10.0",
"i18next-resources-to-backend": "^1.2.0",
"nostr-tools": "^2.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^14.0.5",
"slate": "^0.101.5",
"slate-react": "^0.101.6",
"sonner": "^1.4.3",
"virtua": "^0.27.5"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@tanstack/router-devtools": "^1.18.1",
"@tanstack/router-vite-plugin": "^1.18.1",
"@types/react": "^18.2.61",
"@types/react-dom": "^18.2.19",
"@vitejs/plugin-react-swc": "^3.6.0",
"autoprefixer": "^10.4.18",
"postcss": "^8.4.35",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3",
"vite": "^5.1.4",
"vite-plugin-top-level-await": "^1.4.1",
"vite-tsconfig-paths": "^4.3.1"
}
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 KiB

+46
View File
@@ -0,0 +1,46 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
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;
}
media-controller {
@apply w-full;
}
@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);
}
}
+81
View File
@@ -0,0 +1,81 @@
import { useArk } from "@lume/ark";
import { ArkProvider } from "./ark";
import { QueryClient } 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 i18n from "./locale";
import { Toaster } from "sonner";
import { locale, platform } from "@tauri-apps/plugin-os";
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister";
import { routeTree } from "./router.gen"; // auto generated file
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours
staleTime: 1000 * 60 * 5, // 5 minutes
},
},
});
const persister = createSyncStoragePersister({
storage: window.localStorage,
});
const platformName = await platform();
const osLocale = (await locale()).slice(0, 2);
// Set up a Router instance
const router = createRouter({
routeTree,
context: {
ark: undefined!,
platform: platformName,
locale: osLocale,
queryClient,
},
});
// Register things for typesafety
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
function InnerApp() {
const ark = useArk();
return <RouterProvider router={router} context={{ ark }} />;
}
function App() {
return (
<ArkProvider>
<InnerApp />
</ArkProvider>
);
}
// biome-ignore lint/style/noNonNullAssertion: idk
const rootElement = document.getElementById("root")!;
if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement);
root.render(
<I18nextProvider i18n={i18n} defaultNS={"translation"}>
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister }}
>
<StrictMode>
<Toaster position="top-center" richColors />
<App />
</StrictMode>
</PersistQueryClientProvider>
</I18nextProvider>,
);
}
+7
View File
@@ -0,0 +1,7 @@
import { Ark, ArkContext } from "@lume/ark";
import { PropsWithChildren, useMemo } from "react";
export const ArkProvider = ({ children }: PropsWithChildren<object>) => {
const ark = useMemo(() => new Ark(), []);
return <ArkContext.Provider value={ark}>{children}</ArkContext.Provider>;
};
+149
View File
@@ -0,0 +1,149 @@
import { useArk } from "@lume/ark";
import { Account } from "@lume/types";
import { User } from "@lume/ui";
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import * as Popover from "@radix-ui/react-popover";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { BackupDialog } from "./backup";
import { LoginDialog } from "./login";
export function Accounts() {
const ark = useArk();
const params = useParams({ strict: false });
const [accounts, setAccounts] = useState<Account[]>(null);
useEffect(() => {
async function getAllAccounts() {
const data = await ark.get_all_accounts();
if (data) setAccounts(data);
}
getAllAccounts();
}, []);
return (
<div data-tauri-drag-region className="flex items-center gap-4">
{accounts
? accounts.map((account) =>
// @ts-ignore, useless
account.npub === params.account ? (
<Active key={account.npub} pubkey={account.npub} />
) : (
<Inactive key={account.npub} pubkey={account.npub} />
),
)
: null}
</div>
);
}
function Inactive({ pubkey }: { pubkey: string }) {
const ark = useArk();
const navigate = useNavigate();
const changeAccount = async (npub: string) => {
const select = await ark.load_selected_account(npub);
if (select) navigate({ to: "/$account/home", params: { account: npub } });
};
return (
<button type="button" onClick={() => changeAccount(pubkey)}>
<User.Provider pubkey={pubkey}>
<User.Root className="rounded-full">
<User.Avatar className="aspect-square h-auto w-8 rounded-full object-cover" />
</User.Root>
</User.Provider>
</button>
);
}
function Active({ pubkey }: { pubkey: string }) {
const [open, setOpen] = useState(true);
// @ts-ignore, magic !!!
const { guest } = useSearch({ strict: false });
if (guest) {
return (
<Popover.Root open={open} onOpenChange={setOpen}>
<Popover.Trigger asChild>
<button type="button">
<User.Provider pubkey={pubkey}>
<User.Root className="rounded-full ring-1 ring-teal-500 ring-offset-2 ring-offset-neutral-200 dark:ring-offset-neutral-950">
<User.Avatar className="aspect-square h-auto w-7 rounded-full object-cover" />
</User.Root>
</User.Provider>
</button>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content
className="flex w-[280px] flex-col gap-4 rounded-xl bg-black p-5 text-neutral-100 focus:outline-none dark:bg-white dark:text-neutral-900 dark:shadow-none"
sideOffset={10}
side="bottom"
>
<div>
<h1 className="mb-1 font-semibold">
You're using random account
</h1>
<p className="text-sm text-neutral-500 dark:text-neutral-600">
You can continue by claim and backup this account, or you can
import your own account.
</p>
</div>
<div className="flex flex-col gap-2">
<BackupDialog />
<LoginDialog />
</div>
<Popover.Arrow className="fill-black dark:fill-white" />
</Popover.Content>
</Popover.Portal>
</Popover.Root>
);
}
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<User.Provider pubkey={pubkey}>
<User.Root className="rounded-full ring-1 ring-teal-500 ring-offset-2 ring-offset-neutral-200 dark:ring-offset-neutral-950">
<User.Avatar className="aspect-square h-auto w-7 rounded-full object-cover" />
</User.Root>
</User.Provider>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content
className="flex w-[220px] flex-col rounded-xl bg-black p-2 text-neutral-100 focus:outline-none dark:bg-white dark:text-neutral-900 dark:shadow-none"
sideOffset={10}
side="bottom"
>
<DropdownMenu.Item className="group relative flex h-9 select-none items-center rounded-md px-3 text-sm font-medium leading-none outline-none hover:bg-neutral-900 dark:hover:bg-neutral-100">
Add account
<div className="ml-auto pl-5 text-xs text-neutral-800 dark:text-neutral-200">
+Shift+N
</div>
</DropdownMenu.Item>
<DropdownMenu.Item className="group relative flex h-9 select-none items-center rounded-md px-3 text-sm font-medium leading-none outline-none hover:bg-neutral-900 dark:hover:bg-neutral-100">
Profile
<div className="ml-auto pl-5 text-xs text-neutral-800 dark:text-neutral-200">
+Shift+P
</div>
</DropdownMenu.Item>
<DropdownMenu.Item className="group relative flex h-9 select-none items-center rounded-md px-3 text-sm font-medium leading-none outline-none hover:bg-neutral-900 dark:hover:bg-neutral-100">
Settings
<div className="ml-auto pl-5 text-xs text-neutral-800 dark:text-neutral-200">
+Shift+S
</div>
</DropdownMenu.Item>
<DropdownMenu.Item className="group relative flex h-9 select-none items-center rounded-md px-3 text-sm font-medium leading-none outline-none hover:bg-neutral-900 dark:hover:bg-neutral-100">
Logout
<div className="ml-auto pl-5 text-xs text-neutral-800 dark:text-neutral-200">
+Shift+L
</div>
</DropdownMenu.Item>
<DropdownMenu.Arrow className="fill-black dark:fill-white" />
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
}
+121
View File
@@ -0,0 +1,121 @@
import { ArrowRightIcon, CancelIcon } from "@lume/icons";
import * as Dialog from "@radix-ui/react-dialog";
import { Link, useParams } from "@tanstack/react-router";
import { invoke } from "@tauri-apps/api/core";
import { useState } from "react";
import { toast } from "sonner";
export function BackupDialog() {
// @ts-ignore, magic!!!
const { account } = useParams({ strict: false });
const [key, setKey] = useState(null);
const [passphase, setPassphase] = useState("");
const [loading, setLoading] = useState(false);
const encryptKey = async () => {
try {
setLoading(true);
const encrypted: string = await invoke("get_encrypted_key", {
npub: account,
password: passphase,
});
if (encrypted) {
setKey(encrypted);
}
setLoading(false);
} catch (e) {
setLoading(false);
toast.error(String(e));
}
};
return (
<Dialog.Root>
<Dialog.Trigger asChild>
<button
type="button"
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-neutral-900 text-sm font-medium leading-tight text-neutral-100 hover:bg-neutral-800"
>
Claim & Backup
</button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/30 backdrop-blur dark:bg-white/30" />
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<Dialog.Close className="absolute right-5 top-5 flex w-12 flex-col items-center justify-center gap-1 text-white">
<CancelIcon className="size-8" />
<span className="text-sm font-medium uppercase text-neutral-400 dark:text-neutral-600">
Esc
</span>
</Dialog.Close>
<div className="relative flex h-min w-full max-w-xl flex-col gap-8 rounded-xl bg-white p-5 dark:bg-black">
<div className="flex flex-col">
<h3 className="text-lg font-semibold">
This is your account key
</h3>
<p>
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 gap-5">
<div className="flex flex-col gap-2">
<label htmlFor="nsec">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="h-11 w-full resize-none rounded-lg border-transparent bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900"
/>
</div>
</div>
{key ? (
<div className="flex flex-col gap-2">
<label htmlFor="nsec">
Copy this key and keep it in safe place
</label>
<input
name="nsec"
type="text"
value={key}
readOnly
className="h-11 w-full resize-none rounded-lg border-transparent bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900"
/>
</div>
) : null}
</div>
<div className="flex flex-col gap-3">
{!key ? (
<button
type="button"
onClick={encryptKey}
disabled={loading}
className="inline-flex h-11 w-full items-center justify-between gap-1.5 rounded-lg bg-blue-500 px-5 font-medium text-white hover:bg-blue-600"
>
<div className="size-5" />
<div>Submit</div>
<ArrowRightIcon className="size-5" />
</button>
) : (
<Link
to="/$account/home"
params={{ account }}
search={{ guest: false }}
className="inline-flex h-11 w-full items-center justify-center gap-1.5 rounded-lg bg-blue-500 px-5 font-medium text-white hover:bg-blue-600"
>
I've safely store my account key
</Link>
)}
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
+120
View File
@@ -0,0 +1,120 @@
import { useArk } from "@lume/ark";
import { ArrowRightIcon, CancelIcon } from "@lume/icons";
import * as Dialog from "@radix-ui/react-dialog";
import { useNavigate } from "@tanstack/react-router";
import { useState } from "react";
import { toast } from "sonner";
export function LoginDialog() {
const ark = useArk();
const navigate = useNavigate();
const [nsec, setNsec] = useState("");
const [passphase, setPassphase] = useState("");
const [loading, setLoading] = useState(false);
const login = async () => {
try {
setLoading(true);
const save = await ark.save_account(nsec, passphase);
if (save) {
navigate({ to: "/", search: { guest: false } });
}
} catch (e) {
setLoading(false);
toast.error(String(e));
}
};
return (
<Dialog.Root>
<Dialog.Trigger asChild>
<button
type="button"
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-neutral-900 text-sm font-medium leading-tight text-neutral-100 hover:bg-neutral-800"
>
Add account
</button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/30 backdrop-blur dark:bg-white/30" />
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<Dialog.Close className="absolute right-5 top-5 flex w-12 flex-col items-center justify-center gap-1 text-white">
<CancelIcon className="size-8" />
<span className="text-sm font-medium uppercase text-neutral-400 dark:text-neutral-600">
Esc
</span>
</Dialog.Close>
<div className="relative flex h-min w-full max-w-xl flex-col gap-8 rounded-xl bg-white p-5 dark:bg-black">
<div className="flex flex-col gap-1.5">
<h3 className="text-lg font-semibold">Add new account with</h3>
<div className="flex h-11 items-center overflow-hidden rounded-lg bg-neutral-100 p-1 dark:bg-neutral-900">
<button
type="button"
className="h-full flex-1 rounded-md bg-white text-sm font-medium dark:bg-black"
>
nsec
</button>
<button
type="button"
className="flex h-full flex-1 flex-col items-center justify-center rounded-md text-sm font-medium"
>
<span className="leading-tight">nsecBunker</span>
<span className="text-xs font-normal leading-tight text-neutral-700 dark:text-neutral-300">
coming soon
</span>
</button>
<button
type="button"
className="flex h-full flex-1 flex-col items-center justify-center rounded-md text-sm font-medium"
>
<span className="leading-tight">Address</span>
<span className="text-xs font-normal leading-tight text-neutral-700 dark:text-neutral-300">
coming soon
</span>
</button>
</div>
</div>
<div className="flex flex-col gap-5">
<div className="flex flex-col gap-2">
<label htmlFor="nsec">
Enter sign in key start with nsec or ncrypto
</label>
<input
name="nsec"
type="text"
value={nsec}
onChange={(e) => setNsec(e.target.value)}
className="h-11 w-full resize-none rounded-lg border-transparent bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900"
/>
</div>
<div className="flex flex-col gap-2">
<label htmlFor="nsec">Passphase (optional)</label>
<input
name="passphase"
type="password"
value={passphase}
onChange={(e) => setPassphase(e.target.value)}
className="h-11 w-full resize-none rounded-lg border-transparent bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900"
/>
</div>
</div>
<div className="flex flex-col gap-3">
<button
type="button"
onClick={login}
className="inline-flex h-11 w-full items-center justify-between gap-1.5 rounded-lg bg-blue-500 px-5 font-medium text-white hover:bg-blue-600"
>
<div className="size-5" />
<div>Add account</div>
<ArrowRightIcon className="size-5" />
</button>
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
+113
View File
@@ -0,0 +1,113 @@
import { RepostIcon } from "@lume/icons";
import { Event } from "@lume/types";
import { cn } from "@lume/utils";
import { useQuery } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { useArk } from "@lume/ark";
import { Note, User } from "@lume/ui";
export function RepostNote({
event,
className,
}: {
event: Event;
className?: string;
}) {
const ark = useArk();
const { t } = useTranslation();
const {
isLoading,
isError,
data: repostEvent,
} = useQuery({
queryKey: ["repost", event.id],
queryFn: async () => {
try {
if (event.content.length > 50) {
const embed: Event = JSON.parse(event.content);
return embed;
}
const id = event.tags.find((el) => el[0] === "e")[1];
return await ark.get_event(id);
} catch {
throw new Error("Failed to get repost event");
}
},
refetchOnWindowFocus: false,
refetchOnMount: false,
});
if (isLoading) {
return <div className="w-full px-3 pb-3">Loading...</div>;
}
if (isError || !repostEvent) {
return (
<Note.Root className={className}>
<User.Provider pubkey={event.pubkey}>
<User.Root className="flex h-14 gap-2 px-3">
<div className="inline-flex w-10 shrink-0 items-center justify-center">
<RepostIcon className="h-5 w-5 text-blue-500" />
</div>
<div className="inline-flex items-center gap-2">
<User.Avatar className="size-6 shrink-0 rounded object-cover" />
<div className="inline-flex items-baseline gap-1">
<User.Name className="font-medium text-neutral-900 dark:text-neutral-100" />
<span className="text-blue-500">{t("note.reposted")}</span>
</div>
</div>
</User.Root>
</User.Provider>
<div className="mb-3 select-text px-3">
<div className="flex flex-col items-start justify-start rounded-lg bg-red-100 px-3 py-3 dark:bg-red-900">
<p className="text-red-500">Failed to get event</p>
</div>
</div>
</Note.Root>
);
}
return (
<Note.Root
className={cn(
"mb-5 flex flex-col gap-2 border-b border-neutral-100 pb-5 dark:border-neutral-900",
className,
)}
>
<User.Provider pubkey={event.pubkey}>
<User.Root className="flex gap-3">
<div className="inline-flex w-11 shrink-0 items-center justify-center">
<RepostIcon className="h-5 w-5 text-blue-500" />
</div>
<div className="inline-flex items-center gap-2">
<User.Avatar className="size-6 shrink-0 rounded-full object-cover" />
<div className="inline-flex items-baseline gap-1">
<User.Name className="font-medium text-neutral-900 dark:text-neutral-100" />
<span className="text-blue-500">{t("note.reposted")}</span>
</div>
</div>
</User.Root>
</User.Provider>
<Note.Provider event={repostEvent}>
<div className="flex flex-col gap-2">
<Note.User />
<div className="flex gap-3">
<div className="size-11 shrink-0" />
<div className="min-w-0 flex-1">
<Note.Content />
<div className="mt-4 flex items-center justify-between">
<div className="-ml-1 inline-flex items-center gap-4">
<Note.Reply />
<Note.Repost />
<Note.Zap />
</div>
<Note.Menu />
</div>
</div>
</div>
</div>
</Note.Provider>
</Note.Root>
);
}
+56
View File
@@ -0,0 +1,56 @@
import { LoaderIcon } from "@lume/icons";
import { useQuery } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { User } from "@lume/ui";
export function Suggest() {
const { t } = useTranslation();
const { isLoading, isError, data } = useQuery({
queryKey: ["trending-users"],
queryFn: async ({ signal }: { signal: AbortSignal }) => {
const res = await fetch("https://api.nostr.band/v0/trending/profiles", {
signal,
});
if (!res.ok) {
throw new Error("Failed to fetch trending users from nostr.band API.");
}
return res.json();
},
});
return (
<div className="flex flex-col divide-y divide-neutral-100 dark:divide-neutral-900">
<div className="h-10 shrink-0 text-lg font-semibold">
Suggested Follows
</div>
{isLoading ? (
<div className="flex h-44 w-full items-center justify-center">
<LoaderIcon className="size-4 animate-spin" />
</div>
) : isError ? (
<div className="flex h-44 w-full items-center justify-center">
{t("suggestion.error")}
</div>
) : (
data?.profiles.map((item: { pubkey: string }) => (
<div key={item.pubkey} className="h-max w-full overflow-hidden py-5">
<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.5">
<User.Avatar className="size-10 shrink-0 rounded-full" />
<User.Name className="leadning-tight max-w-[15rem] truncate font-semibold" />
</div>
<User.Button className="inline-flex h-8 w-20 items-center justify-center rounded-lg bg-neutral-100 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800" />
</div>
<User.About className="mt-1 line-clamp-3 max-w-none select-text text-neutral-800 dark:text-neutral-400" />
</div>
</User.Root>
</User.Provider>
</div>
))
)}
</div>
);
}
+39
View File
@@ -0,0 +1,39 @@
import { Event } from "@lume/types";
import { Note } from "@lume/ui";
import { cn } from "@lume/utils";
export function TextNote({
event,
className,
}: {
event: Event;
className?: string;
}) {
return (
<Note.Provider event={event}>
<Note.Root
className={cn(
"mb-5 flex flex-col gap-2 border-b border-neutral-100 pb-5 dark:border-neutral-900",
className,
)}
>
<Note.User />
<div className="flex gap-3">
<div className="size-11 shrink-0" />
<div className="min-w-0 flex-1">
<Note.Content className="mb-2" />
<Note.Thread />
<div className="mt-4 flex items-center justify-between">
<div className="-ml-1 inline-flex items-center gap-4">
<Note.Reply />
<Note.Repost />
<Note.Zap />
</div>
<Note.Menu />
</div>
</div>
</div>
</Note.Root>
</Note.Provider>
);
}
+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;
+130
View File
@@ -0,0 +1,130 @@
import {
BellFilledIcon,
BellIcon,
ComposeFilledIcon,
HomeFilledIcon,
HomeIcon,
HorizontalDotsIcon,
SettingsIcon,
SpaceFilledIcon,
SpaceIcon,
} from "@lume/icons";
import { Link } from "@tanstack/react-router";
import { Outlet, createFileRoute } from "@tanstack/react-router";
import { cn } from "@lume/utils";
import { Accounts } from "@/components/accounts";
import { useArk } from "@lume/ark";
import { Box } from "@lume/ui";
export const Route = createFileRoute("/$account")({
component: App,
});
function App() {
const ark = useArk();
const context = Route.useRouteContext();
return (
<div className="flex h-screen w-screen flex-col bg-gradient-to-tr from-neutral-200 to-neutral-100 dark:from-neutral-950 dark:to-neutral-900">
<div
data-tauri-drag-region
className={cn(
"flex h-11 shrink-0 items-center justify-between pr-4",
context.platform === "macos" ? "pl-24" : "pl-4",
)}
>
<Navigation />
<div className="flex items-center gap-3">
<Accounts />
<button
type="button"
onClick={() => ark.open_editor()}
className="inline-flex h-8 w-max items-center justify-center gap-1 rounded-full bg-blue-500 px-3 text-sm font-medium text-white hover:bg-blue-600"
>
<ComposeFilledIcon className="size-4" />
New post
</button>
<button
type="button"
className="inline-flex size-8 items-center justify-center rounded-full bg-neutral-200 text-neutral-800 hover:bg-neutral-400 dark:bg-neutral-800 dark:text-neutral-200 dark:hover:bg-neutral-600"
>
<HorizontalDotsIcon className="size-5" />
</button>
</div>
</div>
<Box>
<Outlet />
</Box>
</div>
);
}
function Navigation() {
// @ts-ignore, useless
const { account } = Route.useParams();
return (
<div
data-tauri-drag-region
className="flex h-full flex-1 items-center gap-2"
>
<Link to="/$account/home" params={{ account }}>
{({ isActive }) => (
<div
className={cn(
"inline-flex h-8 w-max items-center justify-center gap-2 rounded-full px-3",
isActive
? "bg-neutral-300 hover:bg-neutral-400 dark:bg-neutral-800 dark:hover:bg-neutral-700"
: "hover:bg-black/10 dark:hover:bg-white/10",
)}
>
{isActive ? (
<HomeFilledIcon className="size-5" />
) : (
<HomeIcon className="size-5" />
)}
<span className="text-sm font-medium">Home</span>
</div>
)}
</Link>
<Link to="/$account/space" params={{ account }}>
{({ isActive }) => (
<div
className={cn(
"inline-flex h-8 w-max items-center justify-center gap-2 rounded-full px-3 hover:bg-black/10 dark:hover:bg-white/10",
isActive
? "bg-neutral-300 hover:bg-neutral-400 dark:bg-neutral-800 dark:hover:bg-neutral-700"
: "hover:bg-black/10 dark:hover:bg-white/10",
)}
>
{isActive ? (
<SpaceFilledIcon className="size-5" />
) : (
<SpaceIcon className="size-5" />
)}
<span className="text-sm font-medium">Space</span>
</div>
)}
</Link>
<Link to="/$account/activity" params={{ account }}>
{({ isActive }) => (
<div
className={cn(
"inline-flex h-8 w-max items-center justify-center gap-2 rounded-full px-3 hover:bg-black/10 dark:hover:bg-white/10",
isActive
? "bg-neutral-300 hover:bg-neutral-400 dark:bg-neutral-800 dark:hover:bg-neutral-700"
: "hover:bg-black/10 dark:hover:bg-white/10",
)}
>
{isActive ? (
<BellFilledIcon className="size-5" />
) : (
<BellIcon className="size-5" />
)}
<span className="text-sm font-medium">Activity</span>
</div>
)}
</Link>
</div>
);
}
@@ -0,0 +1,13 @@
import { createLazyFileRoute } from "@tanstack/react-router";
export const Route = createLazyFileRoute("/$account/activity")({
component: Activity,
});
function Activity() {
return (
<div className="h-full w-full overflow-hidden rounded-xl bg-white shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-none dark:ring-1 dark:ring-white/5">
<p>Activity</p>
</div>
);
}
@@ -0,0 +1,133 @@
import { RepostNote } from "@/components/repost";
import { Suggest } from "@/components/suggest";
import { TextNote } from "@/components/text";
import { useArk } from "@lume/ark";
import {
LoaderIcon,
ArrowRightCircleIcon,
RefreshIcon,
InfoIcon,
} from "@lume/icons";
import { Event, Kind } from "@lume/types";
import { EmptyFeed } from "@lume/ui";
import { FETCH_LIMIT } from "@lume/utils";
import { useInfiniteQuery } from "@tanstack/react-query";
import { createLazyFileRoute } from "@tanstack/react-router";
import { Virtualizer } from "virtua";
export const Route = createLazyFileRoute("/$account/home")({
component: Home,
});
function Home() {
const ark = useArk();
const currentDate = new Date().toLocaleString("default", {
weekday: "long",
month: "long",
day: "numeric",
});
const { account } = Route.useParams();
const {
data,
hasNextPage,
isLoading,
isRefetching,
isFetchingNextPage,
fetchNextPage,
refetch,
} = useInfiniteQuery({
queryKey: ["local_newsfeed", account],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = await ark.get_events(
"local",
FETCH_LIMIT,
pageParam,
true,
);
return events;
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage?.at(-1);
if (!lastEvent) return;
return lastEvent.created_at - 1;
},
select: (data) => data?.pages.flatMap((page) => page),
refetchOnWindowFocus: false,
});
const renderItem = (event: Event) => {
if (!event) return;
switch (event.kind) {
case Kind.Repost:
return <RepostNote key={event.id} event={event} />;
default:
return <TextNote key={event.id} event={event} />;
}
};
return (
<div className="flex flex-col gap-3">
<div className="mx-auto flex h-12 w-full max-w-xl shrink-0 items-center justify-between border-b border-neutral-100 dark:border-neutral-900">
<h3 className="text-sm font-medium uppercase leading-tight text-neutral-600 dark:text-neutral-400">
{currentDate}
</h3>
<div className="flex items-center gap-3">
<button
type="button"
onClick={() => refetch()}
className="text-neutral-700 hover:text-blue-500 dark:text-neutral-300"
>
<RefreshIcon className="size-4" />
</button>
</div>
</div>
<div className="mx-auto flex w-full max-w-xl flex-1 flex-col">
<div className="flex-1">
{isLoading || isRefetching ? (
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
<LoaderIcon className="size-5 animate-spin" />
</div>
) : !data.length ? (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2 rounded-xl bg-neutral-100 p-3 dark:bg-neutral-900">
<InfoIcon className="size-5" />
<p>
Empty newsfeed. Or you can go to{" "}
<a href="" className="text-blue-500 hover:text-blue-600">
Discover
</a>
</p>
</div>
<Suggest />
</div>
) : (
<Virtualizer overscan={3}>
{data.map((item) => renderItem(item))}
</Virtualizer>
)}
<div className="flex h-20 items-center justify-center">
{hasNextPage ? (
<button
type="button"
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-12 w-36 items-center justify-center gap-2 rounded-full bg-neutral-100 px-3 font-medium hover:bg-neutral-200 focus:outline-none dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
{isFetchingNextPage ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
<>
<ArrowRightCircleIcon className="size-5" />
Load more
</>
)}
</button>
) : null}
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,13 @@
import { createLazyFileRoute } from "@tanstack/react-router";
export const Route = createLazyFileRoute("/$account/space")({
component: Space,
});
function Space() {
return (
<div className="h-full w-full overflow-hidden rounded-xl bg-white shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-none dark:ring-1 dark:ring-white/5">
<p>Space</p>
</div>
);
}
+35
View File
@@ -0,0 +1,35 @@
import { LoaderIcon } from "@lume/icons";
import {
Outlet,
ScrollRestoration,
createRootRouteWithContext,
} from "@tanstack/react-router";
import { type Ark } from "@lume/ark";
import { type QueryClient } from "@tanstack/react-query";
import { type Platform } from "@tauri-apps/plugin-os";
interface RouterContext {
ark: Ark;
queryClient: QueryClient;
platform: Platform;
locale: string;
}
export const Route = createRootRouteWithContext<RouterContext>()({
component: () => (
<>
<ScrollRestoration />
<Outlet />
</>
),
pendingComponent: Pending,
wrapInSuspense: true,
});
function Pending() {
return (
<div className="flex h-screen w-screen flex-col items-center justify-center">
<LoaderIcon className="size-5 animate-spin" />
</div>
);
}
@@ -0,0 +1,108 @@
import { LoaderIcon } from "@lume/icons";
import { cn } from "@lume/utils";
import { createLazyFileRoute, useNavigate } from "@tanstack/react-router";
import { useState } from "react";
import { useTranslation } from "react-i18next";
export const Route = createLazyFileRoute("/auth/create/")({
component: Screen,
});
function Screen() {
const navigate = useNavigate();
const [t] = useTranslation();
const [method, setMethod] = useState<"self" | "managed">("self");
const [loading, setLoading] = useState(false);
const next = () => {
setLoading(true);
if (method === "self") {
navigate({ to: "/auth/create/self" });
} else {
navigate({ to: "/auth/create/managed" });
}
};
return (
<div className="flex h-full w-full items-center justify-center">
<div className="mx-auto flex w-full max-w-md flex-col gap-8">
<div className="flex flex-col items-center gap-1 text-center">
<h1 className="text-2xl font-semibold">{t("signup.title")}</h1>
<p className="text-lg leading-snug text-neutral-600 dark:text-neutral-500">
{t("signup.subtitle")}
</p>
</div>
<div className="flex flex-col gap-4">
<button
type="button"
onClick={() => setMethod("self")}
className={cn(
"flex flex-col items-start rounded-xl bg-neutral-100 px-4 py-3.5 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800",
method === "self"
? "ring-1 ring-blue-500 ring-offset-2 ring-offset-white dark:ring-offset-black"
: "",
)}
>
<p className="font-semibold">{t("signup.selfManageMethod")}</p>
<p className="text-sm text-neutral-600 dark:text-neutral-500">
{t("signup.selfManageMethodDescription")}
</p>
</button>
<button
type="button"
onClick={() => setMethod("managed")}
className={cn(
"flex flex-col items-start rounded-xl bg-neutral-100 px-4 py-3.5 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800",
method === "managed"
? "ring-1 ring-blue-500 ring-offset-2 ring-offset-white dark:ring-offset-black"
: "",
)}
>
<p className="font-semibold">{t("signup.providerMethod")}</p>
<p className="text-sm text-neutral-600 dark:text-neutral-500">
{t("signup.providerMethodDescription")}
</p>
</button>
<div className="flex flex-col gap-3">
<button
type="button"
onClick={next}
className="inline-flex h-12 w-full items-center justify-center rounded-xl bg-blue-500 text-lg font-medium text-white hover:bg-blue-600"
>
{loading ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
t("global.continue")
)}
</button>
{method === "managed" ? (
<div className="flex flex-col gap-1.5 rounded-xl border border-red-200 bg-red-100 p-2 text-sm text-red-800 dark:border-red-800 dark:bg-red-900 dark:text-red-200">
<p className="font-semibold text-red-900 dark:text-red-100">
Attention:
</p>
<p>
You're chosing Managed by Provider, this feature still in
"Beta".
</p>
<p>
Some functions still missing or not work as expected, you
shouldn't create your main account with this method
</p>
<a
href="https://github.com/kind-0/nsecbunkerd/blob/master/OAUTH-LIKE-FLOW.md"
target="_blank"
rel="noreferrer"
className="text-blue-500"
>
Learn more
</a>
</div>
) : null}
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,5 @@
import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/auth/create/managed')({
component: () => <div>Hello /auth/create/managed!</div>
})
@@ -0,0 +1,161 @@
import { useArk } from "@lume/ark";
import { CheckIcon, EyeOffIcon, EyeOnIcon, LoaderIcon } from "@lume/icons";
import { Keys } from "@lume/types";
import * as Checkbox from "@radix-ui/react-checkbox";
import { createLazyFileRoute, useNavigate } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
export const Route = createLazyFileRoute("/auth/create/self")({
component: Create,
});
function Create() {
const ark = useArk();
const navigate = useNavigate();
const [t] = useTranslation();
const [loading, setLoading] = useState(false);
const [showKey, setShowKey] = useState(false);
const [confirm, setConfirm] = useState({ c1: false, c2: false, c3: false });
const [keys, setKeys] = useState<Keys>(null);
const submit = async () => {
setLoading(true);
try {
await ark.save_account(keys);
navigate({
to: "/$account/home",
params: { account: keys.npub },
search: { onboarding: true },
replace: true,
});
} catch (e) {
setLoading(false);
toast.error(e);
}
};
useEffect(() => {
async function genKeys() {
const res = await ark.create_keys();
setKeys(res);
}
genKeys();
}, []);
return (
<div className="flex h-full w-full items-center justify-center">
<div className="mx-auto flex w-full max-w-md flex-col gap-8">
<div className="flex flex-col items-center gap-1 text-center">
<h1 className="text-2xl font-semibold">
{t("signupWithSelfManage.title")}
</h1>
<p className="text-lg leading-snug text-neutral-600 dark:text-neutral-500">
{t("signupWithSelfManage.subtitle")}
</p>
</div>
<div className="mb-0 flex flex-col gap-6">
<div className="flex flex-col gap-4">
<div className="relative">
{keys ? (
<input
readOnly
value={keys.nsec}
type={showKey ? "text" : "password"}
className="h-12 w-full resize-none rounded-xl border-transparent bg-neutral-100 pl-3 pr-14 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900"
/>
) : null}
<button
type="button"
onClick={() => setShowKey((state) => !state)}
className="absolute right-2 top-2 inline-flex size-8 items-center justify-center rounded-lg bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700"
>
{showKey ? (
<EyeOnIcon className="size-4" />
) : (
<EyeOffIcon className="size-4" />
)}
</button>
</div>
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<Checkbox.Root
checked={confirm.c1}
onCheckedChange={() =>
setConfirm((state) => ({ ...state, c1: !state.c1 }))
}
className="flex size-7 appearance-none items-center justify-center rounded-lg bg-neutral-100 outline-none dark:bg-neutral-900"
id="confirm1"
>
<Checkbox.Indicator className="text-blue-500">
<CheckIcon className="size-4" />
</Checkbox.Indicator>
</Checkbox.Root>
<label
className="text-sm text-neutral-700 dark:text-neutral-400"
htmlFor="confirm1"
>
{t("signupWithSelfManage.confirm1")}
</label>
</div>
<div className="flex items-center gap-2">
<Checkbox.Root
checked={confirm.c3}
onCheckedChange={() =>
setConfirm((state) => ({ ...state, c3: !state.c3 }))
}
className="flex size-7 appearance-none items-center justify-center rounded-lg bg-neutral-100 outline-none dark:bg-neutral-900"
id="confirm3"
>
<Checkbox.Indicator className="text-blue-500">
<CheckIcon className="size-4" />
</Checkbox.Indicator>
</Checkbox.Root>
<label
className="text-sm text-neutral-700 dark:text-neutral-400"
htmlFor="confirm3"
>
{t("signupWithSelfManage.confirm3")}
</label>
</div>
<div className="flex items-center gap-2">
<Checkbox.Root
checked={confirm.c2}
onCheckedChange={() =>
setConfirm((state) => ({ ...state, c2: !state.c2 }))
}
className="flex size-7 appearance-none items-center justify-center rounded-lg bg-neutral-100 outline-none dark:bg-neutral-900"
id="confirm2"
>
<Checkbox.Indicator className="text-blue-500">
<CheckIcon className="size-4" />
</Checkbox.Indicator>
</Checkbox.Root>
<label
className="text-sm text-neutral-700 dark:text-neutral-400"
htmlFor="confirm2"
>
{t("signupWithSelfManage.confirm2")}
</label>
</div>
</div>
</div>
<button
type="button"
onClick={submit}
disabled={!confirm.c1 || !confirm.c2 || !confirm.c3}
className="inline-flex h-12 w-full items-center justify-center rounded-xl bg-blue-500 text-lg font-medium text-white hover:bg-blue-600 disabled:opacity-50"
>
{loading ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
t("signupWithSelfManage.button")
)}
</button>
</div>
</div>
</div>
);
}
@@ -0,0 +1,101 @@
import { useArk } from "@lume/ark";
import { LoaderIcon } from "@lume/icons";
import { createLazyFileRoute, useNavigate } from "@tanstack/react-router";
import { invoke } from "@tauri-apps/api/core";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
export const Route = createLazyFileRoute("/auth/import")({
component: Import,
});
function Import() {
const ark = useArk();
const navigate = useNavigate();
const [t] = useTranslation();
const [key, setKey] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const submit = async () => {
if (!key.startsWith("nsec1")) return;
if (key.length < 30) return;
setLoading(true);
try {
const npub: string = await invoke("get_public_key", { nsec: key });
await ark.save_account({
npub,
nsec: key,
});
navigate({
to: "/$account/home",
params: { account: npub },
search: { onboarding: true },
replace: true,
});
} catch (e) {
setLoading(false);
toast.error(e);
}
};
const isNip05 = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/.test(key);
const isNip49 = key.startsWith("ncryptsec");
return (
<div className="flex h-full w-full items-center justify-center">
<div className="mx-auto flex w-full max-w-md flex-col gap-8">
<div className="flex flex-col items-center gap-1 text-center">
<h1 className="text-2xl font-semibold">{t("login.title")}</h1>
<p className="text-lg leading-snug text-neutral-600 dark:text-neutral-500">
{t("login.subtitle")}
</p>
</div>
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-4">
<div>
<input
value={key}
type="text"
onChange={(e) => setKey(e.target.value)}
className="h-12 w-full resize-none rounded-xl border-transparent bg-neutral-100 pl-3 pr-10 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900"
/>
</div>
{isNip05 || isNip49 ? (
<div className="flex flex-col gap-1.5">
<label
htmlFor="password"
className="font-medium text-neutral-900 dark:text-neutral-100"
>
Password *
</label>
<input
value={password}
name="password"
type="password"
onChange={(e) => setPassword(e.target.value)}
className="h-12 w-full resize-none rounded-xl border-transparent bg-neutral-100 pl-3 pr-10 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900"
/>
</div>
) : null}
</div>
<button
type="button"
onClick={submit}
className="inline-flex h-12 w-full items-center justify-center rounded-xl bg-blue-500 text-lg font-medium text-white hover:bg-blue-600 disabled:opacity-50"
>
{loading ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
"Import"
)}
</button>
</div>
</div>
</div>
);
}
@@ -0,0 +1,78 @@
import { useArk } from "@lume/ark";
import { AddMediaIcon, LoaderIcon } from "@lume/icons";
import { cn, insertImage, isImagePath } from "@lume/utils";
import { useEffect, useState } from "react";
import { useSlateStatic } from "slate-react";
import { toast } from "sonner";
import { getCurrent } from "@tauri-apps/api/window";
import { UnlistenFn } from "@tauri-apps/api/event";
export function MediaButton({ className }: { className?: string }) {
const ark = useArk();
const editor = useSlateStatic();
const [loading, setLoading] = useState(false);
const uploadToNostrBuild = async () => {
try {
setLoading(true);
const image = await ark.upload();
if (image) {
insertImage(editor, image);
}
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 ark.upload(item);
insertImage(editor, image);
}
}
// stop loading
setLoading(false);
});
}
}
listenFileDrop();
return () => {
if (unlisten) unlisten();
};
}, []);
return (
<button
type="button"
onClick={() => uploadToNostrBuild()}
disabled={loading}
className={cn("inline-flex items-center justify-center", className)}
>
{loading ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
<AddMediaIcon className="size-5" />
)}
</button>
);
}
+438
View File
@@ -0,0 +1,438 @@
import { useArk } from "@lume/ark";
import { LoaderIcon, TrashIcon } from "@lume/icons";
import {
Portal,
cn,
insertImage,
insertMention,
insertNostrEvent,
isImageUrl,
sendNativeNotification,
} from "@lume/utils";
import { createFileRoute } from "@tanstack/react-router";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { MediaButton } from "./-components/media";
import { MentionNote } from "@lume/ui/src/note/mentions/note";
import {
Descendant,
Editor,
Node,
Range,
Transforms,
createEditor,
} from "slate";
import {
ReactEditor,
useSlateStatic,
useSelected,
useFocused,
withReact,
Slate,
Editable,
} from "slate-react";
import { Contact } from "@lume/types";
import { User } from "@lume/ui";
import { nip19 } from "nostr-tools";
import { queryOptions, useSuspenseQuery } from "@tanstack/react-query";
import { invoke } from "@tauri-apps/api/core";
type EditorElement = {
type: string;
children: Descendant[];
eventId?: string;
};
const contactQueryOptions = queryOptions({
queryKey: ["contacts"],
queryFn: () => invoke("get_contact_metadata"),
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
});
export const Route = createFileRoute("/editor/")({
loader: ({ context }) =>
context.queryClient.ensureQueryData(contactQueryOptions),
component: Screen,
pendingComponent: Pending,
});
function Screen() {
// @ts-ignore, useless
const { reply_to, quote } = Route.useSearch();
let initialValue: EditorElement[];
if (quote) {
initialValue = [
{
type: "paragraph",
children: [{ text: "" }],
},
{
type: "event",
eventId: `nostr:${nip19.noteEncode(reply_to)}`,
children: [{ text: "" }],
},
{
type: "paragraph",
children: [{ text: "" }],
},
];
} else {
initialValue = [
{
type: "paragraph",
children: [{ text: "" }],
},
];
}
const ark = useArk();
const ref = useRef<HTMLDivElement | null>();
const contacts = useSuspenseQuery(contactQueryOptions).data as Contact[];
const [t] = useTranslation();
const [editorValue, setEditorValue] = useState(initialValue);
const [target, setTarget] = useState<Range | undefined>();
const [index, setIndex] = useState(0);
const [search, setSearch] = useState("");
const [loading, setLoading] = useState(false);
const [editor] = useState(() =>
withMentions(withNostrEvent(withImages(withReact(createEditor())))),
);
const filters = contacts
?.filter((c) =>
c?.profile.name?.toLowerCase().startsWith(search.toLowerCase()),
)
?.slice(0, 5);
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 ark.publish(content, reply_to, quote);
if (eventId) {
await sendNativeNotification("You've publish new post successfully.");
return reset();
}
// stop loading
setLoading(false);
} catch (e) {
setLoading(false);
await sendNativeNotification(String(e));
}
};
useEffect(() => {
if (target && filters.length > 0) {
const el = ref.current;
const domRange = ReactEditor.toDOMRange(editor, target);
const rect = domRange.getBoundingClientRect();
el.style.top = `${rect.top + window.scrollY + 24}px`;
el.style.left = `${rect.left + window.scrollX}px`;
}
}, [filters.length, editor, index, search, target]);
return (
<div className="flex h-screen w-screen flex-col bg-gradient-to-tr from-neutral-200 to-neutral-100 dark:from-neutral-950 dark:to-neutral-900">
<Slate
editor={editor}
initialValue={editorValue}
onChange={() => {
const { selection } = editor;
if (selection && Range.isCollapsed(selection)) {
const [start] = Range.edges(selection);
const wordBefore = Editor.before(editor, start, { unit: "word" });
const before = wordBefore && Editor.before(editor, wordBefore);
const beforeRange = before && Editor.range(editor, before, start);
const beforeText =
beforeRange && Editor.string(editor, beforeRange);
const beforeMatch = beforeText?.match(/^@(\w+)$/);
const after = Editor.after(editor, start);
const afterRange = Editor.range(editor, start, after);
const afterText = Editor.string(editor, afterRange);
const afterMatch = afterText.match(/^(\s|$)/);
if (beforeMatch && afterMatch) {
setTarget(beforeRange);
setSearch(beforeMatch[1]);
setIndex(0);
return;
}
}
setTarget(null);
}}
>
<div
data-tauri-drag-region
className="flex h-16 w-full shrink-0 items-center justify-end gap-3 px-2"
>
<MediaButton className="size-9 rounded-full bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700" />
<button
type="button"
onClick={publish}
className="inline-flex h-9 w-24 items-center justify-center rounded-full bg-blue-500 px-3 text-sm font-medium text-white hover:bg-blue-600"
>
{loading ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
t("global.post")
)}
</button>
</div>
<div className="flex h-full min-h-0 w-full">
<div className="flex h-full w-full flex-1 flex-col gap-2 px-2 pb-2">
{reply_to && !quote ? (
<div className="flex flex-col rounded-xl bg-white p-5 shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-none dark:ring-1 dark:ring-white/5">
<h3 className="font-medium">Reply to:</h3>
<MentionNote eventId={reply_to} />
</div>
) : null}
<div className="h-full w-full flex-1 overflow-hidden overflow-y-auto rounded-xl bg-white p-5 shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-none dark:ring-1 dark:ring-white/5">
<Editable
key={JSON.stringify(editorValue)}
autoFocus={true}
autoCapitalize="none"
autoCorrect="none"
spellCheck={false}
renderElement={(props) => <Element {...props} />}
placeholder={t("editor.placeholder")}
className="focus:outline-none"
/>
{target && filters.length > 0 && (
<Portal>
<div
ref={ref}
className="absolute left-[-9999px] top-[-9999px] z-10 w-[250px] rounded-xl border border-neutral-50 bg-white p-2 shadow-lg dark:border-neutral-900 dark:bg-neutral-950"
>
{filters.map((contact) => (
<button
key={contact.pubkey}
type="button"
onClick={() => {
Transforms.select(editor, target);
insertMention(editor, contact);
setTarget(null);
}}
className="flex w-full flex-col rounded-lg p-2 hover:bg-neutral-100 dark:hover:bg-neutral-900"
>
<User.Provider pubkey={contact.pubkey}>
<User.Root className="flex w-full items-center gap-2">
<User.Avatar className="size-7 shrink-0 rounded-full object-cover" />
<div className="flex w-full flex-col items-start">
<User.Name className="max-w-[8rem] truncate text-sm font-medium" />
</div>
</User.Root>
</User.Provider>
</button>
))}
</div>
</Portal>
)}
</div>
</div>
</div>
</Slate>
</div>
);
}
function Pending() {
return (
<div className="flex h-full w-full items-center justify-center gap-2.5">
<LoaderIcon className="size-5 animate-spin" />
<p>Loading cache...</p>
</div>
);
}
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("nevent1") || text.startsWith("note1")) {
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, children, element }) => {
const editor = useSlateStatic();
const path = ReactEditor.findPath(editor as ReactEditor, element);
const selected = useSelected();
const focused = useFocused();
return (
<div {...attributes}>
{children}
<div contentEditable={false} className="relative my-2">
<img
src={element.url}
alt={element.url}
className={cn(
"h-auto w-full rounded-lg border border-neutral-100 object-cover ring-2 dark:border-neutral-900",
selected && focused ? "ring-blue-500" : "ring-transparent",
)}
contentEditable={false}
/>
<button
type="button"
contentEditable={false}
onClick={() => Transforms.removeNodes(editor, { at: path })}
className="absolute right-2 top-2 inline-flex size-8 items-center justify-center rounded-lg bg-red-500 text-white hover:bg-red-600"
>
<TrashIcon className="size-4" />
</button>
</div>
</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 align-baseline text-blue-500 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}
{/* biome-ignore lint/a11y/useKeyWithClickEvents: <explanation> */}
<div
contentEditable={false}
onClick={() => Transforms.removeNodes(editor, { at: path })}
className="user-select-none relative my-2"
>
<MentionNote
eventId={element.eventId.replace("nostr:", "")}
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-lg">
{children}
</p>
);
}
};
@@ -0,0 +1,72 @@
import { useEvent } from "@lume/ark";
import { LoaderIcon } from "@lume/icons";
import { Box, Container, Note, User } from "@lume/ui";
import { createLazyFileRoute } from "@tanstack/react-router";
import { WindowVirtualizer } from "virtua";
import { ReplyList } from "./-components/replyList";
import { Event } from "@lume/types";
export const Route = createLazyFileRoute("/events/$eventId")({
component: Event,
});
function Event() {
const { eventId } = Route.useParams();
const { isLoading, isError, data } = useEvent(eventId);
if (isLoading) {
return (
<div className="flex h-full w-full items-center justify-center">
<LoaderIcon className="size-5 animate-spin" />
</div>
);
}
if (isError) {
<div className="flex h-full w-full items-center justify-center">
<p>Not found.</p>
</div>;
}
return (
<WindowVirtualizer>
<Container withDrag>
<Box className="px-3 pt-3">
<MainNote data={data} />
{data ? <ReplyList eventId={eventId} /> : null}
</Box>
</Container>
</WindowVirtualizer>
);
}
function MainNote({ data }: { data: Event }) {
return (
<Note.Provider event={data}>
<Note.Root className="flex flex-col pb-3">
<User.Provider pubkey={data.pubkey}>
<User.Root className="mb-3 flex flex-1 items-center gap-3">
<User.Avatar className="size-11 shrink-0 rounded-full object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50" />
<div className="flex flex-1 flex-col">
<User.Name className="font-semibold text-neutral-900 dark:text-neutral-100" />
<div className="inline-flex items-center gap-2 text-sm text-neutral-600 dark:text-neutral-400">
<User.Time time={data.created_at} />
<span>·</span>
<User.NIP05 />
</div>
</div>
</User.Root>
</User.Provider>
<Note.Thread className="mb-2" />
<Note.Content className="min-w-0" />
<div className="mt-4 flex items-center justify-between">
<div className="-ml-1 inline-flex items-center gap-4">
<Note.Repost />
<Note.Zap />
</div>
<Note.Menu />
</div>
</Note.Root>
</Note.Provider>
);
}
@@ -0,0 +1,47 @@
import { EventWithReplies } from "@lume/types";
import { cn } from "@lume/utils";
import { Note, User } from "@lume/ui";
import { SubReply } from "./subReply";
export function Reply({ event }: { event: EventWithReplies }) {
return (
<Note.Provider event={event}>
<Note.Root className="border-t border-neutral-100 pt-3 dark:border-neutral-900">
<User.Provider pubkey={event.pubkey}>
<User.Root className="mb-2 flex items-center justify-between">
<div className="inline-flex gap-2">
<User.Avatar className="size-6 rounded-full" />
<div className="inline-flex items-center gap-2">
<User.Name className="font-semibold" />
<User.NIP05 className="text-base lowercase text-neutral-600 dark:text-neutral-400" />
</div>
</div>
<User.Time time={event.created_at} />
</User.Root>
</User.Provider>
<Note.Content />
<div className="mt-4 flex items-center justify-between">
<div className="-ml-1 inline-flex items-center gap-4">
<Note.Reply />
<Note.Repost />
<Note.Zap />
</div>
<Note.Menu />
</div>
<div
className={cn(
event.replies?.length > 0
? "my-3 mt-6 flex flex-col gap-3 divide-y divide-neutral-100 border-l-2 border-neutral-100 pl-6 dark:divide-neutral-900 dark:border-neutral-900"
: "",
)}
>
{event.replies?.length > 0
? event.replies?.map((childEvent) => (
<SubReply key={childEvent.id} event={childEvent} />
))
: null}
</div>
</Note.Root>
</Note.Provider>
);
}
@@ -0,0 +1,49 @@
import { useArk } from "@lume/ark";
import { LoaderIcon } from "@lume/icons";
import { cn } from "@lume/utils";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { EventWithReplies } from "@lume/types";
import { Reply } from "./reply";
export function ReplyList({
eventId,
className,
}: {
eventId: string;
className?: string;
}) {
const ark = useArk();
const [t] = useTranslation();
const [data, setData] = useState<null | EventWithReplies[]>(null);
useEffect(() => {
async function getReplies() {
const events = await ark.get_event_thread(eventId);
setData(events);
}
getReplies();
}, [eventId]);
return (
<div className={cn("flex flex-col gap-3", className)}>
{!data ? (
<div className="mt-4 flex h-16 items-center justify-center p-3">
<LoaderIcon className="h-5 w-5 animate-spin" />
</div>
) : data.length === 0 ? (
<div className="mt-4 flex w-full items-center justify-center">
<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">
{t("note.reply.empty")}
</p>
</div>
</div>
) : (
data.map((event) => <Reply key={event.id} event={event} />)
)}
</div>
);
}
@@ -0,0 +1,32 @@
import { Event } from "@lume/types";
import { Note, User } from "@lume/ui";
export function SubReply({ event }: { event: Event; rootEventId?: string }) {
return (
<Note.Provider event={event}>
<Note.Root className="pt-3">
<User.Provider pubkey={event.pubkey}>
<User.Root className="mb-2 flex items-center justify-between">
<div className="inline-flex gap-2">
<User.Avatar className="size-6 rounded-full" />
<div className="inline-flex items-center gap-2">
<User.Name className="font-semibold" />
<User.NIP05 className="text-base lowercase text-neutral-600 dark:text-neutral-400" />
</div>
</div>
<User.Time time={event.created_at} />
</User.Root>
</User.Provider>
<Note.Content />
<div className="mt-4 flex items-center justify-between">
<div className="-ml-1 inline-flex items-center gap-4">
<Note.Reply />
<Note.Repost />
<Note.Zap />
</div>
<Note.Menu />
</div>
</Note.Root>
</Note.Provider>
);
}
+122
View File
@@ -0,0 +1,122 @@
import { useArk } from "@lume/ark";
import { LoaderIcon, PlusIcon } from "@lume/icons";
import { User } from "@lume/ui";
import { Link } from "@tanstack/react-router";
import { createFileRoute, redirect, useNavigate } from "@tanstack/react-router";
import { useState } from "react";
export const Route = createFileRoute("/")({
beforeLoad: async ({ context }) => {
const ark = context.ark;
const accounts = await ark.get_all_accounts();
switch (accounts.length) {
// Guest account
case 0:
const guest = await ark.create_guest_account();
throw redirect({
to: "/$account/home",
params: { account: guest },
search: { guest: true },
replace: true,
});
// Only 1 account, skip account selection screen
case 1:
const account = accounts[0].npub;
const loadAccount = await ark.load_selected_account(account);
if (loadAccount) {
throw redirect({
to: "/$account/home",
params: { account },
replace: true,
});
}
// Account selection
default:
return;
}
},
component: Screen,
});
function Screen() {
const ark = useArk();
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const select = async (npub: string) => {
setLoading(true);
const loadAccount = await ark.load_selected_account(npub);
if (loadAccount) {
navigate({
to: "/$account/home",
params: { account: npub },
replace: true,
});
}
};
const currentDate = new Date().toLocaleString("default", {
weekday: "long",
month: "long",
day: "numeric",
});
return (
<div className="relative flex h-full w-full items-center justify-center">
<div className="relative z-20 flex flex-col items-center gap-16">
<div className="text-center text-white">
<h2 className="mb-1 text-2xl">{currentDate}</h2>
<h2 className="text-2xl font-semibold">Welcome back!</h2>
</div>
<div className="flex items-center justify-center gap-6">
{loading ? (
<LoaderIcon className="size-6 animate-spin text-white" />
) : (
<>
{ark.accounts.map((account) => (
<button
type="button"
key={account.npub}
onClick={() => select(account.npub)}
>
<User.Provider pubkey={account.npub}>
<User.Root className="flex h-36 w-32 flex-col items-center justify-center gap-4 rounded-2xl p-2 hover:bg-white/10 dark:hover:bg-black/10">
<User.Avatar className="size-20 rounded-full object-cover" />
<User.Name className="max-w-[5rem] truncate text-lg font-medium leading-tight text-white" />
</User.Root>
</User.Provider>
</button>
))}
<Link to="/landing">
<div className="flex h-36 w-32 flex-col items-center justify-center gap-4 rounded-2xl p-2 text-white hover:bg-white/10 dark:hover:bg-black/10">
<div className="flex size-20 items-center justify-center rounded-full bg-white/20 dark:bg-black/20">
<PlusIcon className="size-5" />
</div>
<p className="text-lg font-medium leading-tight">Add</p>
</div>
</Link>
</>
)}
</div>
</div>
<div className="absolute z-10 h-full w-full bg-white/10 backdrop-blur-lg dark:bg-black/10" />
<div className="absolute inset-0 h-full w-full">
<img
src="/lock-screen.jpg"
srcSet="/lock-screen@2x.jpg 2x"
alt="Lock Screen Background"
className="h-full w-full object-cover"
/>
<a
href="https://njump.me/nprofile1qqs9tuz9jpn57djg7nxunhyvuvk69g5zqaxdpvpqt9hwqv7395u9rpg6zq5uw"
target="_blank"
className="absolute bottom-3 right-3 z-50 rounded-md bg-white/20 px-2 py-1 text-xs font-medium text-white dark:bg-black/20"
>
Design by NoGood
</a>
</div>
</div>
);
}
@@ -0,0 +1,58 @@
import { Link, createFileRoute } from "@tanstack/react-router";
import { useTranslation } from "react-i18next";
export const Route = createFileRoute("/landing/")({
component: Screen,
});
function Screen() {
const context = Route.useRouteContext();
const { t } = useTranslation();
return (
<div className="flex h-screen w-screen bg-black">
<div className="flex h-full w-full flex-col items-center justify-between">
<div />
<div className="mx-auto flex w-full max-w-4xl flex-col items-center gap-10">
<div className="flex flex-col items-center text-center">
<img
src={`/heading-${context.locale}.png`}
srcSet={`/heading-${context.locale}@2x.png 2x`}
alt="lume"
className="w-2/3"
/>
<p className="mt-5 whitespace-pre-line text-lg font-medium leading-snug text-neutral-700">
{t("welcome.title")}
</p>
</div>
<div className="mx-auto flex w-full max-w-sm flex-col gap-2">
<Link
to="/auth/create"
className="inline-flex h-12 w-full items-center justify-center rounded-xl bg-blue-500 text-lg font-medium text-white hover:bg-blue-600"
>
{t("welcome.signup")}
</Link>
<Link
to="/auth/import"
className="inline-flex h-12 w-full items-center justify-center rounded-xl bg-neutral-950 text-lg font-medium text-white hover:bg-neutral-900"
>
{t("welcome.login")}
</Link>
</div>
</div>
<div className="flex h-11 items-center justify-center">
<p className="text-neutral-800">
{t("welcome.footer")}{" "}
<Link
to="https://nostr.com"
target="_blank"
className="text-blue-500"
>
here
</Link>
</p>
</div>
</div>
</div>
);
}
@@ -0,0 +1,9 @@
import { createLazyFileRoute } from "@tanstack/react-router";
export const Route = createLazyFileRoute("/settings/")({
component: Screen,
});
function Screen() {
return <div>Settings</div>;
}
@@ -0,0 +1,46 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import { WindowVirtualizer } from "virtua";
import { User } from "@lume/ui";
import { EventList } from "./-components/eventList";
export const Route = createLazyFileRoute("/users/$pubkey")({
component: Screen,
});
function Screen() {
const { pubkey } = Route.useParams();
return (
<WindowVirtualizer>
<div className="flex h-screen w-screen flex-col bg-gradient-to-tr from-neutral-200 to-neutral-100 dark:from-neutral-950 dark:to-neutral-900">
<div data-tauri-drag-region className="h-11 w-full shrink-0" />
<div className="flex h-full min-h-0 w-full">
<div className="h-full w-full flex-1 px-2 pb-2">
<div className="h-full w-full overflow-hidden overflow-y-auto rounded-xl bg-white shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-none dark:ring-1 dark:ring-white/5">
<User.Provider pubkey={pubkey}>
<User.Root>
<User.Cover className="h-44 w-full object-cover" />
<div className="relative -mt-8 flex flex-col gap-4 px-5">
<User.Avatar className="size-14 rounded-full" />
<div className="inline-flex items-start justify-between">
<div>
<User.Name className="font-semibold leading-tight" />
<User.NIP05 className="text-sm leading-tight text-neutral-600 dark:text-neutral-400" />
</div>
<User.Button className="h-9 w-24 rounded-full bg-black text-sm font-medium text-white hover:bg-neutral-900 dark:bg-neutral-900" />
</div>
<User.About />
</div>
</User.Root>
</User.Provider>
<div className="mt-4 px-5">
<h3 className="mb-4 text-lg font-semibold">Notes</h3>
<EventList id={pubkey} />
</div>
</div>
</div>
</div>
</div>
</WindowVirtualizer>
);
}
@@ -0,0 +1,71 @@
import { TextNote } from "@/components/text";
import { RepostNote } from "@/components/repost";
import { useArk } from "@lume/ark";
import { ArrowRightCircleIcon, LoaderIcon } from "@lume/icons";
import { Event, Kind } from "@lume/types";
import { EmptyFeed } from "@lume/ui";
import { FETCH_LIMIT } from "@lume/utils";
import { useInfiniteQuery } from "@tanstack/react-query";
export function EventList({ id }: { id: string }) {
const ark = useArk();
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ["events", id],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = await ark.get_events_from(id, FETCH_LIMIT, pageParam);
return events;
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage?.at(-1);
if (!lastEvent) return;
return lastEvent.created_at - 1;
},
select: (data) => data?.pages.flatMap((page) => page),
refetchOnWindowFocus: false,
});
const renderItem = (event: Event) => {
if (!event) return;
switch (event.kind) {
case Kind.Repost:
return <RepostNote key={event.id} event={event} />;
default:
return <TextNote key={event.id} event={event} />;
}
};
return (
<div>
{isLoading ? (
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
<LoaderIcon className="size-5 animate-spin" />
</div>
) : !data.length ? (
<EmptyFeed />
) : (
data.map((item) => renderItem(item))
)}
<div className="flex h-20 items-center justify-center">
{hasNextPage ? (
<button
type="button"
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-12 w-36 items-center justify-center gap-2 rounded-full bg-neutral-100 px-3 font-medium hover:bg-neutral-200 focus:outline-none dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
{isFetchingNextPage ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
<>
<ArrowRightCircleIcon className="size-5" />
Load more
</>
)}
</button>
) : null}
</div>
</div>
);
}
+16
View File
@@ -0,0 +1,16 @@
/** @type {import('tailwindcss').Config} */
import preset from "@lume/tailwindcss";
const config = {
content: [
"./src/**/*.{js,ts,jsx,tsx}",
"../../packages/@columns/**/*{.js,.ts,.jsx,.tsx}",
"../../packages/ark/**/*{.js,.ts,.jsx,.tsx}",
"../../packages/ui/**/*{.js,.ts,.jsx,.tsx}",
"index.html",
],
presets: [preset],
};
export default config;
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "@lume/tsconfig/base.json",
"compilerOptions": {
"outDir": "dist",
"baseUrl": "./",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
+6
View File
@@ -0,0 +1,6 @@
{
"routesDirectory": "./src/routes",
"generatedRouteTree": "./src/router.gen.ts",
"routeFileIgnorePrefix": "-",
"quoteStyle": "single"
}
+25
View File
@@ -0,0 +1,25 @@
import { TanStackRouterVite } from "@tanstack/router-vite-plugin";
import react from "@vitejs/plugin-react-swc";
import { defineConfig } from "vite";
import topLevelAwait from "vite-plugin-top-level-await";
import viteTsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [
react(),
viteTsconfigPaths(),
topLevelAwait({
promiseExportName: "__tla",
promiseImportName: (i) => `__tla_${i}`,
}),
TanStackRouterVite(),
],
build: {
outDir: "../../dist",
},
server: {
strictPort: true,
port: 3000,
},
clearScreen: false,
});
+21
View File
@@ -0,0 +1,21 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
+47
View File
@@ -0,0 +1,47 @@
# Astro Starter Kit: Minimal
```sh
npm create astro@latest -- --template minimal
```
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal)
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/minimal)
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/minimal/devcontainer.json)
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```text
/
├── public/
├── src/
│ └── pages/
│ └── index.astro
└── package.json
```
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
Any static assets, like images, can be placed in the `public/` directory.
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
+8
View File
@@ -0,0 +1,8 @@
import { defineConfig } from 'astro/config';
import tailwind from "@astrojs/tailwind";
// https://astro.build/config
export default defineConfig({
integrations: [tailwind()]
});
+26
View File
@@ -0,0 +1,26 @@
{
"name": "@lume/web",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "^0.4.1",
"@astrojs/tailwind": "^5.1.0",
"@fontsource/geist-mono": "^5.0.1",
"astro": "^4.4.9",
"astro-seo-meta": "^4.1.0",
"astro-seo-schema": "^4.0.0",
"schema-dts": "^1.1.2",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.10"
}
}
+37
View File
@@ -0,0 +1,37 @@
<svg width="824" height="824" viewBox="0 0 824 824" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_564_71)">
<rect width="824" height="824" rx="184" fill="white" style="fill:white;fill-opacity:1;"/>
<circle cx="267" cy="594" r="42" fill="black" style="fill:black;fill-opacity:1;"/>
<circle cx="267" cy="594" r="42" fill="url(#paint0_radial_564_71)" fill-opacity="0.5" style=""/>
<circle cx="267" cy="594" r="42" fill="url(#paint1_radial_564_71)" fill-opacity="0.3" style=""/>
<circle cx="557" cy="594" r="42" fill="black" style="fill:black;fill-opacity:1;"/>
<circle cx="557" cy="594" r="42" fill="url(#paint2_radial_564_71)" fill-opacity="0.5" style=""/>
<circle cx="557" cy="594" r="42" fill="url(#paint3_radial_564_71)" fill-opacity="0.3" style=""/>
<path d="M412 691C382.859 691 353.717 686.063 337.654 682.804C333.024 681.865 329.866 686.676 333.074 690.144C345.098 703.138 370.814 724 412 724C453.186 724 478.902 703.138 490.926 690.144C494.134 686.676 490.976 681.865 486.346 682.804C470.283 686.063 441.141 691 412 691Z" fill="url(#paint4_linear_564_71)" style=""/>
</g>
<defs>
<radialGradient id="paint0_radial_564_71" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(241.807 578.038) rotate(112.103) scale(88.2816 69.6512)">
<stop stop-color="white" style="stop-color:white;stop-opacity:1;"/>
<stop offset="1" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
</radialGradient>
<radialGradient id="paint1_radial_564_71" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(288.309 621.165) rotate(126.504) scale(25.5816 15.4047)">
<stop stop-color="white" style="stop-color:white;stop-opacity:1;"/>
<stop offset="1" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
</radialGradient>
<radialGradient id="paint2_radial_564_71" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(531.807 578.038) rotate(112.103) scale(88.2816 69.6512)">
<stop stop-color="white" style="stop-color:white;stop-opacity:1;"/>
<stop offset="1" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
</radialGradient>
<radialGradient id="paint3_radial_564_71" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(578.309 621.165) rotate(126.504) scale(25.5816 15.4047)">
<stop stop-color="white" style="stop-color:white;stop-opacity:1;"/>
<stop offset="1" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
</radialGradient>
<linearGradient id="paint4_linear_564_71" x1="293.565" y1="686.595" x2="316.497" y2="774.784" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF9F5A" style="stop-color:#FF9F5A;stop-color:color(display-p3 1.0000 0.6235 0.3529);stop-opacity:1;"/>
<stop offset="1" stop-color="#FF9F5A" style="stop-color:#FF9F5A;stop-color:color(display-p3 1.0000 0.6235 0.3529);stop-opacity:1;"/>
</linearGradient>
<clipPath id="clip0_564_71">
<rect width="824" height="824" fill="white" style="fill:white;fill-opacity:1;"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

+1
View File
@@ -0,0 +1 @@
/// <reference types="astro/client" />
+155
View File
@@ -0,0 +1,155 @@
---
import { Seo } from "astro-seo-meta";
---
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>Lume</title>
<Seo
title="Lume"
description="A multiple columns Nostr client for desktop."
keywords={[
"nostr",
"nostr client",
"social network",
"desktop app",
"timeline",
"application",
"columns",
]}
themeColor="#fafafa"
colorScheme="light"
facebook={{
image: "/og-image.jpg",
url: "https://lume.nu",
type: "website",
}}
twitter={{
image: "/og-image.jpg",
card: "summary",
}}
/>
</head>
<body
class="w-full h-full antialiased font-mono bg-neutral-50 dark:bg-neutral-950 text-neutral-950 dark:text-neutral-50"
>
<div class="max-w-2xl mx-auto w-full py-16 md:px-0 px-2">
<div class="flex flex-col gap-16">
<div class="prose dark:prose-invert prose-neutral max-w-none">
<h3>About Lume</h3>
<p>
Lume is a <b>Nostr client</b> for desktop include Linux, Windows and
macOS. It is free and open source, you can look at source code <a
href="https://github.com/lumehq/lume"
target="_blank">on Github</a
>. Lume is actively improving the app and adding new features, you
can expect new update every month.
</p>
<a href="#download">Download</a>
<h3>What is nostr & how does it work?</h3>
<p>
Nostr stands for Notes and Other Stuff Transmitted by Relays. It is
an open, permission-less protocol that aims to provide
censorship-resistance and interoperability. It can be used to create
social networks or just about any other type of app (other stuff
part of the acronym). It is not a single website or app, but the
glue that holds together many apps (clients) and <b>Lume</b> is one of
it.
</p>
<p>
At its core, nostr consists of relays and events. A person does
something (event) and this event is sent to a relay. The relay
stores the event, then waits for another person to request it. The
most common types of events are notes and reactions - the stuff
social media is made of, but there are many other types of events.
It works very similar to how any other app would work with a
database, except in nostr there is no single database, rather a
large number of relays that store the events.
</p>
<h3>Lume is multiple columns experience</h3>
<p>
Lume is display your timeline as multiple column, each column is
each different content and you can define your experience
</p>
<p>
You can create a column to display newsfeed from specific people,
you can create a column to display all contents related to some
hashtags. It all up to you.
</p>
<img
src="https://image.nostr.build/fd3e3cdeb4fb9f0f3de5c5e668a11dcae55f50cc9a78fc2b57b063240191a0f9.png"
alt="columns"
loading="lazy"
decoding="async"
class="w-full h-auto rounded-lg"
/>
<h3>"For You"</h3>
<p>
Unlike some social networks, they feed you by algorithm. In Lume,
you totally control what to will see
</p>
<img
src="https://image.nostr.build/5afd79de15929a4ac6f6e933791c942555baa4206fecee54fed61dde9fe167e1.png"
alt="for you"
loading="lazy"
decoding="async"
class="w-full h-auto rounded-lg"
/>
<h3 id="download">Download and Explore</h3>
<p>
(Universal) macOS: <a
href="https://github.com/lumehq/lume/releases/download/v3.0.0/Lume_3.0.0_universal.dmg"
>Lume_3.0.0_universal.dmg
</a>
</p>
<p>
(x86-64) Windows 11: <a
href="https://github.com/lumehq/lume/releases/download/v3.0.0/Lume_3.0.0_x64-setup.exe"
>Lume_3.0.0_x64-setup.exe
</a>
</p>
<p>
(x86-64) Ubuntu: <a
href="https://github.com/lumehq/lume/releases/download/v3.0.0/lume_3.0.0_amd64.deb"
>lume_3.0.0_amd64.deb
</a>
</p>
<p>
(x86-64) Fedora: <a
href="https://github.com/lumehq/lume/releases/download/v3.0.0/lume-3.0.0-1.x86_64.rpm"
>lume-3.0.0-1.x86_64.rpm
</a>
</p>
<p>
(x86-64) Linux Flatpak: <a
href="https://github.com/lumehq/lume/releases/download/v3.0.0/lume_3.0.0_amd64.flatpak"
>lume_3.0.0_amd64.flatpak
</a>
</p>
<p>
(x86-64) Linux AppImage: <a
href="https://github.com/lumehq/lume/releases/download/v3.0.0/lume_3.0.0_amd64.AppImage"
>lume_3.0.0_amd64.AppImage
</a>
</p>
<p>
Support for ARM, RISC-V and Loongarch architecture are coming soon.
</p>
</div>
<div class="text-center">
<p class="text-sm text-neutral-500 dark:text-neutral-600">
Supported by <a
href="https://opensats.org"
target="_blank"
class="text-orange-500">Open Sats</a
> and Community
</p>
</div>
</div>
</div>
</body>
</html>
+15
View File
@@ -0,0 +1,15 @@
/** @type {import('tailwindcss').Config} */
const defaultTheme = require("tailwindcss/defaultTheme");
export default {
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
theme: {
extend: {
fontFamily: {
mono: ["Geist Mono", ...defaultTheme.fontFamily.mono],
},
},
},
plugins: [require("@tailwindcss/typography")],
};
+3
View File
@@ -0,0 +1,3 @@
{
"extends": "astro/tsconfigs/strict"
}
+21
View File
@@ -0,0 +1,21 @@
{
"$schema": "https://biomejs.dev/schemas/1.4.1/schema.json",
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"style": {
"noNonNullAssertion": "warn"
},
"correctness": {
"useExhaustiveDependencies": "off"
},
"a11y": {
"noSvgWithoutTitle": "off"
}
}
}
}
Generated
+130
View File
@@ -0,0 +1,130 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1694529238,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1697723726,
"narHash": "sha256-SaTWPkI8a5xSHX/rrKzUe+/uVNy6zCGMXgoeMb7T9rg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "7c9cc5a6e5d38010801741ac830a3f8fd667a7a0",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1681358109,
"narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1697940838,
"narHash": "sha256-eyk92QqAoRNC0V99KOcKcBZjLPixxNBS0PRc4KlSQVs=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "a3e829c06eadf848f13d109c7648570ce37ebccd",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
+72
View File
@@ -0,0 +1,72 @@
# Nix.flake to build Lume based on Tauri's Guides:
# Prerequisites -> Installing -> Setting Up Linux -> NixOS
# https://tauri.app/v1/guides/getting-started/prerequisites/#1-system-dependencies
#
# To build Rust backend of Tauri `rust-overlay` is used
# https://github.com/oxalica/rust-overlay
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
rust-overlay.url = "github:oxalica/rust-overlay";
};
outputs = { self, nixpkgs, flake-utils, rust-overlay }:
flake-utils.lib.eachDefaultSystem (system:
let
overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs {
inherit system overlays;
};
libraries = with pkgs;[
webkitgtk
gtk3
cairo
gdk-pixbuf
glib
dbus
openssl_3
librsvg
libappindicator-gtk3
];
packages = with pkgs; [
curl
wget
pkg-config
dbus
openssl_3
glib
gtk3
libsoup
webkitgtk
librsvg
];
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
extensions = [ "rust-src" ]; # needed by rust-analyzer
};
in
{
devShells.default = pkgs.mkShell {
buildInputs = [
rustToolchain
pkgs.nodejs
pkgs.nodePackages.pnpm
pkgs.bun # experimental in Lume
] ++ packages;
shellHook =
''
export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath libraries}:$LD_LIBRARY_PATH
export XDG_DATA_DIRS=${pkgs.gsettings-desktop-schemas}/share/gsettings-schemas/${pkgs.gsettings-desktop-schemas.name}:${pkgs.gtk3}/share/gsettings-schemas/${pkgs.gtk3.name}:$XDG_DATA_DIRS
'';
# Avoid white screen running with Nix
# https://github.com/tauri-apps/tauri/issues/4315#issuecomment-1207755694
WEBKIT_DISABLE_COMPOSITING_MODE = 1;
};
});
}
+13
View File
@@ -0,0 +1,13 @@
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 21f5d9a5..9a46f36d 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -64,7 +64,7 @@
"shortDescription": "",
"targets": "all",
"updater": {
- "active": true,
+ "active": false,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEU3OTdCMkM3RjU5QzE2NzkKUldSNUZwejF4N0tYNTVHYjMrU0JkL090SlEyNUVLYU5TM2hTU3RXSWtEWngrZWJ4a0pydUhXZHEK",
"windows": {
"installMode": "quiet"
+52
View File
@@ -0,0 +1,52 @@
FROM node:20-slim as prepare
RUN apt update && apt install -y git
# Taken from tauri docs https://beta.tauri.app/guides/prerequisites/#rust
RUN apt install libwebkit2gtk-4.1-dev -y \
build-essential \
curl \
wget \
file \
libssl-dev \
libayatana-appindicator3-dev \
protobuf-compiler \
librsvg2-dev
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
FROM prepare as build
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
ENV PATH="/root/.cargo/bin:${PATH}"
#RUN corepack prepare pnpm@latest --activate
RUN corepack enable
ADD . /lume/.
WORKDIR /lume
RUN pnpm install --frozen-lockfile
# Path for disable updater
#ADD flatpak/0001-disable-tauri-updater.patch .
#RUN patch -p1 -t -i flatpak/0001-disable-tauri-updater.patch
#ENV VITE_FLATPAK_RESOURCE="/app/lib/lume/resources/config.toml"
# debian build
RUN pnpm tauri build -b deb
ARG VERSION=3.0.1
ARG ARCH=amd64
RUN cp -r ./src-tauri/target/release/bundle/deb/lume_${VERSION}_${ARCH}/data lume-package
FROM scratch as final
COPY --from=build lume/lume-package prepare-dist
#ADD flatpak/*.xml flatpak/*.desktop flatpak/*.yml prepare-dist
+78
View File
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>
nu.lume.Lume
</id>
<launchable type="desktop-id">
nu.lume.Lume.desktop
</launchable>
<name>
Lume
</name>
<summary>
A cross-platform desktop nostr client
</summary>
<developer_name>
Ren Amamiya
</developer_name>
<metadata_license>
CC0-1.0
</metadata_license>
<project_license>
GPL-3.0-only
</project_license>
<url type="homepage">
https://lume.nu
</url>
<url type="bugtracker">
https://github.com/lumehq/lume/issues
</url>
<url type="donation">
https://nostree.me/npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445
</url>
<supports>
<control>
pointing
</control>
<control>
keyboard
</control>
<control>
touch
</control>
</supports>
<description>
<p>
Lume a cross-platform nostr client, supported nsecbunker, chats and notifications
</p>
</description>
<custom>
<value key="Purism::form_factor">
workstation
</value>
<value key="Purism::form_factor">
mobile
</value>
</custom>
<screenshots>
<screenshot type="default">
<image>
https://raw.githubusercontent.com/lumehq/lume/flatpak/screenshots/login-screen.png
</image>
</screenshot>
<screenshot>
<image>
https://raw.githubusercontent.com/lumehq/lume/flatpak/screenshots/collumns.png
</image>
</screenshot>
<screenshot>
<image>
https://raw.githubusercontent.com/lumehq/lume/flatpak/screenshots/home-screen.png
</image>
</screenshot>
</screenshots>
<releases>
<release version="3.0.1" date="2024-02-02" />
</releases>
<content_rating type="oars-1.1" />
</component>
+12
View File
@@ -0,0 +1,12 @@
[Desktop Entry]
Version=1.0
Type=Application
Name=Lume
Comment=A cross-platform desktop nostr client
Icon=lume
Exec=lume
Terminal=false
Categories=Network;InstantMessaging;
Keywords=nostr;client;chat;
X-Purism-FormFactor=Workstation;
+40
View File
@@ -0,0 +1,40 @@
id: nu.lume.Lume
runtime: org.gnome.Platform
runtime-version: '45'
sdk: org.gnome.Sdk
command: lume
rename-icon: lume
finish-args:
- --socket=wayland
- --socket=fallback-x11
- --socket=pulseaudio
- --share=ipc
- --share=network
#- --filesystem=home
#- --filesystem=xdg-download
- --talk-name=org.freedesktop.secrets
- --talk-name=org.freedesktop.Notifications
- --talk-name=org.kde.StatusNotifierWatcher
- --filesystem=xdg-run/keyring
- --device=dri
modules:
- shared-modules/libappindicator/libappindicator-gtk3-12.10.json
- name: lume
sources:
- type: dir
path: usr
- type: file
path: nu.lume.Lume.desktop
- type: file
path: nu.lume.Lume.appdata.xml
buildsystem: simple
build-commands:
- install -Dm755 bin/lume /app/bin/lume
- mkdir -p /app/lib/lume/resources
- cp -r lib/lume/resources /app/lib/lume/resources
- mkdir -p /app/share/icons/hicolor/
- cp -r share/icons/hicolor/ /app/share/icons/
- install -Dm644 nu.lume.Lume.appdata.xml /app/share/metainfo/nu.lume.Lume.appdata.xml
- install -Dm644 nu.lume.Lume.desktop /app/share/applications/nu.lume.Lume.desktop
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 KiB

-11
View File
@@ -1,11 +0,0 @@
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lume</title>
</head>
<body class="relative cursor-default select-none overflow-hidden font-sans antialiased h-screen w-screen text-white">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+29 -105
View File
@@ -1,113 +1,37 @@
{
"name": "lume",
"description": "the communication app",
"private": true,
"version": "1.2.1",
"version": "4.0.0-alpha.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"tauri": "tauri",
"add-migrate": "cd src-tauri/ && sqlx migrate add",
"prepare": "husky install",
"lint": "eslint ./src --fix",
"format": "prettier ./src --write",
"dep-update": "pnpm update && cd src-tauri/ && cargo update"
},
"lint-staged": {
"**/*.{ts, tsx}": "eslint --fix",
"**/*.{ts, tsx, css, md, html, json}": "prettier --cache --write"
},
"dependencies": {
"@ctrl/magnet-link": "^3.1.2",
"@nostr-dev-kit/ndk": "^0.8.23",
"@nostr-fetch/adapter-ndk": "^0.12.2",
"@radix-ui/react-alert-dialog": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-tooltip": "^1.0.6",
"@tanstack/react-query": "^4.33.0",
"@tanstack/react-virtual": "3.0.0-beta.54",
"@tauri-apps/api": "2.0.0-alpha.6",
"@tauri-apps/cli": "2.0.0-alpha.11",
"@tauri-apps/plugin-app": "github:tauri-apps/tauri-plugin-app#v2",
"@tauri-apps/plugin-autostart": "github:tauri-apps/tauri-plugin-autostart#v2",
"@tauri-apps/plugin-clipboard-manager": "github:tauri-apps/tauri-plugin-clipboard-manager#v2",
"@tauri-apps/plugin-dialog": "github:tauri-apps/tauri-plugin-dialog#v2",
"@tauri-apps/plugin-fs": "github:tauri-apps/tauri-plugin-fs#v2",
"@tauri-apps/plugin-http": "github:tauri-apps/tauri-plugin-http#v2",
"@tauri-apps/plugin-notification": "github:tauri-apps/tauri-plugin-notification#v2",
"@tauri-apps/plugin-os": "github:tauri-apps/tauri-plugin-os#v2",
"@tauri-apps/plugin-process": "github:tauri-apps/tauri-plugin-process#v2",
"@tauri-apps/plugin-shell": "github:tauri-apps/tauri-plugin-shell#v2",
"@tauri-apps/plugin-sql": "github:tauri-apps/tauri-plugin-sql#v2",
"@tauri-apps/plugin-store": "github:tauri-apps/tauri-plugin-store#v2",
"@tauri-apps/plugin-stronghold": "github:tauri-apps/tauri-plugin-stronghold#v2",
"@tauri-apps/plugin-upload": "github:tauri-apps/tauri-plugin-upload#v2",
"@tauri-apps/plugin-window": "github:tauri-apps/tauri-plugin-window#v2",
"@tiptap/extension-image": "^2.1.7",
"@tiptap/extension-mention": "^2.1.7",
"@tiptap/extension-placeholder": "^2.1.7",
"@tiptap/pm": "^2.1.7",
"@tiptap/react": "^2.1.7",
"@tiptap/starter-kit": "^2.1.7",
"@tiptap/suggestion": "^2.1.7",
"@void-cat/api": "^1.0.7",
"dayjs": "^1.11.9",
"destr": "^2.0.1",
"get-urls": "^12.1.0",
"html-to-text": "^9.0.5",
"light-bolt11-decoder": "^3.0.0",
"lru-cache": "^10.0.1",
"minidenticons": "^4.2.0",
"nostr-fetch": "^0.12.2",
"nostr-tools": "^1.14.2",
"qrcode.react": "^3.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.45.4",
"react-hotkeys-hook": "^4.4.1",
"react-markdown": "^8.0.7",
"react-player": "^2.12.0",
"react-router-dom": "^6.15.0",
"react-textarea-autosize": "^8.5.3",
"react-virtuoso": "^4.5.0",
"remark-gfm": "^3.0.1",
"tippy.js": "^6.3.7",
"zustand": "^4.4.1"
"build": "turbo run build",
"dev": "turbo run dev",
"web:dev": "turbo run dev --filter web",
"desktop:dev": "turbo run dev --filter desktop2",
"desktop:build": "turbo run build --filter desktop2",
"tauri": "tauri"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.9",
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
"@types/html-to-text": "^9.0.1",
"@types/node": "^20.5.6",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"@types/youtube-player": "^5.5.7",
"@typescript-eslint/eslint-plugin": "^6.4.1",
"@typescript-eslint/parser": "^6.4.1",
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.15",
"clsx": "^2.0.0",
"cross-env": "^7.0.3",
"csstype": "^3.1.2",
"encoding": "^0.1.13",
"eslint": "^8.48.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-simple-import-sort": "^10.0.0",
"husky": "^8.0.3",
"lint-staged": "^14.0.1",
"postcss": "^8.4.28",
"prettier": "^3.0.2",
"prettier-plugin-tailwindcss": "^0.5.3",
"prop-types": "^15.8.1",
"tailwind-merge": "^1.14.0",
"tailwindcss": "^3.3.3",
"typescript": "^5.2.2",
"vite": "^4.4.9",
"vite-tsconfig-paths": "^4.2.0"
"@biomejs/biome": "^1.5.3",
"@tauri-apps/cli": "2.0.0-beta.6",
"turbo": "^1.12.4"
},
"packageManager": "pnpm@8.9.0",
"engines": {
"node": ">=18"
},
"dependencies": {
"@tauri-apps/api": "2.0.0-beta.3",
"@tauri-apps/plugin-autostart": "2.0.0-beta.1",
"@tauri-apps/plugin-clipboard-manager": "2.0.0-beta.1",
"@tauri-apps/plugin-dialog": "2.0.0-beta.1",
"@tauri-apps/plugin-fs": "2.0.0-beta.1",
"@tauri-apps/plugin-http": "2.0.0-beta.1",
"@tauri-apps/plugin-notification": "2.0.0-beta.1",
"@tauri-apps/plugin-os": "2.0.0-beta.1",
"@tauri-apps/plugin-process": "2.0.0-beta.1",
"@tauri-apps/plugin-shell": "2.0.0-beta.1",
"@tauri-apps/plugin-sql": "2.0.0-beta.1",
"@tauri-apps/plugin-updater": "2.0.0-beta.1",
"@tauri-apps/plugin-upload": "2.0.0-beta.1"
}
}
+40
View File
@@ -0,0 +1,40 @@
{
"name": "@lume/ark",
"version": "0.0.0",
"private": true,
"main": "./src/index.ts",
"dependencies": {
"@getalby/sdk": "^3.3.1",
"@lume/icons": "workspace:^",
"@lume/utils": "workspace:^",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-query": "^5.24.1",
"get-urls": "^12.1.0",
"media-chrome": "^2.2.5",
"minidenticons": "^4.2.1",
"nanoid": "^5.0.6",
"qrcode.react": "^3.1.0",
"re-resizable": "^6.9.11",
"react": "^18.2.0",
"react-currency-input-field": "^3.8.0",
"react-i18next": "^14.0.5",
"react-string-replace": "^1.1.1",
"sonner": "^1.4.3",
"string-strip-html": "^13.4.6",
"virtua": "^0.27.5"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.61",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
}
}
+529
View File
@@ -0,0 +1,529 @@
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
import type {
Account,
Contact,
Event,
EventWithReplies,
Keys,
Metadata,
} from "@lume/types";
import { invoke } from "@tauri-apps/api/core";
import { open } from "@tauri-apps/plugin-dialog";
import { readFile } from "@tauri-apps/plugin-fs";
import { generateContentTags } from "@lume/utils";
export class Ark {
public accounts: Account[];
constructor() {
this.accounts = [];
}
public async get_all_accounts() {
try {
const accounts: Account[] = [];
const cmd: string[] = await invoke("get_accounts");
if (cmd) {
for (const item of cmd) {
accounts.push({ npub: item.replace(".npub", "") });
}
this.accounts = accounts;
return accounts;
}
} catch {
return [];
}
}
public async load_selected_account(npub: string) {
try {
const cmd: boolean = await invoke("load_selected_account", {
npub,
});
return cmd;
} catch (e) {
throw new Error(String(e));
}
}
public async create_guest_account() {
try {
const keys = await this.create_keys();
await this.save_account(keys.nsec, "");
return keys.npub;
} catch (e) {
throw new Error(String(e));
}
}
public async create_keys() {
try {
const cmd: Keys = await invoke("create_keys");
return cmd;
} catch (e) {
console.error(String(e));
}
}
public async save_account(nsec: string, password: string = "") {
try {
const cmd: boolean = await invoke("save_key", {
nsec,
password,
});
return cmd;
} catch (e) {
throw new Error(String(e));
}
}
public async event_to_bech32(id: string, relays: string[]) {
try {
const cmd: string = await invoke("event_to_bech32", {
id,
relays,
});
return cmd;
} catch (e) {
throw new Error(String(e));
}
}
public async get_event(id: string) {
try {
const eventId: string = id
.replace("nostr:", "")
.split("'")[0]
.split(".")[0];
const cmd: string = await invoke("get_event", { id: eventId });
const event: Event = JSON.parse(cmd);
return event;
} catch (e) {
throw new Error(String(e));
}
}
public async get_events_from(id: string, limit: number, asOf?: number) {
try {
let until: string = undefined;
if (asOf && asOf > 0) until = asOf.toString();
const nostrEvents: Event[] = await invoke("get_events_from", {
id,
limit,
until,
});
return nostrEvents.sort((a, b) => b.created_at - a.created_at);
} catch {
return [];
}
}
public async get_events(
type: "local" | "global",
limit: number,
asOf?: number,
dedup?: boolean,
) {
try {
let until: string = undefined;
if (asOf && asOf > 0) until = asOf.toString();
const seenIds = new Set<string>();
const dedupQueue = new Set<string>();
const nostrEvents: Event[] = await invoke(`get_${type}_events`, {
limit,
until,
});
if (dedup) {
for (const event of nostrEvents) {
const tags = event.tags
.filter((el) => el[0] === "e")
?.map((item) => item[1]);
if (tags.length) {
for (const tag of tags) {
if (seenIds.has(tag)) {
dedupQueue.add(event.id);
break;
}
seenIds.add(tag);
}
}
}
return nostrEvents
.filter((event) => !dedupQueue.has(event.id))
.sort((a, b) => b.created_at - a.created_at);
}
return nostrEvents.sort((a, b) => b.created_at - a.created_at);
} catch {
return [];
}
}
public async publish(content: string, reply_to?: string, quote?: boolean) {
try {
const g = await generateContentTags(content);
const eventContent = g.content;
const eventTags = g.tags;
if (reply_to) {
const replyEvent = await this.get_event(reply_to);
if (quote) {
eventTags.push([
"e",
replyEvent.id,
replyEvent.relay || "",
"mention",
]);
} else {
const rootEvent = replyEvent.tags.find((ev) => ev[3] === "root");
if (rootEvent) {
eventTags.push(["e", rootEvent[1], rootEvent[2] || "", "root"]);
}
eventTags.push(["e", replyEvent.id, replyEvent.relay || "", "reply"]);
eventTags.push(["p", replyEvent.pubkey]);
}
}
const cmd: string = await invoke("publish", {
content: eventContent,
tags: eventTags,
});
return cmd;
} catch (e) {
throw new Error(String(e));
}
}
public async reply_to(content: string, tags: string[]) {
try {
const cmd: string = await invoke("reply_to", { content, tags });
return cmd;
} catch (e) {
throw new Error(String(e));
}
}
public async repost(id: string, author: string) {
try {
const cmd: string = await invoke("repost", { id, pubkey: author });
return cmd;
} catch (e) {
throw new Error(String(e));
}
}
public async upvote(id: string, author: string) {
try {
const cmd: string = await invoke("upvote", { id, pubkey: author });
return cmd;
} catch (e) {
throw new Error(String(e));
}
}
public async downvote(id: string, author: string) {
try {
const cmd: string = await invoke("downvote", { id, pubkey: author });
return cmd;
} catch (e) {
throw new Error(String(e));
}
}
public async get_event_thread(id: string) {
try {
const events: EventWithReplies[] = await invoke("get_event_thread", {
id,
});
if (events.length > 0) {
const replies = new Set();
for (const event of events) {
const tags = event.tags.filter(
(el) => el[0] === "e" && el[1] !== id && el[3] !== "mention",
);
if (tags.length > 0) {
for (const tag of tags) {
const rootIndex = events.findIndex((el) => el.id === tag[1]);
if (rootIndex !== -1) {
const rootEvent = events[rootIndex];
if (rootEvent?.replies) {
rootEvent.replies.push(event);
} else {
rootEvent.replies = [event];
}
replies.add(event.id);
}
}
}
}
const cleanEvents = events.filter((ev) => !replies.has(ev.id));
return cleanEvents;
}
return events;
} catch (e) {
return [];
}
}
public parse_event_thread({
content,
tags,
}: { content: string; tags: string[][] }) {
let rootEventId: string = null;
let replyEventId: string = null;
// Ignore quote repost
if (content.includes("nostr:note1") || content.includes("nostr:nevent1"))
return null;
// Get all event references from tags, ignore mention
const events = tags.filter((el) => el[0] === "e" && el[3] !== "mention");
if (!events.length) return null;
if (events.length === 1) {
return {
rootEventId: events[0][1],
replyEventId: null,
};
}
if (events.length > 1) {
rootEventId = events.find((el) => el[3] === "root")?.[1];
replyEventId = events.find((el) => el[3] === "reply")?.[1];
if (!rootEventId && !replyEventId) {
rootEventId = events[0][1];
replyEventId = events[1][1];
}
}
return {
rootEventId,
replyEventId,
};
}
public async get_profile(pubkey: string) {
try {
const id = pubkey
.replace("nostr:", "")
.split("'")[0]
.split(".")[0]
.split(",")[0]
.split("?")[0];
const cmd: Metadata = await invoke("get_profile", { id });
return cmd;
} catch {
return null;
}
}
public async get_contact_list() {
try {
const cmd: string[] = await invoke("get_contact_list");
return cmd;
} catch (e) {
console.error(e);
return [];
}
}
public async get_contact_metadata() {
try {
const cmd: Contact[] = await invoke("get_contact_metadata");
return cmd;
} catch (e) {
console.error(e);
return [];
}
}
public async follow(id: string, alias?: string) {
try {
const cmd: string = await invoke("follow", { id, alias });
return cmd;
} catch (e) {
throw new Error(String(e));
}
}
public async unfollow(id: string) {
try {
const cmd: string = await invoke("unfollow", { id });
return cmd;
} catch (e) {
throw new Error(String(e));
}
}
public async user_to_bech32(key: string, relays: string[]) {
try {
const cmd: string = await invoke("user_to_bech32", {
key,
relays,
});
return cmd;
} catch (e) {
throw new Error(String(e));
}
}
public async verify_nip05(pubkey: string, nip05: string) {
try {
const cmd: boolean = await invoke("verify_nip05", {
key: pubkey,
nip05,
});
return cmd;
} catch {
return false;
}
}
public async set_nwc(uri: string) {
try {
const cmd: boolean = await invoke("set_nwc", { uri });
return cmd;
} catch {
return false;
}
}
public async zap_profile(id: string, amount: number, message: string) {
try {
const cmd: boolean = await invoke("zap_profile", { id, amount, message });
return cmd;
} catch {
return false;
}
}
public async zap_event(id: string, amount: number, message: string) {
try {
const cmd: boolean = await invoke("zap_event", { id, amount, message });
return cmd;
} catch {
return false;
}
}
public async upload(filePath?: string) {
try {
const allowExts = [
"png",
"jpeg",
"jpg",
"gif",
"mp4",
"mp3",
"webm",
"mkv",
"avi",
"mov",
];
const selected =
filePath ||
(
await open({
multiple: false,
filters: [
{
name: "Media",
extensions: allowExts,
},
],
})
).path;
if (!selected) return null;
const file = await readFile(selected);
const blob = new Blob([file]);
const data = new FormData();
data.append("fileToUpload", blob);
data.append("submit", "Upload Image");
const res = await fetch("https://nostr.build/api/v2/upload/files", {
method: "POST",
body: data,
});
if (!res.ok) return null;
const json = await res.json();
const content = json.data[0];
return content.url as string;
} catch (e) {
console.error(String(e));
return null;
}
}
public open_thread(id: string) {
return new WebviewWindow(`event-${id}`, {
title: "Thread",
url: `/events/${id}`,
minWidth: 500,
width: 600,
height: 800,
hiddenTitle: true,
titleBarStyle: "overlay",
});
}
public open_profile(pubkey: string) {
return new WebviewWindow(`user-${pubkey}`, {
title: "Profile",
url: `/users/${pubkey}`,
minWidth: 500,
width: 500,
height: 800,
hiddenTitle: true,
titleBarStyle: "overlay",
});
}
public open_editor(reply_to?: string, quote: boolean = false) {
let url: string;
if (reply_to) {
url = `/editor?reply_to=${reply_to}&quote=${quote}`;
} else {
url = "/editor";
}
return new WebviewWindow("editor", {
title: "Editor",
url,
minWidth: 500,
width: 600,
height: 400,
hiddenTitle: true,
titleBarStyle: "overlay",
fileDropEnabled: true,
});
}
}
+4
View File
@@ -0,0 +1,4 @@
import { createContext } from "react";
import { type Ark } from "./ark";
export const ArkContext = createContext<Ark | null>(undefined);
+10
View File
@@ -0,0 +1,10 @@
import { useContext } from "react";
import { ArkContext } from "../context";
export const useArk = () => {
const context = useContext(ArkContext);
if (context === undefined) {
throw new Error("useArk must be used within an ArkProvider");
}
return context;
};
+24
View File
@@ -0,0 +1,24 @@
import { useQuery } from "@tanstack/react-query";
import { useArk } from "./useArk";
export function useEvent(id: string) {
const ark = useArk();
const { isLoading, isError, data } = useQuery({
queryKey: ["event", id],
queryFn: async () => {
try {
const event = await ark.get_event(id);
return event;
} catch (e) {
throw new Error(e);
}
},
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
staleTime: Infinity,
retry: 2,
});
return { isLoading, isError, data };
}
+28
View File
@@ -0,0 +1,28 @@
import { useQuery } from "@tanstack/react-query";
import { useArk } from "./useArk";
export function useProfile(pubkey: string) {
const ark = useArk();
const {
isLoading,
isError,
data: profile,
} = useQuery({
queryKey: ["user", pubkey],
queryFn: async () => {
try {
const profile = await ark.get_profile(pubkey);
return profile;
} catch (e) {
throw new Error(e);
}
},
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
staleTime: Infinity,
retry: 2,
});
return { isLoading, isError, profile };
}
+5
View File
@@ -0,0 +1,5 @@
export * from "./ark";
export * from "./context";
export * from "./hooks/useArk";
export * from "./hooks/useEvent";
export * from "./hooks/useProfile";
+10
View File
@@ -0,0 +1,10 @@
/** @type {import('tailwindcss').Config} */
import preset from "@lume/tailwindcss";
const config = {
content: ["./src/**/*.{js,ts,jsx,tsx}"],
presets: [preset],
};
export default config;
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "@lume/tsconfig/base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
+118
View File
@@ -0,0 +1,118 @@
export * from "./src/addWidget";
export * from "./src/arrowLeft";
export * from "./src/arrowRight";
export * from "./src/bell";
export * from "./src/cancel";
export * from "./src/checkCircle";
export * from "./src/chevronDown";
export * from "./src/chevronRight";
export * from "./src/compose";
export * from "./src/copy";
export * from "./src/edit";
export * from "./src/enter";
export * from "./src/eyeOff";
export * from "./src/eyeOn";
export * from "./src/feed";
export * from "./src/heartbeat";
export * from "./src/hide";
export * from "./src/image";
export * from "./src/like";
export * from "./src/lume";
export * from "./src/media";
export * from "./src/mute";
export * from "./src/space";
export * from "./src/spaceFilled";
export * from "./src/navArrowDown";
export * from "./src/plus";
export * from "./src/plusCircle";
export * from "./src/refresh";
export * from "./src/reply";
export * from "./src/replyMessage";
export * from "./src/repost";
export * from "./src/threads";
export * from "./src/trash";
export * from "./src/world";
export * from "./src/zap";
export * from "./src/loader";
export * from "./src/trending";
export * from "./src/empty";
export * from "./src/cmd";
export * from "./src/verticalDots";
export * from "./src/signal";
export * from "./src/unverified";
export * from "./src/settings";
export * from "./src/logout";
export * from "./src/follow";
export * from "./src/unfollow";
export * from "./src/reaction";
export * from "./src/thread";
export * from "./src/strangers";
export * from "./src/download";
export * from "./src/horizontalDots";
export * from "./src/arrowRightCircle";
export * from "./src/hashtag";
export * from "./src/file";
export * from "./src/share";
export * from "./src/expand";
export * from "./src/focus";
export * from "./src/chevronUp";
export * from "./src/secure";
export * from "./src/verified";
export * from "./src/mention";
export * from "./src/groupFeeds";
export * from "./src/article";
export * from "./src/follows";
export * from "./src/alby";
export * from "./src/stars";
export * from "./src/nwc";
export * from "./src/timeline";
export * from "./src/dots";
export * from "./src/handArrowDown";
export * from "./src/relay";
export * from "./src/explore";
export * from "./src/explore2";
export * from "./src/home";
export * from "./src/chats";
export * from "./src/community";
export * from "./src/heading1";
export * from "./src/heading2";
export * from "./src/heading3";
export * from "./src/bold";
export * from "./src/italic";
export * from "./src/user";
export * from "./src/advancedSettings";
export * from "./src/info";
export * from "./src/light";
export * from "./src/dark";
export * from "./src/system";
export * from "./src/announcement";
export * from "./src/depot";
export * from "./src/search";
export * from "./src/run";
export * from "./src/gossip";
export * from "./src/userAdd";
export * from "./src/userRemove";
export * from "./src/pin";
export * from "./src/homeFilled";
export * from "./src/relayFilled";
export * from "./src/depotFilled";
export * from "./src/nwcFilled";
export * from "./src/moveLeft";
export * from "./src/moveRight";
export * from "./src/help";
export * from "./src/plusSquare";
export * from "./src/column";
export * from "./src/addMedia";
export * from "./src/check";
export * from "./src/popperFilled";
export * from "./src/composeFilled";
export * from "./src/settingsFilled";
export * from "./src/bellFilled";
export * from "./src/foryou";
export * from "./src/editInterest";
export * from "./src/newColumn";
export * from "./src/searchFilled";
export * from "./src/arrowUp";
export * from "./src/arrowUpSquare";
export * from "./src/arrowDown";
export * from "./src/link";
+14
View File
@@ -0,0 +1,14 @@
{
"name": "@lume/icons",
"version": "0.0.0",
"private": true,
"main": "./index.ts",
"dependencies": {
"react": "^18.2.0"
},
"devDependencies": {
"@lume/tsconfig": "workspace:*",
"@types/react": "^18.2.61",
"typescript": "^5.3.3"
}
}
+20
View File
@@ -0,0 +1,20 @@
export function AddMediaIcon(props: JSX.IntrinsicElements["svg"]) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M19 22v-3m0 0v-3m0 3h-3m3 0h3m0-5.648V11l-.001-1m-9.464 11H10c-.756 0-1.41 0-1.983-.01M22 10H21c-1.393 0-2.09 0-2.676.06A11.5 11.5 0 008.06 20.324c-.02.2-.034.415-.043.665M22 10c-.008-2.15-.068-3.336-.544-4.27a5 5 0 00-2.185-2.185C18.2 3 16.8 3 14 3h-4c-2.8 0-4.2 0-5.27.545A5 5 0 002.545 5.73C2 6.8 2 8.2 2 11v2c0 2.8 0 4.2.545 5.27a5 5 0 002.185 2.185c.78.398 1.738.505 3.287.534M7.5 9.5a1 1 0 110-2 1 1 0 010 2z"
/>
</svg>
);
}
@@ -16,7 +16,7 @@ export function AddWidgetIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGEl
strokeLinejoin="round"
strokeWidth="1.5"
d="M12.25 21.25h-6.5a1 1 0 01-1-1V3.75a1 1 0 011-1h12.5a1 1 0 011 1v8.5m-1 3v3m0 0v3m0-3h-3m3 0h3"
></path>
/>
</svg>
);
}
@@ -1,6 +1,8 @@
import { SVGProps } from 'react';
export function FocusIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
export function AdvancedSettingsIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>
) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -15,7 +17,7 @@ export function FocusIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElemen
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M8.25 3.75h-3.5a1 1 0 00-1 1v3.5m12-4.5h3.5a1 1 0 011 1v3.5m0 7.5v3.5a1 1 0 01-1 1h-3.5m-7.5 0h-3.5a1 1 0 01-1-1v-3.5"
d="M13.75 7h-10m10 0a3.25 3.25 0 116.5 0 3.25 3.25 0 11-6.5 0zm6.5 10h-8m0 0a3.25 3.25 0 11-6.5 0m6.5 0a3.25 3.25 0 10-6.5 0m0 0h-2"
></path>
</svg>
);
+74
View File
@@ -0,0 +1,74 @@
import { SVGProps } from 'react';
export function AlbyIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="400"
height="578"
fill="none"
viewBox="0 0 400 578"
{...props}
>
<path
fill="#000"
d="M201.283 577.511c54.122 0 97.998-8.1 97.998-18.092 0-9.992-43.876-18.092-97.998-18.092-54.123 0-97.998 8.1-97.998 18.092 0 9.992 43.875 18.092 97.998 18.092z"
opacity="0.1"
></path>
<path
fill="#fff"
stroke="#000"
strokeWidth="15.077"
d="M295.75 471.344c50.627 0 73.67-112.102 73.67-154.608 0-33.13-22.86-53.208-52.913-53.208-29.866 0-54.113 12.843-54.414 28.747-.001 41.971-7.388 179.069 33.657 179.069zM110.837 471.344c-50.627 0-73.67-112.102-73.67-154.608 0-33.13 22.86-53.208 52.913-53.208 29.866 0 54.113 12.843 54.414 28.747.001 41.971 7.388 179.069-33.657 179.069z"
></path>
<path
fill="#FFDF6F"
stroke="#000"
strokeWidth="15"
d="M68.83 303.262v-.002c-.054-.519.052-.82.16-1.016.127-.232.368-.508.773-.738.84-.477 2.014-.563 3.108.076 37.603 22.042 80.976 34.678 128.13 34.678 47.163 0 91.339-12.881 129.184-35.307 1.087-.645 2.26-.565 3.102-.091.407.229.65.504.779.737.109.197.216.499.163 1.019-5.854 58.014-37.322 105.977-79.618 128.054-13.969 7.293-23.576 19.962-32.013 31.089l-.452.597-.002.002c-6.857 9.046-13.063 17.147-20.648 23.116-7.584-5.969-13.791-14.07-20.648-23.116l-.001-.002-.452-.597c-8.437-11.127-18.043-23.796-32.013-31.089-42.135-21.992-73.523-69.677-79.551-127.41z"
></path>
<path
fill="#000"
stroke="#000"
strokeWidth="15.077"
d="M201.786 346.338c73.274 0 132.674-19.8 132.674-44.225s-59.4-44.225-132.674-44.225-132.674 19.8-132.674 44.225 59.4 44.225 132.674 44.225z"
></path>
<path
stroke="#000"
strokeLinecap="round"
strokeWidth="15.077"
d="M95.245 376.491s65.44 22.112 107.546 22.112c42.105 0 107.546-22.112 107.546-22.112"
></path>
<path
fill="#000"
d="M77 143c-16.569 0-30-13.431-30-30 0-16.569 13.431-30 30-30 16.569 0 30 13.431 30 30 0 16.569-13.431 30-30 30z"
></path>
<path stroke="#000" strokeWidth="15" d="M72 108.5l56 56"></path>
<path
fill="#000"
d="M322 143c16.569 0 30-13.431 30-30 0-16.569-13.431-30-30-30-16.569 0-30 13.431-30 30 0 16.569 13.431 30 30 30z"
></path>
<path stroke="#000" strokeWidth="15" d="M327.5 108.5l-56 56"></path>
<path
fill="#FFDF6F"
fillRule="evenodd"
d="M85.516 292.019c-16.17-7.698-25.58-24.983-22.427-42.612C76.618 173.747 133 117 200.5 117c67.663 0 124.155 57.023 137.509 132.958 3.106 17.66-6.381 34.937-22.605 42.572C280.687 308.868 241.91 318 201 318c-41.335 0-80.493-9.323-115.484-25.981z"
clipRule="evenodd"
></path>
<path
fill="#000"
d="M70.472 250.728C83.544 177.62 137.582 124.5 200.5 124.5v-15c-72.082 0-130.809 60.375-144.794 138.587l14.766 2.641zM200.5 124.5c63.069 0 117.218 53.379 130.122 126.757l14.774-2.598C331.592 170.166 272.758 109.5 200.5 109.5v15zm111.71 161.244C278.472 301.621 240.783 310.5 201 310.5v15c42.037 0 81.902-9.386 117.597-26.183l-6.387-13.573zM201 310.5c-40.196 0-78.255-9.064-112.26-25.253l-6.448 13.544C118.269 315.918 158.526 325.5 201 325.5v-15zm129.622-59.243c2.49 14.159-5.091 28.219-18.412 34.487l6.387 13.573c19.128-9.002 30.52-29.497 26.799-50.658l-14.774 2.598zm-274.916-3.17c-3.778 21.124 7.524 41.629 26.586 50.704l6.447-13.544c-13.276-6.32-20.795-20.387-18.267-34.519l-14.766-2.641z"
></path>
<path
fill="#000"
fillRule="evenodd"
d="M114.365 273.209c-13.015-5.301-20.736-19.149-16.226-32.459C112.047 199.704 152.618 170 200.5 170c47.882 0 88.453 29.704 102.361 70.75 4.51 13.31-3.211 27.158-16.226 32.459C260.053 284.035 230.973 290 200.5 290c-30.473 0-59.553-5.965-86.135-16.791z"
clipRule="evenodd"
></path>
<path
fill="#fff"
d="M235 254c13.807 0 25-8.954 25-20s-11.193-20-25-20-25 8.954-25 20 11.193 20 25 20zM163.432 254.012c13.807 0 25-8.954 25-20s-11.193-20-25-20-25 8.954-25 20 11.193 20 25 20z"
></path>
</svg>
);
}
+18
View File
@@ -0,0 +1,18 @@
export function AnnouncementIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg
{...props}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
>
<path d="M16.36 3.014A27.429 27.429 0 0 1 8.143 8.04l-4.67 1.825a5.126 5.126 0 0 0 1.7 6.34l1.631-.25m9.556-12.94c-.875.234-.824 3.262.114 6.764.938 3.501 2.408 6.15 3.283 5.915M16.36 3.014c.875-.234 2.345 2.414 3.284 5.915.938 3.502.989 6.53.113 6.765m0 0a27.428 27.428 0 0 0-8.595-.382m0 0L13.295 22H8.92l-2.116-6.044m4.358-.644c-.345.04-.69.085-1.034.138l-3.324.506" />
</svg>
);
}
+19
View File
@@ -0,0 +1,19 @@
export function AntenasIcon(props: JSX.IntrinsicElements["svg"]) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M8 14a5 5 0 118 0m1 4.483a9 9 0 10-10 0M12 22l1.367-4.103a1.441 1.441 0 10-2.735 0L12 22zm0-10a1 1 0 110-2 1 1 0 010 2z"
/>
</svg>
);
}
+24
View File
@@ -0,0 +1,24 @@
import { SVGProps } from "react";
export function ArrowDownIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6.5 14.17a30.23 30.23 0 005.406 5.62c.174.14.384.21.594.21m6-5.83a30.232 30.232 0 01-5.406 5.62.949.949 0 01-.594.21m0 0V4"
/>
</svg>
);
}
+18
View File
@@ -0,0 +1,18 @@
export function ArrowLeftIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg
{...props}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
>
<path d="M8.83 6a30.23 30.23 0 0 0-5.62 5.406A.949.949 0 0 0 3 12m5.83 6a30.233 30.233 0 0 1-5.62-5.406A.949.949 0 0 1 3 12m0 0h18" />
</svg>
);
}

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