275 Commits

Author SHA1 Message Date
97b6755504 Add missing localized strings and export strings for translation 2025-09-04 22:22:27 -04:00
William Casarin
c765b031e9 ui/note: fix actionbar note responses
they are not responsive on android?

Signed-off-by: William Casarin <jb55@jb55.com>
2025-09-04 15:34:13 -07:00
William Casarin
024cf3ef91 Merge back nav threshold fix by kernel #1118 2025-09-04 14:25:43 -07:00
kernelkind
3a0da9a3e0 nav: reduce back nav threshold from 1/2 to 1/4
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-04 14:24:07 -07:00
kernelkind
10b62a073b test: NoteUnits repost test
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-04 15:53:53 -04:00
kernelkind
ac212b96a6 process repost notes
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-04 15:53:49 -04:00
kernelkind
637b05c1e2 make get_reposted_note pub
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-04 15:53:46 -04:00
kernelkind
f436b49fec add Repost to composite unit & fragment
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-04 15:53:43 -04:00
kernelkind
04ce29d1dd ui: render repost cluster impl
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-04 15:53:40 -04:00
kernelkind
ae1d5ab1c5 add CompositeType::Repost
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-04 15:53:37 -04:00
kernelkind
7caf77aa1c image: repost_image wrapper
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-04 15:53:34 -04:00
kernelkind
80ae489967 ui: repost description impl
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-04 15:53:31 -04:00
kernelkind
259c0b677a add RepostUnit & RepostFragment
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-04 15:53:27 -04:00
kernelkind
3b7f1f1b39 test: better naming for NoteUnits tests
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-04 15:53:24 -04:00
kernelkind
f2258ab16b add NotePayload::noteref helper
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-04 15:53:21 -04:00
kernelkind
571435cf85 ui: modularize composite entry rendering & fix tr
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-04 15:53:11 -04:00
kernelkind
8f8ff42156 NoteUnits: use UnitKey instead of just NoteKey
in preparation for multiple composite types

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-04 15:45:39 -04:00
kernelkind
3a95ba05a8 add ReactionFragment error msg
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-04 15:45:31 -04:00
William Casarin
73e44d1497 Merge Translations by terry et al #1082
Terry Yiu (2):
      Export strings for translation
      Import translations
2025-09-02 15:10:26 -07:00
William Casarin
43b98fc6ed Merge add keys section to settings by kernel #1096
kernelkind (4):
      add `AnimationHelper::scaled_rect`
      add copy to clipboard img
      make eye button public
      add keys section to settings
2025-09-02 15:09:38 -07:00
William Casarin
95ee275153 Merge custom-zap: dont force keyboard by kernel #1097
kernelkind (1):
      custom-zap: dont force keyboard
2025-09-02 15:08:53 -07:00
kernelkind
8bc54cc519 zap: add requirements for zapping user
these requirements are specified by nip 57 but weren't implemented

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-01 17:07:53 -04:00
kernelkind
5282373434 use PayCache when zapping
to avoid needlessly querying ln endpoint

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-01 17:07:43 -04:00
kernelkind
14c59a6c94 introduce PayCache
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-01 17:03:57 -04:00
kernelkind
09238baee0 add LNUrlPayResponse
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-01 17:03:52 -04:00
kernelkind
594072cfb8 make get_users_zap_address Result
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-01 17:03:41 -04:00
kernelkind
2882b1c2d9 move ZapAddress to zaps/mod.rs
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-01 17:01:08 -04:00
kernelkind
f4b8d235eb rename get_users_zap_endpoint -> get_users_zap_address
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-01 17:01:05 -04:00
kernelkind
cf48b29fd8 make endpoint error into struct
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-01 17:01:01 -04:00
kernelkind
2a7c5eb983 rename LNUrlPayRequest -> LNUrlPayResponseRaw
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-01 13:33:33 -04:00
kernelkind
72d696beb2 actionbar: reintroduce error messages
there was a regression that caused error messages to not be displayed
any more when zapping.

Now when you click the zap button and the zap fails for some reason, the
zap button will be replaced with an X and hovering over the X displays
the error message

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-31 19:34:51 -04:00
kernelkind
dea695fa8e actionbar: don't early return
it's not good practice to early return while rendering,
super easy to introduce flickering

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-31 19:29:11 -04:00
kernelkind
fc1caf5eb4 custom-zap: dont force keyboard
it's not necessary

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-28 16:46:26 -04:00
kernelkind
5539e4ef82 add keys section to settings
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-28 16:27:51 -04:00
kernelkind
408afbda50 make eye button public
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-28 16:27:32 -04:00
kernelkind
af4b896739 add copy to clipboard img
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-28 16:27:08 -04:00
kernelkind
d448caa369 add AnimationHelper::scaled_rect
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-28 16:26:36 -04:00
b84ad4f1cd Import translations
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-08-28 09:08:37 -04:00
736ce50f64 Export strings for translation
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-08-28 09:08:13 -04:00
William Casarin
e9ca793509 macos: fix build script
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-28 07:11:04 +08:00
William Casarin
ea65af8d5b v0.7.1
William Casarin (2):
      fix android-activity crash on unhandled app cmds

Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-28 06:54:44 +08:00
William Casarin
11aa2142cf fix android-activity crash on unhandled app cmds
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-26 17:46:13 -07:00
Fernando López Guevara
6ee2b28e70 media: handle upload on android 2025-08-26 17:10:38 -07:00
William Casarin
31ee64827a v0.7.0
First official Android Release!

- Keyboard visibiliy
- core lightning node ui (experimental)
- Onboarding follow packs
- Reaction notifications!
- Japanese translations

Terry Yiu (3):
      Remove unused strings from translation files
      Import translations
      Add Japanese and Portuguese (Portugal) languages

William Casarin (38):
      battery: disable render every 100ms
      dave: switch to logical time
      force oled with --mobile flag
      gif: disable continuous gif rendering
      ui: add AnimationMode to control GIF rendering behavior
      debug: add repaint causes debug tool
      Merge thread scroll fix by kernel
      chrome: add virtual keyboard ui
      android: fix dark/light mode and folding screen crash
      notedeck app: add clndash
      clndash: initial peer channel listing
      default logs
      clndash: channels ui
      clndash: summary cards
      clndash: include listpeerchannel errors
      clndash: invoice loading
      clndash: zap rendering
      clndash: fix invoice order, return more stuff
      clndash: reorganize
      clndash: configurable host
      clndash: add readme
      clndash: readme
      clndash: tweak readme
      clndash: tweak links in readme
      clndash: specify you need --clndash
      clndash: dont forget CLNDASH_ID
      remove hjkl bindings
      Merge Japanese and Portuguese translations from Terry
      clippy: fix lint errors
      Implement soft keyboard visibility on Android
      chrome: greatly improve soft-keyboard visibility & layout handling
      args: parse hashtag columns from cli
      debug: fix memory debug builds
      Merge remote-tracking branch 'github/pr/1087' into notifications
      Merge remote-tracking branch 'github/pr/1081' into notifications  especially if it merges an updated upstream into a topic branch.
      tweak follow pack design
      chrome: remove dev log
      v0.7.0

kernelkind (53):
      TMP: use new egui-nav to fix scroll offset issues
      add `scroll_offset` to `NoteAction::Note`
      add `ThreadNote::set_scroll_offset`
      set scroll offset when routing to thread
      appease clippy
      make search icon more customizable
      make compose button animate horiz rather than vert
      add toolbar icons to `notedeck_ui`
      add select_by_route
      add toolbar related logic
      add toolbar defaults
      copy toolbar rendering to `notedeck_ui`
      use toolbar in columns rather than chrome
      clippy: allow collapsible match
      add flags to `ScaledTexture`
      extract a pub `render_media` from image_carousel
      add impl for `ScaledTextureFlags::RESPECT_MAX_DIMS`
      add nip51 set caching structs
      nip 51 set widget
      add onboarding 'manager'
      TMP: temporary author for trusted pks list
      add onboarding view
      add onboarding related state to app
      integrate onboarding
      fix contact list bug
      use the onboarding follow pack curator pubkey
      make `TimelineCache::notes` private
      remove commented out code...
      move `HybridSet` to own file
      ui: add like icon
      appease clippy
      add muted helper
      unknownids: use pk bytes
      replace `HybridSet` with `NoteUnits`
      add reactions kind to notifications filter
      add `TimelineUnits`
      note: account for mutes in the notifications dot
      make since optimize accept Option<&NoteRef> instead of notes
      prop `UnknownIds` for initial timeline
      ui: add rendering for `NoteUnit`s
      upgrade `TimelineOpenResult` to hold new pubkeys too
      use `TimelineUnits` instead of `Vec<NoteRef>`
      ui: remove unnecessary reverse
      introduce failing test for reaction duplication bug
      fix duplicate ReactionUnit for multiple kth indices
      fix reaction target bug
      ui: reactions closer approximation of iOS design
      expose indexmap to notedeck
      use indexmap
      add Nip51SetCache helper methods
      add virtual list to `Onboarding`
      prop `Onboarding` as mut
      render follow pack by index from virtual list

Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-26 15:05:12 -07:00
William Casarin
f243adc855 chrome: remove dev log
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-26 14:40:14 -07:00
William Casarin
5224a5d8ae tweak follow pack design
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-26 10:26:14 -07:00
William Casarin
2c96dd99a8 Merge remote-tracking branch 'github/pr/1081' into notifications
especially if it merges an updated upstream into a topic branch.
2025-08-26 10:01:09 -07:00
William Casarin
e7843bad2f Merge remote-tracking branch 'github/pr/1087' into notifications 2025-08-26 09:58:50 -07:00
William Casarin
c2f012ff75 debug: fix memory debug builds
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-26 09:53:37 -07:00
William Casarin
76fd7a9753 args: parse hashtag columns from cli
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-26 09:53:37 -07:00
kernelkind
8b5464641d render follow pack by index from virtual list
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-25 21:16:05 -04:00
kernelkind
c06d18f76b prop Onboarding as mut
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-25 21:14:52 -04:00
kernelkind
84e60e0642 add virtual list to Onboarding
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-25 21:14:02 -04:00
kernelkind
23f35c60bb add Nip51SetCache helper methods
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-25 21:13:17 -04:00
kernelkind
30c2ebdcc2 use indexmap
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-25 21:12:41 -04:00
kernelkind
1658600604 expose indexmap to notedeck
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-25 21:11:28 -04:00
kernelkind
529377a706 ui: reactions closer approximation of iOS design
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-25 20:15:40 -04:00
kernelkind
30af03cfcc fix reaction target bug
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-25 20:02:34 -04:00
kernelkind
bb878d3772 fix duplicate ReactionUnit for multiple kth indices
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-25 20:02:34 -04:00
kernelkind
5c9eb492b6 introduce failing test for reaction duplication bug
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-25 20:02:34 -04:00
kernelkind
0b584a773f ui: remove unnecessary reverse
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-25 10:57:00 -04:00
kernelkind
78504a6673 use TimelineUnits instead of Vec<NoteRef>
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-25 10:56:56 -04:00
kernelkind
ae204cbd5c upgrade TimelineOpenResult to hold new pubkeys too
for handling unknown profiles

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-25 10:56:53 -04:00
kernelkind
7d4e9799e5 ui: add rendering for NoteUnits
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-25 10:56:48 -04:00
kernelkind
55d7cd3379 prop UnknownIds for initial timeline
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-25 10:32:10 -04:00
kernelkind
697040d862 make since optimize accept Option<&NoteRef> instead of notes
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-25 10:32:07 -04:00
kernelkind
49866418a6 note: account for mutes in the notifications dot
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-25 10:32:03 -04:00
kernelkind
9b784dfdf7 add TimelineUnits
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-25 10:31:59 -04:00
kernelkind
c1d6c0f535 add reactions kind to notifications filter
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-25 10:31:54 -04:00
kernelkind
1a93663b1a replace HybridSet with NoteUnits
This will unify the collections that hold the notes to timelines
and threads and allow the notifications timeline to have grouped
notifications, among other things

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-25 10:31:50 -04:00
kernelkind
4992e25b3a unknownids: use pk bytes
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-25 10:31:47 -04:00
kernelkind
7b1ace328f add muted helper
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-25 10:31:43 -04:00
kernelkind
2973a0c6c5 appease clippy
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-25 10:31:42 -04:00
kernelkind
4f63629715 ui: add like icon
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-25 10:31:37 -04:00
kernelkind
686dea9831 move HybridSet to own file
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-25 10:31:34 -04:00
kernelkind
01171ff9d7 remove commented out code...
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-25 10:31:27 -04:00
kernelkind
b421e7e45f make TimelineCache::notes private
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-25 10:31:20 -04:00
William Casarin
ccc188c0ae chrome: greatly improve soft-keyboard visibility & layout handling
Some checks failed
CI / Rustfmt + Clippy (push) Has been cancelled
CI / Check (android) (push) Has been cancelled
CI / Test (Linux) (push) Has been cancelled
CI / Test (macOS) (push) Has been cancelled
CI / Test (Windows) (push) Has been cancelled
CI / rpm/deb (aarch64) (push) Has been cancelled
CI / rpm/deb (x86_64) (push) Has been cancelled
CI / macOS dmg (aarch64) (push) Has been cancelled
CI / macOS dmg (x86_64) (push) Has been cancelled
CI / Windows Installer (aarch64) (push) Has been cancelled
CI / Windows Installer (x86_64) (push) Has been cancelled
CI / Upload Artifacts to Server (push) Has been cancelled
This reworks how we detect and respond to the on-screen keyboard so inputs
don’t get buried and the UI doesn’t “jump”.

- Add SoftKeyboardAnim + AnimState FSM for smooth IME open/close animation
- Centralize logic in keyboard_visibility() with clear edge states
- Animate keyboard height via animate_value_with_time instead of layer
  transforms
- Add ChromeOptions::KeyboardVisibility flag when focused input would be
  occluded
- Add SidebarOptions::Compact to collapse sidebar while typing
- Hide mobile toolbar when keyboard is open (columns app)
- Use .stick_to_bottom(true) in reply + profile editors; remove old spacer hack
- Virtual keyboard toggle moved to F1 in Debug builds
- Introduce SoftKeyboardContext::platform(ctx) helper
- Cleanup dead/commented code and wire up soft_kb_anim_state in Chrome

Result: inputs stay visible, open/close is smooth, and UI adjusts gracefully
when typing.

Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-20 15:28:28 -07:00
kernelkind
86641c6121 use the onboarding follow pack curator pubkey
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-19 15:05:28 -04:00
William Casarin
77ac91e810 Implement soft keyboard visibility on Android
- Added `SoftKeyboardContext` enum and support for calculating keyboard
  insets from both virtual and platform sources

- Updated `AppContext` to provide `soft_keyboard_rect` for determining
  visible keyboard area

- Adjusted UI rendering to shift content when input boxes intersect with
  the soft keyboard, preventing overlap

- Modified `MainActivity` and Android manifest to use
  `windowSoftInputMode="adjustResize"` and updated window inset handling

- Introduced helper functions (`include_input`, `input_rect`,
  `clear_input_rect`) in `notedeck_ui` for tracking focused input boxes

- Fixed Android JNI keyboard height reporting to clamp negative values

Together, these changes allow the app to correctly detect and respond
to soft keyboard visibility on Android, ensuring input fields remain
accessible when typing.

Fixes: https://github.com/damus-io/notedeck/issues/946
Fixes: https://github.com/damus-io/notedeck/issues/1043
2025-08-19 11:29:45 -07:00
William Casarin
3aa4d00053 clippy: fix lint errors
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-19 09:45:36 -07:00
kernelkind
9ef72ec7de fix contact list bug
not a great solution but we're going to get a new sub manager
soon so it'll probably get replaced anyway

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-17 15:07:24 -04:00
kernelkind
1566cd5cf4 integrate onboarding
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-17 15:07:21 -04:00
kernelkind
bdcd31cda0 add onboarding related state to app
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-17 15:07:19 -04:00
kernelkind
a782d01ec2 add onboarding view
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-17 15:07:16 -04:00
kernelkind
8d4c0cfdbe TMP: temporary author for trusted pks list
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-17 15:07:14 -04:00
kernelkind
f8f720c193 add onboarding 'manager'
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-17 15:07:09 -04:00
kernelkind
2a439b1f30 nip 51 set widget
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-17 15:07:04 -04:00
kernelkind
8399c951fa add nip51 set caching structs
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-17 15:07:00 -04:00
kernelkind
ac1bbeac1b add impl for ScaledTextureFlags::RESPECT_MAX_DIMS
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-17 15:06:57 -04:00
kernelkind
dc91b6ffae extract a pub render_media from image_carousel
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-17 15:06:55 -04:00
kernelkind
28bd13d110 add flags to ScaledTexture
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-17 15:06:48 -04:00
kernelkind
0b12b08c59 clippy: allow collapsible match
clippy being annoying

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-17 15:06:42 -04:00
William Casarin
c79d5f1b9e Merge Japanese and Portuguese translations from Terry
Some checks failed
CI / Rustfmt + Clippy (push) Has been cancelled
CI / Check (android) (push) Has been cancelled
CI / Test (Linux) (push) Has been cancelled
CI / Test (macOS) (push) Has been cancelled
CI / Test (Windows) (push) Has been cancelled
CI / rpm/deb (aarch64) (push) Has been cancelled
CI / rpm/deb (x86_64) (push) Has been cancelled
CI / macOS dmg (aarch64) (push) Has been cancelled
CI / macOS dmg (x86_64) (push) Has been cancelled
CI / Windows Installer (aarch64) (push) Has been cancelled
CI / Windows Installer (x86_64) (push) Has been cancelled
CI / Upload Artifacts to Server (push) Has been cancelled
Terry Yiu (3):
      Remove unused strings from translation files
      Import translations
      Add Japanese and Portuguese (Portugal) languages
2025-08-16 12:33:21 -07:00
William Casarin
507cf113a3 remove hjkl bindings
these interfere with input

we'll need to come back to this

Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-15 11:55:29 -07:00
kernelkind
b750c0a927 use toolbar in columns rather than chrome
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-13 19:20:38 -04:00
kernelkind
49ef85aef6 copy toolbar rendering to notedeck_ui
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-13 19:18:28 -04:00
kernelkind
29f59459d2 add toolbar defaults
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-13 19:18:28 -04:00
kernelkind
cd0bd53b3d add toolbar related logic
copied from chrome

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-13 19:18:28 -04:00
kernelkind
5c0546deab add select_by_route
selects the column containing the desired route. Add it if it
doesn't exist and it's easy to do

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-13 19:18:28 -04:00
kernelkind
1469f9a074 add toolbar icons to notedeck_ui
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-13 19:07:42 -04:00
kernelkind
3d8018bb9a make compose button animate horiz rather than vert
it animating over the toolbar made the bar dissapear for
some reason

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-13 19:06:18 -04:00
kernelkind
361d0e3708 make search icon more customizable
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-13 19:04:16 -04:00
William Casarin
c5df47dc73 clndash: dont forget CLNDASH_ID
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-11 12:37:28 -07:00
William Casarin
ea85799007 clndash: specify you need --clndash
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-11 12:17:13 -07:00
William Casarin
9ba071c5ed clndash: tweak links in readme
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-11 12:15:47 -07:00
William Casarin
81393f8468 clndash: tweak readme
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-11 12:14:24 -07:00
William Casarin
87d9308435 clndash: readme
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-11 12:13:24 -07:00
William Casarin
1f8fd395ed clndash: add readme
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-11 12:09:13 -07:00
William Casarin
2f3a3de7cc clndash: configurable host
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-11 12:03:14 -07:00
William Casarin
35e9354217 clndash: reorganize
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-11 10:36:44 -07:00
William Casarin
08a97c946d clndash: fix invoice order, return more stuff
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-10 21:33:11 -07:00
William Casarin
2fde5addeb clndash: zap rendering
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-10 17:46:09 -07:00
04f5725a9d Add Japanese and Portuguese (Portugal) languages
Changelog-Added: Added Japanese and Portuguese (Portugal) languages
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-08-10 20:02:01 -04:00
59199d8197 Import translations
Changelog-Changed: Imported translations
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-08-10 20:01:54 -04:00
William Casarin
f77e7898b6 clndash: invoice loading
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-10 16:28:21 -07:00
e6a27a53fe Remove unused strings from translation files
Changelog-Removed: Removed unused strings from translation files
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-08-09 21:00:18 -04:00
William Casarin
8138a0a1ca clndash: include listpeerchannel errors
in response

Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-08 20:19:58 -07:00
William Casarin
2444e24fb5 clndash: summary cards
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-08 19:57:43 -07:00
William Casarin
fc509b1b26 clndash: channels ui
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-08 18:34:42 -07:00
William Casarin
1fd92e9e00 default logs
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-08 15:32:28 -07:00
William Casarin
382ef772f5 clndash: initial peer channel listing
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-08 15:31:58 -07:00
William Casarin
53b4a8da5c notedeck app: add clndash
a core-lightning dashboard i'm working on

feature-gate it behind --clndash

Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-08 13:19:39 -07:00
William Casarin
cb72592f4b android: fix dark/light mode and folding screen crash
We have to tell android not to restart the activity when a dark/light
mode is switched or when the phone is folded/unfolded. Otherwise
it will crash.

Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-07 16:27:29 -07:00
William Casarin
c60e1af3eb chrome: add virtual keyboard ui 2025-08-06 19:00:30 -07:00
William Casarin
87cb5ed515 Merge thread scroll fix by kernel
kernelkind (5):
      TMP: use new egui-nav to fix scroll offset issues
      add `scroll_offset` to `NoteAction::Note`
      add `ThreadNote::set_scroll_offset`
      set scroll offset when routing to thread
      appease clippy
2025-08-04 15:08:32 -07:00
William Casarin
9cbba37507 debug: add repaint causes debug tool
enable with --debug, click on fps/frame time counter

Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-04 15:04:38 -07:00
William Casarin
b94e715539 ui: add AnimationMode to control GIF rendering behavior
Introduces an `AnimationMode` enum with `Reactive`, `Continuous`, and
`NoAnimation` variants to allow fine-grained control over GIF playback
across the UI. This supports performance optimizations and accessibility
features, such as disabling animations when requested.

- Plumbs AnimationMode through image rendering paths
- Replaces hardcoded gif frame logic with reusable `process_gif_frame`
- Supports customizable FPS in Continuous mode
- Enables global animation opt-out via `NoteOptions::NoAnimations`
- Applies mode-specific logic in profile pictures, posts, media carousels, and viewer

Animation behavior by context
-----------------------------

- Profile pictures: Reactive (render only on interaction/activity)
- PostView: NoAnimation if disabled in NoteOptions, else Continuous (uncapped)
- Media carousels: NoAnimation or Continuous (capped at 24fps)
- Viewer/gallery: Always Continuous (full animation)

In the future, we can customize these by power settings.

Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-04 13:41:24 -07:00
kernelkind
d12f66e5cd appease clippy
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-04 16:13:53 -04:00
kernelkind
e8be471608 set scroll offset when routing to thread
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-04 16:12:45 -04:00
kernelkind
97d15e41e7 add ThreadNote::set_scroll_offset
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-04 16:12:42 -04:00
kernelkind
ea5c876da6 add scroll_offset to NoteAction::Note
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-04 16:12:38 -04:00
kernelkind
75eefcbf72 TMP: use new egui-nav to fix scroll offset issues
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-04 16:12:27 -04:00
William Casarin
54b86ee5a6 gif: disable continuous gif rendering
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-04 12:19:21 -07:00
William Casarin
f6c44bba8a force oled with --mobile flag
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-04 12:06:58 -07:00
William Casarin
3451206f1a dave: switch to logical time
Some checks failed
CI / Rustfmt + Clippy (push) Has been cancelled
CI / Check (android) (push) Has been cancelled
CI / Test (Linux) (push) Has been cancelled
CI / Test (macOS) (push) Has been cancelled
CI / Test (Windows) (push) Has been cancelled
CI / rpm/deb (aarch64) (push) Has been cancelled
CI / rpm/deb (x86_64) (push) Has been cancelled
CI / macOS dmg (aarch64) (push) Has been cancelled
CI / macOS dmg (x86_64) (push) Has been cancelled
CI / Windows Installer (aarch64) (push) Has been cancelled
CI / Windows Installer (x86_64) (push) Has been cancelled
CI / Upload Artifacts to Server (push) Has been cancelled
this fixes jumpy animations when we stop rendering

Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-04 11:35:22 -07:00
William Casarin
0770bab37c battery: disable render every 100ms
our multicast poller was causing this

Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-04 11:29:03 -07:00
William Casarin
48a11b9bab v0.6.0
What's new
==========

- New notifications indiciator dot on toolbar
- Fixed mentions/tagging
- Gave dave a new swarm look
- Persist some more settings
- Allow sorting thread replies newest first in options
- Show full created date format on selected notes
- Show client name on selected notes
- Higher quality media
- Increase media viewer transition animation
- Fix some ui glitches when replying
- Fix gpu crash on adrendo devices (some samsung galaxy tablets)

Fernando López Guevara (14):
      feat(note): show full created date format on selected notes
      feat(notedeck): add cross-platform URI opener
      feat(settings): allow sorting thread replies newest first
      feat(settings): persist settings to storage
      feat(settings): show note full date
      fix(media): add spacing
      fix(note-content): avoid empty text blocks
      fix(settings): use localization
      refactor(settings): add settings sections methods
      settings: use timed serializer, handle zoom properly, use custom text style for note body font size, added font size slider, added preview note
      update i18n comments for source client options
      Update crates/notedeck/src/persist/settings_handler.rs

Terry Yiu (2):
      Import Spanish translations
      Fix localization issues and export strings for translation

William Casarin (31):
      add NotedeckOptions and feature flags, add notebook feature
      android: fix build
      chrome: remove duplication in app setup
      columns: clean up flags, refactor content rendering
      columns: fix double reference
      dave: switch to use standard vertex/index buffers
      evolve dave into a swarm
      init notebook
      lint: fix format issue
      make clippy happy
      media: less blurry media
      mediaviewer: decrease transition anim from 500ms to 300ms
      note/ui: fix reply line when replying in narrow mode
      note: small doc fix
      note: turn off full date view for previews
      notebook: draw edges and arrows
      notebook: fix heights of nodes
      notebook: fix node sizes
      notebook: move ui code into its own file
      notebook: remove redundant closure
      perf: a few micro optimizations
      post: set client tag to Damus Android on android
      refactor: collapse client label settings; drop CLI/settings toggles
      remove explicit loop continue
      ui/note: fix extra padding in block renderer
      ui/note: fix indented actionbar in non-wide mode
      ui/note: fix reply description item spacing
      ui/note: fix width instabilities because of spacing_mut
      ui/note: slightly more spacing between blocks
      ui: keep original design on non-narrow

kernelkind (12):
      TMP: update egui for better TextInputState handling
      add `NotesFreshness` to `TimelineTab`
      chrome: method to find whether there are unseen notifications
      extract notifications filter to own method
      fix scroll regression
      insert space after mention selection
      mention-picker: re-add spacing from inner_margin
      mentions: don't lose focus after select mention
      paint unseen indicator
      rename `SearchResultsView` => `MentionPickerView`
      set fresh from `TimelineCache`
      use unseen notification indicator

Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-04 08:41:46 -07:00
William Casarin
603de6bbab evolve dave into a swarm
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 23:07:48 -07:00
William Casarin
571bf35109 dave: switch to use standard vertex/index buffers
Fixes: https://github.com/damus-io/notedeck/issues/902
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 20:17:24 -07:00
William Casarin
0dda26791a perf: a few micro optimizations
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 20:17:16 -07:00
William Casarin
7e73ed2760 ui/note: slightly more spacing between blocks
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 20:17:16 -07:00
William Casarin
2fb9470ee6 note/ui: fix reply line when replying in narrow mode
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 16:54:04 -07:00
William Casarin
af2c556700 post: set client tag to Damus Android on android
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 16:48:34 -07:00
William Casarin
27df33dc83 ui/note: fix reply description item spacing
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 16:30:37 -07:00
William Casarin
2edc19fbcc ui/note: fix extra padding in block renderer
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 16:26:26 -07:00
William Casarin
edf0e2498b note: small doc fix
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 16:18:00 -07:00
William Casarin
ad35547582 refactor: collapse client label settings; drop CLI/settings toggles
The "top vs bottom" client label setting was cluttering the UI and
codebase with toggles that added little value. This consolidates client
label handling into one option, removes unused CLI/settings knobs, and
makes NoteView’s API consistent and fluent. Result: fewer knobs, less
branching, and a clearer, more predictable UI.

Now client labels are only shown in one place: selected notes.

- Drop `--show-client` arg in notedeck and `--show-note-client=top|bottom`
  args in notedeck_columns

- Remove `NotedeckOptions::ShowClient` and related CLI parsing

- Delete `ShowSourceClientOption` enum, settings UI, and
  `SettingsAction::SetShowSourceClient`

- Collapse `NoteOptions::{ClientNameTop, ClientNameBottom}` into a single
  `NoteOptions::ClientName`

- Add `NoteOptions::{Framed, UnreadIndicator}`

- Move “framed” and unread indicator into flags (no more ad‑hoc bools)

- Add new NoteView builder methods: `.client_name()`, `.frame()`,
  `.unread_indicator()`, and `.selected_style()`

- CLI flags for showing client labels have been removed

- `ClientNameTop`/`ClientNameBottom` replaced with `ClientName`

- API using `framed` or `show_unread_indicator` booleans must now use
  the new flag setters

Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 16:16:15 -07:00
William Casarin
24f70930eb note: turn off full date view for previews
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 14:53:14 -07:00
William Casarin
5b1bc442d4 Pull spanish translations from terry
Terry Yiu (2):
      Import Spanish translations
      Fix localization issues and export strings for translation
2025-08-03 14:02:43 -07:00
William Casarin
391abe817d columns: clean up flags, refactor content rendering
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 14:02:05 -07:00
William Casarin
30eb2e0258 columns: fix double reference
its not needed

Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 14:00:12 -07:00
William Casarin
21fe3527a8 lint: fix format issue
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 13:58:14 -07:00
William Casarin
249e166a95 remove explicit loop continue
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 10:44:07 -07:00
William Casarin
3f9d030046 Merge remote-tracking branch 'github/pr/1025' 2025-08-03 10:38:38 -07:00
fa13884908 Fix localization issues and export strings for translation
Changelog-Fixed: Fixed localization issues
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-08-01 14:36:29 -04:00
f8ae0825c4 Import Spanish translations
Changelog-Added: Imported Spanish translations
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-08-01 13:39:03 -04:00
Fernando López Guevara
26ece3bc05 feat(note): show full created date format on selected notes 2025-08-01 08:42:58 -03:00
Fernando López Guevara
a64ff3b630 feat(note): created at show full date format 2025-08-01 08:40:10 -03:00
Fernando López Guevara
ab84304265 feat(settings): show note full date 2025-08-01 08:38:49 -03:00
William Casarin
6a08d4b1b2 ui/note: fix width instabilities because of spacing_mut
TODO: get rid of all spacing_mut in the codebase

Fixes: 9ff5753bca ("settings: use timed serializer, handle zoom properly...")
2025-07-31 17:54:53 -07:00
William Casarin
d6d7e4c35e android: fix build
Fixes: dac786e60f ("chrome: remove duplication in app setup")
2025-07-31 17:29:06 -07:00
William Casarin
c3499729f2 Merge notification dot by kernel
kernelkind (6):
      extract notifications filter to own method
      add `NotesFreshness` to `TimelineTab`
      set fresh from `TimelineCache`
      chrome: method to find whether there are unseen notifications
      paint unseen indicator
      use unseen notification indicator

Changelog-Added: Add notification dot on toolbar
2025-07-31 17:18:20 -07:00
William Casarin
dac786e60f chrome: remove duplication in app setup
Also move debug warning to chrome so that headless
notedeck apps don't hit that.
2025-07-31 17:07:51 -07:00
kernelkind
41aa2db3c7 use unseen notification indicator
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-31 19:08:08 -04:00
kernelkind
10225158e5 paint unseen indicator
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-31 19:07:36 -04:00
kernelkind
557608db9b chrome: method to find whether there are unseen notifications
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-31 19:07:35 -04:00
kernelkind
8697a5cb0a set fresh from TimelineCache
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-31 19:07:28 -04:00
kernelkind
7aca39aae8 add NotesFreshness to TimelineTab
necessary for notifications indicator

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-31 19:07:24 -04:00
kernelkind
aa467b9be0 extract notifications filter to own method
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-31 19:07:13 -04:00
William Casarin
09eeb57bd9 Merge notebook (feature gated)
This merges the experimental notebook app, which can be
enabled with --notebook.

We also switch to bitflags for notedeck options
2025-07-31 16:06:25 -07:00
William Casarin
b1a5dd6cab add NotedeckOptions and feature flags, add notebook feature
This switches from bools to flags in our Args struct. We also add
notebook as an optional feature flag (--notebook) since its not ready.
2025-07-31 16:03:13 -07:00
William Casarin
d12e5b363c notebook: move ui code into its own file
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-31 15:08:23 -07:00
William Casarin
cc8bafddff notebook: remove redundant closure
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-31 15:08:23 -07:00
William Casarin
3766308ce6 notebook: fix node sizes
make sure we always allocate the correct amount of space,
even if we use less.

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-31 15:08:23 -07:00
William Casarin
17f72f6127 notebook: draw edges and arrows
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-31 15:08:22 -07:00
William Casarin
f592015c0c notebook: fix heights of nodes
some nodes can overflow their contents, so let's use a scroll view to
fix

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-31 15:07:51 -07:00
William Casarin
1ab4eeb48c init notebook
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-31 15:07:50 -07:00
William Casarin
a8c6baeacb make clippy happy 2025-07-31 11:55:39 -07:00
William Casarin
a896a6ecfa Merge remote-tracking branch 'fernando/feat/persist_settings' 2025-07-31 11:48:57 -07:00
Fernando López Guevara
f282363748 feat(notedeck): add cross-platform URI opener 2025-07-30 16:27:51 -07:00
William Casarin
ba76b20ad2 Merge tagging fixes from kernel
Fixes the following:
1. space added after mention
2. can scroll the mention picker
3. don't lose focus of textedit after mention selection

kernelkind (6):
      rename `SearchResultsView` => `MentionPickerView`
      fix scroll regression
      mention-picker: re-add spacing from inner_margin
      mentions: don't lose focus after select mention
      TMP: update egui for better TextInputState handling
      insert space after mention selection

Fixes: https://github.com/damus-io/notedeck/issues/985
Fixes: https://github.com/damus-io/notedeck/issues/728
Fixes: https://github.com/damus-io/notedeck/issues/986

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-30 16:23:12 -07:00
kernelkind
b04f50a9f6 insert space after mention selection
closes: https://github.com/damus-io/notedeck/issues/985

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-30 18:11:27 -04:00
kernelkind
233be47659 TMP: update egui for better TextInputState handling
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-30 17:45:12 -04:00
kernelkind
173972f920 mentions: don't lose focus after select mention
Closes: https://github.com/damus-io/notedeck/issues/728

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-30 17:45:09 -04:00
kernelkind
31ec21ea02 mention-picker: re-add spacing from inner_margin
shouldn't do this in Frame, for some reason that captures the drag

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-30 17:45:05 -04:00
kernelkind
d3d8d7be4b fix scroll regression
Closes: https://github.com/damus-io/notedeck/issues/986

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-30 17:45:02 -04:00
kernelkind
09dc101c1b rename SearchResultsView => MentionPickerView
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-30 17:44:51 -04:00
Fernando López Guevara
261477339b Update crates/notedeck/src/persist/settings_handler.rs
Co-authored-by: Terry Yiu <963907+tyiu@users.noreply.github.com>
2025-07-30 08:34:47 -03:00
Fernando López Guevara
9ff5753bca settings: use timed serializer, handle zoom properly, use custom text style for note body font size, added font size slider, added preview note 2025-07-29 21:43:26 -03:00
Fernando López Guevara
b9e2fe5dd1 fix(media): add spacing 2025-07-29 21:38:04 -03:00
Fernando López Guevara
d1a9e0020e fix(note-content): avoid empty text blocks
(cherry picked from commit baa7031c25d0f3d3e8952f49f6625252413559a3)
2025-07-29 21:34:28 -03:00
Fernando López Guevara
1163dd8461 feat(settings): persist settings to storage 2025-07-29 21:33:05 -03:00
Fernando López Guevara
692f4889cf update i18n comments for source client options
Co-authored-by: Terry Yiu <963907+tyiu@users.noreply.github.com>
2025-07-29 21:31:36 -03:00
Fernando López Guevara
f2153f53dc feat(settings): allow sorting thread replies newest first 2025-07-29 21:30:35 -03:00
Fernando López Guevara
40764d7368 fix(settings): use localization 2025-07-29 21:22:39 -03:00
Fernando López Guevara
be720c0f76 fix(settings): use localization 2025-07-29 21:21:06 -03:00
Fernando López Guevara
5848f1c355 refactor(settings): add settings sections methods 2025-07-29 21:09:33 -03:00
Fernando López Guevara
0dcf70bc15 feat(settings): persist settings to storage 2025-07-29 21:02:18 -03:00
William Casarin
0fc8e70180 ui/note: fix indented actionbar in non-wide mode
Some checks failed
CI / Rustfmt + Clippy (push) Has been cancelled
CI / Check (android) (push) Has been cancelled
CI / Test (Linux) (push) Has been cancelled
CI / Test (macOS) (push) Has been cancelled
CI / Test (Windows) (push) Has been cancelled
CI / rpm/deb (aarch64) (push) Has been cancelled
CI / rpm/deb (x86_64) (push) Has been cancelled
CI / macOS dmg (aarch64) (push) Has been cancelled
CI / macOS dmg (x86_64) (push) Has been cancelled
CI / Windows Installer (aarch64) (push) Has been cancelled
CI / Windows Installer (x86_64) (push) Has been cancelled
CI / Upload Artifacts to Server (push) Has been cancelled
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-29 13:58:17 -07:00
William Casarin
2de6851fbd mediaviewer: decrease transition anim from 500ms to 300ms
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-29 13:45:22 -07:00
William Casarin
f57d582307 ui: keep original design on non-narrow
Changed my mind

This reverts commit 6e81b98d2f.
This reverts commit 217f1e45da.
2025-07-29 13:25:19 -07:00
William Casarin
09e608ca75 media: less blurry media
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-29 13:17:52 -07:00
William Casarin
2bd636ce0a v0.5.9 - Better Media!
- Persist settings to storage
- New fullscreen media viewer with panning and zoom
- Changed note rendering to use the full screen width
- Fixed more wrapping issues
- Fixed crash on large images
- Fix nwc copy/paste
- Portugese translations
- Show locale language names instead of identifier

Fernando López Guevara (5):
      feat(settings): persist settings to storage
      fix(columns): render wide notes on narrow screen
      fix(media): edge-to-edge image display on narrow screen
      fix(media): use ScaledTexture
      fix(note_actionbar): add invisible label to stabilize section width ¯\_(ツ)_/¯

Terry Yiu (5):
      Add human-readable names to locales in settings
      Add Portuguese (Brazil) language and translations
      Export strings for translation
      Import translations
      Internationalize ShowNoteClientOptions labels

William Casarin (19):
      Fullscreen MediaViewer refactor
      images: always resize large images
      media: change is_narrow logic to is_scaled
      media/viewer: click anywhere to close
      media/viewer: fix broken culling
      media/viewer: fix flicker on escape-close
      media/viewer: fullscreen transition animations
      media/viewer: handle click-to-close interactions
      media/viewer: provide image-click provenance
      media/viewer: slower animation
      note/options: made wide the default
      threads: disable wide in threads
      ui/note: fix another reply_desc wrapping issue
      ui/note: simplify weird hack and make note of it
      ui/settings: fix small double clone nit
      ui/wallet: small refactor to use return instead of break
      wallet: fix nwc copy/paste

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-29 12:24:17 -07:00
William Casarin
79bf6cf126 media/viewer: fix flicker on escape-close
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-29 12:22:04 -07:00
Fernando López Guevara
b8207106d7 feat(settings): persist settings to storage 2025-07-29 11:41:06 -07:00
William Casarin
5280028a82 media/viewer: fix broken culling
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-29 11:03:42 -07:00
William Casarin
f4a6e8f9bb media: change is_narrow logic to is_scaled
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-29 10:59:27 -07:00
William Casarin
83fd6de076 Merge remote-tracking branch 'github/pr/1032' 2025-07-29 10:46:55 -07:00
William Casarin
b80a0ab0f1 ui/settings: fix small double clone nit
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-29 10:32:39 -07:00
William Casarin
e437a0db1c Merge Portuguese translations by terry #1036
Terry Yiu (5):
      Export strings for translation
      Add human-readable names to locales in settings
      Internationalize ShowNoteClientOptions labels
      Import translations
      Add Portuguese (Brazil) language and translations
2025-07-29 10:26:21 -07:00
William Casarin
6e81b98d2f note/options: made wide the default
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-29 10:24:38 -07:00
William Casarin
217f1e45da Revert "fix(columns): render wide notes on narrow screen"
We're just gonna make it default

This reverts commit 0f00dcf7a7.
2025-07-29 10:22:57 -07:00
William Casarin
96e0366787 threads: disable wide in threads
Since it breaks the reply line rendering

Fixes: 0f00dcf7a7 ("fix(columns): render wide notes on narrow screen")
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-29 10:18:20 -07:00
William Casarin
2a85ee562c ui/note: simplify weird hack and make note of it
Fixes: https://github.com/damus-io/notedeck/issues/842
Fixes: f2e01f0e40 ("fix(note_actionbar): add invisible label to stabilize section width ¯\_(ツ)_/¯")
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-29 10:13:43 -07:00
William Casarin
1fabd347ca Merge remote-tracking branch 'github/pr/1031' 2025-07-29 10:08:43 -07:00
William Casarin
0087fe7dff media/viewer: slower animation
so you can actually see whats going on

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-28 16:37:53 -07:00
William Casarin
51f7744149 media/viewer: fullscreen transition animations
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-28 16:12:29 -07:00
William Casarin
6d393c9c37 media/viewer: provide image-click provenance
We will be using this for transitions

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-28 14:19:03 -07:00
39e932c674 Add Portuguese (Brazil) language and translations
Changelog-Added: Added Portuguese (Brazil) language and translations
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-07-28 16:38:02 -04:00
6919460d18 Import translations
Changelog-Changed: Imported translations
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-07-28 16:38:01 -04:00
bf58fdce1f Internationalize ShowNoteClientOptions labels
Changelog-Fixed: Internationalize ShowNoteClientOptions labels
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-07-28 16:38:01 -04:00
419102959f Add human-readable names to locales in settings
Changelog-Added: Added human-readable names to locales in settings
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-07-28 16:38:01 -04:00
9bcbcae688 Export strings for translation
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-07-28 16:38:01 -04:00
William Casarin
5c8ab0ce07 media/viewer: handle click-to-close interactions
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-28 12:19:45 -07:00
William Casarin
590ffa0680 media/viewer: click anywhere to close
this should help mobile ...

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-28 12:10:00 -07:00
William Casarin
3d18db8fd2 Fullscreen MediaViewer refactor
- Moved media related logic into notedeck instead of the ui crate,
  since they pertain to Images/ImageCache based systems

- Made RenderableMedia owned to make it less of a nightmware
  to work with and the perf should be negligible

- Added a ImageMetadata cache to Images. This is referenced
  whenever we encounter an image so we don't have to
  redo the work all of the time

- Relpaced our ad-hoc, hand(vibe?)-coded panning and zoom logic
  with the Scene widget, which is explicitly designed for
  this use case

- Extracted and detangle fullscreen media rendering from inside of note
  rendering.  We instead let the application decide what action they
  want to perform when note media is clicked on.

- We add an on_view_media action to MediaAction for the application to
  handle. The Columns app uses this toggle a FullscreenMedia app
  option bits whenever we get a MediaAction::ViewMedis(urls).

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-28 08:57:57 -07:00
Fernando López Guevara
661acb3a12 fix(media): use ScaledTexture 2025-07-25 16:35:22 -03:00
Fernando López Guevara
8306003f6f fix(media): edge-to-edge image display on narrow screen 2025-07-25 16:17:45 -03:00
William Casarin
96ab4ee681 ui/note: fix another reply_desc wrapping issue
Some checks failed
CI / Rustfmt + Clippy (push) Has been cancelled
CI / Check (android) (push) Has been cancelled
CI / Test (Linux) (push) Has been cancelled
CI / Test (macOS) (push) Has been cancelled
CI / Test (Windows) (push) Has been cancelled
CI / rpm/deb (aarch64) (push) Has been cancelled
CI / rpm/deb (x86_64) (push) Has been cancelled
CI / macOS dmg (aarch64) (push) Has been cancelled
CI / macOS dmg (x86_64) (push) Has been cancelled
CI / Windows Installer (aarch64) (push) Has been cancelled
CI / Windows Installer (x86_64) (push) Has been cancelled
CI / Upload Artifacts to Server (push) Has been cancelled
Fixes: https://github.com/damus-io/notedeck/issues/892
Changelog-Fixed: Fix another wrapping issue
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-25 12:12:25 -07:00
William Casarin
2524ff1061 wallet: fix nwc copy/paste
Fixes: https://github.com/damus-io/notedeck/issues/1012
Changelog-Fixed: Fix NWC copy/paste
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-25 12:09:15 -07:00
William Casarin
eb0ab75e87 ui/wallet: small refactor to use return instead of break
we don't need this weird break syntax when we're in a closure

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-25 12:04:38 -07:00
William Casarin
009b4cf6b0 images: always resize large images
Fixes: https://github.com/damus-io/notedeck/issues/451
Fixes: https://linear.app/damus/issue/DECK-556/resize-images-to-device-screen-size
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-25 10:52:27 -07:00
Fernando López Guevara
f2e01f0e40 fix(note_actionbar): add invisible label to stabilize section width ¯\_(ツ)_/¯ 2025-07-25 12:13:39 -03:00
William Casarin
c891f8585d v0.5.8
we got back swipe on everything now fam

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-24 15:45:45 -07:00
William Casarin
2648967d7b lockfile: fixup
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-24 15:37:33 -07:00
William Casarin
438dbb2397 Merge drag to nav back on all views by kernel #1035
HE FUCKING DID IT LADS

Made a small tweak on the merge commit to update url to
damus-io/egui-nav upstream

William Casarin (2):
      Merge drag to nav back on all views by kernel #1035

kernelkind (9):
      TMP: update egui-nav
      refactor scrolling for post, reply & quote views
      enforce scroll_id for `ThreadView`
      add `scroll_id` for all views with vertical scroll
      add `DragSwitch`
      use `DragSwitch` in `Column`
      get scroll id for `Route`
      add `route_uses_frame`
      use `DragSwitch` to allow dragging anywhere in navigation
2025-07-24 15:28:30 -07:00
kernelkind
2bd139ef9e use DragSwitch to allow dragging anywhere in navigation
instead of just the top header when there is a vertical scroll

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-24 17:54:36 -04:00
kernelkind
cda0a68854 add route_uses_frame
need to know this to get the correct drag id

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-24 17:54:32 -04:00
kernelkind
a555707f67 get scroll id for Route
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-24 17:54:11 -04:00
kernelkind
1601914b8b use DragSwitch in Column
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-24 17:53:43 -04:00
kernelkind
aac0f54991 add DragSwitch
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-24 17:53:40 -04:00
kernelkind
8960b3f052 add scroll_id for all views with vertical scroll
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-24 17:53:35 -04:00
kernelkind
6db6cf7b7a enforce scroll_id for ThreadView
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-24 17:53:28 -04:00
kernelkind
0bc32272d2 refactor scrolling for post, reply & quote views
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-24 17:53:25 -04:00
kernelkind
b05d39cc81 TMP: update egui-nav
need this to make drag switching work

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-24 17:53:12 -04:00
William Casarin
7a83483758 nip10: switch to NoteReply instead of handrolled logic
Cc: kernelkind
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-24 13:32:19 -07:00
William Casarin
1a3112d8ef Merge remote-tracking branch 'github/pr/1027' 2025-07-24 12:29:11 -07:00
William Casarin
c1d0ea1901 Merge remote-tracking branch 'github/jb55-deck-733-profile-sidebar-action-should-route-in-the-active-column' 2025-07-24 12:28:06 -07:00
William Casarin
db6103d448 router: fix router selection
Many times we get the router selection wrong. This fixes that

Changelog-Fixed: Fix some routing issues when routing from the Chrome
Fixes: https://github.com/damus-io/notedeck/issues/1024
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-24 12:11:19 -07:00
Fernando López Guevara
0f00dcf7a7 fix(columns): render wide notes on narrow screen 2025-07-24 15:57:42 -03:00
William Casarin
8f63546524 ui: wrap reply description
This is similar to our fix in:

- Fixes: ee85b754dd ("Fix text wrapping issues")

Where removing the ui.horizontal call fixes subsequent main wrap layout
issues. It's still not clear to me where wrap state is getting mutated
where it would affect subsequent ui calls...

Fixes: https://github.com/damus-io/notedeck/issues/892
Changelog-Fixed: Fixed wrapping issues in Notes & Replies timeslines
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-24 09:11:12 -07:00
William Casarin
90975180f5 ui/replydesc: quick TextSegment cleanup/optimize
most a micro-optimize + cleanup

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-24 09:03:47 -07:00
Jakub Gladysz
bd9a78b305 Do not crash on unknown arg
Signed-off-by: Jakub Gladysz <jakub.gladysz@protonmail.com>
2025-07-24 11:02:43 +03:00
William Casarin
4e27c1f491 v0.5.7
Whats new
=========

- Swipe nav on full screen media
- Made action buttons bigger
- Fix zap button not appearing
- Allow removal of Damoose account
- Profile is now clickable from side bar

- New settings view:
  * Resize zoom level
  * Clear cache
  * Change locale

- Localization support
  * German
  * Spanish
  * French
  * Chinese
  * Thai

Log
---

Fernando López Guevara (3):
      feat(full-screen-media): add swipe navigation
      feat(settings): add settings view
      fix(columns): prevent crash when switching to account with no columns

Terry Yiu (9):
      Add Fluent-based localization manager and add script to export source strings for translations
      Add French, German, Simplified Chinese, and Traditional Chinese translations
      Add Spanish (Latin America and Spain) translations
      Add Thai translations
      Add localization documentation to notedeck DEVELOPER.md
      Clean up time_ago_since, add tests, and internationalize strings
      Fix export_source_strings.py to adjust for tr! and tr_plural! macro signature changes
      Internationalize user-facing strings and export them for translations
      Update Chinese, French, and German translations

William Casarin (15):
      args: add --locale option
      debug: add startup query debug log
      fix missing zap button
      fix one missing home string
      gitignore: remove cache
      i18n: always have en-XA available
      i18n: disable bidi for tests
      i18n: disable broken tests for now
      i18n: make localization context non-global
      media/trust: always show if its yourself
      ripgrep: add ignore file for ftl files
      settings: fix route to relay
      ui/note: make buttons larger
      ui/note: small refactor to use returns instead of break
      wallet: remove unused flag in note context

kernelkind (14):
      add ChromePanelAction::Profile & use for pfp
      add new Accounts button to chrome sidebar
      allow removal of Damoose account
      appease clippy
      bugfix: properly sub to new selected acc after removal of selected
      bugfix: unsubscribe all decks when log out account
      bugfix: unsubscribe from timelines on deck deletion
      expose `AccountCache::falback`
      fix: sometimes most recent contacts list wasn't used
      make `UserAccount` cloneable
      move select account logic to own method
      use `NwcError` instead of nwc::Error
      use saturating sub

name                                                  added  removed  commits
kernelkind <kernelkind@gmail.com>                     +328   -50      14
Fernando López Guevara <fernando.lguevara@gmail.com>  +802   -36      3
William Casarin <jb55@jb55.com>                       +1603  -1297    15
Terry Yiu <git@tyiu.xyz>                              +7547  -1024    9

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-23 13:03:28 -07:00
William Casarin
f9f8b3fe1b Merge remote-tracking branch 'github/pr/1023' 2025-07-23 12:31:51 -07:00
William Casarin
5ddd8660a3 settings: fix route to relay
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-23 12:29:09 -07:00
William Casarin
fe30704496 Merge remote-tracking branch 'fernando/feat/settings-view' 2025-07-23 12:00:29 -07:00
William Casarin
e997f1bf68 ui/note: make buttons larger
Changelog-Changed: Make buttons larger
Fixes: https://github.com/damus-io/notedeck/issues/879
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-23 11:49:06 -07:00
William Casarin
ff0428550b fix missing zap button
Changelog-Fixed: Fix missing zap button
Fixes: 397bfce817 ("add `Accounts` to `NoteContext`")
Fixes: https://github.com/damus-io/notedeck/issues/1021
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-23 11:49:03 -07:00
Fernando López Guevara
da6ede5f69 feat(settings): add settings view 2025-07-23 15:33:17 -03:00
William Casarin
56cbf68ea5 ui/note: small refactor to use returns instead of break
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-23 09:39:05 -07:00
William Casarin
ebf31abafa wallet: remove unused flag in note context
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-23 09:36:38 -07:00
William Casarin
e317c57769 ripgrep: add ignore file for ftl files
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-23 09:36:21 -07:00
William Casarin
f722a58d66 Merge new Accounts button to chrome sidebar by kernel #994
kernelkind (3):
      use saturating sub
      add new Accounts button to chrome sidebar
      add ChromePanelAction::Profile & use for pfp
2025-07-23 09:13:49 -07:00
William Casarin
ffcd38ef96 Merge prevent crash when switching cols from fernando #997
Fernando López Guevara (1):
      fix(columns): prevent crash when switching to account with no columns
2025-07-23 09:10:21 -07:00
William Casarin
088704a768 Merge media swipe nav from fernando #1010
Fernando López Guevara (1):
      feat(full-screen-media): add swipe navigation
2025-07-23 09:09:04 -07:00
William Casarin
10eedc0ca6 Merge contact list fixes by kernel #998
kernelkind (2):
      appease clippy
      fix: sometimes most recent contacts list wasn't used
2025-07-23 08:54:12 -07:00
Fernando López Guevara
ed38c75193 feat(full-screen-media): add swipe navigation 2025-07-18 13:46:25 -03:00
kernelkind
fdef74c353 fix: sometimes most recent contacts list wasn't used
`ndb::poll_for_notes` appears to give notes as they arrive. We
need to make sure we only use the most recent for contacts

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-17 18:17:56 -04:00
kernelkind
030e4226f8 appease clippy
```
error: large size difference between variants
   --> crates/notedeck_columns/src/column.rs:249:1
    |
249 | / pub enum IntermediaryRoute {
250 | |     Timeline(Timeline),
    | |     ------------------ the largest variant contains at least 280 bytes
251 | |     Route(Route),
    | |     ------------ the second-largest variant contains at least 72 bytes
252 | | }
    | |_^ the entire enum is at least 280 bytes
    |
    = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#large_enum_variant
    = note: `-D clippy::large-enum-variant` implied by `-D warnings`
    = help: to override `-D warnings` add `#[allow(clippy::large_enum_variant)]`
help: consider boxing the large fields to reduce the total size of the enum
    |
250 -     Timeline(Timeline),
250 +     Timeline(Box<Timeline>),
    |

error: could not compile `notedeck_columns` (lib) due to 1 previous error
```

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-17 18:17:53 -04:00
Fernando López Guevara
508d8dc0ba fix(columns): prevent crash when switching to account with no columns 2025-07-17 19:09:10 -03:00
kernelkind
34afa755b8 add ChromePanelAction::Profile & use for pfp
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-17 15:23:41 -04:00
kernelkind
45490c918d add new Accounts button to chrome sidebar
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-17 15:23:32 -04:00
kernelkind
a31fdd3ed2 use saturating sub
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-17 15:23:15 -04:00
183 changed files with 14678 additions and 4286 deletions

3
.envrc
View File

@@ -18,4 +18,7 @@ export OLLAMA_HOST=http://ollama.jb55.com
# simple todo reminders
export TODO_FILE=TODO
export RUST_LOG="egui=debug,egui-winit=debug,notedeck=debug,notedeck_columns=debug,notedeck_chrome=debug,enostr=debug,android_activity=debug,lnsocket=trace,notedeck_clndash=debug"
2>/dev/null todo.sh ls || :

4
.gitignore vendored
View File

@@ -19,3 +19,7 @@ queries/damus-notifs.json
.direnv/
scripts/macos_build_secrets.sh
/tags
.zed
.lsp
.idea
local.properties

1
.rgignore Normal file
View File

@@ -0,0 +1 @@
*.ftl

424
Cargo.lock generated
View File

@@ -105,7 +105,8 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android-activity"
version = "0.6.0"
source = "git+https://github.com/damus-io/android-activity?rev=a8948332c7c551303d32eb26a59d0abd676e47a5#a8948332c7c551303d32eb26a59d0abd676e47a5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046"
dependencies = [
"android-properties",
"bitflags 2.9.1",
@@ -125,7 +126,7 @@ dependencies = [
[[package]]
name = "android-activity"
version = "0.6.0"
source = "git+https://github.com/damus-io/android-activity?rev=c3c0decc83c4d6c94d2c448391fc8dd51b13f3d9#c3c0decc83c4d6c94d2c448391fc8dd51b13f3d9"
source = "git+https://github.com/damus-io/android-activity?rev=4ee16f1585e4a75031dc10785163d4b920f95805#4ee16f1585e4a75031dc10785163d4b920f95805"
dependencies = [
"android-properties",
"bitflags 2.9.1",
@@ -192,7 +193,7 @@ dependencies = [
"objc2-foundation 0.3.1",
"parking_lot",
"percent-encoding",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
"x11rb",
]
@@ -765,6 +766,25 @@ dependencies = [
"generic-array",
]
[[package]]
name = "block-sys"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae85a0696e7ea3b835a453750bf002770776609115e6d25c6d2ff28a8200f7e7"
dependencies = [
"objc-sys",
]
[[package]]
name = "block2"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e58aa60e59d8dbfcc36138f5f18be5f24394d33b38b24f7fd0b1caa33095f22f"
dependencies = [
"block-sys",
"objc2 0.5.2",
]
[[package]]
name = "block2"
version = "0.5.1"
@@ -802,6 +822,17 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e79769241dcd44edf79a732545e8b5cec84c247ac060f5252cd51885d093a8fc"
[[package]]
name = "bstr"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
dependencies = [
"memchr",
"regex-automata 0.4.9",
"serde",
]
[[package]]
name = "built"
version = "0.7.7"
@@ -978,6 +1009,7 @@ dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
]
@@ -1233,6 +1265,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
dependencies = [
"powerfmt",
"serde",
]
[[package]]
@@ -1370,7 +1403,7 @@ checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
name = "dpi"
version = "0.1.1"
source = "git+https://github.com/damus-io/winit?rev=eaff639ab0a14fccf595241f687be883154b267c#eaff639ab0a14fccf595241f687be883154b267c"
source = "git+https://github.com/damus-io/winit?rev=a07ea4c4d76cdc5306da8e8aabb05cab1a7d8e4d#a07ea4c4d76cdc5306da8e8aabb05cab1a7d8e4d"
[[package]]
name = "dpi"
@@ -1378,20 +1411,26 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
[[package]]
name = "dyn-clone"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "ecolor"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
dependencies = [
"bytemuck",
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc)",
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72)",
"serde",
]
[[package]]
name = "eframe"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
dependencies = [
"ahash",
"bytemuck",
@@ -1427,24 +1466,25 @@ dependencies = [
[[package]]
name = "egui"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
dependencies = [
"accesskit",
"ahash",
"backtrace",
"bitflags 2.9.1",
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc)",
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72)",
"epaint",
"log",
"nohash-hasher",
"profiling",
"serde",
"similar",
]
[[package]]
name = "egui-wgpu"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
dependencies = [
"ahash",
"bytemuck",
@@ -1463,7 +1503,7 @@ dependencies = [
[[package]]
name = "egui-winit"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
dependencies = [
"ahash",
"arboard",
@@ -1481,7 +1521,7 @@ dependencies = [
[[package]]
name = "egui_extras"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
dependencies = [
"ahash",
"egui",
@@ -1498,7 +1538,7 @@ dependencies = [
[[package]]
name = "egui_glow"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
dependencies = [
"ahash",
"bytemuck",
@@ -1515,7 +1555,7 @@ dependencies = [
[[package]]
name = "egui_nav"
version = "0.2.0"
source = "git+https://github.com/damus-io/egui-nav?rev=111de8ac40b5d18df53e9691eb18a50d49cb31d8#111de8ac40b5d18df53e9691eb18a50d49cb31d8"
source = "git+https://github.com/damus-io/egui-nav?rev=e4231c19dda9e6791d2f7b5cd610b8db5ff9a7f9#e4231c19dda9e6791d2f7b5cd610b8db5ff9a7f9"
dependencies = [
"egui",
"egui_extras",
@@ -1577,7 +1617,7 @@ checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b"
[[package]]
name = "emath"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
dependencies = [
"bytemuck",
"serde",
@@ -1595,7 +1635,7 @@ version = "0.3.0"
dependencies = [
"bech32",
"ewebsock",
"hashbrown",
"hashbrown 0.15.4",
"hex",
"mio",
"nostr 0.37.0",
@@ -1675,13 +1715,13 @@ dependencies = [
[[package]]
name = "epaint"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
dependencies = [
"ab_glyph",
"ahash",
"bytemuck",
"ecolor",
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc)",
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72)",
"epaint_default_fonts",
"log",
"nohash-hasher",
@@ -1693,7 +1733,7 @@ dependencies = [
[[package]]
name = "epaint_default_fonts"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
[[package]]
name = "equator"
@@ -2269,7 +2309,7 @@ checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca"
dependencies = [
"bitflags 2.9.1",
"gpu-descriptor-types",
"hashbrown",
"hashbrown 0.15.4",
]
[[package]]
@@ -2291,6 +2331,18 @@ dependencies = [
"crunchy",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
[[package]]
name = "hashbrown"
version = "0.15.4"
@@ -2319,6 +2371,9 @@ name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
dependencies = [
"serde",
]
[[package]]
name = "hex-conservative"
@@ -2335,6 +2390,17 @@ dependencies = [
"arrayvec",
]
[[package]]
name = "hex_color"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d37f101bf4c633f7ca2e4b5e136050314503dd198e78e325ea602c327c484ef0"
dependencies = [
"arrayvec",
"rand 0.8.5",
"serde",
]
[[package]]
name = "hex_lit"
version = "0.1.1"
@@ -2496,6 +2562,16 @@ dependencies = [
"cc",
]
[[package]]
name = "icrate"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb69199826926eb864697bddd27f73d9fddcffc004f5733131e15b465e30642"
dependencies = [
"block2 0.4.0",
"objc2 0.5.2",
]
[[package]]
name = "icu_collections"
version = "2.0.0"
@@ -2654,6 +2730,17 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408"
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
"serde",
]
[[package]]
name = "indexmap"
version = "2.9.0"
@@ -2661,7 +2748,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
"hashbrown",
"hashbrown 0.15.4",
"serde",
]
@@ -2733,25 +2820,6 @@ dependencies = [
"serde",
]
[[package]]
name = "is-docker"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
dependencies = [
"once_cell",
]
[[package]]
name = "is-wsl"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
dependencies = [
"is-docker",
"once_cell",
]
[[package]]
name = "itertools"
version = "0.10.5"
@@ -2868,6 +2936,19 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "jsoncanvas"
version = "0.1.6"
source = "git+https://github.com/jb55/jsoncanvas?rev=ae60f96e4d022cf037e086b793cacc3225bc14e5#ae60f96e4d022cf037e086b793cacc3225bc14e5"
dependencies = [
"hex_color",
"serde",
"serde_json",
"serde_with",
"thiserror 1.0.69",
"url",
]
[[package]]
name = "khronos-egl"
version = "6.0.0"
@@ -2958,6 +3039,7 @@ dependencies = [
"bech32",
"bitcoin",
"lightning-types",
"serde",
]
[[package]]
@@ -2993,6 +3075,22 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
[[package]]
name = "lnsocket"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "724c7fba2188a49ab31316e52dd410d4d3168b8e6482aa2ac3889dd840d28712"
dependencies = [
"bitcoin",
"hashbrown 0.13.2",
"hex",
"lightning-types",
"serde",
"serde_json",
"tokio",
"tracing",
]
[[package]]
name = "lock_api"
version = "0.4.13"
@@ -3190,7 +3288,7 @@ dependencies = [
"cfg_aliases",
"codespan-reporting",
"hexf-parse",
"indexmap",
"indexmap 2.9.0",
"log",
"rustc-hash 1.1.0",
"spirv",
@@ -3304,6 +3402,15 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]]
name = "normpath"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8911957c4b1549ac0dc74e30db9c8b0e66ddcd6d7acc33098f4c63a64a6d7ed"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "nostr"
version = "0.37.0"
@@ -3383,8 +3490,8 @@ dependencies = [
[[package]]
name = "nostrdb"
version = "0.7.0"
source = "git+https://github.com/damus-io/nostrdb-rs?rev=a307f5d3863b5319c728b2782959839b8df544cb#a307f5d3863b5319c728b2782959839b8df544cb"
version = "0.8.0"
source = "git+https://github.com/damus-io/nostrdb-rs?rev=2b2e5e43c019b80b98f1db6a03a1b88ca699bfa3#2b2e5e43c019b80b98f1db6a03a1b88ca699bfa3"
dependencies = [
"bindgen",
"cc",
@@ -3398,27 +3505,35 @@ dependencies = [
[[package]]
name = "notedeck"
version = "0.5.6"
version = "0.7.1"
dependencies = [
"android-activity 0.6.0 (git+https://github.com/damus-io/android-activity?rev=4ee16f1585e4a75031dc10785163d4b920f95805)",
"base32",
"bech32",
"bincode",
"bitflags 2.9.1",
"blurhash",
"chrono",
"crossbeam-channel",
"dirs",
"eframe",
"egui",
"egui-winit",
"egui_extras",
"ehttp",
"enostr",
"fluent",
"fluent-langneg",
"fluent-resmgr",
"hashbrown",
"hashbrown 0.15.4",
"hex",
"image",
"indexmap 2.9.0",
"jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
"lightning-invoice",
"md5",
"mime_guess",
"ndk-context",
"nostr 0.37.0",
"nostrdb",
"nwc",
@@ -3446,8 +3561,9 @@ dependencies = [
[[package]]
name = "notedeck_chrome"
version = "0.5.6"
version = "0.7.1"
dependencies = [
"bitflags 2.9.1",
"eframe",
"egui",
"egui-winit",
@@ -3455,8 +3571,10 @@ dependencies = [
"egui_tabs",
"nostrdb",
"notedeck",
"notedeck_clndash",
"notedeck_columns",
"notedeck_dave",
"notedeck_notebook",
"notedeck_ui",
"profiling",
"puffin",
@@ -3473,9 +3591,28 @@ dependencies = [
"tracing-subscriber",
]
[[package]]
name = "notedeck_clndash"
version = "0.7.1"
dependencies = [
"eframe",
"egui",
"egui_extras",
"hex",
"lightning-invoice",
"lnsocket",
"nostrdb",
"notedeck",
"notedeck_ui",
"serde",
"serde_json",
"tokio",
"tracing",
]
[[package]]
name = "notedeck_columns"
version = "0.5.6"
version = "0.7.1"
dependencies = [
"base64 0.22.1",
"bech32",
@@ -3490,16 +3627,18 @@ dependencies = [
"egui_virtual_list",
"ehttp",
"enostr",
"hashbrown",
"hashbrown 0.15.4",
"hex",
"human_format",
"image",
"indexmap",
"indexmap 2.9.0",
"jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
"ndk-context",
"nostrdb",
"notedeck",
"notedeck_ui",
"oot_bitset",
"open",
"opener",
"poll-promise",
"pretty_assertions",
"profiling",
@@ -3507,6 +3646,7 @@ dependencies = [
"puffin_egui",
"rfd",
"rmpv",
"robius-open",
"security-framework 2.11.1",
"serde",
"serde_derive",
@@ -3528,7 +3668,7 @@ dependencies = [
[[package]]
name = "notedeck_dave"
version = "0.5.6"
version = "0.7.1"
dependencies = [
"async-openai",
"bytemuck",
@@ -3536,6 +3676,7 @@ dependencies = [
"eframe",
"egui",
"egui-wgpu",
"egui_extras",
"enostr",
"futures",
"hex",
@@ -3550,19 +3691,27 @@ dependencies = [
"tracing",
]
[[package]]
name = "notedeck_notebook"
version = "0.7.1"
dependencies = [
"egui",
"jsoncanvas",
"notedeck",
]
[[package]]
name = "notedeck_ui"
version = "0.5.6"
version = "0.7.1"
dependencies = [
"bitflags 2.9.1",
"blurhash",
"eframe",
"egui",
"egui-winit",
"egui_extras",
"ehttp",
"enostr",
"hashbrown",
"hashbrown 0.15.4",
"image",
"nostrdb",
"notedeck",
@@ -3992,14 +4141,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "open"
version = "5.3.2"
name = "opener"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95"
checksum = "771b9704f8cd8b424ec747a320b30b47517a6966ba2c7da90047c16f4a962223"
dependencies = [
"is-wsl",
"libc",
"pathdiff",
"bstr",
"normpath",
"windows-sys 0.59.0",
]
[[package]]
@@ -4103,12 +4252,6 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pathdiff"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]]
name = "pbkdf2"
version = "0.12.2"
@@ -4397,7 +4540,7 @@ source = "git+https://github.com/jb55/puffin?rev=c6a6242adaf90b6292c0f462d2acd34
dependencies = [
"egui",
"egui_extras",
"indexmap",
"indexmap 2.9.0",
"natord",
"once_cell",
"parking_lot",
@@ -4725,6 +4868,26 @@ dependencies = [
"thiserror 1.0.69",
]
[[package]]
name = "ref-cast"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf"
dependencies = [
"ref-cast-impl",
]
[[package]]
name = "ref-cast-impl"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
]
[[package]]
name = "regex"
version = "1.11.1"
@@ -4916,6 +5079,30 @@ dependencies = [
"rmp",
]
[[package]]
name = "robius-android-env"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "087fcb3061ccc432658a605cb868edd44e0efb08e7a159b486f02804a7616bef"
dependencies = [
"jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
"ndk-context",
]
[[package]]
name = "robius-open"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243e2abbc8c1ca8ddc283056d4675b67e452fd527c3741c5318642da37840ff3"
dependencies = [
"cfg-if",
"icrate",
"jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
"objc2 0.5.2",
"robius-android-env",
"windows 0.54.0",
]
[[package]]
name = "roxmltree"
version = "0.19.0"
@@ -5062,6 +5249,30 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "schemars"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
dependencies = [
"dyn-clone",
"ref-cast",
"serde",
"serde_json",
]
[[package]]
name = "schemars"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0"
dependencies = [
"dyn-clone",
"ref-cast",
"serde",
"serde_json",
]
[[package]]
name = "scoped-tls"
version = "1.0.1"
@@ -5215,7 +5426,7 @@ version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"indexmap",
"indexmap 2.9.0",
"itoa",
"memchr",
"ryu",
@@ -5254,6 +5465,38 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_with"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5"
dependencies = [
"base64 0.22.1",
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.9.0",
"schemars 0.9.0",
"schemars 1.0.4",
"serde",
"serde_derive",
"serde_json",
"serde_with_macros",
"time",
]
[[package]]
name = "serde_with_macros"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.104",
]
[[package]]
name = "sha1"
version = "0.10.6"
@@ -5315,6 +5558,12 @@ dependencies = [
"quote",
]
[[package]]
name = "similar"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
[[package]]
name = "simplecss"
version = "0.2.2"
@@ -5848,7 +6097,7 @@ version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"indexmap 2.9.0",
"serde",
"serde_spanned",
"toml_datetime",
@@ -6631,7 +6880,7 @@ dependencies = [
"bitflags 2.9.1",
"cfg_aliases",
"document-features",
"indexmap",
"indexmap 2.9.0",
"log",
"naga",
"once_cell",
@@ -6752,6 +7001,16 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows"
version = "0.54.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49"
dependencies = [
"windows-core 0.54.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows"
version = "0.58.0"
@@ -6771,6 +7030,16 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.54.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65"
dependencies = [
"windows-result 0.1.2",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.58.0"
@@ -6847,6 +7116,15 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-result"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-result"
version = "0.2.0"
@@ -7174,10 +7452,10 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winit"
version = "0.30.8"
source = "git+https://github.com/damus-io/winit?rev=eaff639ab0a14fccf595241f687be883154b267c#eaff639ab0a14fccf595241f687be883154b267c"
source = "git+https://github.com/damus-io/winit?rev=a07ea4c4d76cdc5306da8e8aabb05cab1a7d8e4d#a07ea4c4d76cdc5306da8e8aabb05cab1a7d8e4d"
dependencies = [
"ahash",
"android-activity 0.6.0 (git+https://github.com/damus-io/android-activity?rev=c3c0decc83c4d6c94d2c448391fc8dd51b13f3d9)",
"android-activity 0.6.0 (git+https://github.com/damus-io/android-activity?rev=4ee16f1585e4a75031dc10785163d4b920f95805)",
"atomic-waker",
"bitflags 2.9.1",
"block2 0.5.1",
@@ -7229,7 +7507,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4409c10174df8779dc29a4788cac85ed84024ccbc1743b776b21a520ee1aaf4"
dependencies = [
"ahash",
"android-activity 0.6.0 (git+https://github.com/damus-io/android-activity?rev=a8948332c7c551303d32eb26a59d0abd676e47a5)",
"android-activity 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"atomic-waker",
"bitflags 2.9.1",
"block2 0.5.1",

View File

@@ -1,17 +1,21 @@
[workspace]
resolver = "2"
package.version = "0.5.6"
package.version = "0.7.1"
members = [
"crates/notedeck",
"crates/notedeck_chrome",
"crates/notedeck_columns",
"crates/notedeck_dave",
"crates/notedeck_notebook",
"crates/notedeck_ui",
"crates/notedeck_clndash",
"crates/enostr", "crates/tokenator", "crates/notedeck_dave", "crates/notedeck_ui",
"crates/enostr", "crates/tokenator", "crates/notedeck_dave", "crates/notedeck_ui", "crates/notedeck_clndash",
]
[workspace.dependencies]
opener = "0.8.2"
chrono = "0.4.40"
base32 = "0.4.0"
base64 = "0.22.1"
rmpv = "1.3.0"
@@ -23,7 +27,7 @@ egui = { version = "0.31.1", features = ["serde"] }
egui-wgpu = "0.31.1"
egui_extras = { version = "0.31.1", features = ["all_loaders"] }
egui-winit = { version = "0.31.1", features = ["android-game-activity", "clipboard"] }
egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "111de8ac40b5d18df53e9691eb18a50d49cb31d8" }
egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "e4231c19dda9e6791d2f7b5cd610b8db5ff9a7f9" }
egui_tabs = { git = "https://github.com/damus-io/egui-tabs", rev = "6eb91740577b374a8a6658c09c9a4181299734d0" }
#egui_virtual_list = "0.6.0"
egui_virtual_list = { git = "https://github.com/jb55/hello_egui", rev = "a66b6794f5e707a2f4109633770e02b02fb722e1" }
@@ -33,7 +37,7 @@ ewebsock = { version = "0.2.0", features = ["tls"] }
fluent = "0.17.0"
fluent-resmgr = "0.0.8"
fluent-langneg = "0.13"
hex = "0.4.3"
hex = { version = "0.4.3", features = ["serde"] }
image = { version = "0.25", features = ["jpeg", "png", "webp"] }
indexmap = "2.6.0"
log = "0.4.17"
@@ -41,16 +45,18 @@ md5 = "0.7.0"
nostr = { version = "0.37.0", default-features = false, features = ["std", "nip49"] }
nwc = "0.39.0"
mio = { version = "1.0.3", features = ["os-poll", "net"] }
nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "a307f5d3863b5319c728b2782959839b8df544cb" }
nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "2b2e5e43c019b80b98f1db6a03a1b88ca699bfa3" }
#nostrdb = "0.6.1"
notedeck = { path = "crates/notedeck" }
notedeck_chrome = { path = "crates/notedeck_chrome" }
notedeck_clndash = { path = "crates/notedeck_clndash" }
notedeck_columns = { path = "crates/notedeck_columns" }
notedeck_dave = { path = "crates/notedeck_dave" }
notedeck_notebook = { path = "crates/notedeck_notebook" }
notedeck_ui = { path = "crates/notedeck_ui" }
tokenator = { path = "crates/tokenator" }
once_cell = "1.19.0"
open = "5.3.0"
robius-open = "0.1"
poll-promise = { version = "0.3.0", features = ["tokio"] }
puffin = { git = "https://github.com/jb55/puffin", package = "puffin", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
puffin_egui = { git = "https://github.com/jb55/puffin", package = "puffin_egui", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
@@ -75,12 +81,14 @@ mime_guess = "2.0.5"
pretty_assertions = "1.4.1"
jni = "0.21.1"
profiling = "1.0"
lightning-invoice = "0.33.1"
lightning-invoice = { version = "0.33.1", features = ["serde"] }
secp256k1 = "0.30.0"
hashbrown = "0.15.2"
openai-api-rs = "6.0.3"
re_memory = "0.23.4"
oot_bitset = "0.1.1"
blurhash = "0.2.3"
android-activity = { git = "https://github.com/damus-io/android-activity", rev = "4ee16f1585e4a75031dc10785163d4b920f95805", features = [ "game-activity" ] }
[profile.small]
inherits = 'release'
@@ -98,15 +106,15 @@ strip = true # Strip symbols from binary*
#egui_extras = { path = "/home/jb55/dev/github/emilk/egui/crates/egui_extras" }
#epaint = { path = "/home/jb55/dev/github/emilk/egui/crates/epaint" }
egui = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
eframe = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
egui-winit = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
egui-wgpu = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
egui_extras = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
epaint = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
egui = { git = "https://github.com/damus-io/egui", rev = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
eframe = { git = "https://github.com/damus-io/egui", rev = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
egui-winit = { git = "https://github.com/damus-io/egui", rev = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
egui-wgpu = { git = "https://github.com/damus-io/egui", rev = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
egui_extras = { git = "https://github.com/damus-io/egui", rev = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
epaint = { git = "https://github.com/damus-io/egui", rev = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
puffin = { git = "https://github.com/jb55/puffin", package = "puffin", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
puffin_egui = { git = "https://github.com/jb55/puffin", package = "puffin_egui", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
#winit = { git = "https://github.com/damus-io/winit", rev = "14d61a74bee0c9863abe7ef28efae2c4d8bd3743" }
#winit = { git = "https://github.com/damus-io/winit", rev = "701a43d3c6479b0a3869acd2cebbfd410d399a59" }
#winit = { path = "/home/jb55/dev/github/rust-windowing/winit" }
android-activity = { git = "https://github.com/damus-io/android-activity", rev = "a8948332c7c551303d32eb26a59d0abd676e47a5" }
#android-activity = { git = "https://github.com/damus-io/android-activity", rev = "f56c974aa5182d5fbd361879f5899eb8f11a37ec" }
#android-activity = { path = "/home/jb55/dev/github/rust-mobile/android-activity/android-activity" }

View File

@@ -27,4 +27,4 @@ push-android-config:
android: jni
cd $(ANDROID_DIR) && ./gradlew installDebug
adb shell am start -n com.damus.notedeck/.MainActivity
adb logcat -v color -s RustStdoutStderr -s threaded_app | tee logcat.txt
adb logcat -v color -s GameActivity -s RustStdoutStderr -s threaded_app | tee logcat.txt

11
android Executable file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env bash
root_dir=$PWD
cargo ndk --target arm64-v8a -o ./crates/notedeck_chrome/android/app/src/main/jniLibs/ build --profile release
cd ./crates/notedeck_chrome/android
./gradlew build && ./gradlew installDebug
cd $root_dir

BIN
assets/icons/accounts.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

57
assets/icons/clnlogo.svg Normal file
View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="256mm"
height="256mm"
viewBox="0 0 256 256"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="clnlogo.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="1.078823"
inkscape:cx="396.72867"
inkscape:cy="561.25984"
inkscape:window-width="2020"
inkscape:window-height="1420"
inkscape:window-x="270"
inkscape:window-y="20"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="matrix(1.0800571,0,0,1.0347966,-2.6149197,-3.0116377)"
style="display:inline">
<g
id="g4"
transform="matrix(0.43515072,0,0,0.43515072,68.289343,9.0200629)">
<path
class="st1"
d="M 214.6,0 2.2,285.8 246.4,222.3 100.1,222.4 Z"
id="path3"
style="fill:#f0d003" />
<path
fill="#fffae6"
d="M 31.8,550.7 244.1,264.9 0,328.4 146.3,328.3 Z"
id="path4" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.33337 5.33337V3.46671C5.33337 2.71997 5.33337 2.3466 5.4787 2.06139C5.60653 1.8105 5.8105 1.60653 6.06139 1.4787C6.3466 1.33337 6.71997 1.33337 7.46671 1.33337H12.5334C13.2801 1.33337 13.6535 1.33337 13.9387 1.4787C14.1896 1.60653 14.3936 1.8105 14.5214 2.06139C14.6667 2.3466 14.6667 2.71997 14.6667 3.46671V8.53337C14.6667 9.28011 14.6667 9.65351 14.5214 9.93871C14.3936 10.1896 14.1896 10.3936 13.9387 10.5214C13.6535 10.6667 13.2801 10.6667 12.5334 10.6667H10.6667M3.46671 14.6667H8.53337C9.28011 14.6667 9.65351 14.6667 9.93871 14.5214C10.1896 14.3936 10.3936 14.1896 10.5214 13.9387C10.6667 13.6535 10.6667 13.2801 10.6667 12.5334V7.46671C10.6667 6.71997 10.6667 6.3466 10.5214 6.06139C10.3936 5.8105 10.1896 5.60653 9.93871 5.4787C9.65351 5.33337 9.28011 5.33337 8.53337 5.33337H3.46671C2.71997 5.33337 2.3466 5.33337 2.06139 5.4787C1.8105 5.60653 1.60653 5.8105 1.4787 6.06139C1.33337 6.3466 1.33337 6.71997 1.33337 7.46671V12.5334C1.33337 13.2801 1.33337 13.6535 1.4787 13.9387C1.60653 14.1896 1.8105 14.3936 2.06139 14.5214C2.3466 14.6667 2.71997 14.6667 3.46671 14.6667Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -6,7 +6,7 @@
# Regular strings
# Profile about/bio field label
About_00c0 = Über
About_00c0 = Über mich
# Column title for account management
Accounts_f018 = Konten
# Button label to add a relay
@@ -45,6 +45,8 @@ Algo_2452 = Algorithmus
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Algorithmische Feeds zur Hilfe bei der Entdeckung von Notizen
# Label for zap amount input field
Amount_70f0 = Menge
# Label for appearance settings section
Appearance_4c7f = Darstellung
# Button to send message to Dave AI assistant
Ask_b7f4 = Fragen
# Placeholder text for Dave AI input field
@@ -59,10 +61,18 @@ Broadcast_fe43 = Senden
Broadcast_Local_7e50 = Lokal senden
# Button label to cancel an action
Cancel_ed3b = Abbrechen
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = Abbrechen
# Label for clear cache button, Storage settings section
Clear_cache_dccb = Zwischenspeicher leeren
# Hover text for editable zap amount
Click_to_edit_0414 = Zum Bearbeiten anklicken
# Column title for note composition
Compose_Note_c094 = Notiz erstellen
# Label for configure relays, settings section
Configure_relays_d156 = Relays konfigurieren
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = Bestätigen
# Button label to confirm an action
Confirm_f8a6 = Bestätigen
# Status label for connected relay
@@ -88,19 +98,19 @@ Copy_Pubkey_9cc4 = Pubkey kopieren
# Copy the text content of the note to clipboard
Copy_Text_f81c = Text kopieren
# Relative time in days
count_d_b9be = { $count }T.
count_d_b9be = { $count }T
# Relative time in hours
count_h_3ecb = { $count }Std.
count_h_3ecb = { $count }h
# Relative time in minutes
count_m_b41e = { $count }Min.
count_m_b41e = { $count }min
# Relative time in months
count_mo_7aba = { $count }Mon.
count_mo_7aba = { $count }M
# Relative time in seconds
count_s_aa26 = { $count }Sek.
count_s_aa26 = { $count }s
# Relative time in weeks
count_w_7468 = { $count }Wo.
count_w_7468 = { $count }W
# Relative time in years
count_y_9408 = { $count }J.
count_y_9408 = { $count }J
# Button to create a new account
Create_Account_6994 = Konto erstellen
# Button label to create a new deck
@@ -111,6 +121,8 @@ Custom_a69e = Benutzerdefiniert
Customize_Zap_Amount_cfc4 = Zap-Betrag anpassen
# Column title for support page
Damus_Support_27c0 = Damus Support
# Label for Theme Dark, Appearance settings section
Dark_85fe = Dunkel
# Label for deck name input field
Deck_name_cd32 = Deck-Name
# Label for decks section in side panel
@@ -151,12 +163,16 @@ Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__ns
Für das Veröffentlichen von Beiträgen und andere Aktionen ist dein privater Schlüssel erforderlich.
# Label for find user button
Find_User_bd12 = Profil finden
# Label for font size, Appearance settings section
Font_size_dd73 = Schriftgröße:
# Title for hashtags column
Hashtags_f8e0 = Hashtags
# Title for Home column
Home_8c19 = Startseite
# Label for deck icon selection
Icon_b0ab = Symbol
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = Bildcache Größe:
# Title for individual user column
Individual_b776 = Individuell
# Error message for invalid zap amount
@@ -177,8 +193,12 @@ k_50K_c2dc = 50K
k_5K_f7e6 = 5K
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = Behalte den Überblick über deine Notizen & Antworten
# Label for language, Appearance settings section
Language_e264 = Sprache:
# Title for last note per user column
Last_Note_per_User_17ad = Letzte Notiz pro Profil
# Label for Theme Light, Appearance settings section
Light_7475 = Hell
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = Lightning-Netzwerkadresse (lud16)
# Login page title
@@ -216,11 +236,17 @@ Notifications_d673 = Benachrichtigungen
# Title for notifications column
Notifications_ef56 = Benachrichtigungen
# Relative time for very recent events (less than 3 seconds)
now_2181 = Jetzt
now_2181 = Gerade eben
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = An
# Column title for finding users to follow
Onboarding_4a25 = Neue Leute finden
# Button label to open email client
Open_Email_25e9 = E-Mail öffnen
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Öffne deinen Standard-E-Mail-Client, um Hilfe vom Damus-Team zu erhalten
# Label for others settings section
Others_7267 = Andere
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = Füge hier deine NWC-URI ein...
# Error message for missing deck name
@@ -267,6 +293,10 @@ replying_to_a_note_e0bc = Antwort auf eine Notiz
Repost_this_note_8e56 = Diese Notiz teilen
# Label for reposted notes
Reposted_61c8 = Teilen
# Label for reset note body font size, Appearance settings section
Reset_4e60 = Zurücksetzen
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Zurücksetzen
# Heading for support section
Running_into_a_bug_1796 = Ein Fehler aufgetreten?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
@@ -287,8 +317,12 @@ Searching_for___query_5d18 = Suche nach '{ $query }'
See_notes_from_your_contacts_ac16 = Notizen von deinen Kontakten ansehen
# Description for universe column
See_the_whole_nostr_universe_7694 = Sieh dir das ganze Nostr-Universum an
# Button to select all profiles in follow pack
Select_All_a319 = Alle auswählen
# Button label to send a zap
Send_1ea4 = Senden
# Column title for app settings
Settings_7a4f = Einstellungen
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = Zeige die letzte Notiz für jedes Profil aus einer Liste
# Button label to sign out of account
@@ -297,6 +331,8 @@ Sign_out_337b = Abmelden
Someone_else_s_Notes_7e5f = Notizen anderer Profile
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Mitteilungen anderer Profile
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Neueste Antworten zuerst sortieren:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Die letzte Notiz für jedes Profil aus deiner Kontaktliste anzeigen
# Description for hashtags column
@@ -315,10 +351,14 @@ Stay_up_to_date_with_your_notifications_and_mentions_e73e = Bleib bei deinen Ben
Step_1_8656 = Schritt 1
# Step 2 label in support instructions
Step_2_d08d = Schritt 2
# Label for storage settings section
Storage_ed65 = Speicher
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = Abonniere die Notizen eines anderen
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = Abonniere die Notizen von jemandem
# Support email address
Support_email_44d9 = E-Mail Support:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Zum Dunkelmodus wechseln
# Hover text for light mode toggle button
@@ -327,8 +367,10 @@ Switch_to_light_mode_72ce = Zum Hellmodus wechseln
Tap_to_Load_4b05 = Zum Laden antippen
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = Die Testphase des Dave Nostr KI-Assistenten ist beendet :(. Vielen Dank fürs Ausprobieren! Zap-fähiger Dave kommt bald!
# Label for theme, Appearance settings section
Theme_4aac = Design:
# Column title for note thread view
Thread_0f20 = Unterhaltungen
Thread_0f20 = Unterhaltung
# Link text for thread references
thread_ad1f = Unterhaltung
# Title for universe column
@@ -341,6 +383,8 @@ Use_this_wallet_for_the_current_account_only_61dc = Diese Wallet nur für das ak
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" bei "{ $domain }" wird für die Identifikation verwendet werden
# Profile username field label
Username_daa7 = Benutzername
# Label for view folder button, Storage settings section
View_folder_9742 = Ordner anzeigen
# Column title for wallet management
Wallet_5e50 = Wallet
# Hint for deck name input field
@@ -359,6 +403,8 @@ Your_Notifications_080d = Deine Benachrichtigungen
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = Zappe diese Notiz
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = Zoomstufe:
# Pluralized strings

View File

@@ -46,6 +46,9 @@ Add_Hashtag_Column_ebf4 = Add Hashtag Column
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = Add Last Notes Column
# Tooltip text for adding a new deck button
Add_new_deck_f2fc = Add new deck
# Column title for adding notifications column
Add_Notifications_Column_79f8 = Add Notifications Column
@@ -64,6 +67,9 @@ Algorithmic_feeds_to_aid_in_note_discovery_d344 = Algorithmic feeds to aid in no
# Label for zap amount input field
Amount_70f0 = Amount
# Label for appearance settings section
Appearance_4c7f = Appearance
# Button to send message to Dave AI assistant
Ask_b7f4 = Ask
@@ -85,12 +91,24 @@ Broadcast_Local_7e50 = Broadcast Local
# Button label to cancel an action
Cancel_ed3b = Cancel
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = Cancel
# Label for clear cache button, Storage settings section
Clear_cache_dccb = Clear cache
# Hover text for editable zap amount
Click_to_edit_0414 = Click to edit
# Column title for note composition
Compose_Note_c094 = Compose Note
# Label for configure relays, settings section
Configure_relays_d156 = Configure relays
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = Confirm
# Button label to confirm an action
Confirm_f8a6 = Confirm
@@ -121,6 +139,9 @@ Copy_Note_ID_6b45 = Copy Note ID
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = Copy Note JSON
# Tooltip text for copying npub to clipboard
Copy_npub_to_clipboard_c105 = Copy npub to clipboard
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = Copy Pubkey
@@ -163,6 +184,9 @@ Customize_Zap_Amount_cfc4 = Customize Zap Amount
# Column title for support page
Damus_Support_27c0 = Damus Support
# Label for Theme Dark, Appearance settings section
Dark_85fe = Dark
# Label for deck name input field
Deck_name_cd32 = Deck name
@@ -190,6 +214,9 @@ Display_name_f9d9 = Display name
# Domain identification message
domain___will_be_used_for_identification_b67e = "{$domain}" will be used for identification
# Button to indicate that the user is done going through the onboarding process.
Done_50dd = Done
# Column title for editing deck
Edit_Deck_4018 = Edit Deck
@@ -220,6 +247,9 @@ Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__ns
# Label for find user button
Find_User_bd12 = Find User
# Label for font size, Appearance settings section
Font_size_dd73 = Font size:
# Title for hashtags column
Hashtags_f8e0 = Hashtags
@@ -229,6 +259,9 @@ Home_8c19 = Home
# Label for deck icon selection
Icon_b0ab = Icon
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = Image cache size:
# Title for individual user column
Individual_b776 = Individual
@@ -259,9 +292,18 @@ k_5K_f7e6 = 5K
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = Keep track of your notes & replies
# label for keys setting section
Keys_435f = Keys
# Label for language, Appearance settings section
Language_e264 = Language:
# Title for last note per user column
Last_Note_per_User_17ad = Last Note per User
# Label for Theme Light, Appearance settings section
Light_7475 = Light
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = Lightning network address (lud16)
@@ -280,6 +322,18 @@ Moves_this_column_to_another_position_0d4b = Moves this column to another positi
# Title for the user's deck
My_Deck_4ac5 = My Deck
# reaction from user to a note you were tagged in
name__reacted_to_a_note_you_were_tagged_in_4b62 = {$name} reacted to a note you were tagged in
# reaction from user to your note
name__reacted_to_your_note_ead9 = {$name} reacted to your note
# repost from user
name__reposted_a_note_you_were_tagged_in_1379 = {$name} reposted a note you were tagged in
# repost from user
name__reposted_your_note_1379 = {$name} reposted your note
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = New to Nostr?
@@ -319,12 +373,21 @@ Notifications_ef56 = Notifications
# Relative time for very recent events (less than 3 seconds)
now_2181 = now
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = On
# Column title for finding users to follow
Onboarding_4a25 = Onboarding
# Button label to open email client
Open_Email_25e9 = Open Email
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Open your default email client to get help from the Damus team
# Label for others settings section
Others_7267 = Others
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = Paste your NWC URI here...
@@ -346,6 +409,9 @@ Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard_
# Profile picture URL field label
Profile_picture_81ff = Profile picture
# label describing public key
PUBLIC_ACCOUNT_ID_4394 = PUBLIC ACCOUNT ID
# Column title for quote composition
Quote_475c = Quote
@@ -394,6 +460,12 @@ Repost_this_note_8e56 = Repost this note
# Label for reposted notes
Reposted_61c8 = Reposted
# Label for reset note body font size, Appearance settings section
Reset_4e60 = Reset
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Reset
# Heading for support section
Running_into_a_bug_1796 = Running into a bug?
@@ -418,15 +490,24 @@ Search_notes_42a6 = Search notes...
# Search in progress message
Searching_for___query_5d18 = Searching for '{$query}'
# label describing secret key
SECRET_ACCOUNT_LOGIN_KEY_8440 = SECRET ACCOUNT LOGIN KEY
# Description for Home column
See_notes_from_your_contacts_ac16 = See notes from your contacts
# Description for universe column
See_the_whole_nostr_universe_7694 = See the whole nostr universe
# Button to select all profiles in follow pack
Select_All_a319 = Select All
# Button label to send a zap
Send_1ea4 = Send
# Column title for app settings
Settings_7a4f = Settings
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = Show the last note for each user from a list
@@ -439,6 +520,9 @@ Someone_else_s_Notes_7e5f = Someone else's Notes
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Someone else's Notifications
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Sort replies newest first:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Source the last note for each user in your contact list
@@ -466,12 +550,18 @@ Step_1_8656 = Step 1
# Step 2 label in support instructions
Step_2_d08d = Step 2
# Label for storage settings section
Storage_ed65 = Storage
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = Subscribe to someone else's notes
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = Subscribe to someone's notes
# Support email address
Support_email_44d9 = Support email:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Switch to dark mode
@@ -484,6 +574,9 @@ Tap_to_Load_4b05 = Tap to Load
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = The Dave Nostr AI assistant trial has ended :(. Thanks for testing! Zap-enabled Dave coming soon!
# Label for theme, Appearance settings section
Theme_4aac = Theme:
# Column title for note thread view
Thread_0f20 = Thread
@@ -505,6 +598,9 @@ username___at___domain___will_be_used_for_identification_a4fd = "{$username}" at
# Profile username field label
Username_daa7 = Username
# Label for view folder button, Storage settings section
View_folder_9742 = View folder
# Column title for wallet management
Wallet_5e50 = Wallet
@@ -532,6 +628,9 @@ Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = Zap this note
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = Zoom Level:
# Pluralized strings
# Search results count
@@ -540,3 +639,35 @@ Got__count__results_for___query_85fb =
[one] Got {$count} result for '{$query}'
*[other] Got {$count} results for '{$query}'
}
# amount of reactions a note you were tagged in received
name__and__count__others_reacted_to_a_note_you_were_tagged_in_181a =
{ $count ->
[one] {$name} and {$count} other reacted to a note you were tagged in
*[other] {$name} and {$count} others reacted to a note you were tagged in
}
# describing the amount of reactions your note received
name__and__count__others_reacted_to_your_note_0f6a =
{ $count ->
[one] {$name} and {$count} other reacted to your note
*[other] {$name} and {$count} others reacted to your note
}
# describing the amount of reposts a note you were tagged in received
name__and__count__others_reposted_a_note_you_were_tagged_in_08e1 =
{ $count ->
[one] {$name} and {$count} other reposted a note you were tagged in
*[other] {$name} and {$count} others reposted a note you were tagged in
}
# describing the amount of reposts your note received
name__and__count__others_reposted_your_note_70a0 =
{ $count ->
[one] {$name} and {$count} other reposted your note
*[other] {$name} and {$count} others reposted your note
}

View File

@@ -46,6 +46,9 @@ Add_Hashtag_Column_ebf4 = {"["}Àdd Hàshtàg Çólúmñ{"]"}
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = {"["}Àdd Làst Ñótés Çólúmñ{"]"}
# Tooltip text for adding a new deck button
Add_new_deck_f2fc = {"["}Àdd ñéw déçk{"]"}
# Column title for adding notifications column
Add_Notifications_Column_79f8 = {"["}Àdd Ñótífíçàtíóñs Çólúmñ{"]"}
@@ -64,6 +67,9 @@ Algorithmic_feeds_to_aid_in_note_discovery_d344 = {"["}Àlgóríthmíç fééds
# Label for zap amount input field
Amount_70f0 = {"["}Àmóúñt{"]"}
# Label for appearance settings section
Appearance_4c7f = {"["}Àppéàràñçé{"]"}
# Button to send message to Dave AI assistant
Ask_b7f4 = {"["}Àsk{"]"}
@@ -85,12 +91,24 @@ Broadcast_Local_7e50 = {"["}Bróàdçàst Lóçàl{"]"}
# Button label to cancel an action
Cancel_ed3b = {"["}Çàñçél{"]"}
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = {"["}Çàñçél{"]"}
# Label for clear cache button, Storage settings section
Clear_cache_dccb = {"["}Çléàr çàçhé{"]"}
# Hover text for editable zap amount
Click_to_edit_0414 = {"["}Çlíçk tó édít{"]"}
# Column title for note composition
Compose_Note_c094 = {"["}Çómpósé Ñóté{"]"}
# Label for configure relays, settings section
Configure_relays_d156 = {"["}Çóñfígúré rélàys{"]"}
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = {"["}Çóñfírm{"]"}
# Button label to confirm an action
Confirm_f8a6 = {"["}Çóñfírm{"]"}
@@ -121,6 +139,9 @@ Copy_Note_ID_6b45 = {"["}Çópy Ñóté ÍD{"]"}
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = {"["}Çópy Ñóté JSÓÑ{"]"}
# Tooltip text for copying npub to clipboard
Copy_npub_to_clipboard_c105 = {"["}Çópy ñpúb tó çlípbóàrd{"]"}
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = {"["}Çópy Púbkéy{"]"}
@@ -163,6 +184,9 @@ Customize_Zap_Amount_cfc4 = {"["}Çústómízé Zàp Àmóúñt{"]"}
# Column title for support page
Damus_Support_27c0 = {"["}Dàmús Súppórt{"]"}
# Label for Theme Dark, Appearance settings section
Dark_85fe = {"["}Dàrk{"]"}
# Label for deck name input field
Deck_name_cd32 = {"["}Déçk ñàmé{"]"}
@@ -190,6 +214,9 @@ Display_name_f9d9 = {"["}Dísplày ñàmé{"]"}
# Domain identification message
domain___will_be_used_for_identification_b67e = {"["}"{$domain}" wíll bé úséd fór ídéñtífíçàtíóñ{"]"}
# Button to indicate that the user is done going through the onboarding process.
Done_50dd = {"["}Dóñé{"]"}
# Column title for editing deck
Edit_Deck_4018 = {"["}Édít Déçk{"]"}
@@ -220,6 +247,9 @@ Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__ns
# Label for find user button
Find_User_bd12 = {"["}Fíñd Úsér{"]"}
# Label for font size, Appearance settings section
Font_size_dd73 = {"["}Fóñt sízé:{"]"}
# Title for hashtags column
Hashtags_f8e0 = {"["}Hàshtàgs{"]"}
@@ -229,6 +259,9 @@ Home_8c19 = {"["}Hómé{"]"}
# Label for deck icon selection
Icon_b0ab = {"["}Íçóñ{"]"}
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = {"["}Ímàgé çàçhé sízé:{"]"}
# Title for individual user column
Individual_b776 = {"["}Íñdívídúàl{"]"}
@@ -259,9 +292,18 @@ k_5K_f7e6 = {"["}5K{"]"}
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = {"["}Kéép tràçk óf yóúr ñótés & réplíés{"]"}
# label for keys setting section
Keys_435f = {"["}Kéys{"]"}
# Label for language, Appearance settings section
Language_e264 = {"["}Làñgúàgé:{"]"}
# Title for last note per user column
Last_Note_per_User_17ad = {"["}Làst Ñóté pér Úsér{"]"}
# Label for Theme Light, Appearance settings section
Light_7475 = {"["}Líght{"]"}
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = {"["}Líghtñíñg ñétwórk àddréss (lúd16){"]"}
@@ -280,6 +322,18 @@ Moves_this_column_to_another_position_0d4b = {"["}Móvés thís çólúmñ tó
# Title for the user's deck
My_Deck_4ac5 = {"["}My Déçk{"]"}
# reaction from user to a note you were tagged in
name__reacted_to_a_note_you_were_tagged_in_4b62 = {"["}{$name} réàçtéd tó à ñóté yóú wéré tàggéd íñ{"]"}
# reaction from user to your note
name__reacted_to_your_note_ead9 = {"["}{$name} réàçtéd tó yóúr ñóté{"]"}
# repost from user
name__reposted_a_note_you_were_tagged_in_1379 = {"["}{$name} répóstéd à ñóté yóú wéré tàggéd íñ{"]"}
# repost from user
name__reposted_your_note_1379 = {"["}{$name} répóstéd yóúr ñóté{"]"}
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = {"["}Ñéw tó Ñóstr?{"]"}
@@ -319,12 +373,21 @@ Notifications_ef56 = {"["}Ñótífíçàtíóñs{"]"}
# Relative time for very recent events (less than 3 seconds)
now_2181 = {"["}ñów{"]"}
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = {"["}Óñ{"]"}
# Column title for finding users to follow
Onboarding_4a25 = {"["}Óñbóàrdíñg{"]"}
# Button label to open email client
Open_Email_25e9 = {"["}Ópéñ Émàíl{"]"}
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = {"["}Ópéñ yóúr défàúlt émàíl çlíéñt tó gét hélp fróm thé Dàmús téàm{"]"}
# Label for others settings section
Others_7267 = {"["}Óthérs{"]"}
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = {"["}Pàsté yóúr ÑWÇ ÚRÍ héré...{"]"}
@@ -346,6 +409,9 @@ Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard_
# Profile picture URL field label
Profile_picture_81ff = {"["}Prófílé píçtúré{"]"}
# label describing public key
PUBLIC_ACCOUNT_ID_4394 = {"["}PÚBLÍÇ ÀÇÇÓÚÑT ÍD{"]"}
# Column title for quote composition
Quote_475c = {"["}Qúóté{"]"}
@@ -394,6 +460,12 @@ Repost_this_note_8e56 = {"["}Répóst thís ñóté{"]"}
# Label for reposted notes
Reposted_61c8 = {"["}Répóstéd{"]"}
# Label for reset note body font size, Appearance settings section
Reset_4e60 = {"["}Rését{"]"}
# Label for reset zoom level, Appearance settings section
Reset_62d4 = {"["}Rését{"]"}
# Heading for support section
Running_into_a_bug_1796 = {"["}Rúññíñg íñtó à búg?{"]"}
@@ -418,15 +490,24 @@ Search_notes_42a6 = {"["}Séàrçh ñótés...{"]"}
# Search in progress message
Searching_for___query_5d18 = {"["}Séàrçhíñg fór '{$query}'{"]"}
# label describing secret key
SECRET_ACCOUNT_LOGIN_KEY_8440 = {"["}SÉÇRÉT ÀÇÇÓÚÑT LÓGÍÑ KÉY{"]"}
# Description for Home column
See_notes_from_your_contacts_ac16 = {"["}Séé ñótés fróm yóúr çóñtàçts{"]"}
# Description for universe column
See_the_whole_nostr_universe_7694 = {"["}Séé thé whólé ñóstr úñívérsé{"]"}
# Button to select all profiles in follow pack
Select_All_a319 = {"["}Séléçt Àll{"]"}
# Button label to send a zap
Send_1ea4 = {"["}Séñd{"]"}
# Column title for app settings
Settings_7a4f = {"["}Séttíñgs{"]"}
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = {"["}Shów thé làst ñóté fór éàçh úsér fróm à líst{"]"}
@@ -439,6 +520,9 @@ Someone_else_s_Notes_7e5f = {"["}Sóméóñé élsé's Ñótés{"]"}
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = {"["}Sóméóñé élsé's Ñótífíçàtíóñs{"]"}
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = {"["}Sórt réplíés ñéwést fírst:{"]"}
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = {"["}Sóúrçé thé làst ñóté fór éàçh úsér íñ yóúr çóñtàçt líst{"]"}
@@ -466,12 +550,18 @@ Step_1_8656 = {"["}Stép 1{"]"}
# Step 2 label in support instructions
Step_2_d08d = {"["}Stép 2{"]"}
# Label for storage settings section
Storage_ed65 = {"["}Stóràgé{"]"}
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = {"["}Súbsçríbé tó sóméóñé élsé's ñótés{"]"}
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = {"["}Súbsçríbé tó sóméóñé's ñótés{"]"}
# Support email address
Support_email_44d9 = {"["}Súppórt émàíl:{"]"}
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = {"["}Swítçh tó dàrk módé{"]"}
@@ -484,6 +574,9 @@ Tap_to_Load_4b05 = {"["}Tàp tó Lóàd{"]"}
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = {"["}Thé Dàvé Ñóstr ÀÍ àssístàñt tríàl hàs éñdéd :(. Thàñks fór téstíñg! Zàp-éñàbléd Dàvé çómíñg sóóñ!{"]"}
# Label for theme, Appearance settings section
Theme_4aac = {"["}Thémé:{"]"}
# Column title for note thread view
Thread_0f20 = {"["}Thréàd{"]"}
@@ -505,6 +598,9 @@ username___at___domain___will_be_used_for_identification_a4fd = {"["}"{$username
# Profile username field label
Username_daa7 = {"["}Úsérñàmé{"]"}
# Label for view folder button, Storage settings section
View_folder_9742 = {"["}Víéw fóldér{"]"}
# Column title for wallet management
Wallet_5e50 = {"["}Wàllét{"]"}
@@ -532,6 +628,9 @@ Zap_16b4 = {"["}Zàp{"]"}
# Hover text for zap button
Zap_this_note_42b2 = {"["}Zàp thís ñóté{"]"}
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = {"["}Zóóm Lévél:{"]"}
# Pluralized strings
# Search results count
@@ -540,3 +639,35 @@ Got__count__results_for___query_85fb =
[one] {"["}Gót {$count} résúlt fór '{$query}'{"]"}
*[other] {"["}Gót {$count} résúlts fór '{$query}'{"]"}
}
# amount of reactions a note you were tagged in received
name__and__count__others_reacted_to_a_note_you_were_tagged_in_181a =
{ $count ->
[one] {"["}{$name} àñd {$count} óthér réàçtéd tó à ñóté yóú wéré tàggéd íñ{"]"}
*[other] {"["}{$name} àñd {$count} óthérs réàçtéd tó à ñóté yóú wéré tàggéd íñ{"]"}
}
# describing the amount of reactions your note received
name__and__count__others_reacted_to_your_note_0f6a =
{ $count ->
[one] {"["}{$name} àñd {$count} óthér réàçtéd tó yóúr ñóté{"]"}
*[other] {"["}{$name} àñd {$count} óthérs réàçtéd tó yóúr ñóté{"]"}
}
# describing the amount of reposts a note you were tagged in received
name__and__count__others_reposted_a_note_you_were_tagged_in_08e1 =
{ $count ->
[one] {"["}{$name} àñd {$count} óthér répóstéd à ñóté yóú wéré tàggéd íñ{"]"}
*[other] {"["}{$name} àñd {$count} óthérs répóstéd à ñóté yóú wéré tàggéd íñ{"]"}
}
# describing the amount of reposts your note received
name__and__count__others_reposted_your_note_70a0 =
{ $count ->
[one] {"["}{$name} àñd {$count} óthér répóstéd yóúr ñóté{"]"}
*[other] {"["}{$name} àñd {$count} óthérs répóstéd yóúr ñóté{"]"}
}

View File

@@ -22,15 +22,15 @@ Add_account_1cfc = Agregar cuenta
# Column title for adding new account
Add_Account_d06c = Agregar cuenta
# Column title for adding algorithm column
Add_Algo_Column_0d75 = Añadir columna algorítmica
Add_Algo_Column_0d75 = Agregar columna algorítmica
# Column title for adding new column
Add_Column_c764 = Agregar columna
# Column title for adding new deck
Add_Deck_fabf = Agregar Deck
Add_Deck_fabf = Agregar deck
# Column title for adding external notifications column
Add_External_Notifications_Column_41ae = Agregar columna de notificaciones externas
# Column title for adding hashtag column
Add_Hashtag_Column_ebf4 = Agregar columna de hashtag
Add_Hashtag_Column_ebf4 = Agregar columna de hashtags
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = Agregar columna de últimas notas
# Column title for adding notifications column
@@ -45,6 +45,8 @@ Algo_2452 = Algo
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Feeds algorítmicos para ayudar en el descubrimiento de notas
# Label for zap amount input field
Amount_70f0 = Cantidad
# Label for appearance settings section
Appearance_4c7f = Aspecto
# Button to send message to Dave AI assistant
Ask_b7f4 = Preguntar
# Placeholder text for Dave AI input field
@@ -59,10 +61,18 @@ Broadcast_fe43 = Transmitir
Broadcast_Local_7e50 = Transmitir localmente
# Button label to cancel an action
Cancel_ed3b = Cancelar
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = Cancelar
# Label for clear cache button, Storage settings section
Clear_cache_dccb = Limpiar caché
# Hover text for editable zap amount
Click_to_edit_0414 = Haz clic para editar
# Column title for note composition
Compose_Note_c094 = Redactar nota
# Label for configure relays, settings section
Configure_relays_d156 = Configurar relés
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = Confirmar
# Button label to confirm an action
Confirm_f8a6 = Confirmar
# Status label for connected relay
@@ -84,7 +94,7 @@ Copy_Note_ID_6b45 = Copiar ID de nota
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = Copiar JSON de nota
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = Copiar Pubkey
Copy_Pubkey_9cc4 = Copiar pubkey
# Copy the text content of the note to clipboard
Copy_Text_f81c = Copiar texto
# Relative time in days
@@ -94,7 +104,7 @@ count_h_3ecb = { $count }h
# Relative time in minutes
count_m_b41e = { $count }m
# Relative time in months
count_mo_7aba = { $count }ms
count_mo_7aba = { $count }mes
# Relative time in seconds
count_s_aa26 = { $count }s
# Relative time in weeks
@@ -104,23 +114,25 @@ count_y_9408 = { $count }a
# Button to create a new account
Create_Account_6994 = Crear cuenta
# Button label to create a new deck
Create_Deck_16b7 = Crear Deck
Create_Deck_16b7 = Crear deck
# Column title for custom timelines
Custom_a69e = Personalizado
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = Personalizar monto de zap
Customize_Zap_Amount_cfc4 = Personalizar cantidad de zap
# Column title for support page
Damus_Support_27c0 = Ayuda de Damus
# Label for Theme Dark, Appearance settings section
Dark_85fe = Oscuro
# Label for deck name input field
Deck_name_cd32 = Nombre del Deck
Deck_name_cd32 = Nombre del deck
# Label for decks section in side panel
DECKS_1fad = DECKS
# Label for default zap amount input
Default_amount_per_zap_399d = Monto predeterminado por zap:
Default_amount_per_zap_399d = Cantidad predeterminada por zap:
# Name of the default deck feed
Default_Deck_fcca = Deck predeterminado
# Button label to delete a deck
Delete_Deck_bb29 = Eliminar Deck
Delete_Deck_bb29 = Eliminar deck
# Tooltip for deleting a column
Delete_this_column_8d5a = Eliminar esta columna
# Button label to delete a wallet
@@ -130,9 +142,9 @@ Display_name_f9d9 = Nombre para mostrar
# Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" se utilizará para la identificación
# Column title for editing deck
Edit_Deck_4018 = Editar Deck
Edit_Deck_4018 = Editar deck
# Button label to edit a deck
Edit_Deck_fd93 = Editar Deck
Edit_Deck_fd93 = Editar deck
# Button label to edit user profile
Edit_Profile_49e6 = Editar perfil
# Column title for profile editing
@@ -149,16 +161,20 @@ Enter_your_key_0fca = Ingresa tu clave
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Ingresa tu clave pública (npub), dirección de Nostr (por ejemplo, { $address }) o clave privada (nsec). Debes ingresar tu clave privada para poder publicar, responder, etc.
# Label for find user button
Find_User_bd12 = Buscar usuario
# Label for font size, Appearance settings section
Font_size_dd73 = Font size:
# Title for hashtags column
Hashtags_f8e0 = Hashtags
# Title for Home column
Home_8c19 = Inicio
# Label for deck icon selection
Icon_b0ab = Ícono
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = Tamaño de caché de imágenes:
# Title for individual user column
Individual_b776 = Individual
# Error message for invalid zap amount
Invalid_amount_6630 = Importe no válido
Invalid_amount_6630 = Cantidad no válida
# Error message for invalid key input
Invalid_key_4726 = Clave no válida.
# Error message for invalid Nostr Wallet Connect URI
@@ -175,8 +191,12 @@ k_50K_c2dc = 50.000
k_5K_f7e6 = 5.000
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = Haz seguimiento de tus notas y respuestas
# Label for language, Appearance settings section
Language_e264 = Idioma:
# Title for last note per user column
Last_Note_per_User_17ad = Última nota por usuario
# Label for Theme Light, Appearance settings section
Light_7475 = Claro
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = Dirección de la red Lightning (lud16)
# Login page title
@@ -184,11 +204,11 @@ Login_9eef = Inicio de sesión
# Login button text
Login_now___let_s_do_this_5630 = Inicia sesión ahora, ¡manos a la obra!
# Text shown on blurred media from unfollowed users
Media_from_someone_you_don_t_follow_5611 = Medios de alguien que no sigues
Media_from_someone_you_don_t_follow_5611 = Contenido multimedia de alguien que no sigues
# Tooltip for moving a column
Moves_this_column_to_another_position_0d4b = Mueve esta columna a otra posición
# Title for the user's deck
My_Deck_4ac5 = Mi Deck
My_Deck_4ac5 = Mi deck
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = ¿Primera vez en Nostr?
# NIP-05 identity field label
@@ -215,18 +235,22 @@ Notifications_d673 = Notificaciones
Notifications_ef56 = Notificaciones
# Relative time for very recent events (less than 3 seconds)
now_2181 = ahora
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = On
# Button label to open email client
Open_Email_25e9 = Abrir correo electrónico
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Abre tu cliente de correo predeterminado para recibir ayuda del equipo de Damus
# Label for others settings section
Others_7267 = Otros
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = Pega tu URI NWC aquí...
Paste_your_NWC_URI_here_b471 = Pega tu NWC URI aquí...
# Error message for missing deck name
Please_create_a_name_for_the_deck_38e7 = Crea un nombre para el Deck.
Please_create_a_name_for_the_deck_38e7 = Crea un nombre para el deck.
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Crea un nombre para el Deck y selecciona un icono.
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Crea un nombre para el deck y selecciona un ícono.
# Error message for missing deck icon
Please_select_an_icon_655b = Selecciona un icono.
Please_select_an_icon_655b = Selecciona un ícono.
# Button label to post a note
Post_now_8a49 = Publicar ahora
# Instruction for copying logs
@@ -250,7 +274,7 @@ Reply_to_this_note_f5de = Responder a esta nota
# Error message when reply note cannot be found
Reply_to_unknown_note_4401 = Responder a nota desconocida
# Fallback template for replying to user
replying_to__user_15ab = responder a { $user }
replying_to__user_15ab = respondiendo a { $user }
# Template for replying to user in unknown thread
replying_to__user__in_someone_s_thread_e148 = respondiendo a { $user } en la conversación de alguien
# Template for replying to note in different user's thread
@@ -265,6 +289,10 @@ replying_to_a_note_e0bc = respondiendo a una nota
Repost_this_note_8e56 = Volver a publicar esta nota
# Label for reposted notes
Reposted_61c8 = Publicadas de nuevo
# Label for reset note body font size, Appearance settings section
Reset_4e60 = Reset
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Restablecer
# Heading for support section
Running_into_a_bug_1796 = ¿Encontraste un error?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
@@ -287,6 +315,8 @@ See_notes_from_your_contacts_ac16 = Ver notas de tus contactos
See_the_whole_nostr_universe_7694 = Ver todo el universo de nostr
# Button label to send a zap
Send_1ea4 = Enviar
# Column title for app settings
Settings_7a4f = Configuración
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar la última nota para cada usuario de una lista
# Button label to sign out of account
@@ -295,6 +325,8 @@ Sign_out_337b = Cerrar sesión
Someone_else_s_Notes_7e5f = Notas de otra persona
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Notificaciones de otra persona
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Sort replies newest first:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Busca la última nota de cada usuario en tu lista de contactos
# Description for hashtags column
@@ -313,10 +345,14 @@ Stay_up_to_date_with_your_notifications_and_mentions_e73e = Mantente al día con
Step_1_8656 = Paso 1
# Step 2 label in support instructions
Step_2_d08d = Paso 2
# Label for storage settings section
Storage_ed65 = Almacenamiento
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = Suscribirse a las notas de otra persona
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = Suscribirse a las notas de alguien
# Support email address
Support_email_44d9 = Support email:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Cambiar a modo oscuro
# Hover text for light mode toggle button
@@ -325,6 +361,8 @@ Switch_to_light_mode_72ce = Cambiar a modo claro
Tap_to_Load_4b05 = Toca para cargar
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = La prueba del asistente de IA Dave de Nostr finalizó :(. ¡Gracias por probarlo! ¡Dave con zaps estará disponible muy pronto!
# Label for theme, Appearance settings section
Theme_4aac = Tema:
# Column title for note thread view
Thread_0f20 = Conversación
# Link text for thread references
@@ -339,6 +377,8 @@ Use_this_wallet_for_the_current_account_only_61dc = Usar esta billetera solo par
username___at___domain___will_be_used_for_identification_a4fd = Se utilizará "{ $username }" en "{ $domain }" para la identificación
# Profile username field label
Username_daa7 = Nombre de usuario
# Label for view folder button, Storage settings section
View_folder_9742 = Ver carpeta
# Column title for wallet management
Wallet_5e50 = Billetera
# Hint for deck name input field
@@ -356,7 +396,9 @@ Your_Notifications_080d = Tus notificaciones
# Heading for zap (tip) action
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = Enviar zap a esta nota
Zap_this_note_42b2 = Enviar un zap a esta nota
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = Nivel de zoom:
# Pluralized strings

View File

@@ -26,11 +26,11 @@ Add_Algo_Column_0d75 = Añadir columna algorítmica
# Column title for adding new column
Add_Column_c764 = Añadir columna
# Column title for adding new deck
Add_Deck_fabf = Añadir Deck
Add_Deck_fabf = Añadir deck
# Column title for adding external notifications column
Add_External_Notifications_Column_41ae = Añadir columna de notificaciones externas
# Column title for adding hashtag column
Add_Hashtag_Column_ebf4 = Añadir columna de hashtag
Add_Hashtag_Column_ebf4 = Añadir columna de hashtags
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = Añadir columna de últimas notas
# Column title for adding notifications column
@@ -45,6 +45,8 @@ Algo_2452 = Algo
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Feeds algorítmicos para ayudar en el descubrimiento de notas
# Label for zap amount input field
Amount_70f0 = Cantidad
# Label for appearance settings section
Appearance_4c7f = Aspecto
# Button to send message to Dave AI assistant
Ask_b7f4 = Preguntar
# Placeholder text for Dave AI input field
@@ -59,10 +61,18 @@ Broadcast_fe43 = Transmitir
Broadcast_Local_7e50 = Transmitir localmente
# Button label to cancel an action
Cancel_ed3b = Cancelar
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = Cancelar
# Label for clear cache button, Storage settings section
Clear_cache_dccb = Limpiar caché
# Hover text for editable zap amount
Click_to_edit_0414 = Haz clic para editar
# Column title for note composition
Compose_Note_c094 = Redactar nota
# Label for configure relays, settings section
Configure_relays_d156 = Configurar relés
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = Confirmar
# Button label to confirm an action
Confirm_f8a6 = Confirmar
# Status label for connected relay
@@ -84,7 +94,7 @@ Copy_Note_ID_6b45 = Copiar ID de nota
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = Copiar JSON de nota
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = Copiar Pubkey
Copy_Pubkey_9cc4 = Copiar pubkey
# Copy the text content of the note to clipboard
Copy_Text_f81c = Copiar texto
# Relative time in days
@@ -94,7 +104,7 @@ count_h_3ecb = { $count }h
# Relative time in minutes
count_m_b41e = { $count }m
# Relative time in months
count_mo_7aba = { $count }ms
count_mo_7aba = { $count }mes
# Relative time in seconds
count_s_aa26 = { $count }s
# Relative time in weeks
@@ -104,23 +114,25 @@ count_y_9408 = { $count }a
# Button to create a new account
Create_Account_6994 = Crear cuenta
# Button label to create a new deck
Create_Deck_16b7 = Crear Deck
Create_Deck_16b7 = Crear deck
# Column title for custom timelines
Custom_a69e = Personalizado
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = Personalizar monto de zap
Customize_Zap_Amount_cfc4 = Personalizar cantidad de zap
# Column title for support page
Damus_Support_27c0 = Ayuda de Damus
# Label for Theme Dark, Appearance settings section
Dark_85fe = Oscuro
# Label for deck name input field
Deck_name_cd32 = Nombre del Deck
Deck_name_cd32 = Nombre del deck
# Label for decks section in side panel
DECKS_1fad = DECKS
# Label for default zap amount input
Default_amount_per_zap_399d = Monto predeterminado por zap:
Default_amount_per_zap_399d = Cantidad predeterminada por zap:
# Name of the default deck feed
Default_Deck_fcca = Deck predeterminado
# Button label to delete a deck
Delete_Deck_bb29 = Eliminar Deck
Delete_Deck_bb29 = Eliminar deck
# Tooltip for deleting a column
Delete_this_column_8d5a = Eliminar esta columna
# Button label to delete a wallet
@@ -130,9 +142,9 @@ Display_name_f9d9 = Nombre para mostrar
# Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" se utilizará para la identificación
# Column title for editing deck
Edit_Deck_4018 = Editar Deck
Edit_Deck_4018 = Editar deck
# Button label to edit a deck
Edit_Deck_fd93 = Editar Deck
Edit_Deck_fd93 = Editar deck
# Button label to edit user profile
Edit_Profile_49e6 = Editar perfil
# Column title for profile editing
@@ -149,16 +161,20 @@ Enter_your_key_0fca = Ingresa tu clave
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Ingresa tu clave pública (npub), dirección de Nostr (por ejemplo, { $address }) o clave privada (nsec). Debes ingresar tu clave privada para poder publicar, responder, etc.
# Label for find user button
Find_User_bd12 = Buscar usuario
# Label for font size, Appearance settings section
Font_size_dd73 = Font size:
# Title for hashtags column
Hashtags_f8e0 = Hashtags
# Title for Home column
Home_8c19 = Inicio
# Label for deck icon selection
Icon_b0ab = Ícono
Icon_b0ab = Icono
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = Tamaño de caché de imágenes:
# Title for individual user column
Individual_b776 = Individual
# Error message for invalid zap amount
Invalid_amount_6630 = Importe no válido
Invalid_amount_6630 = Cantidad no válida
# Error message for invalid key input
Invalid_key_4726 = Clave no válida.
# Error message for invalid Nostr Wallet Connect URI
@@ -175,8 +191,12 @@ k_50K_c2dc = 50.000
k_5K_f7e6 = 5.000
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = Haz seguimiento de tus notas y respuestas
# Label for language, Appearance settings section
Language_e264 = Idioma:
# Title for last note per user column
Last_Note_per_User_17ad = Última nota por usuario
# Label for Theme Light, Appearance settings section
Light_7475 = Claro
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = Dirección de la red Lightning (lud16)
# Login page title
@@ -184,11 +204,11 @@ Login_9eef = Inicio de sesión
# Login button text
Login_now___let_s_do_this_5630 = Inicia sesión ahora, ¡manos a la obra!
# Text shown on blurred media from unfollowed users
Media_from_someone_you_don_t_follow_5611 = Medios de alguien que no sigues
Media_from_someone_you_don_t_follow_5611 = Contenido multimedia de alguien que no sigues
# Tooltip for moving a column
Moves_this_column_to_another_position_0d4b = Mueve esta columna a otra posición
# Title for the user's deck
My_Deck_4ac5 = Mi Deck
My_Deck_4ac5 = Mi deck
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = ¿Primera vez en Nostr?
# NIP-05 identity field label
@@ -215,16 +235,20 @@ Notifications_d673 = Notificaciones
Notifications_ef56 = Notificaciones
# Relative time for very recent events (less than 3 seconds)
now_2181 = ahora
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = On
# Button label to open email client
Open_Email_25e9 = Abrir correo electrónico
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Abre tu cliente de correo predeterminado para recibir ayuda del equipo de Damus
# Label for others settings section
Others_7267 = Otros
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = Pega tu URI NWC aquí...
Paste_your_NWC_URI_here_b471 = Pega tu NWC URI aquí...
# Error message for missing deck name
Please_create_a_name_for_the_deck_38e7 = Crea un nombre para el Deck.
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Crea un nombre para el Deck y selecciona un icono.
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Crea un nombre para el deck y selecciona un icono.
# Error message for missing deck icon
Please_select_an_icon_655b = Selecciona un icono.
# Button label to post a note
@@ -250,7 +274,7 @@ Reply_to_this_note_f5de = Responder a esta nota
# Error message when reply note cannot be found
Reply_to_unknown_note_4401 = Responder a nota desconocida
# Fallback template for replying to user
replying_to__user_15ab = responder a { $user }
replying_to__user_15ab = respondiendo a { $user }
# Template for replying to user in unknown thread
replying_to__user__in_someone_s_thread_e148 = respondiendo a { $user } en la conversación de alguien
# Template for replying to note in different user's thread
@@ -265,6 +289,10 @@ replying_to_a_note_e0bc = respondiendo a una nota
Repost_this_note_8e56 = Volver a publicar esta nota
# Label for reposted notes
Reposted_61c8 = Publicadas de nuevo
# Label for reset note body font size, Appearance settings section
Reset_4e60 = Reset
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Restablecer
# Heading for support section
Running_into_a_bug_1796 = ¿Has encontrado un error?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
@@ -287,6 +315,8 @@ See_notes_from_your_contacts_ac16 = Ver notas de tus contactos
See_the_whole_nostr_universe_7694 = Ver todo el universo de nostr
# Button label to send a zap
Send_1ea4 = Enviar
# Column title for app settings
Settings_7a4f = Configuración
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar la última nota para cada usuario de una lista
# Button label to sign out of account
@@ -295,6 +325,8 @@ Sign_out_337b = Cerrar sesión
Someone_else_s_Notes_7e5f = Notas de otra persona
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Notificaciones de otra persona
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Sort replies newest first:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Busca la última nota de cada usuario en tu lista de contactos
# Description for hashtags column
@@ -313,10 +345,14 @@ Stay_up_to_date_with_your_notifications_and_mentions_e73e = Mantente al día con
Step_1_8656 = Paso 1
# Step 2 label in support instructions
Step_2_d08d = Paso 2
# Label for storage settings section
Storage_ed65 = Almacenamiento
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = Suscribirse a las notas de otra persona
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = Suscribirse a las notas de alguien
# Support email address
Support_email_44d9 = Support email:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Cambiar a modo oscuro
# Hover text for light mode toggle button
@@ -325,6 +361,8 @@ Switch_to_light_mode_72ce = Cambiar a modo claro
Tap_to_Load_4b05 = Toca para cargar
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = La prueba del asistente de IA Dave de Nostr ha finalizado :(. ¡Gracias por probarlo! ¡Dave con zaps estará disponible muy pronto!
# Label for theme, Appearance settings section
Theme_4aac = Tema:
# Column title for note thread view
Thread_0f20 = Conversación
# Link text for thread references
@@ -339,6 +377,8 @@ Use_this_wallet_for_the_current_account_only_61dc = Usar este monedero solo para
username___at___domain___will_be_used_for_identification_a4fd = Se utilizará "{ $username }" en "{ $domain }" para la identificación
# Profile username field label
Username_daa7 = Nombre de usuario
# Label for view folder button, Storage settings section
View_folder_9742 = Ver carpeta
# Column title for wallet management
Wallet_5e50 = Monedero
# Hint for deck name input field
@@ -356,7 +396,9 @@ Your_Notifications_080d = Tus notificaciones
# Heading for zap (tip) action
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = Enviar zap a esta nota
Zap_this_note_42b2 = Enviar un zap a esta nota
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = Nivel de zoom:
# Pluralized strings

View File

@@ -45,6 +45,8 @@ Algo_2452 = Algo
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Des fils algorithmiques pour faciliter la découverte de notes
# Label for zap amount input field
Amount_70f0 = Montant
# Label for appearance settings section
Appearance_4c7f = Apparence
# Button to send message to Dave AI assistant
Ask_b7f4 = Demander
# Placeholder text for Dave AI input field
@@ -59,10 +61,18 @@ Broadcast_fe43 = Diffusion
Broadcast_Local_7e50 = Diffusion locale
# Button label to cancel an action
Cancel_ed3b = Annuler
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = Annuler
# Label for clear cache button, Storage settings section
Clear_cache_dccb = Vider le cache
# Hover text for editable zap amount
Click_to_edit_0414 = Cliquer pour modifier
# Column title for note composition
Compose_Note_c094 = Ecrire une note
# Label for configure relays, settings section
Configure_relays_d156 = Configurer les relais
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = Confirmer
# Button label to confirm an action
Confirm_f8a6 = Confirmer
# Status label for connected relay
@@ -111,6 +121,8 @@ Custom_a69e = Personnaliser
Customize_Zap_Amount_cfc4 = Personnaliser le montant du Zap
# Column title for support page
Damus_Support_27c0 = Assistance Damus
# Label for Theme Dark, Appearance settings section
Dark_85fe = Sombre
# Label for deck name input field
Deck_name_cd32 = Nom du deck
# Label for decks section in side panel
@@ -149,12 +161,16 @@ Enter_your_key_0fca = Entrez votre clé
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Entrez votre clé publique (npub), votre adresse nostr (par exemple { $address }), ou votre clé privée (nsec). Vous devez entrer votre clé privée pour pouvoir poster, répondre, etc.
# Label for find user button
Find_User_bd12 = Trouver un utilisateur
# Label for font size, Appearance settings section
Font_size_dd73 = Taille du texte :
# Title for hashtags column
Hashtags_f8e0 = Hashtags
# Title for Home column
Home_8c19 = Accueil
# Label for deck icon selection
Icon_b0ab = Icone
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = Taille du cache des images :
# Title for individual user column
Individual_b776 = Individuel
# Error message for invalid zap amount
@@ -175,8 +191,12 @@ k_50K_c2dc = 50K
k_5K_f7e6 = 5K
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = Gardez une trace de vos notes & réponses
# Label for language, Appearance settings section
Language_e264 = Langue :
# Title for last note per user column
Last_Note_per_User_17ad = Dernière note par utilisateur
# Label for Theme Light, Appearance settings section
Light_7475 = Clair
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = Adresse réseau Lightning (lud16)
# Login page title
@@ -215,10 +235,16 @@ Notifications_d673 = Notifications
Notifications_ef56 = Notifications
# Relative time for very recent events (less than 3 seconds)
now_2181 = maintenant
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = Activé
# Column title for finding users to follow
Onboarding_4a25 = Utilisateurs recommandés
# Button label to open email client
Open_Email_25e9 = Ouvrir Email
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Ouvrez votre service d'email par défaut pour obtenir de l'aide de l'équipe Damus
# Label for others settings section
Others_7267 = Autres
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = Collez ici votre NWC URI...
# Error message for missing deck name
@@ -265,6 +291,10 @@ replying_to_a_note_e0bc = répondre à une note
Repost_this_note_8e56 = Republier cette note
# Label for reposted notes
Reposted_61c8 = Republier
# Label for reset note body font size, Appearance settings section
Reset_4e60 = Réinitialiser
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Réinitialiser
# Heading for support section
Running_into_a_bug_1796 = Vous rencontrez un problème ?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
@@ -285,8 +315,12 @@ Searching_for___query_5d18 = Recherche par '{ $query }'
See_notes_from_your_contacts_ac16 = Afficher les notes de vos contacts
# Description for universe column
See_the_whole_nostr_universe_7694 = Voir l'ensemble de l'univers nostr
# Button to select all profiles in follow pack
Select_All_a319 = Tout sélectionner
# Button label to send a zap
Send_1ea4 = Envoyer
# Column title for app settings
Settings_7a4f = Paramètres
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = Afficher la dernière note de chaque utilisateur à partir d'une liste
# Button label to sign out of account
@@ -295,6 +329,8 @@ Sign_out_337b = Se déconnecter
Someone_else_s_Notes_7e5f = Notes de quelqu'un d'autre
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Notifications de quelqu'un d'autre
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Trier les réponses les plus récentes en premier :
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Source de la dernière note pour chaque utilisateur de votre liste de contacts
# Description for hashtags column
@@ -313,10 +349,14 @@ Stay_up_to_date_with_your_notifications_and_mentions_e73e = Restez informé pour
Step_1_8656 = Etape 1
# Step 2 label in support instructions
Step_2_d08d = Etape 2
# Label for storage settings section
Storage_ed65 = Stockage
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = S'abonner aux notes de quelqu'un d'autre
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = S'abonner aux notes de quelqu'un
# Support email address
Support_email_44d9 = Adresse email de l'assistance :
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Passer en mode sombre
# Hover text for light mode toggle button
@@ -325,6 +365,8 @@ Switch_to_light_mode_72ce = Passer en mode clair
Tap_to_Load_4b05 = Appuyer pour charger
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = La période d'essai de l'assistant IA Dave Nostr est terminée :(. Merci de l'avoir testé ! Un Dave compatible-Zap sera bientôt disponible !
# Label for theme, Appearance settings section
Theme_4aac = Thème :
# Column title for note thread view
Thread_0f20 = Fil
# Link text for thread references
@@ -339,6 +381,8 @@ Use_this_wallet_for_the_current_account_only_61dc = Utiliser ce portefeuille pou
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" à "{ $domain }" sera utilisé pour l'identification
# Profile username field label
Username_daa7 = Nom d'utilisateur
# Label for view folder button, Storage settings section
View_folder_9742 = Voir le dossier
# Column title for wallet management
Wallet_5e50 = Portefeuille
# Hint for deck name input field
@@ -357,6 +401,8 @@ Your_Notifications_080d = Vos notifications
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = Zap cette note
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = Niveau de zoom :
# Pluralized strings

View File

@@ -0,0 +1,410 @@
# Main translation file for Notedeck
# This file contains common UI strings used throughout the application
# Auto-generated by extract_i18n.py - DO NOT EDIT MANUALLY
# Regular strings
# Profile about/bio field label
About_00c0 = 概要
# Column title for account management
Accounts_f018 = アカウント
# Button label to add a relay
Add_269d = 追加
# Label for add column button
Add_47df = 追加
# Button label to add a different wallet
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = このアカウントでのみ使用される別のウォレットを追加
# Error message for missing wallet
Add_a_wallet_to_continue_d170 = 続行するにはウォレットを追加してください
# Button label to add a new account
Add_account_1cfc = アカウントを追加
# Column title for adding new account
Add_Account_d06c = アカウントの追加
# Column title for adding algorithm column
Add_Algo_Column_0d75 = アルゴカラムの追加
# Column title for adding new column
Add_Column_c764 = カラムの追加
# Column title for adding new deck
Add_Deck_fabf = デッキの追加
# Column title for adding external notifications column
Add_External_Notifications_Column_41ae = 外部通知カラムの追加
# Column title for adding hashtag column
Add_Hashtag_Column_ebf4 = ハッシュタグカラムの追加
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = 最後の投稿カラムの追加
# Column title for adding notifications column
Add_Notifications_Column_79f8 = 外部通知カラムの追加
# Button label to add a relay
Add_relay_269d = リレーを追加
# Button label to add a wallet
Add_Wallet_d1be = ウォレットを追加
# Title for algorithmic feeds column
Algo_2452 = アルゴ
# Description for algorithmic feeds column
Algorithmic_feeds_to_aid_in_note_discovery_d344 = 投稿の発見に役立つアルゴリズムフィードです
# Label for zap amount input field
Amount_70f0 = 金額
# Label for appearance settings section
Appearance_4c7f = 外観
# Button to send message to Dave AI assistant
Ask_b7f4 = 質問
# Placeholder text for Dave AI input field
Ask_dave_anything_33d1 = Dave に何でも質問してみましょう…
# Profile banner URL field label
Banner_52ef = バナー
# Beta version label
BETA_8e5d = ベータ
# Broadcast the note to all connected relays
Broadcast_fe43 = ブロードキャスト
# Broadcast the note only to local network relays
Broadcast_Local_7e50 = ローカルにブロードキャスト
# Button label to cancel an action
Cancel_ed3b = キャンセル
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = キャンセル
# Label for clear cache button, Storage settings section
Clear_cache_dccb = キャッシュを消去
# Hover text for editable zap amount
Click_to_edit_0414 = クリックして編集
# Column title for note composition
Compose_Note_c094 = メモの作成
# Label for configure relays, settings section
Configure_relays_d156 = リレーを設定
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = 決定
# Button label to confirm an action
Confirm_f8a6 = 決定
# Status label for connected relay
Connected_f8cc = 接続済
# Status label for connecting relay
Connecting_6b7e = 接続中…
# Title for contact list column
Contact_List_f85a = フォロイーリスト
# Column title for contact lists
Contacts_7533 = フォロー
# Column title for last notes per contact
Contacts__last_notes_3f84 = フォロー (最後の投稿)
# Button label to copy logs
Copy_a688 = コピー
# Button to copy media link to clipboard
Copy_Link_dc7c = リンクをコピー
# Copy the unique note identifier to clipboard
Copy_Note_ID_6b45 = 投稿 ID をコピー
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = 投稿の JSON をコピー
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = 公開鍵をコピー
# Copy the text content of the note to clipboard
Copy_Text_f81c = テキストをコピー
# Relative time in days
count_d_b9be = { $count }日
# Relative time in hours
count_h_3ecb = { $count }時間
# Relative time in minutes
count_m_b41e = { $count }分
# Relative time in months
count_mo_7aba = { $count }ヶ月
# Relative time in seconds
count_s_aa26 = { $count }秒
# Relative time in weeks
count_w_7468 = { $count }週間
# Relative time in years
count_y_9408 = { $count }年
# Button to create a new account
Create_Account_6994 = アカウントを作成
# Button label to create a new deck
Create_Deck_16b7 = デッキを作成
# Column title for custom timelines
Custom_a69e = カスタマイズ
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = Zap 金額をカスタマイズ
# Column title for support page
Damus_Support_27c0 = Damus サポート
# Label for Theme Dark, Appearance settings section
Dark_85fe = ダーク
# Label for deck name input field
Deck_name_cd32 = デッキ名
# Label for decks section in side panel
DECKS_1fad = デッキ
# Label for default zap amount input
Default_amount_per_zap_399d = Zap ごとのデフォルトの金額:
# Name of the default deck feed
Default_Deck_fcca = 既定のデッキ
# Button label to delete a deck
Delete_Deck_bb29 = デッキを削除
# Tooltip for deleting a column
Delete_this_column_8d5a = このカラムを削除します
# Button label to delete a wallet
Delete_Wallet_d1d4 = ウォレットを削除
# Profile display name field label
Display_name_f9d9 = 表示名
# Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" が識別に使用されます
# Column title for editing deck
Edit_Deck_4018 = デッキの編集
# Button label to edit a deck
Edit_Deck_fd93 = デッキを編集
# Button label to edit user profile
Edit_Profile_49e6 = プロファイルを編集
# Column title for profile editing
Edit_Profile_8ad4 = プロファイルの編集
# Placeholder for hashtag input field
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = 必要なハッシュタグをここに入力してください (複数スペースで区切る場合)
# Placeholder for relay input field
Enter_the_relay_here_1c8b = ここにリレーを入力してください
# Hint text to prompt entering the user's public key.
Enter_the_user_s_key__npub__hex__nip05__here_650c = ユーザーの鍵 (npub, hex, nip05) を入力してください...
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
Enter_your_key_0fca = 鍵を入力してください
# Instructions for entering Nostr credentials
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = 公開鍵 (npub)、nostr アドレス (例: { $address })、秘密鍵 (nsec) を入力してください。 投稿、返信などを行うには秘密鍵を入力する必要があります。
# Label for find user button
Find_User_bd12 = ユーザーを探す
# Label for font size, Appearance settings section
Font_size_dd73 = フォントサイズ:
# Title for hashtags column
Hashtags_f8e0 = ハッシュタグ
# Title for Home column
Home_8c19 = ホーム
# Label for deck icon selection
Icon_b0ab = アイコン
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = 画像キャッシュのサイズ:
# Title for individual user column
Individual_b776 = 個人用
# Error message for invalid zap amount
Invalid_amount_6630 = 無効な金額です
# Error message for invalid key input
Invalid_key_4726 = 無効な鍵です。
# Error message for invalid Nostr Wallet Connect URI
Invalid_NWC_URI_031b = 無効な NWC URI です
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
k_100K_686c = 100K
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
k_10K_f7e6 = 10K
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
k_20K_4977 = 20K
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
k_50K_c2dc = 50K
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
k_5K_f7e6 = 5K
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = 投稿と返信を記録します
# Label for language, Appearance settings section
Language_e264 = 言語:
# Title for last note per user column
Last_Note_per_User_17ad = ユーザーごとの最後の投稿
# Label for Theme Light, Appearance settings section
Light_7475 = ライト
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = ライトニングネットワークアドレス (lud16)
# Login page title
Login_9eef = ログイン
# Login button text
Login_now___let_s_do_this_5630 = 今すぐログイン — レッツゴー!
# Text shown on blurred media from unfollowed users
Media_from_someone_you_don_t_follow_5611 = フォローしていない人のメディアです
# Tooltip for moving a column
Moves_this_column_to_another_position_0d4b = このカラムを別の位置に移動します
# Title for the user's deck
My_Deck_4ac5 = あなたのデッキ
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = Nostr は初めてですか?
# NIP-05 identity field label
Nostr_address__NIP-05_identity_74a2 = Nostr アドレス (NIP-05)
# Default username when profile is not available
nostrich_df29 = ノス民
# Status label for disconnected relay
Not_Connected_6292 = 未接続
# Link text for note references
note_cad6 = 投稿
# Beta product warning message
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck はベータ製品です。問題が発生した場合はサポートに問い合わせてください。
# Filter label for notes only view
Notes_03fb = 投稿
# Label for notes-only filter
Notes_60d2 = 投稿
# Filter label for notes and replies view
Notes___Replies_1ec2 = 投稿 & 返信
# Label for notes and replies filter
Notes___Replies_6e3b = 投稿 & 返信
# Column title for notifications
Notifications_d673 = 通知
# Title for notifications column
Notifications_ef56 = 通知
# Relative time for very recent events (less than 3 seconds)
now_2181 = たった今
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = 有効
# Button label to open email client
Open_Email_25e9 = メールを開く
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = デフォルトのメールクライアントを開いて、Damus チームのヘルプを表示しましょう。
# Label for others settings section
Others_7267 = その他
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = ここに NWC の URI を貼り付けてください...
# Error message for missing deck name
Please_create_a_name_for_the_deck_38e7 = デッキの名前を作成してください。
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = デッキの名前を作成してアイコンを選択してください。
# Error message for missing deck icon
Please_select_an_icon_655b = アイコンを選択してください。
# Button label to post a note
Post_now_8a49 = すぐに投稿
# Instruction for copying logs
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = 下のボタンを押して、最新のログをシステムのクリップボードにコピーします。その後、メールに貼り付けてください。
# Profile picture URL field label
Profile_picture_81ff = プロフィール写真
# Column title for quote composition
Quote_475c = 引用
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = 不明な投稿の引用です
# Label for read-only profile mode
Read_only_82ff = 読み取り専用
# Column title for relay management
Relays_9d89 = リレー
# Label for relay list section
Relays_ad5e = リレー
# Column title for reply composition
Reply_3bf1 = 返信
# Hover text for reply button
Reply_to_this_note_f5de = この投稿に返信
# Error message when reply note cannot be found
Reply_to_unknown_note_4401 = 不明な投稿に返信しています
# Fallback template for replying to user
replying_to__user_15ab = { $user } に返信
# Template for replying to user in unknown thread
replying_to__user__in_someone_s_thread_e148 = 誰かのスレッドで { $user } に返信
# Template for replying to note in different user's thread
replying_to__user__s__note__in__thread_user__s__thread_daa8 = { $user }の { $note } の { $thread_user }の { $thread } に返信
# Template for replying to user's note
replying_to__user__s__note_ccba = { $user }の { $note } に返信
# Template for replying to root thread
replying_to__user__s__thread_444d = { $user }の { $thread } に返信
# Fallback text when reply note is not found
replying_to_a_note_e0bc = 投稿に返信
# Hover text for repost button
Repost_this_note_8e56 = このメモを再投稿
# Label for reposted notes
Reposted_61c8 = 再投稿
# Label for reset note body font size, Appearance settings section
Reset_4e60 = リセット
# Label for reset zoom level, Appearance settings section
Reset_62d4 = リセット
# Heading for support section
Running_into_a_bug_1796 = バグに遭遇しましたか?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
SATS_45d7 = SATS
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
sats_e5ec = sats
# Button to save default zap amount
Save_6f7c = 保存
# Button label to save profile changes
Save_changes_00db = 変更を保存
# Column title for search page
Search_c573 = 検索
# Placeholder for search notes input field
Search_notes_42a6 = 投稿を検索しましょう...
# Search in progress message
Searching_for___query_5d18 = 「{ $query }」を検索中
# Description for Home column
See_notes_from_your_contacts_ac16 = フォローしている人の投稿を表示
# Description for universe column
See_the_whole_nostr_universe_7694 = 全ユニバースを表示します
# Button label to send a zap
Send_1ea4 = 送信
# Column title for app settings
Settings_7a4f = 設定
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = 一覧から各ユーザーの最後の投稿を表示する
# Button label to sign out of account
Sign_out_337b = サインアウト
# Title for someone else's notes column
Someone_else_s_Notes_7e5f = 他の人の投稿
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = 他の人の通知
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = 最新の返信を最初に並べ替え:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = フォローリストにある各ユーザーの最後の投稿を取得します
# Description for hashtags column
Stay_up_to_date_with_a_certain_hashtag_88e3 = 特定のハッシュタグで最新の情報を受け取ります
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = 通知とメンションの最新の情報を受け取ります
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = 他のユーザーの投稿と返信の最新の情報を受け取ります
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = 他のユーザーの投稿と返信の最新の情報を受け取ります
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = 投稿と返信の最新の情報を受け取ります
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = あなたの通知とメンションの最新の情報を受け取ります
# Step 1 label in support instructions
Step_1_8656 = ステップ 1
# Step 2 label in support instructions
Step_2_d08d = ステップ 2
# Label for storage settings section
Storage_ed65 = ストレージ
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = 他のユーザー投稿の購読
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = 投稿の購読
# Support email address
Support_email_44d9 = サポートメール:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = ダークモードに切り替える
# Hover text for light mode toggle button
Switch_to_light_mode_72ce = ライトモードに切り替える
# Button text to load blurred media
Tap_to_Load_4b05 = タップして読み込む
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = Dave Nostr AI アシスタントトライアルが終了しました: (テストしていただきありがとうございます! Zap 対応デイブは近日公開予定です!
# Label for theme, Appearance settings section
Theme_4aac = テーマ:
# Column title for note thread view
Thread_0f20 = スレッド
# Link text for thread references
thread_ad1f = スレッド
# Title for universe column
Universe_e01e = ユニバース
# Column title for universe feed
Universe_ffaa = ユニバース
# Checkbox label for using wallet only for current account
Use_this_wallet_for_the_current_account_only_61dc = このウォレットを現在のアカウントにのみ使用する
# Username and domain identification message
username___at___domain___will_be_used_for_identification_a4fd = "{ $domain }" の "{ $username }" が識別に使用されます
# Profile username field label
Username_daa7 = ユーザー名
# Label for view folder button, Storage settings section
View_folder_9742 = フォルダを表示
# Column title for wallet management
Wallet_5e50 = ウォレット
# Hint for deck name input field
We_recommend_short_names_083e = 短い名前を推奨しています
# Profile website field label
Website_7980 = Web サイト
# Placeholder for note input field
Write_a_banger_note_here_bad2 = アツい一言をどうぞ...
# Placeholder text for key input field
Your_key_here_81bd = ここに鍵を入力...
# Title for your notes column
Your_Notes_f6db = 投稿
# Title for your notifications column
Your_Notifications_080d = 通知
# Heading for zap (tip) action
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = この投稿に Zap
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = 拡大率:
# Pluralized strings
# Search results count
Got__count__results_for___query_85fb =
{ $count ->
[one] { $query } の結果を '{ $count }' 件取得しました
*[other] ' { $query } の結果を '{ $count }' 件取得しました
}

View File

@@ -0,0 +1,414 @@
# Main translation file for Notedeck
# This file contains common UI strings used throughout the application
# Auto-generated by extract_i18n.py - DO NOT EDIT MANUALLY
# Regular strings
# Profile about/bio field label
About_00c0 = Sobre
# Column title for account management
Accounts_f018 = Contas
# Button label to add a relay
Add_269d = Transmitir
# Label for add column button
Add_47df = Adicionar coluna
# Button label to add a different wallet
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = Adicionar outra carteira a ser usada apenas nesta conta
# Error message for missing wallet
Add_a_wallet_to_continue_d170 = Obrigatório adicionar carteira
# Button label to add a new account
Add_account_1cfc = Adicionar conta nova aqui
# Column title for adding new account
Add_Account_d06c = Adicionar nova conta
# Column title for adding algorithm column
Add_Algo_Column_0d75 = Adicionar coluna de algoritmo
# Column title for adding new column
Add_Column_c764 = Adicionar coluna
# Column title for adding new deck
Add_Deck_fabf = Adicionar aba
# Column title for adding external notifications column
Add_External_Notifications_Column_41ae = Adicionar coluna de notificações externas
# Column title for adding hashtag column
Add_Hashtag_Column_ebf4 = Adicionar coluna de #
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = Adicionar última coluna de notas
# Column title for adding notifications column
Add_Notifications_Column_79f8 = Adicionar coluna de notificações
# Button label to add a relay
Add_relay_269d = Adicionar transmissão
# Button label to add a wallet
Add_Wallet_d1be = Adicionar carteira
# Title for algorithmic feeds column
Algo_2452 = Algoritmos
# Description for algorithmic feeds column
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Algoritmos para pesquisar notas
# Label for zap amount input field
Amount_70f0 = Valor
# Label for appearance settings section
Appearance_4c7f = Aparência
# Button to send message to Dave AI assistant
Ask_b7f4 = Perguntar
# Placeholder text for Dave AI input field
Ask_dave_anything_33d1 = Perguntar ao Dave
# Profile banner URL field label
Banner_52ef = Destaque
# Beta version label
BETA_8e5d = Beta
# Broadcast the note to all connected relays
Broadcast_fe43 = Encaminhar
# Broadcast the note only to local network relays
Broadcast_Local_7e50 = Encaminhar especificamente
# Button label to cancel an action
Cancel_ed3b = Cancelar
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = Cancelar
# Label for clear cache button, Storage settings section
Clear_cache_dccb = Limpar cache
# Hover text for editable zap amount
Click_to_edit_0414 = Editar valor
# Column title for note composition
Compose_Note_c094 = Compor nota
# Label for configure relays, settings section
Configure_relays_d156 = Configurar canais
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = Confirmar
# Button label to confirm an action
Confirm_f8a6 = Confirmar
# Status label for connected relay
Connected_f8cc = Conectar
# Status label for connecting relay
Connecting_6b7e = Conectando...
# Title for contact list column
Contact_List_f85a = Lista de contatos
# Column title for contact lists
Contacts_7533 = Contatos
# Column title for last notes per contact
Contacts__last_notes_3f84 = Contatos (últimas notas)
# Button label to copy logs
Copy_a688 = Copiar
# Button to copy media link to clipboard
Copy_Link_dc7c = Copiar link
# Copy the unique note identifier to clipboard
Copy_Note_ID_6b45 = Copiar ID da nota
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = Copiar nota "JSON"
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = Copiar chave pública
# Copy the text content of the note to clipboard
Copy_Text_f81c = Copiar texto
# Relative time in days
count_d_b9be = { $count }D
# Relative time in hours
count_h_3ecb = { $count }H
# Relative time in minutes
count_m_b41e = { $count }M
# Relative time in months
count_mo_7aba = { $count }Mes
# Relative time in seconds
count_s_aa26 = { $count }S
# Relative time in weeks
count_w_7468 = { $count }Sem
# Relative time in years
count_y_9408 = { $count }A
# Button to create a new account
Create_Account_6994 = Criar conta
# Button label to create a new deck
Create_Deck_16b7 = Criar aba
# Column title for custom timelines
Custom_a69e = Personalizar
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = Personalizar valor do ZAP
# Column title for support page
Damus_Support_27c0 = Ajuda
# Label for Theme Dark, Appearance settings section
Dark_85fe = Modo escuro
# Label for deck name input field
Deck_name_cd32 = Nome da aba
# Label for decks section in side panel
DECKS_1fad = ABAS
# Label for default zap amount input
Default_amount_per_zap_399d = Valor padrão de ZAP
# Name of the default deck feed
Default_Deck_fcca = Nome padrão de abas
# Button label to delete a deck
Delete_Deck_bb29 = Deletar aba
# Tooltip for deleting a column
Delete_this_column_8d5a = Deletar esta coluna
# Button label to delete a wallet
Delete_Wallet_d1d4 = Deletar carteira
# Profile display name field label
Display_name_f9d9 = Nome de exibição
# Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" será utilizado para identificação
# Column title for editing deck
Edit_Deck_4018 = Editar aba
# Button label to edit a deck
Edit_Deck_fd93 = Editar
# Button label to edit user profile
Edit_Profile_49e6 = Editar perfil
# Column title for profile editing
Edit_Profile_8ad4 = Editar perfil
# Placeholder for hashtag input field
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = Digite as # desejadas aqui (para múltiplos espaços separados)
# Placeholder for relay input field
Enter_the_relay_here_1c8b = Insira a retransmissão aqui
# Hint text to prompt entering the user's public key.
Enter_the_user_s_key__npub__hex__nip05__here_650c = Digite a chave do usuário (npub, hex, nip05) aqui...
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
Enter_your_key_0fca = Sua chave aqui
# Instructions for entering Nostr credentials
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Insira sua chave pública (npub), endereço do Nostr (e.g. { $address }), ou chave privada (nsec). Você deve digitar sua chave privada para conseguir publicar, responder, etc.
# Label for find user button
Find_User_bd12 = Pesquisar usuário
# Label for font size, Appearance settings section
Font_size_dd73 = Tamanho da letra
# Title for hashtags column
Hashtags_f8e0 = #
# Title for Home column
Home_8c19 = Início
# Label for deck icon selection
Icon_b0ab = Ícone
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = Tamanho do cache de imagem:
# Title for individual user column
Individual_b776 = Individual
# Error message for invalid zap amount
Invalid_amount_6630 = Quantia inválida
# Error message for invalid key input
Invalid_key_4726 = Chave inválida
# Error message for invalid Nostr Wallet Connect URI
Invalid_NWC_URI_031b = NWC URI Inválido
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
k_100K_686c = 100 mil
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
k_10K_f7e6 = 10 mil
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
k_20K_4977 = 20 mil
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
k_50K_c2dc = 50 mil
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
k_5K_f7e6 = 5 mil
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = Acompanhe suas notas e respostas
# Label for language, Appearance settings section
Language_e264 = Idioma
# Title for last note per user column
Last_Note_per_User_17ad = Última Nota por Usuário
# Label for Theme Light, Appearance settings section
Light_7475 = Modo claro
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = Endereço de rede de eletrização (lud16)
# Login page title
Login_9eef = Entrar
# Login button text
Login_now___let_s_do_this_5630 = Entrar agora! Vamos nessa!
# Text shown on blurred media from unfollowed users
Media_from_someone_you_don_t_follow_5611 = Conteúdo de pessoas que você não segue
# Tooltip for moving a column
Moves_this_column_to_another_position_0d4b = Mover esta coluna
# Title for the user's deck
My_Deck_4ac5 = Minha aba
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = Novo no Nostr?
# NIP-05 identity field label
Nostr_address__NIP-05_identity_74a2 = Endereço Nostr (Identidade NIP-05)
# Default username when profile is not available
nostrich_df29 = Nostrich
# Status label for disconnected relay
Not_Connected_6292 = Desconectado
# Link text for note references
note_cad6 = Nota
# Beta product warning message
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck é um produto beta. Espere erros e entre em contato conosco quando tiver problemas.
# Filter label for notes only view
Notes_03fb = Notas
# Label for notes-only filter
Notes_60d2 = Notas
# Filter label for notes and replies view
Notes___Replies_1ec2 = Notas e respostas
# Label for notes and replies filter
Notes___Replies_6e3b = Notas e respostas
# Column title for notifications
Notifications_d673 = Notificações
# Title for notifications column
Notifications_ef56 = Notificações
# Relative time for very recent events (less than 3 seconds)
now_2181 = Agora
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = Ligar
# Column title for finding users to follow
Onboarding_4a25 = Interação
# Button label to open email client
Open_Email_25e9 = Abrir E-mail
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Abra o seu cliente de e-mail padrão para obter ajuda do time Damus
# Label for others settings section
Others_7267 = Outros
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = Cole seu URI NWC aqui...
# Error message for missing deck name
Please_create_a_name_for_the_deck_38e7 = Por favor, crie um nome para a aba.
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Por favor, crie um nome para a aba e selecione um ícone.
# Error message for missing deck icon
Please_select_an_icon_655b = Favor selecionar um ícone.
# Button label to post a note
Post_now_8a49 = Postar
# Instruction for copying logs
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = Clique abaixo para copiar seus registros mais recentes para a área de transferência do seu sistema. Em seguida, cole-os no seu E-mail.
# Profile picture URL field label
Profile_picture_81ff = Foto de perfil
# Column title for quote composition
Quote_475c = Citação
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = Citação de nota desconhecida
# Label for read-only profile mode
Read_only_82ff = Modo leitura
# Column title for relay management
Relays_9d89 = Canais
# Label for relay list section
Relays_ad5e = Canais
# Column title for reply composition
Reply_3bf1 = Responder
# Hover text for reply button
Reply_to_this_note_f5de = Responder esta nota
# Error message when reply note cannot be found
Reply_to_unknown_note_4401 = Responder nota desconhecida
# Fallback template for replying to user
replying_to__user_15ab = Respondendo { $user }
# Template for replying to user in unknown thread
replying_to__user__in_someone_s_thread_e148 = Respondendo { $user } no tópico de alguém
# Template for replying to note in different user's thread
replying_to__user__s__note__in__thread_user__s__thread_daa8 = Resposta { $user }de { $note } em { $thread_user }' { $thread }
# Template for replying to user's note
replying_to__user__s__note_ccba = Respondendo { $user }de { $note }
# Template for replying to root thread
replying_to__user__s__thread_444d = Respondendo { $user }de { $thread }
# Fallback text when reply note is not found
replying_to_a_note_e0bc = Respondendo nota
# Hover text for repost button
Repost_this_note_8e56 = Republicar nota
# Label for reposted notes
Reposted_61c8 = Publicada
# Label for reset note body font size, Appearance settings section
Reset_4e60 = Redefinir
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Resetar
# Heading for support section
Running_into_a_bug_1796 = Precisa de ajuda?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
SATS_45d7 = SATS
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
sats_e5ec = sats
# Button to save default zap amount
Save_6f7c = Salvar
# Button label to save profile changes
Save_changes_00db = Salvo
# Column title for search page
Search_c573 = Pesquisar
# Placeholder for search notes input field
Search_notes_42a6 = Pesquisar notas...
# Search in progress message
Searching_for___query_5d18 = Pesquisando por '{ $query }'
# Description for Home column
See_notes_from_your_contacts_ac16 = Veja notas dos seus contatos
# Description for universe column
See_the_whole_nostr_universe_7694 = Veja todo o universo Nostr
# Button to select all profiles in follow pack
Select_All_a319 = Selecionar todos
# Button label to send a zap
Send_1ea4 = Enviar
# Column title for app settings
Settings_7a4f = Configurações
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar a última nota para cada usuário de uma lista
# Button label to sign out of account
Sign_out_337b = Sair
# Title for someone else's notes column
Someone_else_s_Notes_7e5f = Notas de outra pessoa
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Notificações de outra pessoa
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Ordenar respostas mais recentes primeiro:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Fonte da última nota para cada usuário em sua lista de contatos
# Description for hashtags column
Stay_up_to_date_with_a_certain_hashtag_88e3 = Mantenha-se atualizado com uma certa hashtag
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = Ficar atualizado com notificações e menções
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = Mantenha-se atualizado com as notas e respostas de alguém
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = Mantenha-se atualizado com as notificações e menções de alguém
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = Mantenha-se atualizado com as notas e respostas de alguém
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = Mantenha-se atualizado com suas notificações e menções
# Step 1 label in support instructions
Step_1_8656 = Passo 1
# Step 2 label in support instructions
Step_2_d08d = Passo 2
# Label for storage settings section
Storage_ed65 = Armazenamento
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = Inscrever-se em notas de outra pessoa
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = Inscrever-se nas notas de alguém
# Support email address
Support_email_44d9 = E-mail de suporte
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Mudar para modo escuro
# Hover text for light mode toggle button
Switch_to_light_mode_72ce = Mudar para modo claro
# Button text to load blurred media
Tap_to_Load_4b05 = Toque para carregar
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = O teste do assistente de IA Dave Nostr terminou :(. Obrigado por testar! Em breve teremos Dave habilitado para Zap
# Label for theme, Appearance settings section
Theme_4aac = Tema:
# Column title for note thread view
Thread_0f20 = Fio
# Link text for thread references
thread_ad1f = Fio
# Title for universe column
Universe_e01e = Universo
# Column title for universe feed
Universe_ffaa = Universo
# Checkbox label for using wallet only for current account
Use_this_wallet_for_the_current_account_only_61dc = Use esta carteira apenas para a conta atual
# Username and domain identification message
username___at___domain___will_be_used_for_identification_a4fd = d = "{ $username }" em "{ $domain }" será usado para identificação
# Profile username field label
Username_daa7 = Usuário
# Label for view folder button, Storage settings section
View_folder_9742 = Visualizar pasta
# Column title for wallet management
Wallet_5e50 = Carteira
# Hint for deck name input field
We_recommend_short_names_083e = Recomendamos nomes pequenos
# Profile website field label
Website_7980 = Site
# Placeholder for note input field
Write_a_banger_note_here_bad2 = Escreva uma nota criativa aqui.
# Placeholder text for key input field
Your_key_here_81bd = Sua chave aqui...
# Title for your notes column
Your_Notes_f6db = Suas notas
# Title for your notifications column
Your_Notifications_080d = Suas notificações
# Heading for zap (tip) action
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = Zap esta nota
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = Nível de zoom:
# Pluralized strings
# Search results count
Got__count__results_for___query_85fb =
{ $count ->
[one] Obteve um resultado { $count } para '{ $query }'
*[other] Obteve { $count } resultados para '{ $query }'
}

View File

@@ -0,0 +1,414 @@
# Main translation file for Notedeck
# This file contains common UI strings used throughout the application
# Auto-generated by extract_i18n.py - DO NOT EDIT MANUALLY
# Regular strings
# Profile about/bio field label
About_00c0 = Sobre
# Column title for account management
Accounts_f018 = Contas
# Button label to add a relay
Add_269d = Adicionar
# Label for add column button
Add_47df = Adicionar
# Button label to add a different wallet
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = Adicionar uma carteira diferente que será usada apenas para esta conta
# Error message for missing wallet
Add_a_wallet_to_continue_d170 = Adicionar uma carteira para continuar
# Button label to add a new account
Add_account_1cfc = Adicionar conta
# Column title for adding new account
Add_Account_d06c = Adicionar conta
# Column title for adding algorithm column
Add_Algo_Column_0d75 = Adicionar coluna de algoritmo
# Column title for adding new column
Add_Column_c764 = Adicionar coluna
# Column title for adding new deck
Add_Deck_fabf = Adicionar aba
# Column title for adding external notifications column
Add_External_Notifications_Column_41ae = Adicionar coluna de notificações externas
# Column title for adding hashtag column
Add_Hashtag_Column_ebf4 = Adicionar coluna de marcadores
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = Adicionar coluna de últimas notas
# Column title for adding notifications column
Add_Notifications_Column_79f8 = Adicionar coluna de notificações
# Button label to add a relay
Add_relay_269d = Adicionar relay
# Button label to add a wallet
Add_Wallet_d1be = Adicionar carteira
# Title for algorithmic feeds column
Algo_2452 = Algoritmo
# Description for algorithmic feeds column
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Fontes de algoritmo para ajudar na descoberta de notas
# Label for zap amount input field
Amount_70f0 = Quantia
# Label for appearance settings section
Appearance_4c7f = Aparência
# Button to send message to Dave AI assistant
Ask_b7f4 = Perguntar
# Placeholder text for Dave AI input field
Ask_dave_anything_33d1 = Perguntar qualquer coisa...
# Profile banner URL field label
Banner_52ef = Faixa
# Beta version label
BETA_8e5d = BETA
# Broadcast the note to all connected relays
Broadcast_fe43 = Transmissão
# Broadcast the note only to local network relays
Broadcast_Local_7e50 = Transmissão local
# Button label to cancel an action
Cancel_ed3b = Cancelar
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = Cancelar
# Label for clear cache button, Storage settings section
Clear_cache_dccb = Limpar cache
# Hover text for editable zap amount
Click_to_edit_0414 = Clica para editar
# Column title for note composition
Compose_Note_c094 = Compor nota
# Label for configure relays, settings section
Configure_relays_d156 = Configurar relays
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = Confirmar
# Button label to confirm an action
Confirm_f8a6 = Confirmar
# Status label for connected relay
Connected_f8cc = Conectado
# Status label for connecting relay
Connecting_6b7e = A conectar...
# Title for contact list column
Contact_List_f85a = Lista de contactos
# Column title for contact lists
Contacts_7533 = Contactos
# Column title for last notes per contact
Contacts__last_notes_3f84 = Contactos (últimas notas)
# Button label to copy logs
Copy_a688 = Copiar
# Button to copy media link to clipboard
Copy_Link_dc7c = Copiar link
# Copy the unique note identifier to clipboard
Copy_Note_ID_6b45 = Copiar ID da nota
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = Copiar JSON da nota
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = Copiar chave pública
# Copy the text content of the note to clipboard
Copy_Text_f81c = Copiar texto
# Relative time in days
count_d_b9be = { $count }d
# Relative time in hours
count_h_3ecb = { $count }h
# Relative time in minutes
count_m_b41e = { $count }m
# Relative time in months
count_mo_7aba = { $count } mês(es)
# Relative time in seconds
count_s_aa26 = { $count } s
# Relative time in weeks
count_w_7468 = { $count } semana(s)
# Relative time in years
count_y_9408 = { $count } ano(s)
# Button to create a new account
Create_Account_6994 = Criar conta
# Button label to create a new deck
Create_Deck_16b7 = Criar aba
# Column title for custom timelines
Custom_a69e = Personalizadas
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = Personalizar valor do zap
# Column title for support page
Damus_Support_27c0 = Suporte Damus
# Label for Theme Dark, Appearance settings section
Dark_85fe = Modo escuro
# Label for deck name input field
Deck_name_cd32 = Nome da aba
# Label for decks section in side panel
DECKS_1fad = ABAS
# Label for default zap amount input
Default_amount_per_zap_399d = Valor padrão por zap:
# Name of the default deck feed
Default_Deck_fcca = Aba padrão
# Button label to delete a deck
Delete_Deck_bb29 = Excluir aba
# Tooltip for deleting a column
Delete_this_column_8d5a = Apagar esta coluna
# Button label to delete a wallet
Delete_Wallet_d1d4 = Eliminar carteira
# Profile display name field label
Display_name_f9d9 = Nome a mostrar
# Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" será usado para identificação
# Column title for editing deck
Edit_Deck_4018 = Editar aba
# Button label to edit a deck
Edit_Deck_fd93 = Editar aba
# Button label to edit user profile
Edit_Profile_49e6 = Editar perfil
# Column title for profile editing
Edit_Profile_8ad4 = Editar perfil
# Placeholder for hashtag input field
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = Insere aqui os marcadores desejados (para múltiplos com espaços separados)
# Placeholder for relay input field
Enter_the_relay_here_1c8b = Insere aqui o relay
# Hint text to prompt entering the user's public key.
Enter_the_user_s_key__npub__hex__nip05__here_650c = Insere aqui a chave de utilizador (npub, hex, nip05)
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
Enter_your_key_0fca = Insere a tua chave
# Instructions for entering Nostr credentials
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Insere a tua chave públca (npub), endereço nostr (por exemplo { $address }), ou chave privada (nsec). Tens de inserir a tua chave pública para publicar, responder, etc.
# Label for find user button
Find_User_bd12 = Encontrar utilizador
# Label for font size, Appearance settings section
Font_size_dd73 = Tamanho da letra:
# Title for hashtags column
Hashtags_f8e0 = Marcadores
# Title for Home column
Home_8c19 = Início
# Label for deck icon selection
Icon_b0ab = Ícone
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = Tamanho do cache da imagem:
# Title for individual user column
Individual_b776 = Individual
# Error message for invalid zap amount
Invalid_amount_6630 = Quantia inválida
# Error message for invalid key input
Invalid_key_4726 = Chave inválida.
# Error message for invalid Nostr Wallet Connect URI
Invalid_NWC_URI_031b = NWC URI inválido.
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
k_100K_686c = 100K
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
k_10K_f7e6 = 10K
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
k_20K_4977 = 20K
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
k_50K_c2dc = 50K
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
k_5K_f7e6 = 5K
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = Acompanha as tuas notas e respostas
# Label for language, Appearance settings section
Language_e264 = Idioma:
# Title for last note per user column
Last_Note_per_User_17ad = Última nota por utilizador
# Label for Theme Light, Appearance settings section
Light_7475 = Modo claro
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = Endereço da rede Lightning (lud16)
# Login page title
Login_9eef = Iniciar sessão
# Login button text
Login_now___let_s_do_this_5630 = Entra agora — vamos fazer isto!
# Text shown on blurred media from unfollowed users
Media_from_someone_you_don_t_follow_5611 = Conteúdo de alguém que não segues
# Tooltip for moving a column
Moves_this_column_to_another_position_0d4b = Mover esta coluna para outra posição
# Title for the user's deck
My_Deck_4ac5 = Minha aba
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = Nov@ no Nostr?
# NIP-05 identity field label
Nostr_address__NIP-05_identity_74a2 = Endereço Nostr (identificação NIP-05)
# Default username when profile is not available
nostrich_df29 = nostrich
# Status label for disconnected relay
Not_Connected_6292 = Não conectado
# Link text for note references
note_cad6 = nota
# Beta product warning message
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck é um produto beta. Espere bugs e contacte-nos quando tiver problemas.
# Filter label for notes only view
Notes_03fb = Notas
# Label for notes-only filter
Notes_60d2 = Notas
# Filter label for notes and replies view
Notes___Replies_1ec2 = Notas e respostas
# Label for notes and replies filter
Notes___Replies_6e3b = Notas e respostas
# Column title for notifications
Notifications_d673 = Notificações
# Title for notifications column
Notifications_ef56 = Notificações
# Relative time for very recent events (less than 3 seconds)
now_2181 = agora
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = Ativado
# Column title for finding users to follow
Onboarding_4a25 = Introdução
# Button label to open email client
Open_Email_25e9 = Abrir e-mail
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Abre o teu cliente de e-mail padrão para obteres ajuda da equipa Damus
# Label for others settings section
Others_7267 = Outros
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = Cola o teu NWC URI aqui...
# Error message for missing deck name
Please_create_a_name_for_the_deck_38e7 = Cria um nome para a aba.
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Cria um nome para a aba e seleciona um ícone.
# Error message for missing deck icon
Please_select_an_icon_655b = Seleciona um ícone.
# Button label to post a note
Post_now_8a49 = Publicar agora
# Instruction for copying logs
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = Prime o botão abaixo para copiar os teus registos mais recentes para a área de transferência do teu sistema. Depois cola-os no teu e-mail.
# Profile picture URL field label
Profile_picture_81ff = Foto de perfil
# Column title for quote composition
Quote_475c = Citação
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = Citação de nota desconhecida
# Label for read-only profile mode
Read_only_82ff = Somente leitura
# Column title for relay management
Relays_9d89 = Relays
# Label for relay list section
Relays_ad5e = Relays
# Column title for reply composition
Reply_3bf1 = Responder
# Hover text for reply button
Reply_to_this_note_f5de = Responder a esta nota
# Error message when reply note cannot be found
Reply_to_unknown_note_4401 = Responder a nota desconhecida
# Fallback template for replying to user
replying_to__user_15ab = responder a { $user }
# Template for replying to user in unknown thread
replying_to__user__in_someone_s_thread_e148 = responder a { $user } no tópico de alguém
# Template for replying to note in different user's thread
replying_to__user__s__note__in__thread_user__s__thread_daa8 = respondendo à { $note } de { $user } no { $thread } de { $thread_user }
# Template for replying to user's note
replying_to__user__s__note_ccba = respondendo à { $note } de { $user }
# Template for replying to root thread
replying_to__user__s__thread_444d = respondendo ao { $thread } de { $user }
# Fallback text when reply note is not found
replying_to_a_note_e0bc = respondendo a uma nota
# Hover text for repost button
Repost_this_note_8e56 = Republicar esta nota
# Label for reposted notes
Reposted_61c8 = Republicado
# Label for reset note body font size, Appearance settings section
Reset_4e60 = Redefinir
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Redefinir
# Heading for support section
Running_into_a_bug_1796 = Encontraste um bug?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
SATS_45d7 = SATS
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
sats_e5ec = sats
# Button to save default zap amount
Save_6f7c = Guardar
# Button label to save profile changes
Save_changes_00db = Guardar alterações
# Column title for search page
Search_c573 = Procurar
# Placeholder for search notes input field
Search_notes_42a6 = Procurar notas...
# Search in progress message
Searching_for___query_5d18 = Procurando por '{ $query }'
# Description for Home column
See_notes_from_your_contacts_ac16 = Ver notas dos meus contactos
# Description for universe column
See_the_whole_nostr_universe_7694 = Ver notas de todo o universo nostr
# Button to select all profiles in follow pack
Select_All_a319 = Selecionar todos
# Button label to send a zap
Send_1ea4 = Enviar
# Column title for app settings
Settings_7a4f = Configurações
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar a última nota para cada utilizador a partir de uma lista
# Button label to sign out of account
Sign_out_337b = Terminar sessão
# Title for someone else's notes column
Someone_else_s_Notes_7e5f = Notas de outra pessoa
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Notificações de outra pessoa
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Ordenar respostas mais recentes antes:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Origem da última nota para cada utilizador na minha lista
# Description for hashtags column
Stay_up_to_date_with_a_certain_hashtag_88e3 = Atualizações com um dado marcador
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = Atualizações com notificações e menções
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = Atualizar-me de notas e respostas de outra pessoa
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = Atualizar-me de notificações e menções de outra pessoa
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = Atualizar-me de notas e respostas de outra pessoa
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = Atualizar-me de notificações e menções
# Step 1 label in support instructions
Step_1_8656 = Passo 1
# Step 2 label in support instructions
Step_2_d08d = Passo 2
# Label for storage settings section
Storage_ed65 = Armazenamento
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = Subscrever as notas de outra pessoa
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = Subscrever as notas de alguém
# Support email address
Support_email_44d9 = E-mail de suporte:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Mudar para o modo escuro
# Hover text for light mode toggle button
Switch_to_light_mode_72ce = Mudar para o modo claro
# Button text to load blurred media
Tap_to_Load_4b05 = Toca para carregar
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = O teste do assistente de IA Dave Nost terminou :(. Obrigado por testares! Dave com ativação de ZAPS em breve!
# Label for theme, Appearance settings section
Theme_4aac = Tema:
# Column title for note thread view
Thread_0f20 = Tópico
# Link text for thread references
thread_ad1f = tópico
# Title for universe column
Universe_e01e = Universo
# Column title for universe feed
Universe_ffaa = Universo
# Checkbox label for using wallet only for current account
Use_this_wallet_for_the_current_account_only_61dc = Usar esta carteira apenas para a conta atual
# Username and domain identification message
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" em "{ $domain }" será usado para identificação
# Profile username field label
Username_daa7 = Nome de utilizador
# Label for view folder button, Storage settings section
View_folder_9742 = Ver pasta
# Column title for wallet management
Wallet_5e50 = Carteira
# Hint for deck name input field
We_recommend_short_names_083e = Recomendamos nomes curtos
# Profile website field label
Website_7980 = Website
# Placeholder for note input field
Write_a_banger_note_here_bad2 = Escreve uma nota sonante aqui...
# Placeholder text for key input field
Your_key_here_81bd = A tua chave aqui...
# Title for your notes column
Your_Notes_f6db = Minhas notas
# Title for your notifications column
Your_Notifications_080d = Minhas notificações
# Heading for zap (tip) action
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = Enviar zaps a esta nota
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = Nível de zoom:
# Pluralized strings
# Search results count
Got__count__results_for___query_85fb =
{ $count ->
[one] { $count } resultado obtido para '{ $query }'
*[other] { $count } resultados obtidos para '{ $query }'
}

View File

@@ -45,6 +45,8 @@ Algo_2452 = อัลกอฯ
Algorithmic_feeds_to_aid_in_note_discovery_d344 = ฟีดแบบอัลกอริทึมที่ช่วยในการค้นหาโน้ต
# Label for zap amount input field
Amount_70f0 = จำนวน
# Label for appearance settings section
Appearance_4c7f = ลักษณะ
# Button to send message to Dave AI assistant
Ask_b7f4 = ถาม
# Placeholder text for Dave AI input field
@@ -59,10 +61,18 @@ Broadcast_fe43 = เผยแพร่
Broadcast_Local_7e50 = เผยแพร่เฉพาะที่
# Button label to cancel an action
Cancel_ed3b = ยกเลิก
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = ยกเลิก
# Label for clear cache button, Storage settings section
Clear_cache_dccb = ล้างแคช
# Hover text for editable zap amount
Click_to_edit_0414 = คลิกเพื่อแก้ไข
# Column title for note composition
Compose_Note_c094 = เขียนโน้ต
# Label for configure relays, settings section
Configure_relays_d156 = กำหนดค่ารีเลย์
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = ยืนยัน
# Button label to confirm an action
Confirm_f8a6 = ยืนยัน
# Status label for connected relay
@@ -80,11 +90,11 @@ Copy_a688 = คัดลอก
# Button to copy media link to clipboard
Copy_Link_dc7c = คัดลอกลิงก์
# Copy the unique note identifier to clipboard
Copy_Note_ID_6b45 = คัดลอก ID โน้ต
Copy_Note_ID_6b45 = คัดลอก โน้ต ID
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = คัดลอก JSON ของโน้ต
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = คัดลอก Pubkey
Copy_Pubkey_9cc4 = คัดลอก npub
# Copy the text content of the note to clipboard
Copy_Text_f81c = คัดลอกข้อความ
# Relative time in days
@@ -111,6 +121,8 @@ Custom_a69e = กำหนดเอง
Customize_Zap_Amount_cfc4 = กำหนดจำนวน Zap
# Column title for support page
Damus_Support_27c0 = ฝ่ายสนับสนุน Damus
# Label for Theme Dark, Appearance settings section
Dark_85fe = มืด
# Label for deck name input field
Deck_name_cd32 = ชื่อ Deck
# Label for decks section in side panel
@@ -151,12 +163,16 @@ Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__ns
คุณจำเป็นต้องใส่คีย์ส่วนตัวเพื่อทำการโพสต์, ตอบกลับ และอื่นๆ
# Label for find user button
Find_User_bd12 = ค้นหาผู้ใช้
# Label for font size, Appearance settings section
Font_size_dd73 = ขนาดตัวอักษร:
# Title for hashtags column
Hashtags_f8e0 = แฮชแท็ก
# Title for Home column
Home_8c19 = หน้าแรก
# Label for deck icon selection
Icon_b0ab = ไอคอน
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = ขนาดแคชรูปภาพ:
# Title for individual user column
Individual_b776 = ปัจเจคบุคคล
# Error message for invalid zap amount
@@ -177,8 +193,12 @@ k_50K_c2dc = 50K
k_5K_f7e6 = 5K
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = ติดตามโน้ตและการตอบกลับของคุณ
# Label for language, Appearance settings section
Language_e264 = ภาษา:
# Title for last note per user column
Last_Note_per_User_17ad = โน้ตล่าสุดของผู้ใช้แต่ละคน
# Label for Theme Light, Appearance settings section
Light_7475 = สว่าง
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = ที่อยู่ Lightning Network (lud16)
# Login page title
@@ -217,10 +237,16 @@ Notifications_d673 = การแจ้งเตือน
Notifications_ef56 = การแจ้งเตือน
# Relative time for very recent events (less than 3 seconds)
now_2181 = เมื่อสักครู่
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = เปิด
# Column title for finding users to follow
Onboarding_4a25 = เริ่มใช้
# Button label to open email client
Open_Email_25e9 = เปิดอีเมล
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = เปิดโปรแกรมอีเมลของคุณเพื่อรับความช่วยเหลือจากทีม Damus
# Label for others settings section
Others_7267 = อื่นๆ
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = วาง NWC URI ของคุณที่นี่...
# Error message for missing deck name
@@ -230,7 +256,7 @@ Please_create_a_name_for_the_deck_and_select_an_icon_0add = กรุณาต
# Error message for missing deck icon
Please_select_an_icon_655b = กรุณาเลือกไอคอน
# Button label to post a note
Post_now_8a49 = โพสต์เลย
Post_now_8a49 = โพสต์
# Instruction for copying logs
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = กดปุ่มด้านล่างเพื่อคัดลอกข้อมูลบันทึกล่าสุด ไปยังคลิปบอร์ด จากนั้นนำไปวางในอีเมลของคุณ
# Profile picture URL field label
@@ -267,6 +293,10 @@ replying_to_a_note_e0bc = ตอบกลับโน้ต
Repost_this_note_8e56 = รีโพสต์โน้ตนี้
# Label for reposted notes
Reposted_61c8 = รีโพสต์แล้ว
# Label for reset note body font size, Appearance settings section
Reset_4e60 = รีเซ็ต
# Label for reset zoom level, Appearance settings section
Reset_62d4 = รีเซ็ต
# Heading for support section
Running_into_a_bug_1796 = พบปัญหาในการใช้งานใช่ไหม?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
@@ -287,8 +317,12 @@ Searching_for___query_5d18 = กำลังค้นหา '{ $query }'
See_notes_from_your_contacts_ac16 = ดูโน้ตจากผู้ติดต่อของคุณ
# Description for universe column
See_the_whole_nostr_universe_7694 = ท่องจักรวาล Nostr ทั้งหมด
# Button to select all profiles in follow pack
Select_All_a319 = เลือกทั้งหมด
# Button label to send a zap
Send_1ea4 = ส่ง
# Column title for app settings
Settings_7a4f = การตั้งค่า
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = แสดงโน้ตล่าสุดของผู้ใช้แต่ละคนจากรายการ
# Button label to sign out of account
@@ -297,6 +331,8 @@ Sign_out_337b = ออกจากระบบ
Someone_else_s_Notes_7e5f = โน้ตของผู้อื่น
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = การแจ้งเตือนของผู้อื่น
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = เรียงการตอบกลับจากใหม่ที่สุดไปเก่าที่สุด
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = ดึงโน้ตล่าสุดของผู้ใช้แต่ละคนในรายชื่อผู้ติดต่อ
# Description for hashtags column
@@ -315,10 +351,14 @@ Stay_up_to_date_with_your_notifications_and_mentions_e73e = ติดตาม
Step_1_8656 = ขั้นตอนที่ 1
# Step 2 label in support instructions
Step_2_d08d = ขั้นตอนที่ 2
# Label for storage settings section
Storage_ed65 = พื้นที่จัดเก็บ
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = ติดตามโน้ตของผู้อื่น
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = ติดตามโน้ตของผู้อื่น
# Support email address
Support_email_44d9 = อีเมลฝ่ายสนับสนุน:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = เปลี่ยนเป็นโหมดมืด
# Hover text for light mode toggle button
@@ -327,6 +367,8 @@ Switch_to_light_mode_72ce = เปลี่ยนเป็นโหมดสว
Tap_to_Load_4b05 = แตะเพื่อโหลด
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = ช่วงทดลองใช้ผู้ช่วย AI 'Dave Nostr' ได้สิ้นสุดลงแล้ว :( ขอบคุณที่ร่วมทดสอบ! Dave ที่รองรับการ Zap กำลังจะมาเร็วๆ นี้!
# Label for theme, Appearance settings section
Theme_4aac = ธีม:
# Column title for note thread view
Thread_0f20 = เธรด
# Link text for thread references
@@ -338,9 +380,11 @@ Universe_ffaa = จักรวาล
# Checkbox label for using wallet only for current account
Use_this_wallet_for_the_current_account_only_61dc = ใช้วอลเล็ตนี้สำหรับบัญชีปัจจุบันเท่านั้น
# Username and domain identification message
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" ที่ "{ $domain }" จะถูกใช้สำหรับการระบุตัวตน
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" @ "{ $domain }" จะถูกใช้สำหรับการระบุตัวตน
# Profile username field label
Username_daa7 = ชื่อผู้ใช้
# Label for view folder button, Storage settings section
View_folder_9742 = ดูโฟลเดอร์
# Column title for wallet management
Wallet_5e50 = วอลเล็ต
# Hint for deck name input field
@@ -348,7 +392,7 @@ We_recommend_short_names_083e = เราแนะนำให้ใช้ชื
# Profile website field label
Website_7980 = เว็บไซต์
# Placeholder for note input field
Write_a_banger_note_here_bad2 = เขียนโน้ตปังๆ ที่นี่...
Write_a_banger_note_here_bad2 = เขียนโน้ตที่นี่...
# Placeholder text for key input field
Your_key_here_81bd = ใส่คีย์ของคุณที่นี่...
# Title for your notes column
@@ -359,6 +403,8 @@ Your_Notifications_080d = การแจ้งเตือนของคุณ
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = Zap โน้ตนี้
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = ระดับการซูม:
# Pluralized strings

View File

@@ -45,6 +45,8 @@ Algo_2452 = 算法
Algorithmic_feeds_to_aid_in_note_discovery_d344 = 用于帮助发现笔记的算法源
# Label for zap amount input field
Amount_70f0 = 金额
# Label for appearance settings section
Appearance_4c7f = 外观
# Button to send message to Dave AI assistant
Ask_b7f4 = 询问
# Placeholder text for Dave AI input field
@@ -59,10 +61,18 @@ Broadcast_fe43 = 广播
Broadcast_Local_7e50 = 仅广播至本地中继
# Button label to cancel an action
Cancel_ed3b = 取消
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = 取消
# Label for clear cache button, Storage settings section
Clear_cache_dccb = 清除缓存
# Hover text for editable zap amount
Click_to_edit_0414 = 点击以编辑
# Column title for note composition
Compose_Note_c094 = 撰写笔记
# Label for configure relays, settings section
Configure_relays_d156 = 配置中继器
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = 确认
# Button label to confirm an action
Confirm_f8a6 = 确认
# Status label for connected relay
@@ -111,6 +121,8 @@ Custom_a69e = 自定义
Customize_Zap_Amount_cfc4 = 自定义打闪金额
# Column title for support page
Damus_Support_27c0 = 达摩支持
# Label for Theme Dark, Appearance settings section
Dark_85fe = 暗色
# Label for deck name input field
Deck_name_cd32 = 仪表板名称
# Label for decks section in side panel
@@ -149,12 +161,16 @@ Enter_your_key_0fca = 请输入你的密钥
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = 请输入你的公钥npub、nostr 地址(如 { $address })、或私钥(nsec)。 你必须输入你的私钥才能发帖、回复等等。
# Label for find user button
Find_User_bd12 = 查找用户
# Label for font size, Appearance settings section
Font_size_dd73 = 字体大小:
# Title for hashtags column
Hashtags_f8e0 = 标签
# Title for Home column
Home_8c19 = 主页
# Label for deck icon selection
Icon_b0ab = 图标
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = 图像缓存大小:
# Title for individual user column
Individual_b776 = 个人
# Error message for invalid zap amount
@@ -175,8 +191,12 @@ k_50K_c2dc = 5万
k_5K_f7e6 = 5千
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = 随时查看你的笔记和回复
# Label for language, Appearance settings section
Language_e264 = 语言:
# Title for last note per user column
Last_Note_per_User_17ad = 每个用户的最新笔记
# Label for Theme Light, Appearance settings section
Light_7475 = 亮色
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = 闪电网络地址lud16
# Login page title
@@ -215,10 +235,14 @@ Notifications_d673 = 通知
Notifications_ef56 = 通知
# Relative time for very recent events (less than 3 seconds)
now_2181 = 刚刚
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = 开启
# Button label to open email client
Open_Email_25e9 = 打开电子邮箱
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = 打开你的默认电子邮件客户端以获得达摩团队的帮助
# Label for others settings section
Others_7267 = 其它
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = 在此粘贴你的 NWC URI...
# Error message for missing deck name
@@ -265,6 +289,10 @@ replying_to_a_note_e0bc = 正在回复笔记
Repost_this_note_8e56 = 转发此笔记
# Label for reposted notes
Reposted_61c8 = 已转发
# Label for reset note body font size, Appearance settings section
Reset_4e60 = 重置
# Label for reset zoom level, Appearance settings section
Reset_62d4 = 重置
# Heading for support section
Running_into_a_bug_1796 = 遇到故障了吗?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
@@ -287,6 +315,8 @@ See_notes_from_your_contacts_ac16 = 查看来自你的联系人的笔记
See_the_whole_nostr_universe_7694 = 查看整个 nostr 宇宙
# Button label to send a zap
Send_1ea4 = 发送
# Column title for app settings
Settings_7a4f = 设置
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = 显示列表中每个用户的最新一条笔记
# Button label to sign out of account
@@ -295,6 +325,8 @@ Sign_out_337b = 登出
Someone_else_s_Notes_7e5f = 其他人的笔记
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = 其他人的通知
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = 按最新排序回复:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = 获取你的联系人列表中每个用户的最新一条笔记
# Description for hashtags column
@@ -313,10 +345,14 @@ Stay_up_to_date_with_your_notifications_and_mentions_e73e = 获取你的通知
Step_1_8656 = 第一步
# Step 2 label in support instructions
Step_2_d08d = 第二步
# Label for storage settings section
Storage_ed65 = 存储
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = 订阅他人的笔记
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = 订阅某人的笔记
# Support email address
Support_email_44d9 = 支持电子邮件:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = 切换到暗色模式
# Hover text for light mode toggle button
@@ -325,6 +361,8 @@ Switch_to_light_mode_72ce = 切换到亮色模式
Tap_to_Load_4b05 = 点击加载
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = Dave Nostr AI 助手试用期已经结束 :(。感谢测试!可打闪付款的 Dave 即将来临!
# Label for theme, Appearance settings section
Theme_4aac = 主题:
# Column title for note thread view
Thread_0f20 = 帖子
# Link text for thread references
@@ -339,6 +377,8 @@ Use_this_wallet_for_the_current_account_only_61dc = 此钱包仅限用于当前
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" 于 "{ $domain }" 将被用于身份识别
# Profile username field label
Username_daa7 = 用户名
# Label for view folder button, Storage settings section
View_folder_9742 = 查看文件夹
# Column title for wallet management
Wallet_5e50 = 钱包
# Hint for deck name input field
@@ -357,6 +397,8 @@ Your_Notifications_080d = 你的通知
Zap_16b4 = 打闪
# Hover text for zap button
Zap_this_note_42b2 = 打闪此笔记
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = 缩放大小:
# Pluralized strings

View File

@@ -45,6 +45,8 @@ Algo_2452 = 算法
Algorithmic_feeds_to_aid_in_note_discovery_d344 = 用於幫助發現筆記的算法源
# Label for zap amount input field
Amount_70f0 = 金額
# Label for appearance settings section
Appearance_4c7f = 外觀
# Button to send message to Dave AI assistant
Ask_b7f4 = 詢問
# Placeholder text for Dave AI input field
@@ -59,10 +61,18 @@ Broadcast_fe43 = 廣播
Broadcast_Local_7e50 = 僅廣播至本地中繼
# Button label to cancel an action
Cancel_ed3b = 取消
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = 取消
# Label for clear cache button, Storage settings section
Clear_cache_dccb = 清除快取
# Hover text for editable zap amount
Click_to_edit_0414 = 點擊編輯
# Column title for note composition
Compose_Note_c094 = 撰寫筆記
# Label for configure relays, settings section
Configure_relays_d156 = 配置中繼器
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = 確認
# Button label to confirm an action
Confirm_f8a6 = 確認
# Status label for connected relay
@@ -111,6 +121,8 @@ Custom_a69e = 自訂
Customize_Zap_Amount_cfc4 = 自訂打閃金額
# Column title for support page
Damus_Support_27c0 = 達摩支持
# Label for Theme Dark, Appearance settings section
Dark_85fe = 暗色
# Label for deck name input field
Deck_name_cd32 = 儀表板名稱
# Label for decks section in side panel
@@ -149,12 +161,16 @@ Enter_your_key_0fca = 請輸入你的密鑰
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = 請輸入你的公鑰npub、nostr 地址(如 { $address }、或私鑰nsec。你必須輸入你的私鑰才能發貼、回覆等等。
# Label for find user button
Find_User_bd12 = 查找用戶
# Label for font size, Appearance settings section
Font_size_dd73 = 字體大小:
# Title for hashtags column
Hashtags_f8e0 = 標籤
# Title for Home column
Home_8c19 = 主頁
# Label for deck icon selection
Icon_b0ab = 圖標
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = 圖像快取大小:
# Title for individual user column
Individual_b776 = 個人
# Error message for invalid zap amount
@@ -175,8 +191,12 @@ k_50K_c2dc = 5萬
k_5K_f7e6 = 5千
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = 隨時查看你的筆記和回覆
# Label for language, Appearance settings section
Language_e264 = 語言:
# Title for last note per user column
Last_Note_per_User_17ad = 每個用戶的最新筆記
# Label for Theme Light, Appearance settings section
Light_7475 = 亮色
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = 閃電網絡地址lud16
# Login page title
@@ -215,10 +235,14 @@ Notifications_d673 = 通知
Notifications_ef56 = 通知
# Relative time for very recent events (less than 3 seconds)
now_2181 = 剛剛
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = 開啟
# Button label to open email client
Open_Email_25e9 = 打開電子郵箱
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = 打開你的默認電子郵件客戶端以獲得達摩團隊的幫助
# Label for others settings section
Others_7267 = 其他
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = 在此貼上你的 NWC URI...
# Error message for missing deck name
@@ -265,6 +289,10 @@ replying_to_a_note_e0bc = 正在回覆筆記
Repost_this_note_8e56 = 轉發此筆記
# Label for reposted notes
Reposted_61c8 = 已轉發
# Label for reset note body font size, Appearance settings section
Reset_4e60 = 重置
# Label for reset zoom level, Appearance settings section
Reset_62d4 = 重置
# Heading for support section
Running_into_a_bug_1796 = 遇到故障了嗎?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
@@ -287,6 +315,8 @@ See_notes_from_your_contacts_ac16 = 查看來自你的聯繫人的筆記
See_the_whole_nostr_universe_7694 = 查看整個 nostr 宇宙
# Button label to send a zap
Send_1ea4 = 發送
# Column title for app settings
Settings_7a4f = 設置
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = 顯示列表中每個用戶的最後一條筆記
# Button label to sign out of account
@@ -295,6 +325,8 @@ Sign_out_337b = 登出
Someone_else_s_Notes_7e5f = 其他人的筆記
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = 其他人的通知
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = 按最新排序回覆:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = 獲取你的聯繫人列表中每個用戶的最新一條筆記
# Description for hashtags column
@@ -313,10 +345,14 @@ Stay_up_to_date_with_your_notifications_and_mentions_e73e = 獲取你的通知
Step_1_8656 = 第一步
# Step 2 label in support instructions
Step_2_d08d = 第二步
# Label for storage settings section
Storage_ed65 = 儲存
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = 訂閱他人的筆記
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = 訂閱某人的筆記
# Support email address
Support_email_44d9 = 支持電子郵件:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = 切換到暗色模式
# Hover text for light mode toggle button
@@ -325,6 +361,8 @@ Switch_to_light_mode_72ce = 切換到亮色模式
Tap_to_Load_4b05 = 點擊加載
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = Dave Nostr AI 助手試用期已經結束 :(。感謝測試!可打閃付款的 Dave 即將來臨!
# Label for theme, Appearance settings section
Theme_4aac = 主題:
# Column title for note thread view
Thread_0f20 = 串文
# Link text for thread references
@@ -339,6 +377,8 @@ Use_this_wallet_for_the_current_account_only_61dc = 此錢包僅限用於當前
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" 於 "{ $domain }" 將被用於身份識別
# Profile username field label
Username_daa7 = 用戶名
# Label for view folder button, Storage settings section
View_folder_9742 = 查看文件夾
# Column title for wallet management
Wallet_5e50 = 錢包
# Hint for deck name input field
@@ -357,6 +397,8 @@ Your_Notifications_080d = 你的通知
Zap_16b4 = 打閃
# Hover text for zap button
Zap_this_note_42b2 = 打閃此筆記
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = 縮放大小:
# Pluralized strings

View File

@@ -135,7 +135,7 @@ pub fn setup_multicast_relay(
std::thread::spawn(move || {
let mut events = Events::with_capacity(1);
loop {
if let Err(err) = poll.poll(&mut events, Some(Duration::from_millis(100))) {
if let Err(err) = poll.poll(&mut events, None) {
error!("multicast socket poll error: {err}. ending multicast poller.");
return;
}

View File

@@ -68,7 +68,7 @@ impl From<RelayEvent<'_>> for OwnedRelayEvent {
}
#[derive(PartialEq, Eq, Hash, Clone)]
pub struct RelaySub {
pub struct _RelaySub {
pub(crate) subid: String,
pub(crate) filter: String,
}

View File

@@ -9,11 +9,13 @@ nostrdb = { workspace = true }
jni = { workspace = true }
url = { workspace = true }
strum = { workspace = true }
blurhash = { workspace = true }
strum_macros = { workspace = true }
dirs = { workspace = true }
enostr = { workspace = true }
nostr = { workspace = true }
egui = { workspace = true }
egui_extras = { workspace = true }
eframe = { workspace = true }
image = { workspace = true }
base32 = { workspace = true }
@@ -45,7 +47,11 @@ fluent-langneg = { workspace = true }
unic-langid = { workspace = true }
once_cell = { workspace = true }
md5 = { workspace = true }
bitflags = { workspace = true }
regex = "1"
chrono = { workspace = true }
indexmap = {workspace = true}
crossbeam-channel = "0.5"
[dev-dependencies]
tempfile = { workspace = true }
@@ -53,6 +59,8 @@ tokio = { workspace = true }
[target.'cfg(target_os = "android")'.dependencies]
jni = { workspace = true }
android-activity = { workspace = true }
ndk-context = "0.1"
[features]
puffin = ["puffin_egui", "dep:puffin"]

View File

@@ -207,6 +207,10 @@ impl Accounts {
self.cache.selected_mut()
}
pub fn get_selected_wallet(&self) -> Option<&ZapWallet> {
self.cache.selected().wallet.as_ref()
}
pub fn get_selected_wallet_mut(&mut self) -> Option<&mut ZapWallet> {
self.cache.selected_mut().wallet.as_mut()
}
@@ -263,6 +267,11 @@ impl Accounts {
Box::new(move |note: &Note, thread: &[u8; 32]| muted.is_muted(note, thread))
}
pub fn mute(&self) -> Box<Arc<crate::Muted>> {
let account_data = self.get_selected_account_data();
Box::new(Arc::clone(&account_data.muted.muted))
}
pub fn send_initial_filters(&mut self, pool: &mut RelayPool, relay_url: &str) {
let data = &self.get_selected_account().data;
// send the active account's relay list subscription

View File

@@ -15,6 +15,7 @@ pub enum ContactState {
Received {
contacts: HashSet<Pubkey>,
note_key: NoteKey,
timestamp: u64,
},
}
@@ -41,7 +42,7 @@ impl Contacts {
pub(super) fn query(&mut self, ndb: &Ndb, txn: &Transaction) {
let binding = ndb
.query(txn, &[self.filter.clone()], 1)
.query(txn, std::slice::from_ref(&self.filter), 1)
.expect("query user relays results");
let Some(res) = binding.first() else {
@@ -57,6 +58,7 @@ impl Contacts {
ContactState::Received {
contacts,
note_key: _,
timestamp: _,
} => {
if contacts.contains(other_pubkey) {
IsFollowing::Yes
@@ -82,6 +84,18 @@ impl Contacts {
}
};
if let ContactState::Received {
contacts: _,
note_key: _,
timestamp,
} = self.get_state()
{
if *timestamp > note.created_at() {
// the current contact list is more up to date than the one we just received. ignore it.
return;
}
}
update_state(&mut self.state, &note, *key);
}
@@ -96,11 +110,17 @@ fn update_state(state: &mut ContactState, note: &Note, key: NoteKey) {
*state = ContactState::Received {
contacts: get_contacts_owned(note),
note_key: key,
timestamp: note.created_at(),
};
}
ContactState::Received { contacts, note_key } => {
ContactState::Received {
contacts,
note_key,
timestamp,
} => {
update_contacts(contacts, note);
*note_key = key;
*timestamp = note.created_at();
}
};
}

View File

@@ -33,7 +33,7 @@ impl AccountMutedData {
.limit()
.unwrap_or(crate::filter::default_limit()) as i32;
let nks = ndb
.query(txn, &[self.filter.clone()], lim)
.query(txn, std::slice::from_ref(&self.filter), lim)
.expect("query user muted results")
.iter()
.map(|qr| qr.note_key)

View File

@@ -1,12 +1,11 @@
use std::collections::BTreeSet;
use crate::{AccountData, RelaySpec};
use enostr::{Keypair, Pubkey, RelayPool};
use nostrdb::{Filter, Ndb, NoteBuilder, NoteKey, Subscription, Transaction};
use tracing::{debug, error, info};
use url::Url;
use crate::{AccountData, RelaySpec};
#[derive(Clone)]
pub(crate) struct AccountRelayData {
pub filter: Filter,
@@ -37,7 +36,7 @@ impl AccountRelayData {
.limit()
.unwrap_or(crate::filter::default_limit()) as i32;
let nks = ndb
.query(txn, &[self.filter.clone()], lim)
.query(txn, std::slice::from_ref(&self.filter), lim)
.expect("query user relays results")
.iter()
.map(|qr| qr.note_key)

View File

@@ -1,13 +1,14 @@
use crate::account::FALLBACK_PUBKEY;
use crate::i18n::Localization;
use crate::persist::{AppSizeHandler, ZoomHandler};
use crate::persist::{AppSizeHandler, SettingsHandler};
use crate::wallet::GlobalWallet;
use crate::zaps::Zaps;
use crate::Error;
use crate::JobPool;
use crate::NotedeckOptions;
use crate::{
frame_history::FrameHistory, AccountStorage, Accounts, AppContext, Args, DataPath,
DataPathType, Directory, Images, NoteAction, NoteCache, RelayDebugView, ThemeHandler,
UnknownIds,
DataPathType, Directory, Images, NoteAction, NoteCache, RelayDebugView, UnknownIds,
};
use egui::Margin;
use egui::ThemePreference;
@@ -19,6 +20,10 @@ use std::collections::BTreeSet;
use std::path::Path;
use std::rc::Rc;
use tracing::{error, info};
use unic_langid::{LanguageIdentifier, LanguageIdentifierError};
#[cfg(target_os = "android")]
use android_activity::AndroidApp;
pub enum AppAction {
Note(NoteAction),
@@ -40,9 +45,8 @@ pub struct Notedeck {
global_wallet: GlobalWallet,
path: DataPath,
args: Args,
theme: ThemeHandler,
settings: SettingsHandler,
app: Option<Rc<RefCell<dyn App>>>,
zoom: ZoomHandler,
app_size: AppSizeHandler,
unrecognized_args: BTreeSet<String>,
clipboard: Clipboard,
@@ -50,6 +54,9 @@ pub struct Notedeck {
frame_history: FrameHistory,
job_pool: JobPool,
i18n: Localization,
#[cfg(target_os = "android")]
android_app: Option<AndroidApp>,
}
/// Our chrome, which is basically nothing
@@ -99,10 +106,18 @@ impl eframe::App for Notedeck {
render_notedeck(self, ctx);
self.zoom.try_save_zoom_factor(ctx);
self.settings.update_batch(|settings| {
settings.zoom_factor = ctx.zoom_factor();
settings.locale = self.i18n.get_current_locale().to_string();
settings.theme = if ctx.style().visuals.dark_mode {
ThemePreference::Dark
} else {
ThemePreference::Light
};
});
self.app_size.try_save_app_size(ctx);
if self.args.relay_debug {
if self.args.options.contains(NotedeckOptions::RelayDebug) {
if self.pool.debug.is_none() {
self.pool.use_debug();
}
@@ -129,6 +144,11 @@ fn setup_puffin() {
}
impl Notedeck {
#[cfg(target_os = "android")]
pub fn set_android_context(&mut self, context: AndroidApp) {
self.android_app = Some(context);
}
pub fn new<P: AsRef<Path>>(ctx: &egui::Context, data_path: P, args: &[String]) -> Self {
#[cfg(feature = "puffin")]
setup_puffin();
@@ -159,10 +179,11 @@ impl Notedeck {
1024usize * 1024usize * 1024usize * 1024usize
};
let theme = ThemeHandler::new(&path);
let settings = SettingsHandler::new(&path).load();
let config = Config::new().set_ingester_threads(2).set_mapsize(map_size);
let keystore = if parsed_args.use_keystore {
let keystore = if parsed_args.options.contains(NotedeckOptions::UseKeystore) {
let keys_path = path.path(DataPathType::Keys);
let selected_key_path = path.path(DataPathType::SelectedKey);
Some(AccountStorage::new(
@@ -213,12 +234,8 @@ impl Notedeck {
let img_cache = Images::new(img_cache_dir);
let note_cache = NoteCache::default();
let zoom = ZoomHandler::new(&path);
let app_size = AppSizeHandler::new(&path);
if let Some(z) = zoom.get_zoom_factor() {
ctx.set_zoom_factor(z);
}
let app_size = AppSizeHandler::new(&path);
// migrate
if let Err(e) = img_cache.migrate_v0() {
@@ -231,15 +248,22 @@ impl Notedeck {
// Initialize localization
let mut i18n = Localization::new();
let setting_locale: Result<LanguageIdentifier, LanguageIdentifierError> =
settings.locale().parse();
if let Ok(setting_locale) = setting_locale {
if let Err(err) = i18n.set_locale(setting_locale) {
error!("{err}");
}
}
if let Some(locale) = &parsed_args.locale {
if let Err(err) = i18n.set_locale(locale.to_owned()) {
error!("{err}");
}
}
// Initialize global i18n context
//crate::i18n::init_global_i18n(i18n.clone());
Self {
ndb,
img_cache,
@@ -250,9 +274,8 @@ impl Notedeck {
global_wallet,
path: path.clone(),
args: parsed_args,
theme,
settings,
app: None,
zoom,
app_size,
unrecognized_args,
frame_history: FrameHistory::default(),
@@ -260,9 +283,49 @@ impl Notedeck {
zaps,
job_pool,
i18n,
#[cfg(target_os = "android")]
android_app: None,
}
}
/// Setup egui context
pub fn setup(&self, ctx: &egui::Context) {
// Initialize global i18n context
//crate::i18n::init_global_i18n(i18n.clone());
crate::setup::setup_egui_context(
ctx,
self.args.options,
self.theme(),
self.note_body_font_size(),
self.zoom_factor(),
);
}
/// ensure we recognized all the arguments
pub fn check_args(&self, other_app_args: &BTreeSet<String>) -> Result<(), Error> {
let completely_unrecognized: Vec<String> = self
.unrecognized_args()
.intersection(other_app_args)
.cloned()
.collect();
if !completely_unrecognized.is_empty() {
let err = format!("Unrecognized arguments: {completely_unrecognized:?}");
tracing::error!("{}", &err);
return Err(Error::Generic(err));
}
Ok(())
}
#[inline]
pub fn options(&self) -> NotedeckOptions {
self.args.options
}
pub fn has_option(&self, option: NotedeckOptions) -> bool {
self.options().contains(option)
}
pub fn app<A: App + 'static>(mut self, app: A) -> Self {
self.set_app(app);
self
@@ -279,12 +342,14 @@ impl Notedeck {
global_wallet: &mut self.global_wallet,
path: &self.path,
args: &self.args,
theme: &mut self.theme,
settings: &mut self.settings,
clipboard: &mut self.clipboard,
zaps: &mut self.zaps,
frame_history: &mut self.frame_history,
job_pool: &mut self.job_pool,
i18n: &mut self.i18n,
#[cfg(target_os = "android")]
android: self.android_app.as_ref().unwrap().clone(),
}
}
@@ -297,7 +362,15 @@ impl Notedeck {
}
pub fn theme(&self) -> ThemePreference {
self.theme.load()
self.settings.theme()
}
pub fn note_body_font_size(&self) -> f32 {
self.settings.note_body_font_size()
}
pub fn zoom_factor(&self) -> f32 {
self.settings.zoom_factor()
}
pub fn unrecognized_args(&self) -> &BTreeSet<String> {

View File

@@ -1,23 +1,15 @@
use std::collections::BTreeSet;
use crate::NotedeckOptions;
use enostr::{Keypair, Pubkey, SecretKey};
use tracing::error;
use unic_langid::{LanguageIdentifier, LanguageIdentifierError};
pub struct Args {
pub relays: Vec<String>,
pub is_mobile: Option<bool>,
pub locale: Option<LanguageIdentifier>,
pub show_note_client: bool,
pub keys: Vec<Keypair>,
pub light: bool,
pub debug: bool,
pub relay_debug: bool,
/// Enable when running tests so we don't panic on app startup
pub tests: bool,
pub use_keystore: bool,
pub options: NotedeckOptions,
pub dbpath: Option<String>,
pub datapath: Option<String>,
}
@@ -28,14 +20,8 @@ impl Args {
let mut unrecognized_args = BTreeSet::new();
let mut res = Args {
relays: vec![],
is_mobile: None,
keys: vec![],
light: false,
show_note_client: false,
debug: false,
relay_debug: false,
tests: false,
use_keystore: true,
options: NotedeckOptions::default(),
dbpath: None,
datapath: None,
locale: None,
@@ -47,9 +33,9 @@ impl Args {
let arg = &args[i];
if arg == "--mobile" {
res.is_mobile = Some(true);
res.options.set(NotedeckOptions::Mobile, true);
} else if arg == "--light" {
res.light = true;
res.options.set(NotedeckOptions::LightTheme, true);
} else if arg == "--locale" {
i += 1;
let Some(locale) = args.get(i) else {
@@ -68,11 +54,11 @@ impl Args {
}
}
} else if arg == "--dark" {
res.light = false;
res.options.set(NotedeckOptions::LightTheme, false);
} else if arg == "--debug" {
res.debug = true;
res.options.set(NotedeckOptions::Debug, true);
} else if arg == "--testrunner" {
res.tests = true;
res.options.set(NotedeckOptions::Tests, true);
} else if arg == "--pub" || arg == "--npub" {
i += 1;
let pubstr = if let Some(next_arg) = args.get(i) {
@@ -135,11 +121,13 @@ impl Args {
};
res.relays.push(relay.clone());
} else if arg == "--no-keystore" {
res.use_keystore = false;
res.options.set(NotedeckOptions::UseKeystore, true);
} else if arg == "--relay-debug" {
res.relay_debug = true;
} else if arg == "--show-note-client" {
res.show_note_client = true;
res.options.set(NotedeckOptions::RelayDebug, true);
} else if arg == "--notebook" {
res.options.set(NotedeckOptions::FeatureNotebook, true);
} else if arg == "--clndash" {
res.options.set(NotedeckOptions::FeatureClnDash, true);
} else {
unrecognized_args.insert(arg.clone());
}

View File

@@ -1,6 +1,6 @@
use crate::{
account::accounts::Accounts, frame_history::FrameHistory, i18n::Localization,
wallet::GlobalWallet, zaps::Zaps, Args, DataPath, Images, JobPool, NoteCache, ThemeHandler,
wallet::GlobalWallet, zaps::Zaps, Args, DataPath, Images, JobPool, NoteCache, SettingsHandler,
UnknownIds,
};
use egui_winit::clipboard::Clipboard;
@@ -8,6 +8,9 @@ use egui_winit::clipboard::Clipboard;
use enostr::RelayPool;
use nostrdb::Ndb;
#[cfg(target_os = "android")]
use android_activity::AndroidApp;
use egui::{Pos2, Rect};
// TODO: make this interface more sandboxed
pub struct AppContext<'a> {
@@ -20,10 +23,68 @@ pub struct AppContext<'a> {
pub global_wallet: &'a mut GlobalWallet,
pub path: &'a DataPath,
pub args: &'a Args,
pub theme: &'a mut ThemeHandler,
pub settings: &'a mut SettingsHandler,
pub clipboard: &'a mut Clipboard,
pub zaps: &'a mut Zaps,
pub frame_history: &'a mut FrameHistory,
pub job_pool: &'a mut JobPool,
pub i18n: &'a mut Localization,
#[cfg(target_os = "android")]
pub android: AndroidApp,
}
#[derive(Debug, Clone)]
pub enum SoftKeyboardContext {
Virtual,
Platform { ppp: f32 },
}
impl SoftKeyboardContext {
pub fn platform(context: &egui::Context) -> Self {
Self::Platform {
ppp: context.pixels_per_point(),
}
}
}
impl<'a> AppContext<'a> {
pub fn soft_keyboard_rect(&self, screen_rect: Rect, ctx: SoftKeyboardContext) -> Option<Rect> {
match ctx {
SoftKeyboardContext::Virtual => {
let height = 400.0;
skb_rect_from_screen_rect(screen_rect, height)
}
#[allow(unused_variables)]
SoftKeyboardContext::Platform { ppp } => {
#[cfg(target_os = "android")]
{
use android_activity::InsetType;
// not sure why I need this, it seems to be consistently off by some amount of
// pixels ?
let fudge = 0.0;
let inset = self.android.get_window_insets(InsetType::Ime);
let height = (inset.bottom as f32 / ppp) - fudge;
skb_rect_from_screen_rect(screen_rect, height)
}
#[cfg(not(target_os = "android"))]
{
None
}
}
}
}
}
#[inline]
fn skb_rect_from_screen_rect(screen_rect: Rect, height: f32) -> Option<Rect> {
if height == 0.0 {
return None;
}
let min = Pos2::new(0.0, screen_rect.max.y - height);
Some(Rect::from_min_max(min, screen_rect.max))
}

View File

@@ -33,15 +33,26 @@ pub enum ZapError {
#[error("invalid lud16")]
InvalidLud16(String),
#[error("invalid endpoint response")]
EndpointError(String),
EndpointError(EndpointError),
#[error("bech encoding/decoding error")]
Bech(String),
#[error("serialization/deserialization problem")]
Serialization(String),
#[error("nwc error")]
NWC(String),
#[error("ndb error")]
Ndb(String),
}
impl ZapError {
pub fn endpoint_error(error: String) -> ZapError {
ZapError::EndpointError(EndpointError(error))
}
}
#[derive(Debug, Clone)]
pub struct EndpointError(pub String);
impl From<String> for Error {
fn from(s: String) -> Self {
Error::Generic(s)

View File

@@ -86,6 +86,13 @@ impl FilterStates {
}
self.states.insert(relay, state);
}
/// For contacts, since that sub is managed elsewhere
pub fn set_all_states(&mut self, state: FilterState) {
for cur_state in self.states.values_mut() {
*cur_state = state.clone();
}
}
}
/// We may need to fetch some data from relays before our filter is ready.
@@ -176,21 +183,24 @@ pub fn should_since_optimize(limit: u64, num_notes: usize) -> bool {
limit as usize <= num_notes
}
pub fn since_optimize_filter_with(filter: Filter, notes: &[NoteRef], since_gap: u64) -> Filter {
pub fn since_optimize_filter_with(
filter: Filter,
latest_note: Option<&NoteRef>,
since_gap: u64,
) -> Filter {
// Get the latest entry in the events
if notes.is_empty() {
let Some(latest) = latest_note else {
return filter;
}
};
// get the latest note
let latest = notes[0];
let since = latest.created_at - since_gap;
filter.since_mut(since)
}
pub fn since_optimize_filter(filter: Filter, notes: &[NoteRef]) -> Filter {
since_optimize_filter_with(filter, notes, 60)
pub fn since_optimize_filter(filter: Filter, latest: Option<&NoteRef>) -> Filter {
since_optimize_filter_with(filter, latest, 60)
}
pub fn default_limit() -> u64 {

View File

@@ -1,4 +1,9 @@
use crate::{ui, NotedeckTextStyle};
use egui::FontData;
use egui::FontDefinitions;
use egui::FontTweak;
use std::collections::BTreeMap;
use std::sync::Arc;
pub enum NamedFontFamily {
Medium,
@@ -31,6 +36,7 @@ pub fn desktop_font_size(text_style: &NotedeckTextStyle) -> f32 {
NotedeckTextStyle::Button => 13.0,
NotedeckTextStyle::Small => 12.0,
NotedeckTextStyle::Tiny => 10.0,
NotedeckTextStyle::NoteBody => 16.0,
}
}
@@ -46,6 +52,7 @@ pub fn mobile_font_size(text_style: &NotedeckTextStyle) -> f32 {
NotedeckTextStyle::Button => 13.0,
NotedeckTextStyle::Small => 12.0,
NotedeckTextStyle::Tiny => 10.0,
NotedeckTextStyle::NoteBody => 13.0,
}
}
@@ -56,3 +63,148 @@ pub fn get_font_size(ctx: &egui::Context, text_style: &NotedeckTextStyle) -> f32
desktop_font_size(text_style)
}
}
// Use gossip's approach to font loading. This includes japanese fonts
// for rending stuff from japanese users.
pub fn setup_fonts(ctx: &egui::Context) {
let mut font_data: BTreeMap<String, Arc<FontData>> = BTreeMap::new();
let mut families = BTreeMap::new();
font_data.insert(
"Onest".to_owned(),
Arc::new(FontData::from_static(include_bytes!(
"../../../assets/fonts/onest/OnestRegular1602-hint.ttf"
))),
);
font_data.insert(
"OnestMedium".to_owned(),
Arc::new(FontData::from_static(include_bytes!(
"../../../assets/fonts/onest/OnestMedium1602-hint.ttf"
))),
);
font_data.insert(
"DejaVuSans".to_owned(),
Arc::new(FontData::from_static(include_bytes!(
"../../../assets/fonts/DejaVuSansSansEmoji.ttf"
))),
);
font_data.insert(
"OnestBold".to_owned(),
Arc::new(FontData::from_static(include_bytes!(
"../../../assets/fonts/onest/OnestBold1602-hint.ttf"
))),
);
/*
font_data.insert(
"DejaVuSansBold".to_owned(),
FontData::from_static(include_bytes!(
"../assets/fonts/DejaVuSans-Bold-SansEmoji.ttf"
)),
);
font_data.insert(
"DejaVuSans".to_owned(),
FontData::from_static(include_bytes!("../assets/fonts/DejaVuSansSansEmoji.ttf")),
);
font_data.insert(
"DejaVuSansBold".to_owned(),
FontData::from_static(include_bytes!(
"../assets/fonts/DejaVuSans-Bold-SansEmoji.ttf"
)),
);
*/
font_data.insert(
"Inconsolata".to_owned(),
Arc::new(
FontData::from_static(include_bytes!(
"../../../assets/fonts/Inconsolata-Regular.ttf"
))
.tweak(FontTweak {
scale: 1.22, // This font is smaller than DejaVuSans
y_offset_factor: -0.18, // and too low
y_offset: 0.0,
baseline_offset_factor: 0.0,
}),
),
);
font_data.insert(
"NotoSansCJK".to_owned(),
Arc::new(FontData::from_static(include_bytes!(
"../../../assets/fonts/NotoSansCJK-Regular.ttc"
))),
);
font_data.insert(
"NotoSansThai".to_owned(),
Arc::new(FontData::from_static(include_bytes!(
"../../../assets/fonts/NotoSansThai-Regular.ttf"
))),
);
// Some good looking emojis. Use as first priority:
font_data.insert(
"NotoEmoji".to_owned(),
Arc::new(
FontData::from_static(include_bytes!(
"../../../assets/fonts/NotoEmoji-Regular.ttf"
))
.tweak(FontTweak {
scale: 1.1, // make them a touch larger
y_offset_factor: 0.0,
y_offset: 0.0,
baseline_offset_factor: 0.0,
}),
),
);
let base_fonts = vec![
"DejaVuSans".to_owned(),
"NotoEmoji".to_owned(),
"NotoSansCJK".to_owned(),
"NotoSansThai".to_owned(),
];
let mut proportional = vec!["Onest".to_owned()];
proportional.extend(base_fonts.clone());
let mut medium = vec!["OnestMedium".to_owned()];
medium.extend(base_fonts.clone());
let mut mono = vec!["Inconsolata".to_owned()];
mono.extend(base_fonts.clone());
let mut bold = vec!["OnestBold".to_owned()];
bold.extend(base_fonts.clone());
let emoji = vec!["NotoEmoji".to_owned()];
families.insert(egui::FontFamily::Proportional, proportional);
families.insert(egui::FontFamily::Monospace, mono);
families.insert(
egui::FontFamily::Name(NamedFontFamily::Medium.as_str().into()),
medium,
);
families.insert(
egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()),
bold,
);
families.insert(
egui::FontFamily::Name(NamedFontFamily::Emoji.as_str().into()),
emoji,
);
tracing::debug!("fonts: {:?}", families);
let defs = FontDefinitions {
font_data,
families,
};
ctx.set_fonts(defs);
}

View File

@@ -5,16 +5,32 @@ use std::borrow::Cow;
use std::collections::HashMap;
use unic_langid::{langid, LanguageIdentifier};
const EN_XA: LanguageIdentifier = langid!("en-XA");
const EN_US: LanguageIdentifier = langid!("en-US");
const EN_XA: LanguageIdentifier = langid!("en-XA");
const DE: LanguageIdentifier = langid!("de");
const ES_419: LanguageIdentifier = langid!("es-419");
const ES_ES: LanguageIdentifier = langid!("es-ES");
const FR: LanguageIdentifier = langid!("FR");
const TH: LanguageIdentifier = langid!("TH");
const ZH_CN: LanguageIdentifier = langid!("ZH_CN");
const ZH_TW: LanguageIdentifier = langid!("ZH_TW");
const NUM_FTLS: usize = 9;
const FR: LanguageIdentifier = langid!("fr");
const JA: LanguageIdentifier = langid!("ja");
const PT_BR: LanguageIdentifier = langid!("pt-BR");
const PT_PT: LanguageIdentifier = langid!("pt-PT");
const TH: LanguageIdentifier = langid!("th");
const ZH_CN: LanguageIdentifier = langid!("zh-CN");
const ZH_TW: LanguageIdentifier = langid!("zh-TW");
const NUM_FTLS: usize = 12;
const EN_US_NATIVE_NAME: &str = "English (US)";
const EN_XA_NATIVE_NAME: &str = "Éñglísh (Pséúdólóçàlé)";
const DE_NATIVE_NAME: &str = "Deutsch";
const ES_419_NATIVE_NAME: &str = "Español (Latinoamérica)";
const ES_ES_NATIVE_NAME: &str = "Español (España)";
const FR_NATIVE_NAME: &str = "Français";
const JA_NATIVE_NAME: &str = "日本語";
const PT_BR_NATIVE_NAME: &str = "Português (Brasil)";
const PT_PT_NATIVE_NAME: &str = "Português (Portugal)";
const TH_NATIVE_NAME: &str = "ภาษาไทย";
const ZH_CN_NATIVE_NAME: &str = "简体中文";
const ZH_TW_NATIVE_NAME: &str = "繁體中文";
struct StaticBundle {
identifier: LanguageIdentifier,
@@ -46,6 +62,18 @@ const FTLS: [StaticBundle; NUM_FTLS] = [
identifier: FR,
ftl: include_str!("../../../../assets/translations/fr/main.ftl"),
},
StaticBundle {
identifier: JA,
ftl: include_str!("../../../../assets/translations/ja/main.ftl"),
},
StaticBundle {
identifier: PT_BR,
ftl: include_str!("../../../../assets/translations/pt-BR/main.ftl"),
},
StaticBundle {
identifier: PT_PT,
ftl: include_str!("../../../../assets/translations/pt-PT/main.ftl"),
},
StaticBundle {
identifier: TH,
ftl: include_str!("../../../../assets/translations/th/main.ftl"),
@@ -70,6 +98,8 @@ pub struct Localization {
available_locales: Vec<LanguageIdentifier>,
/// Fallback locale
fallback_locale: LanguageIdentifier,
/// Native names for locales
locale_native_names: HashMap<LanguageIdentifier, String>,
/// Cached string results per locale (only for strings without arguments)
string_cache: HashMap<LanguageIdentifier, HashMap<String, String>>,
@@ -95,15 +125,34 @@ impl Default for Localization {
ES_419.clone(),
ES_ES.clone(),
FR.clone(),
JA.clone(),
PT_BR.clone(),
PT_PT.clone(),
TH.clone(),
ZH_CN.clone(),
ZH_TW.clone(),
];
let locale_native_names = HashMap::from([
(EN_US, EN_US_NATIVE_NAME.to_owned()),
(EN_XA, EN_XA_NATIVE_NAME.to_owned()),
(DE, DE_NATIVE_NAME.to_owned()),
(ES_419, ES_419_NATIVE_NAME.to_owned()),
(ES_ES, ES_ES_NATIVE_NAME.to_owned()),
(FR, FR_NATIVE_NAME.to_owned()),
(JA, JA_NATIVE_NAME.to_owned()),
(PT_BR, PT_BR_NATIVE_NAME.to_owned()),
(PT_PT, PT_PT_NATIVE_NAME.to_owned()),
(TH, TH_NATIVE_NAME.to_owned()),
(ZH_CN, ZH_CN_NATIVE_NAME.to_owned()),
(ZH_TW, ZH_TW_NATIVE_NAME.to_owned()),
]);
Self {
current_locale: default_locale.to_owned(),
available_locales,
fallback_locale,
locale_native_names,
use_isolating: true,
normalized_key_cache: HashMap::new(),
string_cache: HashMap::new(),
@@ -391,6 +440,10 @@ impl Localization {
&self.fallback_locale
}
pub fn get_locale_native_name(&self, locale: &LanguageIdentifier) -> Option<&str> {
self.locale_native_names.get(locale).map(|s| s.as_str())
}
/// Gets cache statistics for monitoring performance
pub fn get_cache_stats(&self) -> Result<CacheStats, Box<dyn std::error::Error + Send + Sync>> {
let mut total_strings = 0;

View File

@@ -1,4 +1,10 @@
use crate::media::gif::ensure_latest_texture_from_cache;
use crate::media::images::ImageType;
use crate::media::AnimationMode;
use crate::urls::{UrlCache, UrlMimes};
use crate::ImageMetadata;
use crate::ObfuscationType;
use crate::RenderableMedia;
use crate::Result;
use egui::TextureHandle;
use image::{Delay, Frame};
@@ -7,9 +13,11 @@ use poll_promise::Promise;
use egui::ColorImage;
use std::collections::HashMap;
use std::fs::{create_dir_all, File};
use std::fs::{self, create_dir_all, File};
use std::sync::mpsc::Receiver;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant, SystemTime};
use std::{io, thread};
use hex::ToHex;
use sha2::Digest;
@@ -19,7 +27,7 @@ use tracing::warn;
#[derive(Default)]
pub struct TexturesCache {
cache: hashbrown::HashMap<String, TextureStateInternal>,
pub cache: hashbrown::HashMap<String, TextureStateInternal>,
}
impl TexturesCache {
@@ -27,7 +35,7 @@ impl TexturesCache {
&mut self,
url: &str,
closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>,
) -> LoadableTextureState {
) -> LoadableTextureState<'_> {
let internal = self.handle_and_get_state_internal(url, true, closure);
internal.into()
@@ -37,7 +45,7 @@ impl TexturesCache {
&mut self,
url: &str,
closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>,
) -> TextureState {
) -> TextureState<'_> {
let internal = self.handle_and_get_state_internal(url, false, closure);
internal.into()
@@ -88,7 +96,7 @@ impl TexturesCache {
});
}
pub fn get_and_handle(&mut self, url: &str) -> Option<LoadableTextureState> {
pub fn get_and_handle(&mut self, url: &str) -> Option<LoadableTextureState<'_>> {
self.cache.get_mut(url).map(|state| {
handle_occupied(state, true);
state.into()
@@ -139,6 +147,12 @@ pub enum TextureState<'a> {
Loaded(&'a mut TexturedImage),
}
impl<'a> TextureState<'a> {
pub fn is_loaded(&self) -> bool {
matches!(self, Self::Loaded(_))
}
}
impl<'a> From<&'a mut TextureStateInternal> for TextureState<'a> {
fn from(value: &'a mut TextureStateInternal) -> Self {
match value {
@@ -220,6 +234,7 @@ pub struct MediaCache {
pub cache_dir: path::PathBuf,
pub textures_cache: TexturesCache,
pub cache_type: MediaCacheType,
pub cache_size: Arc<Mutex<Option<u64>>>,
}
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
@@ -231,10 +246,29 @@ pub enum MediaCacheType {
impl MediaCache {
pub fn new(parent_dir: &Path, cache_type: MediaCacheType) -> Self {
let cache_dir = parent_dir.join(Self::rel_dir(cache_type));
let cache_dir_clone = cache_dir.clone();
let cache_size = Arc::new(Mutex::new(None));
let cache_size_clone = Arc::clone(&cache_size);
thread::spawn(move || {
let mut last_checked = Instant::now() - Duration::from_secs(999);
loop {
// check cache folder size every 60 s
if last_checked.elapsed() >= Duration::from_secs(60) {
let size = compute_folder_size(&cache_dir_clone);
*cache_size_clone.lock().unwrap() = Some(size);
last_checked = Instant::now();
}
thread::sleep(Duration::from_secs(5));
}
});
Self {
cache_dir,
textures_cache: TexturesCache::default(),
cache_type,
cache_size,
}
}
@@ -331,8 +365,14 @@ impl MediaCache {
);
}
}
Ok(())
}
fn clear(&mut self) {
self.textures_cache.cache.clear();
*self.cache_size.try_lock().unwrap() = Some(0);
}
}
fn color_image_to_rgba(color_image: ColorImage) -> image::RgbaImage {
@@ -349,10 +389,33 @@ fn color_image_to_rgba(color_image: ColorImage) -> image::RgbaImage {
.expect("Failed to create RgbaImage from ColorImage")
}
fn compute_folder_size<P: AsRef<Path>>(path: P) -> u64 {
fn walk(path: &Path) -> u64 {
let mut size = 0;
if let Ok(entries) = fs::read_dir(path) {
for entry in entries.flatten() {
let path = entry.path();
if let Ok(metadata) = entry.metadata() {
if metadata.is_file() {
size += metadata.len();
} else if metadata.is_dir() {
size += walk(&path);
}
}
}
}
size
}
walk(path.as_ref())
}
pub struct Images {
pub base_path: path::PathBuf,
pub static_imgs: MediaCache,
pub gifs: MediaCache,
pub urls: UrlMimes,
/// cached imeta data
pub metadata: HashMap<String, ImageMetadata>,
pub gif_states: GifStateMap,
}
@@ -360,10 +423,12 @@ impl Images {
/// path to directory to place [`MediaCache`]s
pub fn new(path: path::PathBuf) -> Self {
Self {
base_path: path.clone(),
static_imgs: MediaCache::new(&path, MediaCacheType::Image),
gifs: MediaCache::new(&path, MediaCacheType::Gif),
urls: UrlMimes::new(UrlCache::new(path.join(UrlCache::rel_dir()))),
gif_states: Default::default(),
metadata: Default::default(),
}
}
@@ -372,6 +437,65 @@ impl Images {
self.gifs.migrate_v0()
}
pub fn get_renderable_media(&mut self, url: &str) -> Option<RenderableMedia> {
Self::find_renderable_media(&mut self.urls, &self.metadata, url)
}
pub fn find_renderable_media(
urls: &mut UrlMimes,
imeta: &HashMap<String, ImageMetadata>,
url: &str,
) -> Option<RenderableMedia> {
let media_type = crate::urls::supported_mime_hosted_at_url(urls, url)?;
let obfuscation_type = match imeta.get(url) {
Some(blur) => ObfuscationType::Blurhash(blur.clone()),
None => ObfuscationType::Default,
};
Some(RenderableMedia {
url: url.to_string(),
media_type,
obfuscation_type,
})
}
pub fn latest_texture(
&mut self,
ui: &mut egui::Ui,
url: &str,
img_type: ImageType,
animation_mode: AnimationMode,
) -> Option<TextureHandle> {
let cache_type = crate::urls::supported_mime_hosted_at_url(&mut self.urls, url)?;
let cache_dir = self.get_cache(cache_type).cache_dir.clone();
let is_loaded = self
.get_cache_mut(cache_type)
.textures_cache
.handle_and_get_or_insert(url, || {
crate::media::images::fetch_img(&cache_dir, ui.ctx(), url, img_type, cache_type)
})
.is_loaded();
if !is_loaded {
return None;
}
let cache = match cache_type {
MediaCacheType::Image => &mut self.static_imgs,
MediaCacheType::Gif => &mut self.gifs,
};
ensure_latest_texture_from_cache(
ui,
url,
&mut self.gif_states,
&mut cache.textures_cache,
animation_mode,
)
}
pub fn get_cache(&self, cache_type: MediaCacheType) -> &MediaCache {
match cache_type {
MediaCacheType::Image => &self.static_imgs,
@@ -385,6 +509,26 @@ impl Images {
MediaCacheType::Gif => &mut self.gifs,
}
}
pub fn clear_folder_contents(&mut self) -> io::Result<()> {
for entry in fs::read_dir(self.base_path.clone())? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
fs::remove_dir_all(path)?;
} else {
fs::remove_file(path)?;
}
}
self.urls.cache.clear();
self.static_imgs.clear();
self.gifs.clear();
self.gif_states.clear();
Ok(())
}
}
pub type GifStateMap = HashMap<String, GifState>;
@@ -395,3 +539,35 @@ pub struct GifState {
pub next_frame_time: Option<SystemTime>,
pub last_frame_index: usize,
}
pub struct LatestTexture {
pub texture: TextureHandle,
pub request_next_repaint: Option<SystemTime>,
}
pub fn get_render_state<'a>(
ctx: &egui::Context,
images: &'a mut Images,
cache_type: MediaCacheType,
url: &str,
img_type: ImageType,
) -> RenderState<'a> {
let cache = match cache_type {
MediaCacheType::Image => &mut images.static_imgs,
MediaCacheType::Gif => &mut images.gifs,
};
let texture_state = cache.textures_cache.handle_and_get_or_insert(url, || {
crate::media::images::fetch_img(&cache.cache_dir, ctx, url, img_type, cache_type)
});
RenderState {
texture_state,
gifs: &mut images.gif_states,
}
}
pub struct RenderState<'a> {
pub texture_state: TextureState<'a>,
pub gifs: &'a mut GifStateMap,
}

View File

@@ -23,6 +23,7 @@ impl JobPool {
pub fn new(num_threads: usize) -> Self {
let (tx, rx) = mpsc::channel::<Job>();
// TODO(jb55) why not mpmc here !???
let arc_rx = Arc::new(Mutex::new(rx));
for _ in 0..num_threads {
let arc_rx_clone = arc_rx.clone();

View File

@@ -1,6 +1,6 @@
use crate::JobPool;
use egui::TextureHandle;
use hashbrown::{hash_map::RawEntryMut, HashMap};
use notedeck::JobPool;
use poll_promise::Promise;
#[derive(Default)]

View File

@@ -12,16 +12,21 @@ mod frame_history;
pub mod i18n;
mod imgcache;
mod job_pool;
mod jobs;
pub mod media;
mod muted;
pub mod name;
mod nip51_set;
pub mod note;
mod notecache;
mod options;
mod persist;
pub mod platform;
pub mod profile;
pub mod relay_debug;
pub mod relayspec;
mod result;
mod setup;
pub mod storage;
mod style;
pub mod theme;
@@ -41,23 +46,33 @@ pub use account::relay::RelayAction;
pub use account::FALLBACK_PUBKEY;
pub use app::{App, AppAction, Notedeck};
pub use args::Args;
pub use context::AppContext;
pub use context::{AppContext, SoftKeyboardContext};
pub use error::{show_one_error_message, Error, FilterError, ZapError};
pub use filter::{FilterState, FilterStates, UnifiedSubscription};
pub use fonts::NamedFontFamily;
pub use i18n::{CacheStats, FluentArgs, FluentValue, LanguageIdentifier, Localization};
pub use imgcache::{
Animation, GifState, GifStateMap, ImageFrame, Images, LoadableTextureState, MediaCache,
MediaCacheType, TextureFrame, TextureState, TexturedImage, TexturesCache,
get_render_state, Animation, GifState, GifStateMap, ImageFrame, Images, LatestTexture,
LoadableTextureState, MediaCache, MediaCacheType, RenderState, TextureFrame, TextureState,
TexturedImage, TexturesCache,
};
pub use job_pool::JobPool;
pub use jobs::{
BlurhashParams, Job, JobError, JobId, JobParams, JobParamsOwned, JobState, JobsCache,
};
pub use media::{
compute_blurhash, update_imeta_blurhashes, ImageMetadata, ImageType, MediaAction,
ObfuscationType, PixelDimensions, PointDimensions, RenderableMedia,
};
pub use muted::{MuteFun, Muted};
pub use name::NostrName;
pub use nip51_set::{create_nip51_set, Nip51Set, Nip51SetCache};
pub use note::{
BroadcastContext, ContextSelection, NoteAction, NoteContext, NoteContextSelection, NoteRef,
RootIdError, RootNoteId, RootNoteIdBuf, ScrollInfo, ZapAction,
};
pub use notecache::{CachedNote, NoteCache};
pub use options::NotedeckOptions;
pub use persist::*;
pub use profile::get_profile_url;
pub use relay_debug::RelayDebugView;
@@ -67,13 +82,14 @@ pub use storage::{AccountStorage, DataPath, DataPathType, Directory};
pub use style::NotedeckTextStyle;
pub use theme::ColorTheme;
pub use time::time_ago_since;
pub use time::time_format;
pub use timecache::TimeCached;
pub use unknowns::{get_unknown_note_ids, NoteRefsUnkIdAction, SingleUnkIdAction, UnknownIds};
pub use urls::{supported_mime_hosted_at_url, SupportedMimeType, UrlMimes};
pub use user_account::UserAccount;
pub use wallet::{
get_current_wallet, get_wallet_for, GlobalWallet, Wallet, WalletError, WalletType,
WalletUIState, ZapWallet,
get_current_wallet, get_current_wallet_mut, get_wallet_for, GlobalWallet, Wallet, WalletError,
WalletType, WalletUIState, ZapWallet,
};
pub use zaps::{
get_current_default_msats, AnyZapState, DefaultZapError, DefaultZapMsats, NoteZapTarget,

View File

@@ -0,0 +1,127 @@
use crate::{Images, MediaCacheType, TexturedImage};
use poll_promise::Promise;
/// Tracks where media was on the screen so that
/// we can do fun animations when opening the
/// Media Viewer
#[derive(Debug, Clone)]
pub struct MediaInfo {
/// The original screen position where it
/// was rendered from. This is not where
/// it should be rendered in the scene.
pub original_position: egui::Rect,
pub url: String,
}
/// Contains various information for when a user
/// clicks a piece of media. It contains the current
/// location on screen for each piece of media.
///
/// Viewers can use this to smoothly transition from
/// the timeline to the viewer
#[derive(Debug, Clone, Default)]
pub struct ViewMediaInfo {
pub clicked_index: usize,
pub medias: Vec<MediaInfo>,
}
impl ViewMediaInfo {
pub fn clicked_media(&self) -> &MediaInfo {
&self.medias[self.clicked_index]
}
}
/// Actions generated by media ui interactions
pub enum MediaAction {
/// An image was clicked on in a carousel, we have
/// the opportunity to open into a fullscreen media viewer
/// with a list of url values
ViewMedias(ViewMediaInfo),
FetchImage {
url: String,
cache_type: MediaCacheType,
no_pfp_promise: Promise<Option<Result<TexturedImage, crate::Error>>>,
},
DoneLoading {
url: String,
cache_type: MediaCacheType,
},
}
impl std::fmt::Debug for MediaAction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ViewMedias(ViewMediaInfo {
clicked_index,
medias,
}) => f
.debug_struct("ViewMedias")
.field("clicked_index", clicked_index)
.field("media", medias)
.finish(),
Self::FetchImage {
url,
cache_type,
no_pfp_promise,
} => f
.debug_struct("FetchNoPfpImage")
.field("url", url)
.field("cache_type", cache_type)
.field("no_pfp_promise ready", &no_pfp_promise.ready().is_some())
.finish(),
Self::DoneLoading { url, cache_type } => f
.debug_struct("DoneLoading")
.field("url", url)
.field("cache_type", cache_type)
.finish(),
}
}
}
impl MediaAction {
/// Handle view media actions
pub fn on_view_media(&self, handler: impl FnOnce(&ViewMediaInfo)) {
if let MediaAction::ViewMedias(view_medias) = self {
handler(view_medias)
}
}
/// Default processing logic for Media Actions. We don't handle ViewMedias here since
/// this may be app specific ?
pub fn process_default_media_actions(self, images: &mut Images) {
match self {
MediaAction::ViewMedias(_urls) => {
// NOTE(jb55): don't assume we want to show a fullscreen
// media viewer we can use on_view_media for that. We
// also don't want to have a notedeck_ui dependency in
// the notedeck lib (MediaViewerState)
//
// In general our notedeck crate should be pretty
// agnostic to functionallity in general unless it low
// level like image rendering.
//
//mview_state.set_urls(urls);
}
MediaAction::FetchImage {
url,
cache_type,
no_pfp_promise: promise,
} => {
images
.get_cache_mut(cache_type)
.textures_cache
.insert_pending(&url, promise);
}
MediaAction::DoneLoading { url, cache_type } => {
let cache = match cache_type {
MediaCacheType::Image => &mut images.static_imgs,
MediaCacheType::Gif => &mut images.gifs,
};
cache.textures_cache.move_to_loaded(&url);
}
}
}
}

View File

@@ -5,8 +5,8 @@ use nostrdb::Note;
use crate::jobs::{Job, JobError, JobParamsOwned};
#[derive(Clone)]
pub struct Blur<'a> {
pub blurhash: &'a str,
pub struct ImageMetadata {
pub blurhash: String,
pub dimensions: Option<PixelDimensions>, // width and height in pixels
}
@@ -44,7 +44,7 @@ impl PointDimensions {
}
}
impl Blur<'_> {
impl ImageMetadata {
pub fn scaled_pixel_dimensions(
&self,
ui: &egui::Ui,
@@ -75,9 +75,8 @@ impl Blur<'_> {
}
}
pub fn imeta_blurhashes<'a>(note: &'a Note) -> HashMap<&'a str, Blur<'a>> {
let mut blurs = HashMap::new();
/// Find blurhashes in image metadata and update our cache
pub fn update_imeta_blurhashes(note: &Note, blurs: &mut HashMap<String, ImageMetadata>) {
for tag in note.tags() {
let mut tag_iter = tag.into_iter();
if tag_iter
@@ -93,13 +92,11 @@ pub fn imeta_blurhashes<'a>(note: &'a Note) -> HashMap<&'a str, Blur<'a>> {
continue;
};
blurs.insert(url, blur);
blurs.insert(url.to_string(), blur);
}
blurs
}
fn find_blur(tag_iter: nostrdb::TagIter) -> Option<(&str, Blur)> {
fn find_blur(tag_iter: nostrdb::TagIter<'_>) -> Option<(String, ImageMetadata)> {
let mut url = None;
let mut blurhash = None;
let mut dims = None;
@@ -138,21 +135,21 @@ fn find_blur(tag_iter: nostrdb::TagIter) -> Option<(&str, Blur)> {
});
Some((
url,
Blur {
blurhash,
url.to_string(),
ImageMetadata {
blurhash: blurhash.to_string(),
dimensions,
},
))
}
#[derive(Clone)]
pub enum ObfuscationType<'a> {
Blurhash(Blur<'a>),
pub enum ObfuscationType {
Blurhash(ImageMetadata),
Default,
}
pub(crate) fn compute_blurhash(
pub fn compute_blurhash(
params: Option<JobParamsOwned>,
dims: PixelDimensions,
) -> Result<Job, JobError> {
@@ -185,9 +182,9 @@ fn generate_blurhash_texturehandle(
url: &str,
width: u32,
height: u32,
) -> notedeck::Result<egui::TextureHandle> {
) -> Result<egui::TextureHandle, crate::Error> {
let bytes = blurhash::decode(blurhash, width, height, 1.0)
.map_err(|e| notedeck::Error::Generic(e.to_string()))?;
.map_err(|e| crate::Error::Generic(e.to_string()))?;
let img = egui::ColorImage::from_rgba_unmultiplied([width as usize, height as usize], &bytes);
Ok(ctx.load_texture(url, img, Default::default()))

View File

@@ -0,0 +1,164 @@
use std::{
sync::mpsc::TryRecvError,
time::{Instant, SystemTime},
};
use crate::media::AnimationMode;
use crate::Animation;
use crate::{GifState, GifStateMap, TextureState, TexturedImage, TexturesCache};
use egui::TextureHandle;
use std::time::Duration;
pub fn ensure_latest_texture_from_cache(
ui: &egui::Ui,
url: &str,
gifs: &mut GifStateMap,
textures: &mut TexturesCache,
animation_mode: AnimationMode,
) -> Option<TextureHandle> {
let tstate = textures.cache.get_mut(url)?;
let TextureState::Loaded(img) = tstate.into() else {
return None;
};
Some(ensure_latest_texture(ui, url, gifs, img, animation_mode))
}
struct ProcessedGifFrame {
texture: TextureHandle,
maybe_new_state: Option<GifState>,
repaint_at: Option<SystemTime>,
}
/// Process a gif state frame, and optionally present a new
/// state and when to repaint it
fn process_gif_frame(
animation: &Animation,
frame_state: Option<&GifState>,
animation_mode: AnimationMode,
) -> ProcessedGifFrame {
let now = Instant::now();
match frame_state {
Some(prev_state) => {
let should_advance = animation_mode.can_animate()
&& (now - prev_state.last_frame_rendered >= prev_state.last_frame_duration);
if should_advance {
let maybe_new_index = if animation.receiver.is_some()
|| prev_state.last_frame_index < animation.num_frames() - 1
{
prev_state.last_frame_index + 1
} else {
0
};
match animation.get_frame(maybe_new_index) {
Some(frame) => {
let next_frame_time = match animation_mode {
AnimationMode::Continuous { fps } => match fps {
Some(fps) => {
let max_delay_ms = Duration::from_millis((1000.0 / fps) as u64);
SystemTime::now().checked_add(frame.delay.max(max_delay_ms))
}
None => SystemTime::now().checked_add(frame.delay),
},
AnimationMode::NoAnimation | AnimationMode::Reactive => None,
};
ProcessedGifFrame {
texture: frame.texture.clone(),
maybe_new_state: Some(GifState {
last_frame_rendered: now,
last_frame_duration: frame.delay,
next_frame_time,
last_frame_index: maybe_new_index,
}),
repaint_at: next_frame_time,
}
}
None => {
let (texture, maybe_new_state) =
match animation.get_frame(prev_state.last_frame_index) {
Some(frame) => (frame.texture.clone(), None),
None => (animation.first_frame.texture.clone(), None),
};
ProcessedGifFrame {
texture,
maybe_new_state,
repaint_at: prev_state.next_frame_time,
}
}
}
} else {
let (texture, maybe_new_state) =
match animation.get_frame(prev_state.last_frame_index) {
Some(frame) => (frame.texture.clone(), None),
None => (animation.first_frame.texture.clone(), None),
};
ProcessedGifFrame {
texture,
maybe_new_state,
repaint_at: prev_state.next_frame_time,
}
}
}
None => ProcessedGifFrame {
texture: animation.first_frame.texture.clone(),
maybe_new_state: Some(GifState {
last_frame_rendered: now,
last_frame_duration: animation.first_frame.delay,
next_frame_time: None,
last_frame_index: 0,
}),
repaint_at: None,
},
}
}
pub fn ensure_latest_texture(
ui: &egui::Ui,
url: &str,
gifs: &mut GifStateMap,
img: &mut TexturedImage,
animation_mode: AnimationMode,
) -> TextureHandle {
match img {
TexturedImage::Static(handle) => handle.clone(),
TexturedImage::Animated(animation) => {
if let Some(receiver) = &animation.receiver {
loop {
match receiver.try_recv() {
Ok(frame) => animation.other_frames.push(frame),
Err(TryRecvError::Empty) => {
break;
}
Err(TryRecvError::Disconnected) => {
animation.receiver = None;
break;
}
}
}
}
let next_state = process_gif_frame(animation, gifs.get(url), animation_mode);
if let Some(new_state) = next_state.maybe_new_state {
gifs.insert(url.to_owned(), new_state);
}
if let Some(repaint) = next_state.repaint_at {
tracing::trace!("requesting repaint for {url} after {repaint:?}");
if let Ok(dur) = repaint.duration_since(SystemTime::now()) {
ui.ctx().request_repaint_after(dur);
}
}
next_state.texture
}
}
}

View File

@@ -0,0 +1,475 @@
use crate::{Animation, ImageFrame, MediaCache, MediaCacheType, TextureFrame, TexturedImage};
use egui::{pos2, Color32, ColorImage, Context, Rect, Sense, SizeHint};
use image::codecs::gif::GifDecoder;
use image::imageops::FilterType;
use image::{AnimationDecoder, DynamicImage, FlatSamples, Frame};
use poll_promise::Promise;
use std::collections::VecDeque;
use std::io::Cursor;
use std::path::PathBuf;
use std::path::{self, Path};
use std::sync::mpsc;
use std::sync::mpsc::SyncSender;
use std::thread;
use std::time::Duration;
use tokio::fs;
// NOTE(jb55): chatgpt wrote this because I was too dumb to
pub fn aspect_fill(
ui: &mut egui::Ui,
sense: Sense,
texture_id: egui::TextureId,
aspect_ratio: f32,
) -> egui::Response {
let frame = ui.available_rect_before_wrap(); // Get the available frame space in the current layout
let frame_ratio = frame.width() / frame.height();
let (width, height) = if frame_ratio > aspect_ratio {
// Frame is wider than the content
(frame.width(), frame.width() / aspect_ratio)
} else {
// Frame is taller than the content
(frame.height() * aspect_ratio, frame.height())
};
let content_rect = Rect::from_min_size(
frame.min
+ egui::vec2(
(frame.width() - width) / 2.0,
(frame.height() - height) / 2.0,
),
egui::vec2(width, height),
);
// Set the clipping rectangle to the frame
//let clip_rect = ui.clip_rect(); // Preserve the original clipping rectangle
//ui.set_clip_rect(frame);
let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0));
let (response, painter) = ui.allocate_painter(ui.available_size(), sense);
// Draw the texture within the calculated rect, potentially clipping it
painter.rect_filled(content_rect, 0.0, ui.ctx().style().visuals.window_fill());
painter.image(texture_id, content_rect, uv, Color32::WHITE);
// Restore the original clipping rectangle
//ui.set_clip_rect(clip_rect);
response
}
#[profiling::function]
pub fn round_image(image: &mut ColorImage) {
// The radius to the edge of of the avatar circle
let edge_radius = image.size[0] as f32 / 2.0;
let edge_radius_squared = edge_radius * edge_radius;
for (pixnum, pixel) in image.pixels.iter_mut().enumerate() {
// y coordinate
let uy = pixnum / image.size[0];
let y = uy as f32;
let y_offset = edge_radius - y;
// x coordinate
let ux = pixnum % image.size[0];
let x = ux as f32;
let x_offset = edge_radius - x;
// The radius to this pixel (may be inside or outside the circle)
let pixel_radius_squared: f32 = x_offset * x_offset + y_offset * y_offset;
// If inside of the avatar circle
if pixel_radius_squared <= edge_radius_squared {
// squareroot to find how many pixels we are from the edge
let pixel_radius: f32 = pixel_radius_squared.sqrt();
let distance = edge_radius - pixel_radius;
// If we are within 1 pixel of the edge, we should fade, to
// antialias the edge of the circle. 1 pixel from the edge should
// be 100% of the original color, and right on the edge should be
// 0% of the original color.
if distance <= 1.0 {
*pixel = Color32::from_rgba_premultiplied(
(pixel.r() as f32 * distance) as u8,
(pixel.g() as f32 * distance) as u8,
(pixel.b() as f32 * distance) as u8,
(pixel.a() as f32 * distance) as u8,
);
}
} else {
// Outside of the avatar circle
*pixel = Color32::TRANSPARENT;
}
}
}
/// If the image's longest dimension is greater than max_edge, downscale
fn resize_image_if_too_big(
image: image::DynamicImage,
max_edge: u32,
filter: FilterType,
) -> image::DynamicImage {
// if we have no size hint, resize to something reasonable
let w = image.width();
let h = image.height();
let long = w.max(h);
if long > max_edge {
let scale = max_edge as f32 / long as f32;
let new_w = (w as f32 * scale).round() as u32;
let new_h = (h as f32 * scale).round() as u32;
image.resize(new_w, new_h, filter)
} else {
image
}
}
///
/// Process an image, resizing so we don't blow up video memory or even crash
///
/// For profile pictures, make them round and small to fit the size hint
/// For everything else, either:
///
/// - resize to the size hint
/// - keep the size if the longest dimension is less than MAX_IMG_LENGTH
/// - resize if any larger, using [`resize_image_if_too_big`]
///
#[profiling::function]
fn process_image(imgtyp: ImageType, mut image: image::DynamicImage) -> ColorImage {
const MAX_IMG_LENGTH: u32 = 2048;
const FILTER_TYPE: FilterType = FilterType::CatmullRom;
match imgtyp {
ImageType::Content(size_hint) => {
let image = match size_hint {
None => resize_image_if_too_big(image, MAX_IMG_LENGTH, FILTER_TYPE),
Some((w, h)) => image.resize(w, h, FILTER_TYPE),
};
let image_buffer = image.into_rgba8();
ColorImage::from_rgba_unmultiplied(
[
image_buffer.width() as usize,
image_buffer.height() as usize,
],
image_buffer.as_flat_samples().as_slice(),
)
}
ImageType::Profile(size) => {
// Crop square
let smaller = image.width().min(image.height());
if image.width() > smaller {
let excess = image.width() - smaller;
image = image.crop_imm(excess / 2, 0, image.width() - excess, image.height());
} else if image.height() > smaller {
let excess = image.height() - smaller;
image = image.crop_imm(0, excess / 2, image.width(), image.height() - excess);
}
let image = image.resize(size, size, FilterType::CatmullRom); // DynamicImage
let image_buffer = image.into_rgba8(); // RgbaImage (ImageBuffer)
let mut color_image = ColorImage::from_rgba_unmultiplied(
[
image_buffer.width() as usize,
image_buffer.height() as usize,
],
image_buffer.as_flat_samples().as_slice(),
);
round_image(&mut color_image);
color_image
}
}
}
#[profiling::function]
fn parse_img_response(
response: ehttp::Response,
imgtyp: ImageType,
) -> Result<ColorImage, crate::Error> {
let content_type = response.content_type().unwrap_or_default();
let size_hint = match imgtyp {
ImageType::Profile(size) => SizeHint::Size(size, size),
ImageType::Content(Some((w, h))) => SizeHint::Size(w, h),
ImageType::Content(None) => SizeHint::default(),
};
if content_type.starts_with("image/svg") {
profiling::scope!("load_svg");
let mut color_image =
egui_extras::image::load_svg_bytes_with_size(&response.bytes, Some(size_hint))?;
round_image(&mut color_image);
Ok(color_image)
} else if content_type.starts_with("image/") {
profiling::scope!("load_from_memory");
let dyn_image = image::load_from_memory(&response.bytes)?;
Ok(process_image(imgtyp, dyn_image))
} else {
Err(format!("Expected image, found content-type {content_type:?}").into())
}
}
fn fetch_img_from_disk(
ctx: &egui::Context,
url: &str,
path: &path::Path,
cache_type: MediaCacheType,
) -> Promise<Option<Result<TexturedImage, crate::Error>>> {
let ctx = ctx.clone();
let url = url.to_owned();
let path = path.to_owned();
Promise::spawn_async(async move {
Some(async_fetch_img_from_disk(ctx, url, &path, cache_type).await)
})
}
async fn async_fetch_img_from_disk(
ctx: egui::Context,
url: String,
path: &path::Path,
cache_type: MediaCacheType,
) -> Result<TexturedImage, crate::Error> {
match cache_type {
MediaCacheType::Image => {
let data = fs::read(path).await?;
let image_buffer = image::load_from_memory(&data).map_err(crate::Error::Image)?;
let img = buffer_to_color_image(
image_buffer.as_flat_samples_u8(),
image_buffer.width(),
image_buffer.height(),
);
Ok(TexturedImage::Static(ctx.load_texture(
&url,
img,
Default::default(),
)))
}
MediaCacheType::Gif => {
let gif_bytes = fs::read(path).await?; // Read entire file into a Vec<u8>
generate_gif(ctx, url, path, gif_bytes, false, |i| {
buffer_to_color_image(i.as_flat_samples_u8(), i.width(), i.height())
})
}
}
}
fn generate_gif(
ctx: egui::Context,
url: String,
path: &path::Path,
data: Vec<u8>,
write_to_disk: bool,
process_to_egui: impl Fn(DynamicImage) -> ColorImage + Send + Copy + 'static,
) -> Result<TexturedImage, crate::Error> {
let decoder = {
let reader = Cursor::new(data.as_slice());
GifDecoder::new(reader)?
};
let (tex_input, tex_output) = mpsc::sync_channel(4);
let (maybe_encoder_input, maybe_encoder_output) = if write_to_disk {
let (inp, out) = mpsc::sync_channel(4);
(Some(inp), Some(out))
} else {
(None, None)
};
let mut frames: VecDeque<Frame> = decoder
.into_frames()
.collect::<std::result::Result<VecDeque<_>, image::ImageError>>()
.map_err(|e| crate::Error::Generic(e.to_string()))?;
let first_frame = frames.pop_front().map(|frame| {
generate_animation_frame(
&ctx,
&url,
0,
frame,
maybe_encoder_input.as_ref(),
process_to_egui,
)
});
let cur_url = url.clone();
thread::spawn(move || {
for (index, frame) in frames.into_iter().enumerate() {
let texture_frame = generate_animation_frame(
&ctx,
&cur_url,
index,
frame,
maybe_encoder_input.as_ref(),
process_to_egui,
);
if tex_input.send(texture_frame).is_err() {
tracing::debug!("AnimationTextureFrame mpsc stopped abruptly");
break;
}
}
});
if let Some(encoder_output) = maybe_encoder_output {
let path = path.to_owned();
thread::spawn(move || {
let mut imgs = Vec::new();
while let Ok(img) = encoder_output.recv() {
imgs.push(img);
}
if let Err(e) = MediaCache::write_gif(&path, &url, imgs) {
tracing::error!("Could not write gif to disk: {e}");
}
});
}
first_frame.map_or_else(
|| {
Err(crate::Error::Generic(
"first frame not found for gif".to_owned(),
))
},
|first_frame| {
Ok(TexturedImage::Animated(Animation {
other_frames: Default::default(),
receiver: Some(tex_output),
first_frame,
}))
},
)
}
fn generate_animation_frame(
ctx: &egui::Context,
url: &str,
index: usize,
frame: image::Frame,
maybe_encoder_input: Option<&SyncSender<ImageFrame>>,
process_to_egui: impl Fn(DynamicImage) -> ColorImage + Send + 'static,
) -> TextureFrame {
let delay = Duration::from(frame.delay());
let img = DynamicImage::ImageRgba8(frame.into_buffer());
let color_img = process_to_egui(img);
if let Some(sender) = maybe_encoder_input {
if let Err(e) = sender.send(ImageFrame {
delay,
image: color_img.clone(),
}) {
tracing::error!("ImageFrame mpsc unexpectedly closed: {e}");
}
}
TextureFrame {
delay,
texture: ctx.load_texture(format!("{url}{index}"), color_img, Default::default()),
}
}
fn buffer_to_color_image(
samples: Option<FlatSamples<&[u8]>>,
width: u32,
height: u32,
) -> ColorImage {
// TODO(jb55): remove unwrap here
let flat_samples = samples.unwrap();
ColorImage::from_rgba_unmultiplied([width as usize, height as usize], flat_samples.as_slice())
}
pub fn fetch_binary_from_disk(path: PathBuf) -> Result<Vec<u8>, crate::Error> {
std::fs::read(path).map_err(|e| crate::Error::Generic(e.to_string()))
}
/// Controls type-specific handling
#[derive(Debug, Clone, Copy)]
pub enum ImageType {
/// Profile Image (size)
Profile(u32),
/// Content Image with optional size hint
Content(Option<(u32, u32)>),
}
pub fn fetch_img(
img_cache_path: &Path,
ctx: &egui::Context,
url: &str,
imgtyp: ImageType,
cache_type: MediaCacheType,
) -> Promise<Option<Result<TexturedImage, crate::Error>>> {
let key = MediaCache::key(url);
let path = img_cache_path.join(key);
if path.exists() {
fetch_img_from_disk(ctx, url, &path, cache_type)
} else {
fetch_img_from_net(img_cache_path, ctx, url, imgtyp, cache_type)
}
// TODO: fetch image from local cache
}
fn fetch_img_from_net(
cache_path: &path::Path,
ctx: &egui::Context,
url: &str,
imgtyp: ImageType,
cache_type: MediaCacheType,
) -> Promise<Option<Result<TexturedImage, crate::Error>>> {
let (sender, promise) = Promise::new();
let request = ehttp::Request::get(url);
let ctx = ctx.clone();
let cloned_url = url.to_owned();
let cache_path = cache_path.to_owned();
ehttp::fetch(request, move |response| {
let handle = response.map_err(crate::Error::Generic).and_then(|resp| {
match cache_type {
MediaCacheType::Image => {
let img = parse_img_response(resp, imgtyp);
img.map(|img| {
let texture_handle =
ctx.load_texture(&cloned_url, img.clone(), Default::default());
// write to disk
std::thread::spawn(move || {
MediaCache::write(&cache_path, &cloned_url, img)
});
TexturedImage::Static(texture_handle)
})
}
MediaCacheType::Gif => {
let gif_bytes = resp.bytes;
generate_gif(
ctx.clone(),
cloned_url,
&cache_path,
gif_bytes,
true,
move |img| process_image(imgtyp, img),
)
}
}
});
sender.send(Some(handle)); // send the results back to the UI thread.
ctx.request_repaint();
});
promise
}
pub fn fetch_no_pfp_promise(
ctx: &Context,
cache: &MediaCache,
) -> Promise<Option<Result<TexturedImage, crate::Error>>> {
crate::media::images::fetch_img(
&cache.cache_dir,
ctx,
crate::profile::no_pfp_url(),
ImageType::Profile(128),
MediaCacheType::Image,
)
}

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,32 @@
pub mod action;
pub mod blur;
pub mod gif;
pub mod images;
pub mod imeta;
pub mod renderable;
pub use action::{MediaAction, MediaInfo, ViewMediaInfo};
pub use blur::{
compute_blurhash, update_imeta_blurhashes, ImageMetadata, ObfuscationType, PixelDimensions,
PointDimensions,
};
pub use images::ImageType;
pub use renderable::RenderableMedia;
#[derive(Copy, Clone, Debug)]
pub enum AnimationMode {
/// Only render when scrolling, network activity, etc
Reactive,
/// Continuous with an optional target fps
Continuous { fps: Option<f32> },
/// Disable animation
NoAnimation,
}
impl AnimationMode {
pub fn can_animate(&self) -> bool {
!matches!(self, Self::NoAnimation)
}
}

View File

@@ -0,0 +1,9 @@
use super::ObfuscationType;
use crate::MediaCacheType;
/// Media that is prepared for rendering. Use [`Images::get_renderable_media`] to get these
pub struct RenderableMedia {
pub url: String,
pub media_type: MediaCacheType,
pub obfuscation_type: ObfuscationType,
}

View File

@@ -80,4 +80,8 @@ impl Muted {
false
}
pub fn is_pk_muted(&self, pk: &[u8; 32]) -> bool {
self.pubkeys.contains(pk)
}
}

View File

@@ -0,0 +1,206 @@
use enostr::{Pubkey, RelayPool};
use indexmap::IndexMap;
use nostrdb::{Filter, Ndb, Note, Transaction};
use uuid::Uuid;
use crate::{UnifiedSubscription, UnknownIds};
/// Keeps track of most recent NIP-51 sets
#[derive(Debug)]
pub struct Nip51SetCache {
pub sub: UnifiedSubscription,
cached_notes: IndexMap<PackId, Nip51Set>,
}
type PackId = String;
impl Nip51SetCache {
pub fn new(
pool: &mut RelayPool,
ndb: &Ndb,
txn: &Transaction,
unknown_ids: &mut UnknownIds,
nip51_set_filter: Vec<Filter>,
) -> Option<Self> {
let subid = Uuid::new_v4().to_string();
let mut cached_notes = IndexMap::default();
let notes: Option<Vec<Note>> = if let Ok(results) = ndb.query(txn, &nip51_set_filter, 500) {
Some(results.into_iter().map(|r| r.note).collect())
} else {
None
};
if let Some(notes) = notes {
add(notes, &mut cached_notes, ndb, txn, unknown_ids);
}
let sub = match ndb.subscribe(&nip51_set_filter) {
Ok(sub) => sub,
Err(e) => {
tracing::error!("Could not ndb subscribe: {e}");
return None;
}
};
pool.subscribe(subid.clone(), nip51_set_filter);
Some(Self {
sub: UnifiedSubscription {
local: sub,
remote: subid,
},
cached_notes,
})
}
pub fn poll_for_notes(&mut self, ndb: &Ndb, unknown_ids: &mut UnknownIds) {
let new_notes = ndb.poll_for_notes(self.sub.local, 5);
if new_notes.is_empty() {
return;
}
let txn = Transaction::new(ndb).expect("txn");
let notes: Vec<Note> = new_notes
.into_iter()
.filter_map(|new_note_key| ndb.get_note_by_key(&txn, new_note_key).ok())
.collect();
add(notes, &mut self.cached_notes, ndb, &txn, unknown_ids);
}
pub fn iter(&self) -> impl IntoIterator<Item = &Nip51Set> {
self.cached_notes.values()
}
pub fn len(&self) -> usize {
self.cached_notes.len()
}
pub fn is_empty(&self) -> bool {
self.cached_notes.is_empty()
}
pub fn at_index(&self, index: usize) -> Option<&Nip51Set> {
self.cached_notes.get_index(index).map(|(_, s)| s)
}
}
fn add(
notes: Vec<Note>,
cache: &mut IndexMap<PackId, Nip51Set>,
ndb: &Ndb,
txn: &Transaction,
unknown_ids: &mut UnknownIds,
) {
for note in notes {
let Some(new_pack) = create_nip51_set(note) else {
continue;
};
if let Some(cur_cached) = cache.get(&new_pack.identifier) {
if new_pack.created_at <= cur_cached.created_at {
continue;
}
}
for pk in &new_pack.pks {
unknown_ids.add_pubkey_if_missing(ndb, txn, pk);
}
cache.insert(new_pack.identifier.clone(), new_pack);
}
}
pub fn create_nip51_set(note: Note) -> Option<Nip51Set> {
let mut identifier = None;
let mut title = None;
let mut image = None;
let mut description = None;
let mut pks = Vec::new();
for tag in note.tags() {
if tag.count() < 2 {
continue;
}
let Some(first) = tag.get_str(0) else {
continue;
};
match first {
"p" => {
let Some(pk) = tag.get_id(1) else {
continue;
};
pks.push(Pubkey::new(*pk));
}
"d" => {
let Some(id) = tag.get_str(1) else {
continue;
};
identifier = Some(id.to_owned());
}
"image" => {
let Some(cur_img) = tag.get_str(1) else {
continue;
};
image = Some(cur_img.to_owned());
}
"title" => {
let Some(cur_title) = tag.get_str(1) else {
continue;
};
title = Some(cur_title.to_owned());
}
"description" => {
let Some(cur_desc) = tag.get_str(1) else {
continue;
};
description = Some(cur_desc.to_owned());
}
_ => {
continue;
}
};
}
let identifier = identifier?;
Some(Nip51Set {
identifier,
title,
image,
description,
pks,
created_at: note.created_at(),
})
}
/// NIP-51 Set. Read only (do not use for writing)
pub struct Nip51Set {
pub identifier: String, // 'd' tag
pub title: Option<String>,
pub image: Option<String>,
pub description: Option<String>,
pub pks: Vec<Pubkey>,
created_at: u64,
}
impl std::fmt::Debug for Nip51Set {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Nip51Set")
.field("identifier", &self.identifier)
.field("title", &self.title)
.field("image", &self.image)
.field("description", &self.description)
.field("pks", &self.pks.len())
.field("created_at", &self.created_at)
.finish()
}
}

View File

@@ -1,8 +1,7 @@
use super::context::ContextSelection;
use crate::{zaps::NoteZapTargetOwned, Images, MediaCacheType, TexturedImage};
use crate::{zaps::NoteZapTargetOwned, MediaAction};
use egui::Vec2;
use enostr::{NoteId, Pubkey};
use poll_promise::Promise;
#[derive(Debug)]
pub struct ScrollInfo {
@@ -25,7 +24,11 @@ pub enum NoteAction {
Profile(Pubkey),
/// User has clicked a note link
Note { note_id: NoteId, preview: bool },
Note {
note_id: NoteId,
preview: bool,
scroll_offset: f32,
},
/// User has selected some context option
Context(ContextSelection),
@@ -45,6 +48,7 @@ impl NoteAction {
NoteAction::Note {
note_id: id,
preview: false,
scroll_offset: 0.0,
}
}
}
@@ -61,62 +65,3 @@ pub struct ZapTargetAmount {
pub target: NoteZapTargetOwned,
pub specified_msats: Option<u64>, // if None use default amount
}
pub enum MediaAction {
FetchImage {
url: String,
cache_type: MediaCacheType,
no_pfp_promise: Promise<Option<Result<TexturedImage, crate::Error>>>,
},
DoneLoading {
url: String,
cache_type: MediaCacheType,
},
}
impl std::fmt::Debug for MediaAction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::FetchImage {
url,
cache_type,
no_pfp_promise,
} => f
.debug_struct("FetchNoPfpImage")
.field("url", url)
.field("cache_type", cache_type)
.field("no_pfp_promise ready", &no_pfp_promise.ready().is_some())
.finish(),
Self::DoneLoading { url, cache_type } => f
.debug_struct("DoneLoading")
.field("url", url)
.field("cache_type", cache_type)
.finish(),
}
}
}
impl MediaAction {
pub fn process(self, images: &mut Images) {
match self {
MediaAction::FetchImage {
url,
cache_type,
no_pfp_promise: promise,
} => {
images
.get_cache_mut(cache_type)
.textures_cache
.insert_pending(&url, promise);
}
MediaAction::DoneLoading { url, cache_type } => {
let cache = match cache_type {
MediaCacheType::Image => &mut images.static_imgs,
MediaCacheType::Gif => &mut images.gifs,
};
cache.textures_cache.move_to_loaded(&url);
}
}
}
}

View File

@@ -1,10 +1,11 @@
mod action;
mod context;
pub use action::{MediaAction, NoteAction, ScrollInfo, ZapAction, ZapTargetAmount};
pub use action::{NoteAction, ScrollInfo, ZapAction, ZapTargetAmount};
pub use context::{BroadcastContext, ContextSelection, NoteContextSelection};
use crate::Accounts;
use crate::GlobalWallet;
use crate::JobPool;
use crate::Localization;
use crate::UnknownIds;
@@ -20,6 +21,7 @@ use std::fmt;
pub struct NoteContext<'d> {
pub ndb: &'d Ndb,
pub accounts: &'d Accounts,
pub global_wallet: &'d GlobalWallet,
pub i18n: &'d mut Localization,
pub img_cache: &'d mut Images,
pub note_cache: &'d mut NoteCache,
@@ -28,7 +30,6 @@ pub struct NoteContext<'d> {
pub job_pool: &'d mut JobPool,
pub unknown_ids: &'d mut UnknownIds,
pub clipboard: &'d mut egui_winit::clipboard::Clipboard,
pub current_account_has_wallet: bool,
}
#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)]

View File

@@ -0,0 +1,39 @@
use bitflags::bitflags;
bitflags! {
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NotedeckOptions: u64 {
// ===== Settings ======
/// Are we on light theme?
const LightTheme = 1 << 0;
/// Debug controls, fps stats
const Debug = 1 << 1;
/// Show relay debug window?
const RelayDebug = 1 << 2;
/// Are we running as tests?
const Tests = 1 << 3;
/// Use keystore?
const UseKeystore = 1 << 4;
/// Simulate is_compiled_as_mobile ?
const Mobile = 1 << 6;
// ===== Feature Flags ======
/// Is notebook enabled?
const FeatureNotebook = 1 << 32;
/// Is clndash enabled?
const FeatureClnDash = 1 << 33;
}
}
impl Default for NotedeckOptions {
fn default() -> Self {
NotedeckOptions::UseKeystore
}
}

View File

@@ -1,9 +1,9 @@
mod app_size;
mod theme_handler;
mod settings_handler;
mod token_handler;
mod zoom;
pub use app_size::AppSizeHandler;
pub use theme_handler::ThemeHandler;
pub use settings_handler::Settings;
pub use settings_handler::SettingsHandler;
pub use settings_handler::DEFAULT_NOTE_BODY_FONT_SIZE;
pub use token_handler::TokenHandler;
pub use zoom::ZoomHandler;

View File

@@ -0,0 +1,253 @@
use crate::{
storage::delete_file, timed_serializer::TimedSerializer, DataPath, DataPathType, Directory,
};
use egui::ThemePreference;
use serde::{Deserialize, Serialize};
use tracing::{error, info};
const THEME_FILE: &str = "theme.txt";
const ZOOM_FACTOR_FILE: &str = "zoom_level.json";
const SETTINGS_FILE: &str = "settings.json";
const DEFAULT_THEME: ThemePreference = ThemePreference::Dark;
const DEFAULT_LOCALE: &str = "en-US";
const DEFAULT_ZOOM_FACTOR: f32 = 1.0;
const DEFAULT_SHOW_SOURCE_CLIENT: &str = "hide";
const DEFAULT_SHOW_REPLIES_NEWEST_FIRST: bool = false;
#[cfg(any(target_os = "android", target_os = "ios"))]
pub const DEFAULT_NOTE_BODY_FONT_SIZE: f32 = 13.0;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub const DEFAULT_NOTE_BODY_FONT_SIZE: f32 = 16.0;
fn deserialize_theme(serialized_theme: &str) -> Option<ThemePreference> {
match serialized_theme {
"dark" => Some(ThemePreference::Dark),
"light" => Some(ThemePreference::Light),
"system" => Some(ThemePreference::System),
_ => None,
}
}
#[derive(Serialize, Deserialize, PartialEq, Clone)]
pub struct Settings {
pub theme: ThemePreference,
pub locale: String,
pub zoom_factor: f32,
pub show_source_client: String,
pub show_replies_newest_first: bool,
pub note_body_font_size: f32,
}
impl Default for Settings {
fn default() -> Self {
Self {
theme: DEFAULT_THEME,
locale: DEFAULT_LOCALE.to_string(),
zoom_factor: DEFAULT_ZOOM_FACTOR,
show_source_client: DEFAULT_SHOW_SOURCE_CLIENT.to_string(),
show_replies_newest_first: DEFAULT_SHOW_REPLIES_NEWEST_FIRST,
note_body_font_size: DEFAULT_NOTE_BODY_FONT_SIZE,
}
}
}
pub struct SettingsHandler {
directory: Directory,
serializer: TimedSerializer<Settings>,
current_settings: Option<Settings>,
}
impl SettingsHandler {
fn read_from_theme_file(&self) -> Option<ThemePreference> {
match self.directory.get_file(THEME_FILE.to_string()) {
Ok(contents) => deserialize_theme(contents.trim()),
Err(_) => None,
}
}
fn read_from_zomfactor_file(&self) -> Option<f32> {
match self.directory.get_file(ZOOM_FACTOR_FILE.to_string()) {
Ok(contents) => serde_json::from_str::<f32>(&contents).ok(),
Err(_) => None,
}
}
fn migrate_to_settings_file(&mut self) -> bool {
let mut settings = Settings::default();
let mut migrated = false;
// if theme.txt exists migrate
if let Some(theme_from_file) = self.read_from_theme_file() {
info!("migrating theme preference from theme.txt file");
_ = delete_file(&self.directory.file_path, THEME_FILE.to_string());
settings.theme = theme_from_file;
migrated = true;
} else {
info!("theme.txt file not found, using default theme");
};
// if zoom_factor.txt exists migrate
if let Some(zom_factor) = self.read_from_zomfactor_file() {
info!("migrating theme preference from zom_factor file");
_ = delete_file(&self.directory.file_path, ZOOM_FACTOR_FILE.to_string());
settings.zoom_factor = zom_factor;
migrated = true;
} else {
info!("zoom_factor.txt exists migrate file not found, using default zoom factor");
};
if migrated {
self.current_settings = Some(settings);
self.try_save_settings();
}
migrated
}
pub fn new(path: &DataPath) -> Self {
let directory = Directory::new(path.path(DataPathType::Setting));
let serializer =
TimedSerializer::new(path, DataPathType::Setting, "settings.json".to_owned());
Self {
directory,
serializer,
current_settings: None,
}
}
pub fn load(mut self) -> Self {
if self.migrate_to_settings_file() {
return self;
}
match self.directory.get_file(SETTINGS_FILE.to_string()) {
Ok(contents_str) => {
// Parse JSON content
match serde_json::from_str::<Settings>(&contents_str) {
Ok(settings) => {
self.current_settings = Some(settings);
}
Err(_) => {
error!("Invalid settings format. Using defaults");
self.current_settings = Some(Settings::default());
}
}
}
Err(_) => {
error!("Could not read settings. Using defaults");
self.current_settings = Some(Settings::default());
}
}
self
}
pub(crate) fn try_save_settings(&mut self) {
let settings = self.get_settings_mut().clone();
self.serializer.try_save(settings);
}
pub fn get_settings_mut(&mut self) -> &mut Settings {
if self.current_settings.is_none() {
self.current_settings = Some(Settings::default());
}
self.current_settings.as_mut().unwrap()
}
pub fn set_theme(&mut self, theme: ThemePreference) {
self.get_settings_mut().theme = theme;
self.try_save_settings();
}
pub fn set_locale<S>(&mut self, locale: S)
where
S: Into<String>,
{
self.get_settings_mut().locale = locale.into();
self.try_save_settings();
}
pub fn set_zoom_factor(&mut self, zoom_factor: f32) {
self.get_settings_mut().zoom_factor = zoom_factor;
self.try_save_settings();
}
pub fn set_show_source_client<S>(&mut self, option: S)
where
S: Into<String>,
{
self.get_settings_mut().show_source_client = option.into();
self.try_save_settings();
}
pub fn set_show_replies_newest_first(&mut self, value: bool) {
self.get_settings_mut().show_replies_newest_first = value;
self.try_save_settings();
}
pub fn set_note_body_font_size(&mut self, value: f32) {
self.get_settings_mut().note_body_font_size = value;
self.try_save_settings();
}
pub fn update_batch<F>(&mut self, update_fn: F)
where
F: FnOnce(&mut Settings),
{
let settings = self.get_settings_mut();
update_fn(settings);
self.try_save_settings();
}
pub fn update_settings(&mut self, new_settings: Settings) {
self.current_settings = Some(new_settings);
self.try_save_settings();
}
pub fn theme(&self) -> ThemePreference {
self.current_settings
.as_ref()
.map(|s| s.theme)
.unwrap_or(DEFAULT_THEME)
}
pub fn locale(&self) -> String {
self.current_settings
.as_ref()
.map(|s| s.locale.clone())
.unwrap_or_else(|| DEFAULT_LOCALE.to_string())
}
pub fn zoom_factor(&self) -> f32 {
self.current_settings
.as_ref()
.map(|s| s.zoom_factor)
.unwrap_or(DEFAULT_ZOOM_FACTOR)
}
pub fn show_source_client(&self) -> String {
self.current_settings
.as_ref()
.map(|s| s.show_source_client.to_string())
.unwrap_or(DEFAULT_SHOW_SOURCE_CLIENT.to_string())
}
pub fn show_replies_newest_first(&self) -> bool {
self.current_settings
.as_ref()
.map(|s| s.show_replies_newest_first)
.unwrap_or(DEFAULT_SHOW_REPLIES_NEWEST_FIRST)
}
pub fn is_loaded(&self) -> bool {
self.current_settings.is_some()
}
pub fn note_body_font_size(&self) -> f32 {
self.current_settings
.as_ref()
.map(|s| s.note_body_font_size)
.unwrap_or(DEFAULT_NOTE_BODY_FONT_SIZE)
}
}

View File

@@ -1,76 +0,0 @@
use egui::ThemePreference;
use tracing::{error, info};
use crate::{storage, DataPath, DataPathType, Directory};
pub struct ThemeHandler {
directory: Directory,
fallback_theme: ThemePreference,
}
const THEME_FILE: &str = "theme.txt";
impl ThemeHandler {
pub fn new(path: &DataPath) -> Self {
let directory = Directory::new(path.path(DataPathType::Setting));
let fallback_theme = ThemePreference::Dark;
Self {
directory,
fallback_theme,
}
}
pub fn load(&self) -> ThemePreference {
match self.directory.get_file(THEME_FILE.to_owned()) {
Ok(contents) => match deserialize_theme(contents) {
Some(theme) => theme,
None => {
error!(
"Could not deserialize theme. Using fallback {:?} instead",
self.fallback_theme
);
self.fallback_theme
}
},
Err(e) => {
error!(
"Could not read {} file: {:?}\nUsing fallback {:?} instead",
THEME_FILE, e, self.fallback_theme
);
self.fallback_theme
}
}
}
pub fn save(&self, theme: ThemePreference) {
match storage::write_file(
&self.directory.file_path,
THEME_FILE.to_owned(),
&theme_to_serialized(&theme),
) {
Ok(_) => info!(
"Successfully saved {:?} theme change to {}",
theme, THEME_FILE
),
Err(_) => error!("Could not save {:?} theme change to {}", theme, THEME_FILE),
}
}
}
fn theme_to_serialized(theme: &ThemePreference) -> String {
match theme {
ThemePreference::Dark => "dark",
ThemePreference::Light => "light",
ThemePreference::System => "system",
}
.to_owned()
}
fn deserialize_theme(serialized_theme: String) -> Option<ThemePreference> {
match serialized_theme.as_str() {
"dark" => Some(ThemePreference::Dark),
"light" => Some(ThemePreference::Light),
"system" => Some(ThemePreference::System),
_ => None,
}
}

View File

@@ -1,26 +0,0 @@
use crate::{DataPath, DataPathType};
use egui::Context;
use crate::timed_serializer::TimedSerializer;
pub struct ZoomHandler {
serializer: TimedSerializer<f32>,
}
impl ZoomHandler {
pub fn new(path: &DataPath) -> Self {
let serializer =
TimedSerializer::new(path, DataPathType::Setting, "zoom_level.json".to_owned());
Self { serializer }
}
pub fn try_save_zoom_factor(&mut self, ctx: &Context) {
let cur_zoom_level = ctx.zoom_factor();
self.serializer.try_save(cur_zoom_level);
}
pub fn get_zoom_factor(&self) -> Option<f32> {
self.serializer.get_item()
}
}

View File

@@ -1,5 +1,14 @@
use crate::platform::{file::emit_selected_file, SelectedMedia};
use jni::{
objects::{JByteArray, JClass, JObject, JObjectArray, JString},
JNIEnv,
};
use std::sync::atomic::{AtomicI32, Ordering};
use tracing::debug;
use tracing::{debug, error, info};
pub fn get_jvm() -> jni::JavaVM {
unsafe { jni::JavaVM::from_raw(ndk_context::android_context().vm().cast()) }.unwrap()
}
// Thread-safe static global
static KEYBOARD_HEIGHT: AtomicI32 = AtomicI32::new(0);
@@ -16,7 +25,7 @@ pub extern "C" fn Java_com_damus_notedeck_KeyboardHeightHelper_nativeKeyboardHei
debug!("updating virtual keyboard height {}", height);
// Convert and store atomically
KEYBOARD_HEIGHT.store(height as i32, Ordering::SeqCst);
KEYBOARD_HEIGHT.store(height.max(0), Ordering::SeqCst);
}
/// Gets the current Android virtual keyboard height. Useful for transforming
@@ -24,3 +33,80 @@ pub extern "C" fn Java_com_damus_notedeck_KeyboardHeightHelper_nativeKeyboardHei
pub fn virtual_keyboard_height() -> i32 {
KEYBOARD_HEIGHT.load(Ordering::SeqCst)
}
#[no_mangle]
pub extern "C" fn Java_com_damus_notedeck_MainActivity_nativeOnFilePickedFailed(
mut env: JNIEnv,
_class: JClass,
juri: JString,
je: JString,
) {
let _uri: String = env.get_string(&juri).unwrap().into();
let _error: String = env.get_string(&je).unwrap().into();
}
#[no_mangle]
pub extern "C" fn Java_com_damus_notedeck_MainActivity_nativeOnFilePickedWithContent(
mut env: JNIEnv,
_class: JClass,
// [display_name, size, mime_type]
juri_info: JObjectArray,
jcontent: JByteArray,
) {
debug!("File picked with content");
let display_name: Option<String> = {
let obj = env.get_object_array_element(&juri_info, 0).unwrap();
if obj.is_null() {
None
} else {
Some(env.get_string(&JString::from(obj)).unwrap().into())
}
};
if let Some(display_name) = display_name {
let length = env.get_array_length(&jcontent).unwrap() as usize;
let mut content: Vec<i8> = vec![0; length];
env.get_byte_array_region(&jcontent, 0, &mut content)
.unwrap();
debug!("selected file: {display_name:?} ({length:?} bytes)",);
emit_selected_file(SelectedMedia::from_bytes(
display_name,
content.into_iter().map(|b| b as u8).collect(),
));
} else {
error!("Received null file name");
}
}
pub fn try_open_file_picker() {
match open_file_picker() {
Ok(()) => {
info!("File picker opened successfully");
}
Err(e) => {
error!("Failed to open file picker: {}", e);
}
}
}
pub fn open_file_picker() -> std::result::Result<(), Box<dyn std::error::Error>> {
// Get the Java VM from AndroidApp
let vm = get_jvm();
// Attach current thread to get JNI environment
let mut env = vm.attach_current_thread()?;
let context = unsafe { JObject::from_raw(ndk_context::android_context().context().cast()) };
// Call the openFilePicker method on the MainActivity
env.call_method(
context,
"openFilePicker",
"()V", // Method signature: no parameters, void return
&[], // No arguments
)?;
Ok(())
}

View File

@@ -0,0 +1,99 @@
use std::{path::PathBuf, str::FromStr};
use crossbeam_channel::{unbounded, Receiver, Sender};
use once_cell::sync::Lazy;
use crate::{Error, SupportedMimeType};
#[derive(Debug)]
pub enum MediaFrom {
PathBuf(PathBuf),
Memory(Vec<u8>),
}
#[derive(Debug)]
pub struct SelectedMedia {
pub from: MediaFrom,
pub file_name: String,
pub media_type: SupportedMimeType,
}
impl SelectedMedia {
pub fn from_path(path: PathBuf) -> Result<Self, Error> {
if let Some(ex) = path.extension().and_then(|f| f.to_str()) {
let media_type = SupportedMimeType::from_extension(ex)?;
let file_name = path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(&format!("file.{ex}"))
.to_owned();
Ok(SelectedMedia {
from: MediaFrom::PathBuf(path),
file_name,
media_type,
})
} else {
Err(Error::Generic(format!(
"{path:?} does not have an extension"
)))
}
}
pub fn from_bytes(file_name: String, content: Vec<u8>) -> Result<Self, Error> {
if let Some(ex) = PathBuf::from_str(&file_name)
.unwrap()
.extension()
.and_then(|f| f.to_str())
{
let media_type = SupportedMimeType::from_extension(ex)?;
Ok(SelectedMedia {
from: MediaFrom::Memory(content),
file_name,
media_type,
})
} else {
Err(Error::Generic(format!(
"{file_name:?} does not have an extension"
)))
}
}
}
pub struct SelectedMediaChannel {
sender: Sender<Result<SelectedMedia, Error>>,
receiver: Receiver<Result<SelectedMedia, Error>>,
}
impl Default for SelectedMediaChannel {
fn default() -> Self {
let (sender, receiver) = unbounded();
Self { sender, receiver }
}
}
impl SelectedMediaChannel {
pub fn new_selected_file(&self, media: Result<SelectedMedia, Error>) {
let _ = self.sender.send(media);
}
pub fn try_receive(&self) -> Option<Result<SelectedMedia, Error>> {
self.receiver.try_recv().ok()
}
pub fn receive(&self) -> Option<Result<SelectedMedia, Error>> {
self.receiver.recv().ok()
}
}
pub static SELECTED_MEDIA_CHANNEL: Lazy<SelectedMediaChannel> =
Lazy::new(SelectedMediaChannel::default);
pub fn emit_selected_file(media: Result<SelectedMedia, Error>) {
SELECTED_MEDIA_CHANNEL.new_selected_file(media);
}
pub fn get_next_selected_file() -> Option<Result<SelectedMedia, Error>> {
SELECTED_MEDIA_CHANNEL.try_receive()
}

View File

@@ -1,12 +1,39 @@
#[cfg(target_os = "android")]
pub mod android;
use crate::{platform::file::SelectedMedia, Error};
#[cfg(target_os = "android")]
pub fn virtual_keyboard_height() -> i32 {
android::virtual_keyboard_height()
pub mod android;
pub mod file;
pub fn get_next_selected_file() -> Option<Result<SelectedMedia, Error>> {
file::get_next_selected_file()
}
const VIRT_HEIGHT: i32 = 400;
#[cfg(target_os = "android")]
pub fn virtual_keyboard_height(virt: bool) -> i32 {
if virt {
VIRT_HEIGHT
} else {
android::virtual_keyboard_height()
}
}
#[cfg(not(target_os = "android"))]
pub fn virtual_keyboard_height() -> i32 {
0
pub fn virtual_keyboard_height(virt: bool) -> i32 {
if virt {
VIRT_HEIGHT
} else {
0
}
}
pub fn virtual_keyboard_rect(ui: &egui::Ui, virt: bool) -> Option<egui::Rect> {
let height = virtual_keyboard_height(virt);
if height <= 0 {
return None;
}
let screen_rect = ui.ctx().screen_rect();
let min = egui::Pos2::new(0.0, screen_rect.max.y - height as f32);
Some(egui::Rect::from_min_max(min, screen_rect.max))
}

View File

@@ -77,6 +77,7 @@ impl PartialEq for RelaySpec {
impl Eq for RelaySpec {}
#[allow(clippy::non_canonical_partial_ord_impl)]
impl PartialOrd for RelaySpec {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.url.cmp(&other.url))

View File

@@ -0,0 +1,46 @@
use crate::fonts;
use crate::theme;
use crate::NotedeckOptions;
use crate::NotedeckTextStyle;
use egui::FontId;
use egui::ThemePreference;
pub fn setup_egui_context(
ctx: &egui::Context,
options: NotedeckOptions,
theme: ThemePreference,
note_body_font_size: f32,
zoom_factor: f32,
) {
let is_mobile = options.contains(NotedeckOptions::Mobile) || crate::ui::is_compiled_as_mobile();
let is_oled = crate::ui::is_oled(is_mobile);
ctx.options_mut(|o| {
tracing::info!("Loaded theme {:?} from disk", theme);
o.theme_preference = theme;
});
ctx.set_visuals_of(egui::Theme::Dark, theme::dark_mode(is_oled));
ctx.set_visuals_of(egui::Theme::Light, theme::light_mode());
fonts::setup_fonts(ctx);
if crate::ui::is_compiled_as_mobile() {
ctx.set_pixels_per_point(ctx.pixels_per_point() + 0.2);
}
egui_extras::install_image_loaders(ctx);
ctx.options_mut(|o| {
o.input_options.max_click_duration = 0.4;
});
ctx.all_styles_mut(|style| crate::theme::add_custom_style(is_mobile, style));
ctx.set_zoom_factor(zoom_factor);
let mut style = (*ctx.style()).clone();
style.text_styles.insert(
NotedeckTextStyle::NoteBody.text_style(),
FontId::proportional(note_body_font_size),
);
ctx.set_style(style);
}

View File

@@ -15,6 +15,7 @@ pub enum NotedeckTextStyle {
Button,
Small,
Tiny,
NoteBody,
}
impl NotedeckTextStyle {
@@ -29,6 +30,7 @@ impl NotedeckTextStyle {
Self::Button => TextStyle::Button,
Self::Small => TextStyle::Small,
Self::Tiny => TextStyle::Name("Tiny".into()),
Self::NoteBody => TextStyle::Name("NoteBody".into()),
}
}
@@ -43,6 +45,7 @@ impl NotedeckTextStyle {
Self::Button => FontFamily::Proportional,
Self::Small => FontFamily::Proportional,
Self::Tiny => FontFamily::Proportional,
Self::NoteBody => FontFamily::Proportional,
}
}

View File

@@ -1,7 +1,35 @@
use egui::{
style::{Selection, WidgetVisuals, Widgets},
Color32, CornerRadius, Stroke, Visuals,
};
use crate::{fonts, NotedeckTextStyle};
use egui::style::Interaction;
use egui::style::Selection;
use egui::style::WidgetVisuals;
use egui::style::Widgets;
use egui::Color32;
use egui::CornerRadius;
use egui::FontId;
use egui::Stroke;
use egui::Style;
use egui::Visuals;
use strum::IntoEnumIterator;
pub const PURPLE: Color32 = Color32::from_rgb(0xCC, 0x43, 0xC5);
const PURPLE_ALT: Color32 = Color32::from_rgb(0x82, 0x56, 0xDD);
//pub const DARK_BG: Color32 = egui::Color32::from_rgb(40, 44, 52);
pub const GRAY_SECONDARY: Color32 = Color32::from_rgb(0x8A, 0x8A, 0x8A);
const BLACK: Color32 = Color32::from_rgb(0x00, 0x00, 0x00);
const RED_700: Color32 = Color32::from_rgb(0xC7, 0x37, 0x5A);
const ORANGE_700: Color32 = Color32::from_rgb(0xF6, 0xB1, 0x4A);
// BACKGROUNDS
const SEMI_DARKER_BG: Color32 = Color32::from_rgb(0x39, 0x39, 0x39);
const DARKER_BG: Color32 = Color32::from_rgb(0x1F, 0x1F, 0x1F);
const DARK_BG: Color32 = Color32::from_rgb(0x2C, 0x2C, 0x2C);
const DARK_ISH_BG: Color32 = Color32::from_rgb(0x25, 0x25, 0x25);
const SEMI_DARK_BG: Color32 = Color32::from_rgb(0x44, 0x44, 0x44);
const LIGHTER_GRAY: Color32 = Color32::from_rgb(0xf8, 0xf8, 0xf8);
const LIGHT_GRAY: Color32 = Color32::from_rgb(0xc8, 0xc8, 0xc8); // 78%
const DARKER_GRAY: Color32 = Color32::from_rgb(0xa5, 0xa5, 0xa5); // 65%
const EVEN_DARKER_GRAY: Color32 = Color32::from_rgb(0x89, 0x89, 0x89); // 54%
pub struct ColorTheme {
// VISUALS
@@ -86,3 +114,131 @@ pub fn create_themed_visuals(theme: ColorTheme, default: Visuals) -> Visuals {
..default
}
}
pub fn desktop_dark_color_theme() -> ColorTheme {
ColorTheme {
// VISUALS
panel_fill: DARKER_BG,
extreme_bg_color: DARK_ISH_BG,
text_color: Color32::WHITE,
err_fg_color: RED_700,
warn_fg_color: ORANGE_700,
hyperlink_color: PURPLE,
selection_color: PURPLE_ALT,
// WINDOW
window_fill: DARK_ISH_BG,
window_stroke_color: DARK_BG,
// NONINTERACTIVE WIDGET
noninteractive_bg_fill: DARK_ISH_BG,
noninteractive_weak_bg_fill: DARK_BG,
noninteractive_bg_stroke_color: SEMI_DARKER_BG,
noninteractive_fg_stroke_color: GRAY_SECONDARY,
// INACTIVE WIDGET
inactive_bg_stroke_color: SEMI_DARKER_BG,
inactive_bg_fill: Color32::from_rgb(0x25, 0x25, 0x25),
inactive_weak_bg_fill: SEMI_DARK_BG,
}
}
pub fn mobile_dark_color_theme() -> ColorTheme {
ColorTheme {
panel_fill: Color32::BLACK,
noninteractive_weak_bg_fill: Color32::from_rgb(0x1F, 0x1F, 0x1F),
..desktop_dark_color_theme()
}
}
pub fn light_color_theme() -> ColorTheme {
ColorTheme {
// VISUALS
panel_fill: Color32::WHITE,
extreme_bg_color: LIGHTER_GRAY,
text_color: BLACK,
err_fg_color: RED_700,
warn_fg_color: ORANGE_700,
hyperlink_color: PURPLE,
selection_color: PURPLE_ALT,
// WINDOW
window_fill: Color32::WHITE,
window_stroke_color: DARKER_GRAY,
// NONINTERACTIVE WIDGET
noninteractive_bg_fill: Color32::WHITE,
noninteractive_weak_bg_fill: LIGHTER_GRAY,
noninteractive_bg_stroke_color: LIGHT_GRAY,
noninteractive_fg_stroke_color: GRAY_SECONDARY,
// INACTIVE WIDGET
inactive_bg_stroke_color: EVEN_DARKER_GRAY,
inactive_bg_fill: LIGHTER_GRAY,
inactive_weak_bg_fill: LIGHTER_GRAY,
}
}
/// Create custom text sizes for any FontSizes
pub fn add_custom_style(is_mobile: bool, style: &mut Style) {
let font_size = if is_mobile {
fonts::mobile_font_size
} else {
fonts::desktop_font_size
};
style.text_styles = NotedeckTextStyle::iter()
.map(|text_style| {
(
text_style.text_style(),
FontId::new(font_size(&text_style), text_style.font_family()),
)
})
.collect();
style.interaction = Interaction {
tooltip_delay: 0.1,
show_tooltips_only_when_still: false,
..Interaction::default()
};
// debug: show callstack for the current widget on hover if all
// modifier keys are pressed down.
/*
#[cfg(feature = "debug-widget-callstack")]
{
#[cfg(not(debug_assertions))]
compile_error!(
"The `debug-widget-callstack` feature requires a debug build, \
release builds are unsupported."
);
style.debug.debug_on_hover_with_all_modifiers = true;
}
// debug: show an overlay on all interactive widgets
#[cfg(feature = "debug-interactive-widgets")]
{
#[cfg(not(debug_assertions))]
compile_error!(
"The `debug-interactive-widgets` feature requires a debug build, \
release builds are unsupported."
);
style.debug.show_interactive_widgets = true;
}
*/
}
pub fn light_mode() -> Visuals {
create_themed_visuals(crate::theme::light_color_theme(), Visuals::light())
}
pub fn dark_mode(is_oled: bool) -> Visuals {
create_themed_visuals(
if is_oled {
mobile_dark_color_theme()
} else {
desktop_dark_color_theme()
},
Visuals::dark(),
)
}

View File

@@ -1,4 +1,5 @@
use crate::{tr, Localization};
use chrono::DateTime;
use std::time::{SystemTime, UNIX_EPOCH};
// Time duration constants in seconds
@@ -83,6 +84,14 @@ fn time_ago_between(i18n: &mut Localization, timestamp: u64, now: u64) -> String
}
}
pub fn time_format(_i18n: &mut Localization, timestamp: u64) -> String {
// TODO: format this using the selected locale
DateTime::from_timestamp(timestamp as i64, 0)
.unwrap()
.format("%l:%M %p %b %d, %Y")
.to_string()
}
pub fn time_ago_since(i18n: &mut Localization, timestamp: u64) -> String {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)

View File

@@ -2,16 +2,16 @@ use crate::debouncer::Debouncer;
use crate::{storage, DataPath, DataPathType, Directory};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use tracing::info; // Adjust this import path as needed
use tracing::info;
pub struct TimedSerializer<T: PartialEq + Copy + Serialize + for<'de> Deserialize<'de>> {
pub struct TimedSerializer<T: PartialEq + Clone + Serialize + for<'de> Deserialize<'de>> {
directory: Directory,
file_name: String,
debouncer: Debouncer,
saved_item: Option<T>,
}
impl<T: PartialEq + Copy + Serialize + for<'de> Deserialize<'de>> TimedSerializer<T> {
impl<T: PartialEq + Clone + Serialize + for<'de> Deserialize<'de>> TimedSerializer<T> {
pub fn new(path: &DataPath, path_type: DataPathType, file_name: String) -> Self {
let directory = Directory::new(path.path(path_type));
let delay = Duration::from_millis(1000);
@@ -30,11 +30,11 @@ impl<T: PartialEq + Copy + Serialize + for<'de> Deserialize<'de>> TimedSerialize
self
}
// returns whether successful
/// Returns whether it actually wrote the new value
pub fn try_save(&mut self, cur_item: T) -> bool {
if self.debouncer.should_act() {
if let Some(saved_item) = self.saved_item {
if saved_item != cur_item {
if let Some(ref saved_item) = self.saved_item {
if *saved_item != cur_item {
return self.save(cur_item);
}
} else {
@@ -45,8 +45,8 @@ impl<T: PartialEq + Copy + Serialize + for<'de> Deserialize<'de>> TimedSerialize
}
pub fn get_item(&self) -> Option<T> {
if self.saved_item.is_some() {
return self.saved_item;
if let Some(ref item) = self.saved_item {
return Some(item.clone());
}
if let Ok(file_contents) = self.directory.get_file(self.file_name.clone()) {
if let Ok(item) = serde_json::from_str::<T>(&file_contents) {

View File

@@ -1,12 +1,23 @@
use crate::NotedeckTextStyle;
pub const NARROW_SCREEN_WIDTH: f32 = 550.0;
pub fn richtext_small<S>(text: S) -> egui::RichText
where
S: Into<String>,
{
egui::RichText::new(text).text_style(NotedeckTextStyle::Small.text_style())
}
/// Determine if the screen is narrow. This is useful for detecting mobile
/// contexts, but with the nuance that we may also have a wide android tablet.
pub fn is_narrow(ctx: &egui::Context) -> bool {
let screen_size = ctx.input(|c| c.screen_rect().size());
screen_size.x < 550.0
screen_size.x < NARROW_SCREEN_WIDTH
}
pub fn is_oled() -> bool {
is_compiled_as_mobile()
pub fn is_oled(is_mobile_override: bool) -> bool {
is_mobile_override || is_compiled_as_mobile()
}
#[inline]

View File

@@ -195,13 +195,13 @@ impl UnknownIds {
}
}
pub fn add_pubkey_if_missing(&mut self, ndb: &Ndb, txn: &Transaction, pubkey: &Pubkey) {
pub fn add_pubkey_if_missing(&mut self, ndb: &Ndb, txn: &Transaction, pubkey: &[u8; 32]) {
// we already have this profile, skip
if ndb.get_profile_by_pubkey(txn, pubkey).is_ok() {
return;
}
let unknown_id = UnknownId::Pubkey(*pubkey);
let unknown_id = UnknownId::Pubkey(Pubkey::new(*pubkey));
if self.ids.contains_key(&unknown_id) {
return;
}

View File

@@ -68,6 +68,17 @@ impl UrlCache {
}
}
}
pub fn clear(&mut self) {
if self.from_disk_promise.is_none() {
let cache = self.cache.clone();
std::thread::spawn(move || {
if let Ok(mut locked_cache) = cache.write() {
locked_cache.clear();
}
});
}
}
}
fn merge_cache(cur_cache: Arc<RwLock<UrlsToMime>>, from_disk: UrlsToMime) {
@@ -227,7 +238,9 @@ impl SupportedMimeType {
{
Ok(Self { mime })
} else {
Err(Error::Generic("Unsupported mime type".to_owned()))
Err(Error::Generic(
format!("{extension} Unsupported mime type",),
))
}
}

View File

@@ -22,7 +22,7 @@ impl UserAccount {
}
}
pub fn keypair(&self) -> KeypairUnowned {
pub fn keypair(&self) -> KeypairUnowned<'_> {
KeypairUnowned {
pubkey: &self.key.pubkey,
secret_key: self.key.secret_key.as_ref(),

View File

@@ -24,7 +24,7 @@ pub fn get_wallet_for<'a>(
global_wallet.wallet.as_ref()
}
pub fn get_current_wallet<'a>(
pub fn get_current_wallet_mut<'a>(
accounts: &'a mut Accounts,
global_wallet: &'a mut GlobalWallet,
) -> Option<&'a mut ZapWallet> {
@@ -35,6 +35,17 @@ pub fn get_current_wallet<'a>(
Some(wallet)
}
pub fn get_current_wallet<'a>(
accounts: &'a Accounts,
global_wallet: &'a GlobalWallet,
) -> Option<&'a ZapWallet> {
let Some(wallet) = accounts.get_selected_wallet() else {
return global_wallet.wallet.as_ref();
};
Some(wallet)
}
#[derive(Clone, Eq, PartialEq, Debug)]
pub enum WalletType {
Auto,

View File

@@ -1,16 +1,23 @@
use std::collections::HashMap;
use enostr::{NoteId, Pubkey};
use nostrdb::{Ndb, Transaction};
use nwc::nostr::nips::nip47::PayInvoiceResponse;
use poll_promise::Promise;
use tokio::task::JoinError;
use url::Url;
use crate::{get_wallet_for, Accounts, GlobalWallet, ZapError};
use super::{
networking::{fetch_invoice_lnurl, fetch_invoice_lud16, FetchedInvoice, FetchingInvoice},
zap::Zap,
use crate::{
get_wallet_for,
zaps::{
get_users_zap_address,
networking::{fetch_invoice_promise, FetchedInvoiceResponse, LNUrlPayResponse, PayEntry},
},
Accounts, GlobalWallet, ZapError,
};
use super::{networking::FetchingInvoice, zap::Zap};
type ZapId = u32;
#[derive(Default)]
@@ -23,11 +30,31 @@ pub struct Zaps {
zaps: std::collections::HashMap<ZapId, ZapState>,
in_flight: Vec<ZapPromise>,
events: Vec<EventResponse>,
pay_cache: PayCache,
}
/// Cache to hold LNURL payRequest responses from the desired LNURL endpoint
#[derive(Default)]
pub struct PayCache {
// endpoint URL to response
pub pay_responses: HashMap<Url, LNUrlPayResponse>,
}
impl PayCache {
pub fn get_response(&self, url: &Url) -> Option<&LNUrlPayResponse> {
self.pay_responses.get(url)
}
pub fn insert(&mut self, entry: PayEntry) {
self.pay_responses.insert(entry.url, entry.response);
}
}
fn process_event(
id: ZapId,
event: ZapEvent,
cache: &PayCache,
accounts: &mut Accounts,
global_wallet: &mut GlobalWallet,
ndb: &Ndb,
@@ -37,7 +64,7 @@ fn process_event(
ZapEvent::FetchInvoice {
zap_ctx,
sender_relays,
} => process_new_zap_event(zap_ctx, accounts, ndb, txn, sender_relays),
} => process_new_zap_event(cache, zap_ctx, accounts, ndb, txn, sender_relays),
ZapEvent::SendNWC {
zap_ctx,
req_noteid,
@@ -74,6 +101,7 @@ fn process_event(
}
fn process_new_zap_event(
cache: &PayCache,
zap_ctx: ZapCtx,
accounts: &Accounts,
ndb: &Ndb,
@@ -96,7 +124,8 @@ fn process_new_zap_event(
};
let id = zap_ctx.id;
let promise = send_note_zap(
let m_promise = send_note_zap(
cache,
ndb,
txn,
note_target,
@@ -106,55 +135,41 @@ fn process_new_zap_event(
)
.map(|promise| ZapPromise::FetchingInvoice {
ctx: zap_ctx,
promise,
promise: Box::new(promise),
});
let Some(promise) = promise else {
return NextState::Event(EventResponse {
id,
event: Err(ZappingError::InvalidZapAddress),
});
let promise = match m_promise {
Ok(promise) => promise,
Err(e) => {
return NextState::Event(EventResponse {
id,
event: Err(ZappingError::InvoiceFetchFailed(e)),
});
}
};
NextState::Transition(promise)
}
fn send_note_zap(
cache: &PayCache,
ndb: &Ndb,
txn: &Transaction,
note_target: NoteZapTargetOwned,
msats: u64,
nsec: &[u8; 32],
relays: Vec<String>,
) -> Option<FetchingInvoice> {
let address = get_users_zap_endpoint(txn, ndb, &note_target.zap_recipient)?;
) -> Result<FetchingInvoice, ZapError> {
let address = get_users_zap_address(txn, ndb, &note_target.zap_recipient)?;
let promise = match address {
ZapAddress::Lud16(s) => {
fetch_invoice_lud16(s, msats, *nsec, ZapTargetOwned::Note(note_target), relays)
}
ZapAddress::Lud06(s) => {
fetch_invoice_lnurl(s, msats, *nsec, ZapTargetOwned::Note(note_target), relays)
}
};
Some(promise)
}
enum ZapAddress {
Lud16(String),
Lud06(String),
}
fn get_users_zap_endpoint(txn: &Transaction, ndb: &Ndb, receiver: &Pubkey) -> Option<ZapAddress> {
let profile = ndb
.get_profile_by_pubkey(txn, receiver.bytes())
.ok()?
.record()
.profile()?;
profile
.lud06()
.map(|l| ZapAddress::Lud06(l.to_string()))
.or(profile.lud16().map(|l| ZapAddress::Lud16(l.to_string())))
fetch_invoice_promise(
cache,
address,
msats,
*nsec,
ZapTargetOwned::Note(note_target),
relays,
)
}
fn try_get_promise_response(
@@ -169,7 +184,7 @@ fn try_get_promise_response(
match promise {
ZapPromise::FetchingInvoice { ctx, promise } => {
let result = promise.block_and_take();
let result = Box::new(promise.block_and_take());
Some(PromiseResponse::FetchingInvoice { ctx, result })
}
@@ -272,6 +287,16 @@ impl Zaps {
continue;
};
if let PromiseResponse::FetchingInvoice { ctx: _, result } = &resp {
if let Ok(resp) = &**result {
if let Some(entry) = &resp.pay_entry {
let url = &entry.url;
tracing::info!("inserting {url} in pay cache");
self.pay_cache.insert(entry.clone());
}
}
}
self.events.push(resp.take_as_event_response());
}
@@ -286,7 +311,15 @@ impl Zaps {
};
let txn = nostrdb::Transaction::new(ndb).expect("txn");
match process_event(event_resp.id, event, accounts, global_wallet, ndb, &txn) {
match process_event(
event_resp.id,
event,
&self.pay_cache,
accounts,
global_wallet,
ndb,
&txn,
) {
NextState::Event(event_resp) => {
self.zaps
.insert(event_resp.id, ZapState::Pending(event_resp.event));
@@ -483,7 +516,7 @@ impl std::fmt::Display for ZappingError {
enum ZapPromise {
FetchingInvoice {
ctx: ZapCtx,
promise: Promise<Result<Result<FetchedInvoice, ZapError>, JoinError>>,
promise: Box<Promise<Result<FetchedInvoiceResponse, JoinError>>>,
},
SendingNWCInvoice {
ctx: SendingNWCInvoiceContext,
@@ -494,7 +527,7 @@ enum ZapPromise {
enum PromiseResponse {
FetchingInvoice {
ctx: ZapCtx,
result: Result<Result<FetchedInvoice, ZapError>, JoinError>,
result: Box<Result<FetchedInvoiceResponse, JoinError>>,
},
SendingNWCInvoice {
ctx: SendingNWCInvoiceContext,
@@ -507,8 +540,8 @@ impl PromiseResponse {
match self {
PromiseResponse::FetchingInvoice { ctx, result } => {
let id = ctx.id;
let event = match result {
Ok(r) => match r {
let event = match *result {
Ok(r) => match r.invoice {
Ok(invoice) => Ok(ZapEvent::SendNWC {
zap_ctx: ctx,
req_noteid: invoice.request_noteid,

View File

@@ -11,3 +11,39 @@ pub use default_zap::{
get_current_default_msats, DefaultZapError, DefaultZapMsats, PendingDefaultZapState,
UserZapMsats,
};
use enostr::Pubkey;
use nostrdb::{Ndb, Transaction};
use crate::ZapError;
pub enum ZapAddress {
Lud16(String),
Lud06(String),
}
pub fn get_users_zap_address(
txn: &Transaction,
ndb: &Ndb,
receiver: &Pubkey,
) -> Result<ZapAddress, ZapError> {
let Some(profile) = ndb
.get_profile_by_pubkey(txn, receiver.bytes())
.map_err(|e| ZapError::Ndb(e.to_string()))?
.record()
.profile()
else {
return Err(ZapError::Ndb(format!("No profile for {receiver}")));
};
let Some(address) = profile
.lud06()
.map(|l| ZapAddress::Lud06(l.to_string()))
.or(profile.lud16().map(|l| ZapAddress::Lud16(l.to_string())))
else {
return Err(ZapError::Ndb(format!(
"profile for {receiver} doesn't have lud06 or lud16"
)));
};
Ok(address)
}

View File

@@ -1,5 +1,9 @@
use crate::{zaps::ZapTargetOwned, ZapError};
use enostr::NoteId;
use crate::{
error::EndpointError,
zaps::{cache::PayCache, ZapAddress, ZapTargetOwned},
ZapError,
};
use enostr::{NoteId, Pubkey};
use nostrdb::NoteBuilder;
use poll_promise::Promise;
use serde::Deserialize;
@@ -11,15 +15,20 @@ pub struct FetchedInvoice {
pub request_noteid: NoteId, // note id of kind 9734 request
}
pub type FetchingInvoice = Promise<Result<Result<FetchedInvoice, ZapError>, JoinError>>;
pub struct FetchedInvoiceResponse {
pub invoice: Result<FetchedInvoice, ZapError>,
pub pay_entry: Option<PayEntry>,
}
async fn fetch_pay_req_async(url: &Url) -> Result<LNUrlPayRequest, ZapError> {
pub type FetchingInvoice = Promise<Result<FetchedInvoiceResponse, JoinError>>;
async fn fetch_pay_req_async(url: &Url) -> Result<LNUrlPayResponseRaw, ZapError> {
let (sender, promise) = Promise::new();
let on_done = move |response: Result<ehttp::Response, String>| {
let handle = response.map_err(ZapError::EndpointError).and_then(|resp| {
let handle = response.map_err(ZapError::endpoint_error).and_then(|resp| {
if !resp.ok {
return Err(ZapError::EndpointError(format!(
return Err(ZapError::endpoint_error(format!(
"bad http response: {}",
resp.status_text
)));
@@ -36,20 +45,9 @@ async fn fetch_pay_req_async(url: &Url) -> Result<LNUrlPayRequest, ZapError> {
tokio::task::block_in_place(|| promise.block_and_take())
}
async fn fetch_pay_req_from_lud16(lud16: &str) -> Result<LNUrlPayRequest, ZapError> {
let url = match generate_endpoint_url(lud16) {
Ok(url) => url,
Err(e) => return Err(e),
};
fetch_pay_req_async(&url).await
}
static HRP_LNURL: bech32::Hrp = bech32::Hrp::parse_unchecked("lnurl");
fn lud16_to_lnurl(lud16: &str) -> Result<String, ZapError> {
let endpoint_url = generate_endpoint_url(lud16)?;
fn endpoint_url_to_lnurl(endpoint_url: &Url) -> Result<String, ZapError> {
let url_str = endpoint_url.to_string();
let data = url_str.as_bytes();
@@ -100,7 +98,7 @@ fn make_kind_9734<'a>(
}
#[derive(Debug, Deserialize)]
pub struct LNUrlPayRequest {
pub struct LNUrlPayResponseRaw {
#[allow(dead_code)]
#[serde(rename = "allowsNostr")]
allow_nostr: bool,
@@ -121,57 +119,117 @@ pub struct LNUrlPayRequest {
max_sendable: u64,
}
impl From<LNUrlPayResponseRaw> for LNUrlPayResponse {
fn from(value: LNUrlPayResponseRaw) -> Self {
let nostr_pubkey = Pubkey::from_hex(&value.nostr_pubkey)
.map_err(|e: enostr::Error| EndpointError(e.to_string()));
let callback_url = Url::parse(&value.callback_url)
.map_err(|e| EndpointError(format!("invalid callback url: {e}")));
Self {
allow_nostr: value.allow_nostr,
nostr_pubkey,
callback_url,
min_sendable: value.min_sendable,
max_sendable: value.max_sendable,
}
}
}
#[derive(Clone, Debug)]
pub struct LNUrlPayResponse {
pub allow_nostr: bool,
pub nostr_pubkey: Result<Pubkey, EndpointError>,
pub callback_url: Result<Url, EndpointError>,
pub min_sendable: u64,
pub max_sendable: u64,
}
#[derive(Clone, Debug)]
pub struct PayEntry {
pub url: Url,
pub response: LNUrlPayResponse,
}
#[derive(Debug, Deserialize)]
struct LNInvoice {
#[serde(rename = "pr")]
invoice: String,
}
fn endpoint_query_for_invoice<'a>(
endpoint_base_url: &'a mut Url,
fn endpoint_query_for_invoice(
endpoint_base_url: &Url,
msats: u64,
lnurl: &str,
note: nostrdb::Note,
) -> Result<&'a Url, ZapError> {
) -> Result<Url, ZapError> {
let mut new_url = endpoint_base_url.clone();
let nostr = note
.json()
.map_err(|e| ZapError::Serialization(format!("failed note to json: {e}")))?;
Ok(endpoint_base_url
new_url
.query_pairs_mut()
.append_pair("amount", &msats.to_string())
.append_pair("lnurl", lnurl)
.append_pair("nostr", &nostr)
.finish())
.finish();
Ok(new_url)
}
pub fn fetch_invoice_lud16(
lud16: String,
pub fn fetch_invoice_promise(
cache: &PayCache,
zap_address: ZapAddress,
msats: u64,
sender_nsec: [u8; 32],
target: ZapTargetOwned,
relays: Vec<String>,
) -> FetchingInvoice {
Promise::spawn_async(tokio::spawn(async move {
fetch_invoice_lud16_async(&lud16, msats, &sender_nsec, target, relays).await
}))
}
) -> Result<FetchingInvoice, ZapError> {
let (url, lnurl) = match zap_address {
ZapAddress::Lud16(lud16) => {
let url = generate_endpoint_url(&lud16)?;
let lnurl = endpoint_url_to_lnurl(&url)?;
(url, lnurl)
}
ZapAddress::Lud06(lnurl) => (convert_lnurl_to_endpoint_url(&lnurl)?, lnurl),
};
pub fn fetch_invoice_lnurl(
lnurl: String,
msats: u64,
sender_nsec: [u8; 32],
target: ZapTargetOwned,
relays: Vec<String>,
) -> FetchingInvoice {
Promise::spawn_async(tokio::spawn(async move {
let pay_req = match fetch_pay_req_from_lnurl_async(&lnurl).await {
Ok(req) => req,
Err(e) => return Err(e),
};
match cache.get_response(&url) {
Some(endpoint_resp) => {
tracing::info!("Using existing endpoint response for {url}");
let response = endpoint_resp.clone();
Ok(Promise::spawn_async(tokio::spawn(async move {
fetch_invoice_lnurl_async(
&lnurl,
PayEntry { url, response },
msats,
&sender_nsec,
relays,
target,
)
.await
})))
}
None => Ok(Promise::spawn_async(tokio::spawn(async move {
tracing::info!("querying ln endpoint: {url}");
let pay_req = match fetch_pay_req_async(&url).await {
Ok(p) => PayEntry {
url,
response: p.into(),
},
Err(e) => {
return FetchedInvoiceResponse {
invoice: Err(e),
pay_entry: None,
}
}
};
fetch_invoice_lnurl_async(&lnurl, &pay_req, msats, &sender_nsec, relays, target).await
}))
fetch_invoice_lnurl_async(&lnurl, pay_req, msats, &sender_nsec, relays, target).await
}))),
}
}
fn convert_lnurl_to_endpoint_url(lnurl: &str) -> Result<Url, ZapError> {
@@ -181,68 +239,96 @@ fn convert_lnurl_to_endpoint_url(lnurl: &str) -> Result<Url, ZapError> {
String::from_utf8(data).map_err(|e| ZapError::Bech(format!("string conversion: {e}")))?;
Url::parse(&url_str)
.map_err(|e| ZapError::EndpointError(format!("endpoint url from lnurl is invalid: {e}")))
}
async fn fetch_pay_req_from_lnurl_async(lnurl: &str) -> Result<LNUrlPayRequest, ZapError> {
let url = match convert_lnurl_to_endpoint_url(lnurl) {
Ok(u) => u,
Err(e) => return Err(e),
};
fetch_pay_req_async(&url).await
.map_err(|e| ZapError::endpoint_error(format!("endpoint url from lnurl is invalid: {e}")))
}
async fn fetch_invoice_lnurl_async(
lnurl: &str,
pay_req: &LNUrlPayRequest,
pay_entry: PayEntry,
msats: u64,
sender_nsec: &[u8; 32],
relays: Vec<String>,
target: ZapTargetOwned,
) -> Result<FetchedInvoice, ZapError> {
//let recipient = Pubkey::from_hex(&pay_req.nostr_pubkey)
//.map_err(|e| ZapError::EndpointError(format!("invalid pubkey hex from endpoint: {e}")))?;
) -> FetchedInvoiceResponse {
if !pay_entry.response.allow_nostr {
return FetchedInvoiceResponse {
invoice: Err(ZapError::endpoint_error(
"endpoint does not allow nostr".to_owned(),
)),
pay_entry: Some(pay_entry),
};
}
let mut base_url = Url::parse(&pay_req.callback_url)
.map_err(|e| ZapError::EndpointError(format!("invalid callback url from endpoint: {e}")))?;
if let Err(e) = &pay_entry.response.nostr_pubkey {
return FetchedInvoiceResponse {
invoice: Err(ZapError::EndpointError(e.clone())),
pay_entry: Some(pay_entry),
};
};
let min_sendable = pay_entry.response.min_sendable;
if msats < min_sendable {
return FetchedInvoiceResponse {
invoice: Err(ZapError::endpoint_error(format!(
"zap amount {msats} is less than minimum sendable: {min_sendable} (in msats)"
))),
pay_entry: Some(pay_entry),
};
}
let max_sendable = pay_entry.response.max_sendable;
if msats > max_sendable {
return FetchedInvoiceResponse {
invoice: Err(ZapError::endpoint_error(format!(
"zap amount {msats} is greater than maximum sendable: {max_sendable} (in msats)"
))),
pay_entry: Some(pay_entry),
};
}
let base_url = match &pay_entry.response.callback_url {
Ok(url) => url.clone(),
Err(error) => {
return FetchedInvoiceResponse {
invoice: Err(ZapError::EndpointError(error.clone())),
pay_entry: None,
};
}
};
let (query, noteid) = {
let comment: &str = "";
let note = make_kind_9734(lnurl, msats, comment, sender_nsec, relays, target);
let noteid = NoteId::new(*note.id());
let query = endpoint_query_for_invoice(&mut base_url, msats, lnurl, note)?;
let query = match endpoint_query_for_invoice(&base_url, msats, lnurl, note) {
Ok(u) => u,
Err(e) => {
return FetchedInvoiceResponse {
invoice: Err(e),
pay_entry: Some(pay_entry),
}
}
};
(query, noteid)
};
let res = fetch_invoice(query).await;
res.map(|i| FetchedInvoice {
invoice: i.invoice,
request_noteid: noteid,
})
let res = fetch_ln_invoice(&query).await;
FetchedInvoiceResponse {
invoice: res.map(|r| FetchedInvoice {
invoice: r.invoice,
request_noteid: noteid,
}),
pay_entry: Some(pay_entry),
}
}
async fn fetch_invoice_lud16_async(
lud16: &str,
msats: u64,
sender_nsec: &[u8; 32],
target: ZapTargetOwned,
relays: Vec<String>,
) -> Result<FetchedInvoice, ZapError> {
let pay_req = fetch_pay_req_from_lud16(lud16).await?;
let lnurl = lud16_to_lnurl(lud16)?;
fetch_invoice_lnurl_async(&lnurl, &pay_req, msats, sender_nsec, relays, target).await
}
async fn fetch_invoice(req: &Url) -> Result<LNInvoice, ZapError> {
async fn fetch_ln_invoice(req: &Url) -> Result<LNInvoice, ZapError> {
let request = ehttp::Request::get(req);
let (sender, promise) = Promise::new();
let on_done = move |response: Result<ehttp::Response, String>| {
let handle = response.map_err(ZapError::EndpointError).and_then(|resp| {
let handle = response.map_err(ZapError::endpoint_error).and_then(|resp| {
if !resp.ok {
return Err(ZapError::EndpointError(format!(
return Err(ZapError::endpoint_error(format!(
"invalid http response: {}",
resp.status_text
)));
@@ -290,25 +376,32 @@ fn generate_endpoint_url(lud16: &str) -> Result<Url, ZapError> {
if use_http { "" } else { "s" }
);
Url::parse(&url_str).map_err(|e| ZapError::EndpointError(e.to_string()))
Url::parse(&url_str).map_err(|e| ZapError::endpoint_error(e.to_string()))
}
#[cfg(test)]
mod tests {
use enostr::{FullKeypair, NoteId};
use crate::zaps::networking::convert_lnurl_to_endpoint_url;
use super::{
fetch_invoice_lnurl, fetch_invoice_lud16, fetch_pay_req_from_lud16, lud16_to_lnurl,
use crate::zaps::{
cache::PayCache,
networking::{
convert_lnurl_to_endpoint_url, endpoint_url_to_lnurl, fetch_pay_req_async,
generate_endpoint_url,
},
};
use super::fetch_invoice_promise;
#[ignore] // don't run this test automatically since it sends real http
#[tokio::test(flavor = "multi_thread")]
async fn test_get_pay_req() {
let lud16 = "jb55@sendsats.lol";
let maybe_res = fetch_pay_req_from_lud16(lud16).await;
let url = generate_endpoint_url(lud16);
assert!(url.is_ok());
let maybe_res = fetch_pay_req_async(&url.unwrap()).await;
assert!(maybe_res.is_ok());
@@ -328,7 +421,10 @@ mod tests {
fn test_lnurl() {
let lud16 = "jb55@sendsats.lol";
let maybe_lnurl = lud16_to_lnurl(lud16);
let url = generate_endpoint_url(lud16);
assert!(url.is_ok());
let maybe_lnurl = endpoint_url_to_lnurl(&url.unwrap());
assert!(maybe_lnurl.is_ok());
let lnurl = maybe_lnurl.unwrap();
@@ -344,9 +440,11 @@ mod tests {
let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime");
let kp = FullKeypair::generate();
let mut cache = PayCache::default();
let maybe_invoice = rt.block_on(async {
fetch_invoice_lud16(
"jb55@sendsats.lol".to_owned(),
fetch_invoice_promise(
&mut cache,
crate::zaps::ZapAddress::Lud16("jb55@sendsats.lol".to_owned()),
1000,
FullKeypair::generate().secret_key.to_secret_bytes(),
crate::zaps::ZapTargetOwned::Note(crate::NoteZapTargetOwned {
@@ -355,14 +453,18 @@ mod tests {
}),
vec!["wss://relay.damus.io".to_owned()],
)
.block_and_take()
.map(|p| p.block_and_take())
});
assert!(maybe_invoice.is_ok());
let inner = maybe_invoice.unwrap();
assert!(inner.is_ok());
let invoice = inner.unwrap();
assert!(invoice.invoice.starts_with("lnbc"));
let inner = inner.unwrap().invoice;
assert!(inner.is_ok());
let inner = inner.unwrap();
assert!(inner.invoice.starts_with("lnbc"));
}
#[test]
@@ -385,9 +487,11 @@ mod tests {
let kp = FullKeypair::generate();
let mut cache = PayCache::default();
let maybe_invoice = rt.block_on(async {
fetch_invoice_lnurl(
lnurl.to_owned(),
fetch_invoice_promise(
&mut cache,
crate::zaps::ZapAddress::Lud06(lnurl.to_owned()),
1000,
kp.secret_key.to_secret_bytes(),
crate::zaps::ZapTargetOwned::Note(crate::NoteZapTargetOwned {
@@ -396,11 +500,17 @@ mod tests {
}),
[relay.to_owned()].to_vec(),
)
.block_and_take()
.map(|p| p.block_and_take())
});
assert!(maybe_invoice.is_ok());
let inner = maybe_invoice.unwrap();
assert!(inner.is_ok());
let inner = inner.unwrap().invoice;
assert!(inner.is_ok());
assert!(maybe_invoice.unwrap().unwrap().invoice.starts_with("lnbc"));
let inner = inner.unwrap();
assert!(inner.invoice.starts_with("lnbc"));
}
}

View File

@@ -9,6 +9,7 @@ license = "GPLv3"
description = "The nostr browser"
[dependencies]
bitflags = { workspace = true }
eframe = { workspace = true }
egui_tabs = { workspace = true }
egui_extras = { workspace = true }
@@ -16,6 +17,8 @@ egui = { workspace = true }
notedeck_columns = { workspace = true }
notedeck_ui = { workspace = true }
notedeck_dave = { workspace = true }
notedeck_notebook = { workspace = true }
notedeck_clndash = { workspace = true }
notedeck = { workspace = true }
nostrdb = { workspace = true }
puffin = { workspace = true, optional = true }
@@ -63,6 +66,12 @@ short_description = "The nostr browser"
identifier = "com.damus.notedeck"
icon = ["assets/app_icon.icns"]
[package.metadata.android.manifest.queries]
intent = [
{ action = ["android.intent.action.MAIN"] },
]
[package.metadata.android]
package = "com.damus.app"
apk_name = "Notedeck"

View File

@@ -8,8 +8,9 @@
android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar">
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode|fontScale|smallestScreenSize"
android:exported="true"
android:windowSoftInputMode="adjustResize"
android:launchMode="singleTask"
>
<intent-filter>
@@ -23,9 +24,16 @@
</activity>
</application>
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
</intent>
</queries>
<uses-feature android:name="android.hardware.vulkan.level"
android:required="true"
android:version="1" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

View File

@@ -1,48 +0,0 @@
package com.damus.notedeck;
import android.app.Activity;
import android.content.res.Configuration;
import android.util.Log;
import android.view.View;
public class KeyboardHeightHelper {
private static final String TAG = "KeyboardHeightHelper";
private KeyboardHeightProvider keyboardHeightProvider;
private Activity activity;
// Static JNI method not tied to any specific activity
private static native void nativeKeyboardHeightChanged(int height);
public KeyboardHeightHelper(Activity activity) {
this.activity = activity;
keyboardHeightProvider = new KeyboardHeightProvider(activity);
// Create observer implementation
KeyboardHeightObserver observer = (height, orientation) -> {
Log.d(TAG, "Keyboard height: " + height + "px, orientation: " +
(orientation == Configuration.ORIENTATION_PORTRAIT ? "portrait" : "landscape"));
// Call the generic native method
nativeKeyboardHeightChanged(height);
};
// Set up the provider
keyboardHeightProvider.setKeyboardHeightObserver(observer);
}
public void start() {
// Start the keyboard height provider after the view is ready
final View contentView = activity.findViewById(android.R.id.content);
contentView.post(() -> {
keyboardHeightProvider.start();
});
}
public void stop() {
keyboardHeightProvider.setKeyboardHeightObserver(null);
}
public void close() {
keyboardHeightProvider.close();
}
}

View File

@@ -1,35 +0,0 @@
/*
* This file is part of Siebe Projects samples.
*
* Siebe Projects samples is free software: you can redistribute it and/or modify
* it under the terms of the Lesser GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Siebe Projects samples 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
* Lesser GNU General Public License for more details.
*
* You should have received a copy of the Lesser GNU General Public License
* along with Siebe Projects samples. If not, see <http://www.gnu.org/licenses/>.
*/
package com.damus.notedeck;
/**
* The observer that will be notified when the height of
* the keyboard has changed
*/
public interface KeyboardHeightObserver {
/**
* Called when the keyboard height has changed, 0 means keyboard is closed,
* >= 1 means keyboard is opened.
*
* @param height The height of the keyboard in pixels
* @param orientation The orientation either: Configuration.ORIENTATION_PORTRAIT or
* Configuration.ORIENTATION_LANDSCAPE
*/
void onKeyboardHeightChanged(int height, int orientation);
}

View File

@@ -1,174 +0,0 @@
/*
* This file is part of Siebe Projects samples.
*
* Siebe Projects samples is free software: you can redistribute it and/or modify
* it under the terms of the Lesser GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Siebe Projects samples 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
* Lesser GNU General Public License for more details.
*
* You should have received a copy of the Lesser GNU General Public License
* along with Siebe Projects samples. If not, see <http://www.gnu.org/licenses/>.
*/
package com.damus.notedeck;
import android.app.Activity;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.util.Log;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.WindowManager.LayoutParams;
import android.widget.PopupWindow;
/**
* The keyboard height provider, this class uses a PopupWindow
* to calculate the window height when the floating keyboard is opened and closed.
*/
public class KeyboardHeightProvider extends PopupWindow {
/** The tag for logging purposes */
private final static String TAG = "sample_KeyboardHeightProvider";
/** The keyboard height observer */
private KeyboardHeightObserver observer;
/** The cached landscape height of the keyboard */
private int keyboardLandscapeHeight;
/** The cached portrait height of the keyboard */
private int keyboardPortraitHeight;
/** The view that is used to calculate the keyboard height */
private View popupView;
/** The parent view */
private View parentView;
/** The root activity that uses this KeyboardHeightProvider */
private Activity activity;
/**
* Construct a new KeyboardHeightProvider
*
* @param activity The parent activity
*/
public KeyboardHeightProvider(Activity activity) {
super(activity);
this.activity = activity;
//LayoutInflater inflator = (LayoutInflater) activity.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
//this.popupView = inflator.inflate(android.R.layout.popupwindow, null, false);
this.popupView = new View(activity);
setContentView(popupView);
setSoftInputMode(LayoutParams.SOFT_INPUT_ADJUST_RESIZE | LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
parentView = activity.findViewById(android.R.id.content);
setWidth(0);
setHeight(LayoutParams.MATCH_PARENT);
popupView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (popupView != null) {
handleOnGlobalLayout();
}
}
});
}
/**
* Start the KeyboardHeightProvider, this must be called after the onResume of the Activity.
* PopupWindows are not allowed to be registered before the onResume has finished
* of the Activity.
*/
public void start() {
if (!isShowing() && parentView.getWindowToken() != null) {
setBackgroundDrawable(new ColorDrawable(0));
showAtLocation(parentView, Gravity.NO_GRAVITY, 0, 0);
}
}
/**
* Close the keyboard height provider,
* this provider will not be used anymore.
*/
public void close() {
this.observer = null;
dismiss();
}
/**
* Set the keyboard height observer to this provider. The
* observer will be notified when the keyboard height has changed.
* For example when the keyboard is opened or closed.
*
* @param observer The observer to be added to this provider.
*/
public void setKeyboardHeightObserver(KeyboardHeightObserver observer) {
this.observer = observer;
}
/**
* Popup window itself is as big as the window of the Activity.
* The keyboard can then be calculated by extracting the popup view bottom
* from the activity window height.
*/
private void handleOnGlobalLayout() {
Point screenSize = new Point();
activity.getWindowManager().getDefaultDisplay().getSize(screenSize);
Rect rect = new Rect();
popupView.getWindowVisibleDisplayFrame(rect);
// REMIND, you may like to change this using the fullscreen size of the phone
// and also using the status bar and navigation bar heights of the phone to calculate
// the keyboard height. But this worked fine on a Nexus.
int orientation = getScreenOrientation();
int keyboardHeight = screenSize.y - rect.bottom;
if (keyboardHeight == 0) {
notifyKeyboardHeightChanged(0, orientation);
}
else if (orientation == Configuration.ORIENTATION_PORTRAIT) {
this.keyboardPortraitHeight = keyboardHeight;
notifyKeyboardHeightChanged(keyboardPortraitHeight, orientation);
}
else {
this.keyboardLandscapeHeight = keyboardHeight;
notifyKeyboardHeightChanged(keyboardLandscapeHeight, orientation);
}
}
private int getScreenOrientation() {
return activity.getResources().getConfiguration().orientation;
}
private void notifyKeyboardHeightChanged(int height, int orientation) {
if (observer != null) {
observer.onKeyboardHeightChanged(height, orientation);
}
}
}

View File

@@ -1,13 +1,18 @@
package com.damus.notedeck;
import android.content.ClipData;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import androidx.core.graphics.Insets;
import androidx.core.view.DisplayCutoutCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
@@ -15,52 +20,38 @@ import androidx.core.view.WindowInsetsControllerCompat;
import com.google.androidgamesdk.GameActivity;
import java.io.ByteArrayOutputStream;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
public class MainActivity extends GameActivity {
static {
System.loadLibrary("notedeck_chrome");
}
static final int REQUEST_CODE_PICK_FILE = 420;
private native void nativeOnKeyboardHeightChanged(int height);
private KeyboardHeightHelper keyboardHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
// Shrink view so it does not get covered by insets.
private native void nativeOnFilePickedFailed(String uri, String e);
private native void nativeOnFilePickedWithContent(Object[] uri_info, byte[] content);
setupInsets();
//setupFullscreen()
keyboardHelper = new KeyboardHeightHelper(this);
super.onCreate(savedInstanceState);
}
private void setupFullscreen() {
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
WindowInsetsControllerCompat controller =
WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
if (controller != null) {
controller.setSystemBarsBehavior(
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
);
controller.hide(WindowInsetsCompat.Type.systemBars());
}
//focus(getContent())
}
// not sure if this does anything
private void focus(View content) {
content.setFocusable(true);
content.setFocusableInTouchMode(true);
content.requestFocus();
}
private View getContent() {
return getWindow().getDecorView().findViewById(android.R.id.content);
public void openFilePicker() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.setType("*/*");
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
intent.addCategory(Intent.CATEGORY_OPENABLE);
startActivityForResult(intent, REQUEST_CODE_PICK_FILE);
}
private void setupInsets() {
// NOTE(jb55): This is needed for keyboard visibility. Without this the
// window still gets the right insets, but theyre consumed before they
// reach the NDK side.
//WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
// NOTE(jb55): This is needed for keyboard visibility. If the bars are
// permanently gone, Android routes the keyboard over the GL surface and
// doesnt change insets.
//WindowInsetsControllerCompat ic = WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
//ic.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
View content = getContent();
ViewCompat.setOnApplyWindowInsetsListener(content, (v, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
@@ -72,38 +63,176 @@ public class MainActivity extends GameActivity {
mlp.rightMargin = insets.right;
v.setLayoutParams(mlp);
return WindowInsetsCompat.CONSUMED;
return windowInsets;
});
WindowCompat.setDecorFitsSystemWindows(getWindow(), true);
}
@Override
public void onResume() {
super.onResume();
keyboardHelper.start();
}
@Override
public void onPause() {
super.onPause();
keyboardHelper.stop();
}
@Override
public void onDestroy() {
super.onDestroy();
keyboardHelper.close();
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// Offset the location so it fits the view with margins caused by insets.
private void processSelectedFile(Uri uri) {
try {
nativeOnFilePickedWithContent(this.getUriInfo(uri), readUriContent(uri));
} catch (Exception e) {
Log.e("MainActivity", "Error processing file: " + uri.toString(), e);
int[] location = new int[2];
findViewById(android.R.id.content).getLocationOnScreen(location);
event.offsetLocation(-location[0], -location[1]);
return super.onTouchEvent(event);
nativeOnFilePickedFailed(uri.toString(), e.toString());
}
}
private Object[] getUriInfo(Uri uri) throws Exception {
if (!uri.getScheme().equals("content")) {
throw new Exception("uri should start with content://");
}
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
while (cursor.moveToNext()) {
Object[] info = new Object[3];
int col_idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
info[0] = cursor.getString(col_idx);
col_idx = cursor.getColumnIndex(OpenableColumns.SIZE);
info[1] = cursor.getLong(col_idx);
col_idx = cursor.getColumnIndex("mime_type");
info[2] = cursor.getString(col_idx);
return info;
}
return null;
}
private byte[] readUriContent(Uri uri) {
InputStream inputStream = null;
ByteArrayOutputStream buffer = null;
try {
inputStream = getContentResolver().openInputStream(uri);
if (inputStream == null) {
Log.e("MainActivity", "Could not open input stream for URI: " + uri);
return null;
}
buffer = new ByteArrayOutputStream();
byte[] data = new byte[8192]; // 8KB buffer
int bytesRead;
while ((bytesRead = inputStream.read(data)) != -1) {
buffer.write(data, 0, bytesRead);
}
byte[] result = buffer.toByteArray();
Log.d("MainActivity", "Successfully read " + result.length + " bytes");
return result;
} catch (IOException e) {
Log.e("MainActivity", "IOException while reading URI: " + uri, e);
return null;
} catch (SecurityException e) {
Log.e("MainActivity", "SecurityException while reading URI: " + uri, e);
return null;
} finally {
// Close streams
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
Log.e("MainActivity", "Error closing input stream", e);
}
}
if (buffer != null) {
try {
buffer.close();
} catch (IOException e) {
Log.e("MainActivity", "Error closing buffer", e);
}
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// Shrink view so it does not get covered by insets.
setupInsets();
//setupFullscreen()
super.onCreate(savedInstanceState);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_PICK_FILE && resultCode == RESULT_OK) {
if (data == null) return;
if (data.getClipData() != null) {
// Multiple files selected
ClipData clipData = data.getClipData();
for (int i = 0; i < clipData.getItemCount(); i++) {
Uri uri = clipData.getItemAt(i).getUri();
processSelectedFile(uri);
}
} else if (data.getData() != null) {
// Single file selected
Uri uri = data.getData();
processSelectedFile(uri);
}
}
}
private void setupFullscreen() {
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
WindowInsetsControllerCompat controller =
WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
if (controller != null) {
controller.setSystemBarsBehavior(
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
);
controller.hide(WindowInsetsCompat.Type.systemBars());
}
//focus(getContent())
}
// not sure if this does anything
private void focus(View content) {
content.setFocusable(true);
content.setFocusableInTouchMode(true);
content.requestFocus();
}
private View getContent() {
return getWindow().getDecorView().findViewById(android.R.id.content);
}
@Override
public void onResume() {
super.onResume();
}
@Override
public void onPause() {
super.onPause();
}
@Override
public void onDestroy() {
super.onDestroy();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// Offset the location so it fits the view with margins caused by insets.
int[] location = new int[2];
findViewById(android.R.id.content).getLocationOnScreen(location);
event.offsetLocation(-location[0], -location[1]);
return super.onTouchEvent(event);
}
}

View File

@@ -2,24 +2,21 @@
//use egui_android::run_android;
use egui_winit::winit::platform::android::activity::AndroidApp;
use notedeck_columns::Damus;
use notedeck_dave::Dave;
use crate::{app::NotedeckApp, chrome::Chrome, setup::setup_chrome};
use crate::chrome::Chrome;
use notedeck::Notedeck;
#[no_mangle]
#[tokio::main]
pub async fn android_main(app: AndroidApp) {
pub async fn android_main(android_app: AndroidApp) {
//use tracing_logcat::{LogcatMakeWriter, LogcatTag};
use tracing_subscriber::{prelude::*, EnvFilter};
std::env::set_var("RUST_BACKTRACE", "full");
//std::env::set_var("DAVE_ENDPOINT", "http://ollama.jb55.com/v1");
//std::env::set_var("DAVE_MODEL", "hhao/qwen2.5-coder-tools:latest");
std::env::set_var(
"RUST_LOG",
"egui=debug,egui-winit=debug,notedeck=debug,notedeck_columns=debug,notedeck_chrome=debug,enostr=debug,android_activity=debug",
"egui=debug,egui-winit=debug,winit=debug,notedeck=debug,notedeck_columns=debug,notedeck_chrome=debug,enostr=debug,android_activity=debug",
);
//std::env::set_var(
@@ -44,7 +41,7 @@ pub async fn android_main(app: AndroidApp) {
.with(fmt_layer)
.init();
let path = app.internal_data_path().expect("data path");
let path = android_app.internal_data_path().expect("data path");
let mut options = eframe::NativeOptions {
depth_buffer: 24,
..eframe::NativeOptions::default()
@@ -57,41 +54,20 @@ pub async fn android_main(app: AndroidApp) {
// builder.with_android_app(app_clone_for_event_loop);
//}));
options.android_app = Some(app.clone());
options.android_app = Some(android_app.clone());
let app_args = get_app_args(app);
let app_args = get_app_args();
let _res = eframe::run_native(
"Damus Notedeck",
options,
Box::new(move |cc| {
let ctx = &cc.egui_ctx;
let mut notedeck = Notedeck::new(ctx, path, &app_args);
setup_chrome(ctx, &notedeck.args(), notedeck.theme());
let context = &mut notedeck.app_context();
let dave = Dave::new(cc.wgpu_render_state.as_ref());
let columns = Damus::new(context, &app_args);
let mut chrome = Chrome::new();
// ensure we recognized all the arguments
let completely_unrecognized: Vec<String> = notedeck
.unrecognized_args()
.intersection(columns.unrecognized_args())
.cloned()
.collect();
assert!(
completely_unrecognized.is_empty(),
"unrecognized args: {:?}",
completely_unrecognized
);
chrome.add_app(NotedeckApp::Columns(columns));
chrome.add_app(NotedeckApp::Dave(dave));
// test dav
chrome.set_active(0);
notedeck.set_android_context(android_app);
notedeck.setup(ctx);
let chrome = Chrome::new_with_apps(cc, &app_args, &mut notedeck)?;
notedeck.set_app(chrome);
Ok(Box::new(notedeck))
@@ -128,7 +104,7 @@ Using internal storage would be better but it seems hard to get the config file
the device ...
*/
fn get_app_args(_app: AndroidApp) -> Vec<String> {
fn get_app_args() -> Vec<String> {
vec!["argv0-placeholder".to_string()]
/*
use serde_json::value;

View File

@@ -1,11 +1,15 @@
use notedeck::{AppAction, AppContext};
use notedeck_clndash::ClnDash;
use notedeck_columns::Damus;
use notedeck_dave::Dave;
use notedeck_notebook::Notebook;
#[allow(clippy::large_enum_variant)]
pub enum NotedeckApp {
Dave(Dave),
Columns(Damus),
Dave(Box<Dave>),
Columns(Box<Damus>),
Notebook(Box<Notebook>),
ClnDash(Box<ClnDash>),
Other(Box<dyn notedeck::App>),
}
@@ -14,6 +18,8 @@ impl notedeck::App for NotedeckApp {
match self {
NotedeckApp::Dave(dave) => dave.update(ctx, ui),
NotedeckApp::Columns(columns) => columns.update(ctx, ui),
NotedeckApp::Notebook(notebook) => notebook.update(ctx, ui),
NotedeckApp::ClnDash(clndash) => clndash.update(ctx, ui),
NotedeckApp::Other(other) => other.update(ctx, ui),
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,151 +0,0 @@
use egui::{FontData, FontDefinitions, FontTweak};
use std::collections::BTreeMap;
use std::sync::Arc;
use tracing::debug;
use notedeck::fonts::NamedFontFamily;
// Use gossip's approach to font loading. This includes japanese fonts
// for rending stuff from japanese users.
pub fn setup_fonts(ctx: &egui::Context) {
let mut font_data: BTreeMap<String, Arc<FontData>> = BTreeMap::new();
let mut families = BTreeMap::new();
font_data.insert(
"Onest".to_owned(),
Arc::new(FontData::from_static(include_bytes!(
"../../../assets/fonts/onest/OnestRegular1602-hint.ttf"
))),
);
font_data.insert(
"OnestMedium".to_owned(),
Arc::new(FontData::from_static(include_bytes!(
"../../../assets/fonts/onest/OnestMedium1602-hint.ttf"
))),
);
font_data.insert(
"DejaVuSans".to_owned(),
Arc::new(FontData::from_static(include_bytes!(
"../../../assets/fonts/DejaVuSansSansEmoji.ttf"
))),
);
font_data.insert(
"OnestBold".to_owned(),
Arc::new(FontData::from_static(include_bytes!(
"../../../assets/fonts/onest/OnestBold1602-hint.ttf"
))),
);
/*
font_data.insert(
"DejaVuSansBold".to_owned(),
FontData::from_static(include_bytes!(
"../assets/fonts/DejaVuSans-Bold-SansEmoji.ttf"
)),
);
font_data.insert(
"DejaVuSans".to_owned(),
FontData::from_static(include_bytes!("../assets/fonts/DejaVuSansSansEmoji.ttf")),
);
font_data.insert(
"DejaVuSansBold".to_owned(),
FontData::from_static(include_bytes!(
"../assets/fonts/DejaVuSans-Bold-SansEmoji.ttf"
)),
);
*/
font_data.insert(
"Inconsolata".to_owned(),
Arc::new(
FontData::from_static(include_bytes!(
"../../../assets/fonts/Inconsolata-Regular.ttf"
))
.tweak(FontTweak {
scale: 1.22, // This font is smaller than DejaVuSans
y_offset_factor: -0.18, // and too low
y_offset: 0.0,
baseline_offset_factor: 0.0,
}),
),
);
font_data.insert(
"NotoSansCJK".to_owned(),
Arc::new(FontData::from_static(include_bytes!(
"../../../assets/fonts/NotoSansCJK-Regular.ttc"
))),
);
font_data.insert(
"NotoSansThai".to_owned(),
Arc::new(FontData::from_static(include_bytes!(
"../../../assets/fonts/NotoSansThai-Regular.ttf"
))),
);
// Some good looking emojis. Use as first priority:
font_data.insert(
"NotoEmoji".to_owned(),
Arc::new(
FontData::from_static(include_bytes!(
"../../../assets/fonts/NotoEmoji-Regular.ttf"
))
.tweak(FontTweak {
scale: 1.1, // make them a touch larger
y_offset_factor: 0.0,
y_offset: 0.0,
baseline_offset_factor: 0.0,
}),
),
);
let base_fonts = vec![
"DejaVuSans".to_owned(),
"NotoEmoji".to_owned(),
"NotoSansCJK".to_owned(),
"NotoSansThai".to_owned(),
];
let mut proportional = vec!["Onest".to_owned()];
proportional.extend(base_fonts.clone());
let mut medium = vec!["OnestMedium".to_owned()];
medium.extend(base_fonts.clone());
let mut mono = vec!["Inconsolata".to_owned()];
mono.extend(base_fonts.clone());
let mut bold = vec!["OnestBold".to_owned()];
bold.extend(base_fonts.clone());
let emoji = vec!["NotoEmoji".to_owned()];
families.insert(egui::FontFamily::Proportional, proportional);
families.insert(egui::FontFamily::Monospace, mono);
families.insert(
egui::FontFamily::Name(NamedFontFamily::Medium.as_str().into()),
medium,
);
families.insert(
egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()),
bold,
);
families.insert(
egui::FontFamily::Name(NamedFontFamily::Emoji.as_str().into()),
emoji,
);
debug!("fonts: {:?}", families);
let defs = FontDefinitions {
font_data,
families,
};
ctx.set_fonts(defs);
}

View File

@@ -1,12 +1,12 @@
pub mod fonts;
pub mod setup;
pub mod theme;
#[cfg(target_os = "android")]
mod android;
mod app;
mod chrome;
mod options;
pub use app::NotedeckApp;
pub use chrome::Chrome;
pub use options::ChromeOptions;

View File

@@ -10,12 +10,7 @@ static GLOBAL: AccountingAllocator<std::alloc::System> =
AccountingAllocator::new(std::alloc::System);
use notedeck::{DataPath, DataPathType, Notedeck};
use notedeck_chrome::{
setup::{generate_native_options, setup_chrome},
Chrome, NotedeckApp,
};
use notedeck_columns::Damus;
use notedeck_dave::Dave;
use notedeck_chrome::{setup::generate_native_options, Chrome};
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::EnvFilter;
@@ -91,29 +86,8 @@ async fn main() {
let ctx = &cc.egui_ctx;
let mut notedeck = Notedeck::new(ctx, base_path, &args);
let mut chrome = Chrome::new();
let columns = Damus::new(&mut notedeck.app_context(), &args);
let dave = Dave::new(cc.wgpu_render_state.as_ref());
setup_chrome(ctx, notedeck.args(), notedeck.theme());
// ensure we recognized all the arguments
let completely_unrecognized: Vec<String> = notedeck
.unrecognized_args()
.intersection(columns.unrecognized_args())
.cloned()
.collect();
assert!(
completely_unrecognized.is_empty(),
"unrecognized args: {completely_unrecognized:?}"
);
chrome.add_app(NotedeckApp::Columns(columns));
chrome.add_app(NotedeckApp::Dave(dave));
chrome.set_active(0);
notedeck.setup(ctx);
let chrome = Chrome::new_with_apps(cc, &args, &mut notedeck)?;
notedeck.set_app(chrome);
Ok(Box::new(notedeck))
@@ -147,7 +121,8 @@ pub fn main() {
#[cfg(test)]
mod tests {
use super::{Damus, Notedeck};
use super::Notedeck;
use notedeck_columns::Damus;
use std::path::{Path, PathBuf};
fn create_tmp_dir() -> PathBuf {
@@ -208,21 +183,9 @@ mod tests {
let ctx = egui::Context::default();
let mut notedeck = Notedeck::new(&ctx, &tmpdir, &args);
let unrecognized_args = notedeck.unrecognized_args().clone();
let mut app_ctx = notedeck.app_context();
let app = Damus::new(&mut app_ctx, &args);
// ensure we recognized all the arguments
let completely_unrecognized: Vec<String> = unrecognized_args
.intersection(app.unrecognized_args())
.cloned()
.collect();
assert!(
completely_unrecognized.is_empty(),
"unrecognized args: {:?}",
completely_unrecognized
);
assert_eq!(app.columns(app_ctx.accounts).columns().len(), 2);
let tl1 = app

View File

@@ -0,0 +1,38 @@
use bitflags::bitflags;
bitflags! {
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ChromeOptions: u64 {
/// Is the chrome currently open?
const NoOptions = 0;
/// Is the chrome currently open?
const IsOpen = 1 << 0;
/// Are we simulating a virtual keyboard? This is mostly for debugging
/// if we are too lazy to open up a real mobile device with soft
/// keyboard
const VirtualKeyboard = 1 << 1;
/// Are we showing the memory debug window?
const MemoryDebug = 1 << 2;
/// Repaint debug
const RepaintDebug = 1 << 3;
/// We need soft keyboard visibility
const KeyboardVisibility = 1 << 4;
}
}
impl Default for ChromeOptions {
fn default() -> Self {
let mut options = ChromeOptions::NoOptions;
options.set(
ChromeOptions::IsOpen,
!notedeck::ui::is_compiled_as_mobile(),
);
options
}
}

View File

@@ -38,7 +38,13 @@ impl PreviewRunner {
"unrecognized args: {:?}",
notedeck.unrecognized_args()
);
setup_chrome(ctx, notedeck.args(), notedeck.theme());
setup_chrome(
ctx,
notedeck.args(),
notedeck.theme(),
notedeck.note_body_font_size(),
notedeck.zoom_factor(),
);
notedeck.set_app(PreviewApp::new(preview));

View File

@@ -1,55 +1,6 @@
use crate::{fonts, theme};
use eframe::NativeOptions;
use egui::ThemePreference;
use notedeck::{AppSizeHandler, DataPath};
use notedeck_ui::app_images;
use tracing::info;
pub fn setup_chrome(ctx: &egui::Context, args: &notedeck::Args, theme: ThemePreference) {
let is_mobile = args
.is_mobile
.unwrap_or(notedeck::ui::is_compiled_as_mobile());
let is_oled = notedeck::ui::is_oled();
// Some people have been running notedeck in debug, let's catch that!
if !args.tests && cfg!(debug_assertions) && !args.debug {
println!("--- WELCOME TO DAMUS NOTEDECK! ---");
println!("It looks like are running notedeck in debug mode, unless you are a developer, this is not likely what you want.");
println!("If you are a developer, run `cargo run -- --debug` to skip this message.");
println!("For everyone else, try again with `cargo run --release`. Enjoy!");
println!("---------------------------------");
panic!();
}
ctx.options_mut(|o| {
info!("Loaded theme {:?} from disk", theme);
o.theme_preference = theme;
});
ctx.set_visuals_of(egui::Theme::Dark, theme::dark_mode(is_oled));
ctx.set_visuals_of(egui::Theme::Light, theme::light_mode());
setup_cc(ctx, is_mobile);
}
pub fn setup_cc(ctx: &egui::Context, is_mobile: bool) {
fonts::setup_fonts(ctx);
if notedeck::ui::is_compiled_as_mobile() {
ctx.set_pixels_per_point(ctx.pixels_per_point() + 0.2);
}
//ctx.set_pixels_per_point(1.0);
//
//
//ctx.tessellation_options_mut(|to| to.feathering = false);
egui_extras::install_image_loaders(ctx);
ctx.options_mut(|o| {
o.input_options.max_click_duration = 0.4;
});
ctx.all_styles_mut(|style| theme::add_custom_style(is_mobile, style));
}
pub fn generate_native_options(paths: DataPath) -> NativeOptions {
let window_builder = Box::new(move |builder: egui::ViewportBuilder| {

View File

@@ -1,149 +0,0 @@
use egui::{style::Interaction, Color32, FontId, Style, Visuals};
use notedeck::{ColorTheme, NotedeckTextStyle};
use strum::IntoEnumIterator;
pub const PURPLE: Color32 = Color32::from_rgb(0xCC, 0x43, 0xC5);
const PURPLE_ALT: Color32 = Color32::from_rgb(0x82, 0x56, 0xDD);
//pub const DARK_BG: Color32 = egui::Color32::from_rgb(40, 44, 52);
pub const GRAY_SECONDARY: Color32 = Color32::from_rgb(0x8A, 0x8A, 0x8A);
const BLACK: Color32 = Color32::from_rgb(0x00, 0x00, 0x00);
const RED_700: Color32 = Color32::from_rgb(0xC7, 0x37, 0x5A);
const ORANGE_700: Color32 = Color32::from_rgb(0xF6, 0xB1, 0x4A);
// BACKGROUNDS
const SEMI_DARKER_BG: Color32 = Color32::from_rgb(0x39, 0x39, 0x39);
const DARKER_BG: Color32 = Color32::from_rgb(0x1F, 0x1F, 0x1F);
const DARK_BG: Color32 = Color32::from_rgb(0x2C, 0x2C, 0x2C);
const DARK_ISH_BG: Color32 = Color32::from_rgb(0x25, 0x25, 0x25);
const SEMI_DARK_BG: Color32 = Color32::from_rgb(0x44, 0x44, 0x44);
const LIGHTER_GRAY: Color32 = Color32::from_rgb(0xf8, 0xf8, 0xf8);
const LIGHT_GRAY: Color32 = Color32::from_rgb(0xc8, 0xc8, 0xc8); // 78%
const DARKER_GRAY: Color32 = Color32::from_rgb(0xa5, 0xa5, 0xa5); // 65%
const EVEN_DARKER_GRAY: Color32 = Color32::from_rgb(0x89, 0x89, 0x89); // 54%
pub fn desktop_dark_color_theme() -> ColorTheme {
ColorTheme {
// VISUALS
panel_fill: DARKER_BG,
extreme_bg_color: DARK_ISH_BG,
text_color: Color32::WHITE,
err_fg_color: RED_700,
warn_fg_color: ORANGE_700,
hyperlink_color: PURPLE,
selection_color: PURPLE_ALT,
// WINDOW
window_fill: DARK_ISH_BG,
window_stroke_color: DARK_BG,
// NONINTERACTIVE WIDGET
noninteractive_bg_fill: DARK_ISH_BG,
noninteractive_weak_bg_fill: DARK_BG,
noninteractive_bg_stroke_color: SEMI_DARKER_BG,
noninteractive_fg_stroke_color: GRAY_SECONDARY,
// INACTIVE WIDGET
inactive_bg_stroke_color: SEMI_DARKER_BG,
inactive_bg_fill: Color32::from_rgb(0x25, 0x25, 0x25),
inactive_weak_bg_fill: SEMI_DARK_BG,
}
}
pub fn mobile_dark_color_theme() -> ColorTheme {
ColorTheme {
panel_fill: Color32::BLACK,
noninteractive_weak_bg_fill: Color32::from_rgb(0x1F, 0x1F, 0x1F),
..desktop_dark_color_theme()
}
}
pub fn light_color_theme() -> ColorTheme {
ColorTheme {
// VISUALS
panel_fill: Color32::WHITE,
extreme_bg_color: LIGHTER_GRAY,
text_color: BLACK,
err_fg_color: RED_700,
warn_fg_color: ORANGE_700,
hyperlink_color: PURPLE,
selection_color: PURPLE_ALT,
// WINDOW
window_fill: Color32::WHITE,
window_stroke_color: DARKER_GRAY,
// NONINTERACTIVE WIDGET
noninteractive_bg_fill: Color32::WHITE,
noninteractive_weak_bg_fill: LIGHTER_GRAY,
noninteractive_bg_stroke_color: LIGHT_GRAY,
noninteractive_fg_stroke_color: GRAY_SECONDARY,
// INACTIVE WIDGET
inactive_bg_stroke_color: EVEN_DARKER_GRAY,
inactive_bg_fill: LIGHTER_GRAY,
inactive_weak_bg_fill: LIGHTER_GRAY,
}
}
pub fn light_mode() -> Visuals {
notedeck::theme::create_themed_visuals(light_color_theme(), Visuals::light())
}
pub fn dark_mode(is_oled: bool) -> Visuals {
notedeck::theme::create_themed_visuals(
if is_oled {
mobile_dark_color_theme()
} else {
desktop_dark_color_theme()
},
Visuals::dark(),
)
}
/// Create custom text sizes for any FontSizes
pub fn add_custom_style(is_mobile: bool, style: &mut Style) {
let font_size = if is_mobile {
notedeck::fonts::mobile_font_size
} else {
notedeck::fonts::desktop_font_size
};
style.text_styles = NotedeckTextStyle::iter()
.map(|text_style| {
(
text_style.text_style(),
FontId::new(font_size(&text_style), text_style.font_family()),
)
})
.collect();
style.interaction = Interaction {
tooltip_delay: 0.1,
show_tooltips_only_when_still: false,
..Interaction::default()
};
// debug: show callstack for the current widget on hover if all
// modifier keys are pressed down.
#[cfg(feature = "debug-widget-callstack")]
{
#[cfg(not(debug_assertions))]
compile_error!(
"The `debug-widget-callstack` feature requires a debug build, \
release builds are unsupported."
);
style.debug.debug_on_hover_with_all_modifiers = true;
}
// debug: show an overlay on all interactive widgets
#[cfg(feature = "debug-interactive-widgets")]
{
#[cfg(not(debug_assertions))]
compile_error!(
"The `debug-interactive-widgets` feature requires a debug build, \
release builds are unsupported."
);
style.debug.show_interactive_widgets = true;
}
}

View File

@@ -0,0 +1,21 @@
[package]
name = "notedeck_clndash"
edition = "2024"
version.workspace = true
[dependencies]
egui = { workspace = true }
notedeck = { workspace = true }
#notedeck_ui = { workspace = true }
eframe = { workspace = true }
tracing = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
serde = { workspace = true }
egui_extras = { workspace = true }
lightning-invoice = { workspace = true }
hex = { workspace = true }
nostrdb = { workspace = true }
notedeck_ui = { workspace = true }
lnsocket = "0.5.1"

View File

@@ -0,0 +1,77 @@
# ⚡ clndash
Your Core Lightning dashboard, **without the server nonsense**.
clndash is a weird little experiment: a [notedeck][notedeck] app that talks to your node **directly over the Lightning Network** using [lnsocket][lnsocket] + [Commando][commando] RPCs.
No HTTP. No nginx. No VPS.
Just open clndash, point it at your node, and boom — youre in.
<img src="https://jb55.com/s/476285c50d06c3ce.png" width="50%" />
---
## 🤯 Why?
Because sometimes you just want to *see your channels* and *check invoices* without SSH-ing into a box and typing `lightning-cli`.
And because LN is already a secure, encrypted connection layer — why not just use that?
---
## 🔥 Features (as of today)
* **Plug-and-play LN connection** powered by [lnsocket][lnsocket]
* **Commando RPC** all dashboard data is fetched directly from your CLN node over Lightning
* **Channel overview** total capacity, inbound/outbound liquidity, largest channel, and pretty bars
* **Invoices** shows recent paid invoices (with zap previews if they came from Nostr)
* **No extra daemons** you dont need to run a server to use it
---
## 🪄 Nostr Bonus
Because its a notedeck app, clndash can **render zaps** inline.
Yes, your Core Lightning dashboard can now show you when someone on Nostr just sent you sats and why.
---
## 🏗 Still Baking
This is WIP.
Youll probably hit bugs. UI might be janky. Some features may vanish or suddenly mutate.
If youre reading this and still excited — youre the exact audience.
---
## 🛠 How to connect
1. Get your nodes **public address** (host\:port) and a **Commando rune** with safe permissions.
2. Set them as environment variables:
```bash
export CLNDASH_ID="03f3c108ccd536b8526841f0a5c58212bb9e6584a1eb493080e7c1cc34f82dad71"
export CLNDASH_HOST="node.example.com:9735"
export CLNDASH_RUNE="your_rune_here"
```
3. Run clndash inside notedeck by calling notedeck with the `--clndash` argument.
4. Bask in the glow of real-time LN data over an LN connection.
---
## ⚠️ Disclaimer
* Dont give it a rune that can spend your funds.
* Dont blame me if you break something — this is experimental territory.
* If it connects on the first try, buy yourself a beer.
---
If you like living on the edge of LN/Nostr tooling, youll like this.
If you dont… youll probably want to wait a bit.
[commando]: https://docs.corelightning.org/reference/commando
[lnsocket]: https://github.com/jb55/lnsocket-rs
[notedeck]: https://github.com/damus-io/notedeck

View File

@@ -0,0 +1,124 @@
use crate::event::LoadingState;
use crate::ui;
use egui::Color32;
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
pub struct ListPeerChannel {
pub short_channel_id: String,
pub our_reserve_msat: i64,
pub to_us_msat: i64,
pub total_msat: i64,
pub their_reserve_msat: i64,
}
pub struct Channel {
pub to_us: i64,
pub to_them: i64,
pub original: ListPeerChannel,
}
pub struct Channels {
pub max_total_msat: i64,
pub avail_in: i64,
pub avail_out: i64,
pub channels: Vec<Channel>,
}
pub fn channels_ui(ui: &mut egui::Ui, channels: &LoadingState<Channels, lnsocket::Error>) {
match channels {
LoadingState::Loaded(channels) => {
if channels.channels.is_empty() {
ui.label("no channels yet...");
return;
}
for channel in &channels.channels {
channel_ui(ui, channel, channels.max_total_msat);
}
ui.label(format!(
"available out {}",
ui::human_sat(channels.avail_out)
));
ui.label(format!("available in {}", ui::human_sat(channels.avail_in)));
}
LoadingState::Failed(err) => {
ui.label(format!("error fetching channels: {err}"));
}
LoadingState::Loading => {
ui.label("fetching channels...");
}
}
}
pub fn channel_ui(ui: &mut egui::Ui, c: &Channel, max_total_msat: i64) {
// ---------- numbers ----------
let short_channel_id = &c.original.short_channel_id;
let cap_ratio = (c.original.total_msat as f32 / max_total_msat.max(1) as f32).clamp(0.0, 1.0);
// Feel free to switch to log scaling if you have whales:
//let cap_ratio = ((c.original.total_msat as f32 + 1.0).log10() / (max_total_msat as f32 + 1.0).log10()).clamp(0.0, 1.0);
// ---------- colors & style ----------
let out_color = Color32::from_rgb(84, 69, 201); // blue
let in_color = Color32::from_rgb(158, 56, 180); // purple
// Thickness scales with capacity, but keeps a nice minimum
let thickness = 10.0 + cap_ratio * 22.0; // 10 → 32 px
let row_h = thickness + 14.0;
// ---------- layout ----------
let (rect, response) = ui.allocate_exact_size(
egui::vec2(ui.available_width(), row_h),
egui::Sense::hover(),
);
let painter = ui.painter_at(rect);
let bar_rect = egui::Rect::from_min_max(
egui::pos2(rect.left(), rect.center().y - thickness * 0.5),
egui::pos2(rect.right(), rect.center().y + thickness * 0.5),
);
let corner_radius = (thickness * 0.5) as u8;
let out_radius = egui::CornerRadius {
ne: 0,
nw: corner_radius,
sw: corner_radius,
se: 0,
};
let in_radius = egui::CornerRadius {
ne: corner_radius,
nw: 0,
sw: 0,
se: corner_radius,
};
/*
painter.rect_filled(bar_rect, rounding, track_color);
painter.rect_stroke(bar_rect, rounding, track_stroke, egui::StrokeKind::Middle);
*/
// Split widths
let usable = (c.to_us + c.to_them).max(1) as f32;
let out_w = (bar_rect.width() * (c.to_us as f32 / usable)).round();
let split_x = bar_rect.left() + out_w;
// Outbound fill (left)
let out_rect = egui::Rect::from_min_max(bar_rect.min, egui::pos2(split_x, bar_rect.max.y));
if out_rect.width() > 0.5 {
painter.rect_filled(out_rect, out_radius, out_color);
}
// Inbound fill (right)
let in_rect = egui::Rect::from_min_max(egui::pos2(split_x, bar_rect.min.y), bar_rect.max);
if in_rect.width() > 0.5 {
painter.rect_filled(in_rect, in_radius, in_color);
}
// Tooltip
response.on_hover_text_at_pointer(format!(
"Channel ID {short_channel_id}\nOutbound (ours): {} sats\nInbound (theirs): {} sats\nCapacity: {} sats",
ui::human_sat(c.to_us),
ui::human_sat(c.to_them),
ui::human_sat(c.original.total_msat),
));
}

View File

@@ -0,0 +1,80 @@
use crate::channels::Channels;
use crate::invoice::Invoice;
use serde::Serialize;
use serde_json::Value;
pub enum ConnectionState {
Dead(String),
Connecting,
Active,
}
pub enum LoadingState<T, E> {
Loading,
Failed(E),
Loaded(T),
}
impl<T, E> Default for LoadingState<T, E> {
fn default() -> Self {
Self::Loading
}
}
impl<T, E> LoadingState<T, E> {
fn _as_ref(&self) -> LoadingState<&T, &E> {
match self {
Self::Loading => LoadingState::<&T, &E>::Loading,
Self::Failed(err) => LoadingState::<&T, &E>::Failed(err),
Self::Loaded(t) => LoadingState::<&T, &E>::Loaded(t),
}
}
pub fn from_result(res: Result<T, E>) -> LoadingState<T, E> {
match res {
Ok(r) => LoadingState::Loaded(r),
Err(err) => LoadingState::Failed(err),
}
}
/*
fn unwrap(self) -> T {
let Self::Loaded(t) = self else {
panic!("unwrap in LoadingState");
};
t
}
*/
}
#[derive(Serialize, Debug, Clone)]
pub struct _WaitRequest {
pub indexname: String,
pub subsystem: String,
pub nextvalue: u64,
}
#[derive(Clone, Debug)]
pub enum Request {
GetInfo,
ListPeerChannels,
PaidInvoices(u32),
}
/// Responses from the socket
pub enum ClnResponse {
GetInfo(Value),
ListPeerChannels(Result<Channels, lnsocket::Error>),
PaidInvoices(Result<Vec<Invoice>, lnsocket::Error>),
}
pub enum Event {
/// We lost the socket somehow
Ended {
reason: String,
},
Connected,
Response(ClnResponse),
}

View File

@@ -0,0 +1,77 @@
use crate::event::LoadingState;
use crate::ui;
use lightning_invoice::Bolt11Invoice;
use notedeck::AppContext;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Deserialize, Serialize)]
pub struct Invoice {
pub lastpay_index: Option<u64>,
pub label: String,
pub bolt11: Bolt11Invoice,
pub payment_hash: String,
pub amount_msat: u64,
pub status: String,
pub description: String,
pub expires_at: u64,
pub created_index: u64,
pub updated_index: u64,
}
pub fn invoices_ui(
ui: &mut egui::Ui,
invoice_notes: &HashMap<String, [u8; 32]>,
ctx: &mut AppContext,
invoices: &LoadingState<Vec<Invoice>, lnsocket::Error>,
) {
match invoices {
LoadingState::Loading => {
ui.label("loading invoices...");
}
LoadingState::Failed(err) => {
ui.label(format!("failed to load invoices: {err}"));
}
LoadingState::Loaded(invoices) => {
use egui_extras::{Column, TableBuilder};
TableBuilder::new(ui)
.column(Column::auto().resizable(true))
.column(Column::remainder())
.vscroll(false)
.header(20.0, |mut header| {
header.col(|ui| {
ui.strong("description");
});
header.col(|ui| {
ui.strong("amount");
});
})
.body(|mut body| {
for invoice in invoices {
body.row(20.0, |mut row| {
row.col(|ui| {
if invoice.description.starts_with("{") {
ui.label("Zap!").on_hover_ui_at_pointer(|ui| {
ui::note_hover_ui(ui, &invoice.label, ctx, invoice_notes);
});
} else {
ui.label(&invoice.description);
}
});
row.col(|ui| match invoice.bolt11.amount_milli_satoshis() {
None => {
ui.label("any");
}
Some(amt) => {
ui.label(ui::human_verbose_sat(amt as i64));
}
});
});
}
});
}
}
}

View File

@@ -0,0 +1,290 @@
use crate::channels::Channel;
use crate::channels::Channels;
use crate::channels::ListPeerChannel;
use crate::event::ClnResponse;
use crate::event::ConnectionState;
use crate::event::Event;
use crate::event::LoadingState;
use crate::event::Request;
use crate::invoice::Invoice;
use crate::summary::Summary;
use crate::watch::fetch_paid_invoices;
use lnsocket::bitcoin::secp256k1::{PublicKey, SecretKey, rand};
use lnsocket::{CommandoClient, LNSocket};
use nostrdb::Ndb;
use notedeck::{AppAction, AppContext};
use serde_json::json;
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::Arc;
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel};
mod channels;
mod event;
mod invoice;
mod summary;
mod ui;
mod watch;
#[derive(Default)]
pub struct ClnDash {
initialized: bool,
connection_state: ConnectionState,
summary: LoadingState<Summary, lnsocket::Error>,
get_info: LoadingState<String, lnsocket::Error>,
channels: LoadingState<Channels, lnsocket::Error>,
invoices: LoadingState<Vec<Invoice>, lnsocket::Error>,
channel: Option<CommChannel>,
last_summary: Option<Summary>,
// invoice label to zapreq id
invoice_zap_reqs: HashMap<String, [u8; 32]>,
}
#[derive(serde::Deserialize)]
pub struct ZapReqId {
#[serde(with = "hex::serde")]
id: [u8; 32],
}
impl Default for ConnectionState {
fn default() -> Self {
ConnectionState::Dead("uninitialized".to_string())
}
}
struct CommChannel {
req_tx: UnboundedSender<Request>,
event_rx: UnboundedReceiver<Event>,
}
impl notedeck::App for ClnDash {
fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction> {
if !self.initialized {
self.connection_state = ConnectionState::Connecting;
self.setup_connection();
self.initialized = true;
}
self.process_events(ctx.ndb);
self.show(ui, ctx);
None
}
}
impl ClnDash {
fn show(&mut self, ui: &mut egui::Ui, ctx: &mut AppContext) {
egui::Frame::new()
.inner_margin(egui::Margin::same(20))
.show(ui, |ui| {
egui::ScrollArea::vertical().show(ui, |ui| {
ui::connection_state_ui(ui, &self.connection_state);
crate::summary::summary_ui(ui, self.last_summary.as_ref(), &self.summary);
crate::invoice::invoices_ui(ui, &self.invoice_zap_reqs, ctx, &self.invoices);
crate::channels::channels_ui(ui, &self.channels);
crate::ui::get_info_ui(ui, &self.get_info);
});
});
}
fn setup_connection(&mut self) {
let (req_tx, mut req_rx) = unbounded_channel::<Request>();
let (event_tx, event_rx) = unbounded_channel::<Event>();
self.channel = Some(CommChannel { req_tx, event_rx });
tokio::spawn(async move {
let key = SecretKey::new(&mut rand::thread_rng());
let their_pubkey = PublicKey::from_str(&std::env::var("CLNDASH_ID").unwrap_or(
"03f3c108ccd536b8526841f0a5c58212bb9e6584a1eb493080e7c1cc34f82dad71".to_string(),
))
.unwrap();
let host = std::env::var("CLNDASH_HOST").unwrap_or("ln.damus.io:9735".to_string());
let lnsocket = match LNSocket::connect_and_init(key, their_pubkey, &host).await {
Err(err) => {
let _ = event_tx.send(Event::Ended {
reason: err.to_string(),
});
return;
}
Ok(lnsocket) => {
let _ = event_tx.send(Event::Connected);
lnsocket
}
};
let rune = std::env::var("CLNDASH_RUNE").unwrap_or(
"Vns1Zxvidr4J8pP2ZCg3Wjp2SyGyyf5RHgvFG8L36yM9MzMmbWV0aG9kPWdldGluZm8=".to_string(),
);
let commando = Arc::new(CommandoClient::spawn(lnsocket, &rune));
loop {
match req_rx.recv().await {
None => {
let _ = event_tx.send(Event::Ended {
reason: "channel dead?".to_string(),
});
break;
}
Some(req) => {
tracing::debug!("calling {req:?}");
match req {
Request::GetInfo => {
let event_tx = event_tx.clone();
let commando = commando.clone();
tokio::spawn(async move {
match commando.call("getinfo", json!({})).await {
Ok(v) => {
let _ = event_tx
.send(Event::Response(ClnResponse::GetInfo(v)));
}
Err(err) => {
tracing::error!("get_info error {}", err);
}
}
});
}
Request::PaidInvoices(n) => {
let event_tx = event_tx.clone();
let commando = commando.clone();
tokio::spawn(async move {
let invoices = fetch_paid_invoices(commando, n).await;
let _ = event_tx
.send(Event::Response(ClnResponse::PaidInvoices(invoices)));
});
}
Request::ListPeerChannels => {
let event_tx = event_tx.clone();
let commando = commando.clone();
tokio::spawn(async move {
let peer_channels =
commando.call("listpeerchannels", json!({})).await;
let channels = peer_channels.map(|v| {
let peer_channels: Vec<ListPeerChannel> =
serde_json::from_value(v["channels"].clone()).unwrap();
to_channels(peer_channels)
});
let _ = event_tx.send(Event::Response(
ClnResponse::ListPeerChannels(channels),
));
});
}
}
}
}
}
});
}
fn process_events(&mut self, ndb: &Ndb) {
let Some(channel) = &mut self.channel else {
return;
};
while let Ok(event) = channel.event_rx.try_recv() {
match event {
Event::Ended { reason } => {
self.connection_state = ConnectionState::Dead(reason);
}
Event::Connected => {
self.connection_state = ConnectionState::Active;
let _ = channel.req_tx.send(Request::GetInfo);
let _ = channel.req_tx.send(Request::ListPeerChannels);
let _ = channel.req_tx.send(Request::PaidInvoices(100));
}
Event::Response(resp) => match resp {
ClnResponse::ListPeerChannels(chans) => {
if let LoadingState::Loaded(prev) = &self.channels {
self.last_summary = Some(crate::summary::compute_summary(prev));
}
self.summary = match &chans {
Ok(chans) => {
LoadingState::Loaded(crate::summary::compute_summary(chans))
}
Err(err) => LoadingState::Failed(err.clone()),
};
self.channels = LoadingState::from_result(chans);
}
ClnResponse::GetInfo(value) => {
let res = serde_json::to_string_pretty(&value);
self.get_info =
LoadingState::from_result(res.map_err(|_| lnsocket::Error::Json));
}
ClnResponse::PaidInvoices(invoices) => {
// process zap requests
if let Ok(invoices) = &invoices {
for invoice in invoices {
let zap_req_id: Option<ZapReqId> =
serde_json::from_str(&invoice.description).ok();
if let Some(zap_req_id) = zap_req_id {
self.invoice_zap_reqs
.insert(invoice.label.clone(), zap_req_id.id);
let _ = ndb.process_event(&format!(
"[\"EVENT\",\"a\",{}]",
&invoice.description
));
}
}
}
self.invoices = LoadingState::from_result(invoices);
}
},
}
}
}
}
fn to_channels(peer_channels: Vec<ListPeerChannel>) -> Channels {
let mut avail_out: i64 = 0;
let mut avail_in: i64 = 0;
let mut max_total_msat: i64 = 0;
let mut channels: Vec<Channel> = peer_channels
.into_iter()
.map(|c| {
let to_us = (c.to_us_msat - c.our_reserve_msat).max(0);
let to_them_raw = (c.total_msat - c.to_us_msat).max(0);
let to_them = (to_them_raw - c.their_reserve_msat).max(0);
avail_out += to_us;
avail_in += to_them;
if c.total_msat > max_total_msat {
max_total_msat = c.total_msat; // <-- max, not sum
}
Channel {
to_us,
to_them,
original: c,
}
})
.collect();
channels.sort_by(|a, b| {
let a_capacity = a.to_them + a.to_us;
let b_capacity = b.to_them + b.to_us;
a_capacity.partial_cmp(&b_capacity).unwrap().reverse()
});
Channels {
max_total_msat,
avail_out,
avail_in,
channels,
}
}

View File

@@ -0,0 +1,140 @@
use crate::channels::Channels;
use crate::event::LoadingState;
use crate::ui;
#[derive(Clone, Default)]
pub struct Summary {
pub total_msat: i64,
pub avail_out_msat: i64,
pub avail_in_msat: i64,
pub channel_count: usize,
pub largest_msat: i64,
pub outbound_pct: f32, // fraction of total capacity
}
pub fn compute_summary(ch: &Channels) -> Summary {
let total_msat: i64 = ch.channels.iter().map(|c| c.original.total_msat).sum();
let largest_msat: i64 = ch
.channels
.iter()
.map(|c| c.original.total_msat)
.max()
.unwrap_or(0);
let outbound_pct = if total_msat > 0 {
ch.avail_out as f32 / total_msat as f32
} else {
0.0
};
Summary {
total_msat,
avail_out_msat: ch.avail_out,
avail_in_msat: ch.avail_in,
channel_count: ch.channels.len(),
largest_msat,
outbound_pct,
}
}
pub fn summary_ui(
ui: &mut egui::Ui,
last_summary: Option<&Summary>,
summary: &LoadingState<Summary, lnsocket::Error>,
) {
match summary {
LoadingState::Loading => {
ui.label("loading summary");
}
LoadingState::Failed(err) => {
ui.label(format!("Failed to get summary: {err}"));
}
LoadingState::Loaded(summary) => {
summary_cards_ui(ui, summary, last_summary);
ui.add_space(8.0);
}
}
}
pub fn summary_cards_ui(ui: &mut egui::Ui, s: &Summary, prev: Option<&Summary>) {
let old = prev.cloned().unwrap_or_default();
let items: [(&str, String, Option<String>); 6] = [
(
"Total capacity",
ui::human_sat(s.total_msat),
prev.map(|_| ui::delta_str(s.total_msat, old.total_msat)),
),
(
"Avail out",
ui::human_sat(s.avail_out_msat),
prev.map(|_| ui::delta_str(s.avail_out_msat, old.avail_out_msat)),
),
(
"Avail in",
ui::human_sat(s.avail_in_msat),
prev.map(|_| ui::delta_str(s.avail_in_msat, old.avail_in_msat)),
),
("# Channels", s.channel_count.to_string(), None),
("Largest", ui::human_sat(s.largest_msat), None),
(
"Outbound %",
format!("{:.0}%", s.outbound_pct * 100.0),
None,
),
];
// --- responsive columns ---
let min_card = 160.0;
let cols = ((ui.available_width() / min_card).floor() as usize).max(1);
egui::Grid::new("summary_grid")
.num_columns(cols)
.min_col_width(min_card)
.spacing(egui::vec2(8.0, 8.0))
.show(ui, |ui| {
let items_len = items.len();
for (i, (t, v, d)) in items.into_iter().enumerate() {
card_cell(ui, t, v, d, min_card);
// End the row when we filled a row worth of cells
if (i + 1) % cols == 0 {
ui.end_row();
}
}
// If the last row wasn't full, close it anyway
if items_len % cols != 0 {
ui.end_row();
}
});
}
fn card_cell(ui: &mut egui::Ui, title: &str, value: String, delta: Option<String>, min_card: f32) {
let weak = ui.visuals().weak_text_color();
egui::Frame::group(ui.style())
.fill(ui.visuals().extreme_bg_color)
.corner_radius(egui::CornerRadius::same(10))
.inner_margin(egui::Margin::same(10))
.stroke(ui.visuals().widgets.noninteractive.bg_stroke)
.show(ui, |ui| {
ui.set_min_width(min_card);
ui.vertical(|ui| {
ui.add(
egui::Label::new(egui::RichText::new(title).small().color(weak))
.wrap_mode(egui::TextWrapMode::Wrap),
);
ui.add_space(4.0);
ui.add(
egui::Label::new(egui::RichText::new(value).strong().size(18.0))
.wrap_mode(egui::TextWrapMode::Wrap),
);
if let Some(d) = delta {
ui.add_space(2.0);
ui.add(
egui::Label::new(egui::RichText::new(d).small().color(weak))
.wrap_mode(egui::TextWrapMode::Wrap),
);
}
});
ui.set_min_height(20.0);
});
}

View File

@@ -0,0 +1,145 @@
use crate::event::ConnectionState;
use crate::event::LoadingState;
use egui::Color32;
use egui::Label;
use egui::RichText;
use egui::Widget;
use notedeck::AppContext;
use std::collections::HashMap;
pub fn note_hover_ui(
ui: &mut egui::Ui,
label: &str,
ctx: &mut AppContext,
invoice_notes: &HashMap<String, [u8; 32]>,
) -> Option<notedeck::NoteAction> {
let zap_req_id = invoice_notes.get(label)?;
let Ok(txn) = nostrdb::Transaction::new(ctx.ndb) else {
return None;
};
let Ok(zapreq_note) = ctx.ndb.get_note_by_id(&txn, zap_req_id) else {
return None;
};
for tag in zapreq_note.tags() {
let Some("e") = tag.get_str(0) else {
continue;
};
let Some(target_id) = tag.get_id(1) else {
continue;
};
let Ok(note) = ctx.ndb.get_note_by_id(&txn, target_id) else {
return None;
};
let author = ctx
.ndb
.get_profile_by_pubkey(&txn, zapreq_note.pubkey())
.ok();
// TODO(jb55): make this less horrible
let mut note_context = notedeck::NoteContext {
ndb: ctx.ndb,
accounts: ctx.accounts,
img_cache: ctx.img_cache,
note_cache: ctx.note_cache,
zaps: ctx.zaps,
pool: ctx.pool,
job_pool: ctx.job_pool,
unknown_ids: ctx.unknown_ids,
clipboard: ctx.clipboard,
i18n: ctx.i18n,
global_wallet: ctx.global_wallet,
};
let mut jobs = notedeck::JobsCache::default();
let options = notedeck_ui::NoteOptions::default();
notedeck_ui::ProfilePic::from_profile_or_default(note_context.img_cache, author.as_ref())
.ui(ui);
let nostr_name = notedeck::name::get_display_name(author.as_ref());
ui.label(format!("{} zapped you", nostr_name.name()));
return notedeck_ui::NoteView::new(&mut note_context, &note, options, &mut jobs)
.preview_style()
.hide_media(true)
.show(ui)
.action;
}
None
}
pub fn get_info_ui(ui: &mut egui::Ui, info: &LoadingState<String, lnsocket::Error>) {
ui.horizontal_wrapped(|ui| match info {
LoadingState::Loading => {}
LoadingState::Failed(err) => {
ui.label(format!("failed to fetch node info: {err}"));
}
LoadingState::Loaded(info) => {
ui.add(Label::new(info).wrap_mode(egui::TextWrapMode::Wrap));
}
});
}
pub fn connection_state_ui(ui: &mut egui::Ui, state: &ConnectionState) {
match state {
ConnectionState::Active => {
ui.add(Label::new(RichText::new("Connected").color(Color32::GREEN)));
}
ConnectionState::Connecting => {
ui.add(Label::new(
RichText::new("Connecting").color(Color32::YELLOW),
));
}
ConnectionState::Dead(reason) => {
ui.add(Label::new(
RichText::new(format!("Disconnected: {reason}")).color(Color32::RED),
));
}
}
}
// ---------- helper ----------
pub fn human_sat(msat: i64) -> String {
let sats = msat / 1000;
if sats >= 1_000_000 {
format!("{:.1}M", sats as f64 / 1_000_000.0)
} else if sats >= 1_000 {
format!("{:.1}k", sats as f64 / 1_000.0)
} else {
sats.to_string()
}
}
pub fn human_verbose_sat(msat: i64) -> String {
if msat < 1_000 {
// less than 1 sat
format!("{msat} msat")
} else {
let sats = msat / 1_000;
if sats < 100_000_000 {
// less than 1 BTC
format!("{sats} sat")
} else {
let btc = sats / 100_000_000;
format!("{btc} BTC")
}
}
}
pub fn delta_str(new: i64, old: i64) -> String {
let d = new - old;
match d.cmp(&0) {
std::cmp::Ordering::Greater => format!("{}", human_sat(d)),
std::cmp::Ordering::Less => format!("{}", human_sat(-d)),
std::cmp::Ordering::Equal => "·".into(),
}
}

View File

@@ -0,0 +1,198 @@
use crate::invoice::Invoice;
use lnsocket::CallOpts;
use lnsocket::CommandoClient;
use serde::Deserialize;
use serde_json::json;
use std::sync::Arc;
#[derive(Deserialize)]
struct UpdatedInvoicesResponse {
updated: u64,
}
#[derive(Deserialize)]
struct PayIndexInvoices {
invoices: Vec<PayIndexScan>,
}
#[derive(Deserialize)]
struct PayIndexScan {
pay_index: Option<u64>,
}
async fn find_lastpay_index(commando: Arc<CommandoClient>) -> Result<Option<u64>, lnsocket::Error> {
const PAGE: u64 = 250;
// 1) get the current updated tail
let created_value = commando
.call(
"wait",
json!({"subsystem":"invoices","indexname":"updated","nextvalue":0}),
)
.await?;
let response: UpdatedInvoicesResponse =
serde_json::from_value(created_value).map_err(|_| lnsocket::Error::Json)?;
// start our window at the tail
let mut start_at = response
.updated
.saturating_add(1) // +1 because we want max(1, updated - PAGE + 1)
.saturating_sub(PAGE)
.max(1);
loop {
// 2) fetch a window (indexed by "updated")
let val = commando
.call_with_opts(
"listinvoices",
json!({
"index": "updated",
"start": start_at,
"limit": PAGE,
}),
// only fetch the one field we care about
CallOpts::default().filter(json!({
"invoices": [{"pay_index": true}]
})),
)
.await?;
let parsed: PayIndexInvoices =
serde_json::from_value(val).map_err(|_| lnsocket::Error::Json)?;
if let Some(pi) = parsed.invoices.iter().filter_map(|inv| inv.pay_index).max() {
return Ok(Some(pi));
}
// 4) no paid invoice in this slice—step back or bail
if start_at == 1 {
return Ok(None);
}
start_at = start_at.saturating_sub(PAGE).max(1);
}
}
pub async fn fetch_paid_invoices(
commando: Arc<CommandoClient>,
limit: u32,
) -> Result<Vec<Invoice>, lnsocket::Error> {
use tokio::task::JoinSet;
// look for an invoice with the last paid index
let Some(lastpay_index) = find_lastpay_index(commando.clone()).await? else {
// no paid invoices
return Ok(vec![]);
};
let mut set: JoinSet<Result<Invoice, lnsocket::Error>> = JoinSet::new();
let start = lastpay_index.saturating_sub(limit as u64);
// 3) Fire off at most `concurrency` `waitanyinvoice` calls at a time,
// collect all successful responses into a Vec.
// fire them ALL at once
for idx in start..lastpay_index {
let c = commando.clone();
set.spawn(async move {
let val = c
.call(
"waitanyinvoice",
serde_json::json!({ "lastpay_index": idx }),
)
.await?;
let parsed: Invoice = serde_json::from_value(val).map_err(|_| lnsocket::Error::Json)?;
Ok(parsed)
});
}
let mut results = Vec::with_capacity(limit as usize);
while let Some(res) = set.join_next().await {
results.push(res.map_err(|_| lnsocket::Error::Io(std::io::ErrorKind::Interrupted))??);
}
results.sort_by(|a, b| a.updated_index.cmp(&b.updated_index).reverse());
Ok(results)
}
// wip watch subsystem
/*
async fn watch_subsystem(
commando: CommandoClient,
subsystem: WaitSubsystem,
index: WaitIndex,
event_tx: UnboundedSender<Event>,
mut cancel_rx: Receiver<()>,
) {
// Step 1: Fetch current index value so we can back up ~20
let mut nextvalue: u64 = match commando
.call(
"wait",
serde_json::json!({
"indexname": index.as_str(),
"subsystem": subsystem.as_str(),
"nextvalue": 0
}),
)
.await
{
Ok(v) => {
// You showed the result has `updated` as the current highest index
let current = v.get("updated").and_then(|x| x.as_u64()).unwrap_or(0);
current.saturating_sub(20) // back up 20, clamp at 0
}
Err(err) => {
tracing::warn!("initial wait(…nextvalue=0) failed: {}", err);
0
}
};
loop {
// You can add a timeout to avoid hanging forever in weird network states.
let fut = commando.call(
"wait",
serde_json::to_value(WaitRequest {
indexname: "invoices".into(),
subsystem: "lightningd".into(),
nextvalue,
})
.unwrap(),
);
tokio::select! {
_ = &mut cancel_rx => {
// graceful shutdown
break;
}
res = fut => {
match res {
Ok(v) => {
// Typical shape: { "nextvalue": n, "invoicestatus": { ... } } (varies by plugin/index)
// Adjust these lookups for your nodes actual wait payload.
if let Some(nv) = v.get("nextvalue").and_then(|x| x.as_u64()) {
nextvalue = nv + 1;
} else {
// Defensive: never get stuck — bump at least by 1
nextvalue += 1;
}
// Inspect/route
let kind = v.get("status").and_then(|s| s.as_str());
let ev = match kind {
Some("paid") => ClnResponse::Invoice(InvoiceEvent::Paid(v.clone())),
Some("created") => ClnResponse::Invoice(InvoiceEvent::Created(v.clone())),
_ => ClnResponse::Invoice(InvoiceEvent::Other(v.clone())),
};
let _ = event_tx.send(Event::Response(ev));
}
Err(err) => {
tracing::warn!("wait(invoices) error: {err}");
// small backoff so we don't tight-loop on persistent errors
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
}
}
}
}
}
*/

View File

@@ -10,7 +10,12 @@ description = "A tweetdeck-style notedeck app"
[lib]
crate-type = ["lib", "cdylib"]
[target.'cfg(target_os = "android")'.dependencies]
jni = { workspace = true }
ndk-context = "0.1"
[dependencies]
opener = { workspace = true }
rmpv = { workspace = true }
bech32 = { workspace = true }
notedeck = { workspace = true }
@@ -31,7 +36,7 @@ image = { workspace = true }
indexmap = { workspace = true }
nostrdb = { workspace = true }
notedeck_ui = { workspace = true }
open = { workspace = true }
robius-open = { workspace = true }
poll-promise = { workspace = true }
puffin = { workspace = true, optional = true }
puffin_egui = { workspace = true, optional = true }

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