961 Commits

Author SHA1 Message Date
c1d3be4c07 WIP add system locale detection
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-08-01 13:20:35 -04: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
b285be97a1 Add Thai translations
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-07-23 00:15:01 -04:00
7321e82800 Add Spanish (Latin America and Spain) translations
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-07-22 21:24:07 -04:00
e686afed1c Update Chinese, French, and German translations
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-07-22 21:23:35 -04:00
William Casarin
2b48a20ccd Merge initial i18n app translations from terry! #907
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 (6):
      Add Fluent-based localization manager and add script to export source strings for translations
      Internationalize user-facing strings and export them for translations
      Clean up time_ago_since, add tests, and internationalize strings
      Add localization documentation to notedeck DEVELOPER.md
      Fix export_source_strings.py to adjust for tr! and tr_plural! macro signature changes
      Add French, German, Simplified Chinese, and Traditional Chinese translations

William Casarin (7):
      i18n: make localization context non-global
      i18n: always have en-XA available
      args: add --locale option
      debug: add startup query debug log
      i18n: disable bidi for tests
      i18n: disable broken tests for now
2025-07-22 13:51:04 -07:00
William Casarin
b3bd68db3d gitignore: remove cache
so we can `git clean -dn` test folders

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-22 13:49:32 -07:00
William Casarin
3e2a1fa0d7 i18n: disable broken tests for now
sorry

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-22 13:44:35 -07:00
William Casarin
26143cad54 i18n: disable bidi for tests
> This is important for cases such as when a right-to-left user name is
presented in the left-to-right message.

> In some cases, such as testing, the user may want to disable the
isolating.

See: https://docs.rs/fluent/latest/fluent/bundle/struct.FluentBundle.html#method.set_use_isolating
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-22 13:25:31 -07:00
549fdc5da8 Add French, German, Simplified Chinese, and Traditional Chinese translations
Changelog-Added: Added French, German, Simplified Chinese, and Traditional Chinese translations
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-07-22 16:19:22 -04:00
William Casarin
c27aff6bec debug: add startup query debug log
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-22 13:02:29 -07:00
William Casarin
ec87482009 args: add --locale option
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-22 13:02:29 -07:00
William Casarin
a077cae0ee i18n: always have en-XA available
this is statically compiled anyways.

we might want to only have this as a feature
flag in the future to reduce binary size

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-22 13:02:29 -07:00
5e6e5c1b1d Fix export_source_strings.py to adjust for tr! and tr_plural! macro signature changes
Changelog-Fixed: Fixed export_source_strings.py to adjust for tr! and tr_plural! macro signature changes
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-07-22 15:39:00 -04:00
William Casarin
3d4db820b4 i18n: make localization context non-global
- Simplify Localization{Context,Manager} to just Localization
- Fixed a bunch of lifetime issueo
- Removed all Arcs and Locks
- Removed globals
  * widgets now need access to &mut Localization for i18n

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-22 09:49:32 -07:00
d1e222f732 Add localization documentation to notedeck DEVELOPER.md
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-07-21 12:45:30 -07:00
0e65491ef1 Clean up time_ago_since, add tests, and internationalize strings
Changelog-Changed: Internationalized time ago strings
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-07-21 12:45:30 -07:00
3f5036bd32 Internationalize user-facing strings and export them for translations
Changelog-Added: Internationalized user-facing strings and exported them for translations
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-07-21 12:45:29 -07:00
d07c3e9135 Add Fluent-based localization manager and add script to export source strings for translations
Changelog-Added: Added Fluent-based localization manager and added script to export source strings for translations
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-07-21 12:40:06 -07:00
William Casarin
80820a52d2 Merge tag 'v0.5.6' 2025-07-21 05:49:15 -07:00
William Casarin
0248a9ed2a v0.5.6
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-21 05:48:07 -07:00
William Casarin
38b2077a8d Merge kernel's "can't remove damoose fixes" and more! #1001
kernelkind (9):
      appease clippy
      use `NwcError` instead of nwc::Error
      make `UserAccount` cloneable
      allow removal of Damoose account
      expose `AccountCache::falback`
      move select account logic to own method
      bugfix: properly sub to new selected acc after removal of selected
      bugfix: unsubscribe from timelines on deck deletion
      bugfix: unsubscribe all decks when log out account
2025-07-20 17:17:29 -07:00
William Casarin
c94a418474 media/trust: always show if its yourself
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-20 16:54:34 -07:00
William Casarin
28065ec4a3 fix one missing home string
Fixes: f39d554c96 ("rename Contacts to Home")
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-20 16:54:31 -07:00
William Casarin
94be9ccc3e Revert "relay: make multicast a desired relay"
there seems to be a bug where the relay list fails to get
created if multicast fails to connect

This reverts commit 5eae9a55ec.
2025-07-20 15:59:47 -07:00
William Casarin
2c1a42efd4 v0.5.5
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-20 10:23:41 -07:00
Fernando López Guevara
ed38c75193 feat(full-screen-media): add swipe navigation 2025-07-18 13:46:25 -03:00
kernelkind
0b27282985 bugfix: unsubscribe all decks when log out account
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-17 21:39:58 -04:00
kernelkind
1c547bbcaa bugfix: unsubscribe from timelines on deck deletion
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-17 21:25:09 -04:00
kernelkind
d4082eb818 bugfix: properly sub to new selected acc after removal of selected
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-17 20:31:36 -04:00
kernelkind
0b8a4fdf55 move select account logic to own method
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-17 20:22:48 -04:00
kernelkind
8263e56f41 expose AccountCache::falback
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-17 19:33:56 -04:00
kernelkind
8daa1d2adf allow removal of Damoose account
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-17 19:24:33 -04:00
kernelkind
b9cae65b72 make UserAccount cloneable
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-17 19:24:01 -04:00
kernelkind
049bb3e8bb use NwcError instead of nwc::Error
need to clone

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-17 19:01:51 -04:00
kernelkind
d22dd9ed31 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 19:01:46 -04: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
William Casarin
c306ab2912 chrome/readme: mention signer
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-17 14:52:26 -07:00
William Casarin
c421f8f8ff profile: fetch new metadata when visiting profiles
This ensures we always have the latest data

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-17 14:49:16 -07:00
William Casarin
8d2da86f1f enostr: remove raw event type
we rely on the event type for multicast logic,
so remove raw since its not really needed anymore

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-17 14:20:33 -07:00
William Casarin
8a1398face clippy fixes
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-17 13:54:43 -07:00
William Casarin
a4c1b38116 introduce HybridFilter
This introduces a new filter construct called HybridFilter. This allows
filters to have different remote filter than local ones. For example,
adding kind0 to the remote for keeping profiles up to date on your
timeline, but only subscribing to kind1 locally.

Only home/contact filters use this feature for now.

Fixes: https://github.com/damus-io/notedeck/issues/995
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-17 13:54:43 -07: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
William Casarin
d2994fa340 gitignore: ignore logcat
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-17 11:30:29 -07:00
William Casarin
5eae9a55ec relay: make multicast a desired relay
so we can delete everything except multicast and have
multicast accounts

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-17 11:03:20 -07:00
William Casarin
1a7154fab6 reply: add some space after reply box
we need more room on mobile

Fixes: https://github.com/damus-io/notedeck/issues/991
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-17 11:02:59 -07:00
William Casarin
61f4d6b532 db: fix bad query bug in author-kind queries
it was matching authors it shouldn't have

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-17 11:01:58 -07:00
William Casarin
51d2b4414b ui/note: refactor reply line into a function
this is a bit neater

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-17 09:37:20 -07:00
William Casarin
c0c2120f74 android: fix back button
Fixes: https://github.com/damus-io/notedeck/issues/972
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-17 09:18:34 -07:00
William Casarin
db6f02084d input: halve long press input duration
people were saying long press was too long

Fixes: https://github.com/damus-io/notedeck/issues/981
2025-07-16 18:01:48 -07:00
William Casarin
99646f8ff5 ui/mention: fix weird mention text size
Fixes: https://github.com/damus-io/notedeck/issues/975
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-16 17:36:35 -07:00
William Casarin
a603685fac multi_subscriber: switch to debug statements
info is not really the right level for this
2025-07-16 16:36:26 -07:00
kernelkind
5168d50257 add info statements
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-16 18:13:09 -04:00
kernelkind
cc92fc2082 make TimelineCache::timelines private
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-16 18:13:07 -04:00
kernelkind
dc4e3d7510 increment sub count when necessary
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-16 18:13:04 -04:00
kernelkind
95e9e4326a add TimelineCache helper methods
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-16 18:13:00 -04:00
kernelkind
4db6f37017 track all timeline related subscriptions in TimelineSub
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-16 18:12:58 -04:00
kernelkind
6544d43d02 replace MultiSubscriber with TimelineSub
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-16 18:12:46 -04:00
William Casarin
64ac06791a Merge show-note-client option by fernando
We should move this somewhere else before we turn it on
officially

Fernando López Guevara (2):
      refactor: use Margin:ZERO
      feat(note-view): show note client
2025-07-16 14:07:42 -07:00
Fernando López Guevara
4bf75c95de feat(note-view): show note client 2025-07-16 17:09:59 -03:00
Fernando López Guevara
cb5bd75236 refactor: use Margin:ZERO 2025-07-16 16:29:02 -03:00
William Casarin
a6a89307f1 v0.5.4
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-16 11:41:32 -07:00
William Casarin
872aadf279 debug: fix memory debugger
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-16 11:17:13 -07:00
William Casarin
953848ff9a anim: reduce gif fps
it's brrrring my cpu on my 240hz monitor. we don't need 240hz gifs...

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-16 10:07:37 -07:00
William Casarin
461665f599 ui: remove show_pointer
This can just be achieved by on_hover_cursor

Didn't realize this.

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-16 09:17:27 -07:00
William Casarin
f27b1fe957 Merge chrome sidebar features from fernando
Fernando López Guevara (2):
      fix(compose-button): apply icon_color to compose button edge circles & add hover text
      fix(chrome): add hover text and pointer cursor to sidebar elements
2025-07-16 09:01:55 -07:00
William Casarin
45803b6bb0 readme: we're in beta status
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-16 08:59:59 -07:00
William Casarin
a5bbe79c4b readme: fix link
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-16 08:59:23 -07:00
William Casarin
3637dbd5c3 Merge readme changes from elsat 2025-07-16 08:58:31 -07:00
William Casarin
551afb2772 readme: tweaks
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-16 08:58:19 -07:00
William Casarin
e8db7444c3 gitignore: include junk so that git clean can handle it
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-16 08:55:24 -07:00
William Casarin
a517bc69bc Merge is_following fixes from kernel
kernelkind (4):
      add `Accounts` to `NoteContext`
      remove `MuteFun` prop
      make `Contacts::is_following` use bytes instead of `Pubkey`
      migrate to check following through `Contacts::is_following`
2025-07-16 08:50:58 -07:00
William Casarin
e2d79af632 Merge remote-tracking branch 'fernando/feat/full-screen-media-dots' 2025-07-16 08:31:57 -07:00
Fernando López Guevara
44da10dc88 fix(profile): split always 2025-07-16 11:01:13 -03:00
Fernando López Guevara
8e218a1eb1 feat(full-screen-media): add image URL in top bar and navigation dots in bottom bar 2025-07-16 09:30:58 -03:00
Fernando López Guevara
befce76a90 feat(profile): add tooltip on copy npub 2025-07-15 14:42:43 -07:00
Fernando López Guevara
ac85bdc21d feat(profile-view): split nip05 when is_narrow 2025-07-15 14:42:43 -07:00
Fernando López Guevara
e344b09475 feat(profile-view): split profile info entries when is_narrow 2025-07-15 14:42:43 -07:00
Fernando López Guevara
ab43bdb65a fix(deck): show column picker when deck has no columns 2025-07-15 14:42:32 -07:00
William Casarin
23d02a9dd2 note/options: remove redundant has function
there is a contains function generated by the bitflags macro

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-15 14:35:14 -07:00
William Casarin
fa545bc077 ui/note: fix weird ... placement regression
Fixes: c402320ad3 ("ui: fix broken note previews")
Fixes: https://github.com/damus-io/notedeck/issues/974
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-15 14:33:46 -07:00
William Casarin
4735529731 update lock
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-15 14:19:36 -07:00
William Casarin
5c603cd56b v0.5.3
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-15 14:15:16 -07:00
William Casarin
6bf6af7f9e profile: fix crash with ProfileState defaults
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-15 14:14:50 -07:00
William Casarin
e9ee1b5094 v0.5.2
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-15 13:41:31 -07:00
William Casarin
fb6456bdee v0.5.1
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-15 13:37:25 -07:00
William Casarin
ac22fc7072 columns: enable toolbar scroll to top
Fixes: https://github.com/damus-io/notedeck/issues/969
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-15 13:30:40 -07:00
William Casarin
074472eec9 columns/timeline: include column index in timeline view_id
might fix weird scroll issues on profiles

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-15 13:02:05 -07:00
William Casarin
119456e2b3 columns: switch to bitflag app options
we're adding a ScrollToTop bool for an updating change
to the toolbar, but we have too many flags now. Let's switch
to bitflags

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-15 12:42:20 -07:00
William Casarin
cd560cb7bf chrome: make toolbar smaller
its a bit chonky

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-15 12:40:04 -07:00
alltheseas
f8b6ef0c20 Update README.md with reference plans to support building on notedeck
Added mention that notedeck will enable easy app development on nostrdb and notedeck
2025-07-15 13:12:42 -05:00
William Casarin
baff14bbf0 ui/column: include pfp in back response
We were missing the pfp in the back response

Fixes: https://github.com/damus-io/notedeck/issues/923
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-15 10:51:02 -07:00
William Casarin
0cc64da1ca columns/profile: only mutate profile state after navigating
The code currently mutates the profile state during nav rendering,
which screws up profile state updates. This syncs ProfileStates
in the ui. before it was getting out of sync.

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-15 10:15:21 -07:00
William Casarin
f2adb949f6 columns/nav: ocd nevernest
no behavior changed

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-15 09:29:34 -07:00
William Casarin
6f266fc91d columns/profile: rename process -> process_profile_action
lets start clarifying these names for easier searching

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-15 09:28:41 -07:00
William Casarin
443d356cc7 ui/column: remove move/remove column buttons on narrow
It doesn't make sense to move columns in narrow mode

Fixes: https://github.com/damus-io/notedeck/issues/960
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-15 08:35:07 -07:00
William Casarin
a714bef690 ui/profile: fix dubious profile editing
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-15 08:28:37 -07:00
William Casarin
4e3fcad709 ui/note: show full link type in unhandled mentions
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-15 08:28:33 -07:00
William Casarin
744483fbc0 ui: don't auto-repaint that often
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-15 08:28:33 -07:00
kernelkind
efae62024e migrate to check following through Contacts::is_following
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-14 21:34:33 -04:00
kernelkind
142aa879c3 make Contacts::is_following use bytes instead of Pubkey
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-14 21:34:08 -04:00
kernelkind
a7f5319fde remove MuteFun prop
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-14 21:34:05 -04:00
kernelkind
397bfce817 add Accounts to NoteContext
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-14 21:34:02 -04:00
William Casarin
e2295172a2 fix target sdk
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-14 16:15:29 -07:00
William Casarin
45bb00426f andriod: add app icons
Fixes: #958
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-14 16:02:49 -07:00
William Casarin
3a25f3b245 tweak minSdk
24 is lowest vulkan ... so target that

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-14 16:01:48 -07:00
William Casarin
c5b6bf2883 gitignore: ds_store
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-14 16:01:39 -07:00
William Casarin
c4084c4117 note_follows: remove unneeded derefence
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-14 14:16:05 -07:00
William Casarin
e4ca67127e Merge unknown profile improvements by kernel #955
kernelkind (1):
      allow body on unknown profile
2025-07-14 14:10:18 -07:00
William Casarin
c402320ad3 ui: fix broken note previews
Also made the options more clear

Fixes: https://github.com/damus-io/notedeck/issues/959
Fixes: b6348b1507 ("note/options: simplify flag logic")
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-14 14:05:41 -07:00
kernelkind
8c71e154f4 allow body on unknown profile
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-14 15:26:07 -04:00
Fernando López Guevara
d95de84f63 fix(chrome): add hover text and pointer cursor to sidebar elements 2025-07-14 09:58:06 -03:00
Fernando López Guevara
6739ed6d58 fix(compose-button): apply icon_color to compose button edge circles & add hover text 2025-07-14 09:56:01 -03:00
kernelkind
b5d56f7831 remove unnecessary FilterState::NeedsRemote filter
all NeedsRemote states are contact lists currently, which is
managed by `Accounts`

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-12 16:52:01 -04:00
kernelkind
46633d0513 use AccountSubs for timeline contact sub
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-12 16:48:20 -04:00
William Casarin
44edffc596 android/input: add copy/paste context to post input
Fixes: https://github.com/damus-io/notedeck/issues/942
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-11 15:36:56 -07:00
William Casarin
6596e89e29 anim: animate on compose button hide
before we were just nuking it

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-11 14:23:50 -07:00
William Casarin
ade6f57fd5 anim: animate show/hide of compose button
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-11 14:21:26 -07:00
William Casarin
e8444f10b3 Revert "Unify sub for contacts in accounts & timeline"
Since its causing contact timelines to not load

eg: ./target/release/notedeck --datapath new3 -c contacts

This reverts commit 9940537897.
2025-07-11 13:49:47 -07:00
William Casarin
8752a49485 android: fix crash on mobile
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-11 13:39:29 -07:00
William Casarin
14dd7402d0 Merge redundant change 2025-07-11 13:16:45 -07:00
William Casarin
96cb5e26ce Merge follow/unfollow from kernel
Jakub Gladysz (1):
      ui: add follow button

kernelkind (14):
      bump nostrdb
      move polling responsibility to `AccountData`
      `AccountData`: decouple query from constructor
      add constructor for `AccountData`
      add `Contacts`
      use `Contacts` in `AccountData`
      expose `AccountSubs`
      Unify sub for contacts in accounts & timeline
      move `styled_button_toggleable` to notedeck_ui
      construct NoteBuilder from existing note
      send kind 3 event
      add actions for follow/unfollow
      add UI for (un)follow
      send contact list event on account creation
2025-07-11 13:06:24 -07:00
William Casarin
df78605051 nostrdb: update for windows and memleak fixes 2025-07-11 12:53:01 -07:00
William Casarin
217c1e5223 columns/decks: add home and notifications for new accounts
This is way more user friendly, and needed on mobile

Fixes: https://github.com/damus-io/notedeck/issues/937
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-10 17:16:30 -07:00
William Casarin
26d027f03e nav: nav to accounts view for actions that require key
Fixes: https://github.com/damus-io/notedeck/issues/936
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-10 16:17:08 -07:00
William Casarin
605f6f4711 android: hide new post button when navigating
Fixes: https://github.com/damus-io/notedeck/issues/898
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-10 15:45:33 -07:00
William Casarin
4bdfbc6400 onboarding: restore demo deck
for some reason it was getting overwritten ?

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-10 14:54:48 -07:00
William Casarin
b6348b1507 note/options: simplify flag logic
simpler, less macro magic

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-10 14:29:56 -07:00
William Casarin
c5093a7180 columns/add: move home and notifications to top
more intuitive

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-10 13:37:23 -07:00
William Casarin
f39d554c96 rename Contacts to Home
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-10 13:34:41 -07:00
William Casarin
e0f2e467d2 args: switch to oot_bitset for arg flags
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-10 13:34:24 -07:00
William Casarin
cf1814f250 android: hide chrome sidebar by default
When compiled as android, make sure we hide the chrome sidebar
by default

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-10 13:33:27 -07:00
William Casarin
6172777b1a android: remove special load arguments
we want to make sure we have the same onboarding path as desktop

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-10 13:32:52 -07:00
William Casarin
41053dd5a5 ui/carousel: refactor to use indices
This refactors our carousel control a bit, it was getting
messy

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-10 12:09:30 -07:00
kernelkind
dca9d3eeab send contact list event on account creation
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-10 13:47:32 -04:00
kernelkind
7b9db55a05 add UI for (un)follow
Signed-off-by: kernelkind <kernelkind@gmail.com>
Co-authored-by: Jakub Gladysz <jakub.gladysz@protonmail.com>
Co-authored-by: William Casarin <jb55@jb55.com>
2025-07-10 13:47:30 -04:00
kernelkind
a883ac8c34 add actions for follow/unfollow
Signed-off-by: kernelkind <kernelkind@gmail.com>
Co-authored-by: Jakub Gladysz <jakub.gladysz@protonmail.com>
Co-authored-by: William Casarin <jb55@jb55.com>
2025-07-10 13:47:28 -04:00
kernelkind
00d6651533 send kind 3 event
Signed-off-by: kernelkind <kernelkind@gmail.com>
Co-authored-by: Jakub Gladysz <jakub.gladysz@protonmail.com>
Co-authored-by: William Casarin <jb55@jb55.com>
2025-07-10 13:47:25 -04:00
kernelkind
6741ea8a01 construct NoteBuilder from existing note
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-10 13:47:23 -04:00
Jakub Gladysz
cc541cd4ff ui: add follow button
Signed-off-by: kernelkind <kernelkind@gmail.com>
Co-authored-by: Jakub Gladysz <jakub.gladysz@protonmail.com>
Co-authored-by: William Casarin <jb55@jb55.com>
2025-07-10 13:47:21 -04:00
kernelkind
8a77ba5f8f move styled_button_toggleable to notedeck_ui
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-10 13:47:19 -04:00
kernelkind
9940537897 Unify sub for contacts in accounts & timeline
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-10 13:47:07 -04:00
kernelkind
497c102af1 expose AccountSubs
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-10 13:45:42 -04:00
kernelkind
1100e28233 use Contacts in AccountData
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-10 13:45:39 -04:00
kernelkind
9b7033e208 add Contacts
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-10 13:45:27 -04:00
kernelkind
4014d122c9 add constructor for AccountData
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-10 13:45:23 -04:00
kernelkind
c99b99ed52 AccountData: decouple query from constructor
the ndb query must be as close to the subscription as possible to
avoid events falling through the cracks

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-10 13:45:20 -04:00
kernelkind
6c951d1a29 move polling responsibility to AccountData
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-10 13:45:13 -04:00
kernelkind
e4beb954db bump nostrdb
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-10 13:45:03 -04:00
William Casarin
e97574fcdc Merge remote-tracking branch 'github/pr/916' 2025-07-10 09:16:17 -07:00
William Casarin
298fab6471 ui/narrow: restore padding
This is a bit too tight

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-10 09:12:08 -07:00
William Casarin
22cfaaf64a Merge remote-tracking branch 'github/pr/928' 2025-07-10 09:10:28 -07:00
William Casarin
e4e8d7fcf3 note/action: add ScrollInfo
I might need this... lets add it just in case

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-10 09:03:39 -07:00
William Casarin
bb0262e09e android: reapply keyboard changes
but gate them so they don't apply on desktop to avoid the
arrow key and backspace issues. This is a massive hack until
I get time to actually implement this properly.

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-10 08:30:59 -07:00
William Casarin
13021afa58 Revert "feat(note-actionbar): refine icons"
icons are too blurry, lets fix that

This reverts commit e1bd1d3e8b.
2025-07-09 11:18:04 -07:00
Fernando López Guevara
ec25413433 feat(mobile): improve layout and behavior on narrow screens 2025-07-08 16:20:51 -07:00
William Casarin
f25735f89e debug: add memory debug window
enable with:

$ cargo build --release --features memory

and then click the memory widget on the chrome sidepanel

currently doesn't track C allocations... we should fix that

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-08 13:27:37 -07:00
William Casarin
738b5e71da android: default app to columns
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-08 12:12:03 -07:00
William Casarin
559086bc10 Merge remote-tracking branches 'pr/9{29,30}' into master 2025-07-08 08:41:01 -07:00
Fernando López Guevara
157e114124 fix(add_column): add vertical scroll 2025-07-08 11:03:05 -03:00
Fernando López Guevara
e1bd1d3e8b feat(note-actionbar): refine icons 2025-07-07 23:28:08 -03:00
Fernando López Guevara
14421da16d feat(image_carousel): navigate media with arrow left/right keys 2025-07-02 20:25:49 -03:00
kernelkind
b41f4c3359 decouple RelayView UI from state mutation
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-02 15:46:09 -04:00
kernelkind
10c4ac80a1 Revert "tmp: temporary AccountCache"
This reverts commit 726da7dabf5bf089a463309c41be3f6e11d0c43d.
2025-07-02 15:46:05 -04:00
kernelkind
a73596df48 Clarify & enforce selected-only behavior in Accounts subscription
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-02 15:46:01 -04:00
kernelkind
f0158f71b2 don't expose mutable access to UserAccount
it's not preferable that the full mutable access is available to
`ZapWallet`, but this PR is becoming too big already

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-02 15:45:57 -04:00
kernelkind
61e47323ab move modify_advertised_relays into accounts/relay.rs
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-02 15:45:54 -04:00
kernelkind
03c7d11351 move update_relay_configuration to account/relay.rs
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-02 15:45:51 -04:00
kernelkind
11edde45f4 split AccountStorage into reader & writer
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-02 15:45:47 -04:00
kernelkind
329385bd90 move AcountData into UserAccount
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-02 15:45:44 -04:00
kernelkind
a962d67536 tmp: temporary AccountCache
will be removed before PR ends

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-02 15:45:37 -04:00
kernelkind
f357935cca move (de)serialization of wallets & accounts to own structs
for easy cloning

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-02 15:45:31 -04:00
kernelkind
10d6d740b8 migrate accounts to be referenced through pks instead of indices
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-02 11:41:33 -04:00
kernelkind
d092f5c23e move switching related actions from notedeck -> columns
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-02 11:41:29 -04:00
kernelkind
b9cfe87974 wallet: remove unnecessary mut
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-02 11:41:26 -04:00
kernelkind
84026824b2 enostr: add equivalence between Pubkey & bytes
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-02 11:41:20 -04:00
kernelkind
8e92a97a57 make selected accounts non optional
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-02 11:41:17 -04:00
kernelkind
9cacb6bb69 add AccountCache
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-02 11:41:13 -04:00
kernelkind
f318bbb19a remove unnecessary method
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-02 11:41:10 -04:00
kernelkind
2fb2940d56 accounts: make fallback pk non optional
Note: this commit alone is *incorrect* and will cause crashes.
It is part of a greater plan to upgrade accounts. It was done this
way to break commits to smaller, more digestable chunks

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-02 11:41:06 -04:00
kernelkind
4914c637ce move FALLBACK_PUBKEY
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-02 11:41:03 -04:00
kernelkind
320dedc8bd add RelayDefaults
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-02 11:41:00 -04:00
kernelkind
41e141d9a9 move aux code to bottom
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-02 11:40:56 -04:00
kernelkind
e8d833bf89 accounts: move mute stuff to own module
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-02 11:40:52 -04:00
kernelkind
10ed593b6d accounts: move relay stuff to own file
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-02 11:40:44 -04:00
kernelkind
e91684a7d5 accounts: move accounts to own module
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-02 11:40:39 -04:00
kernelkind
e29ea35ee5 remove duplicate UnknownIds initialization
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-02 11:40:33 -04:00
William Casarin
eb76cbf671 Revert "android: fix remaining keyboard issues"
This reverts commit dbba0e1bb0.

It breaks desktop
2025-07-01 11:20:23 -07:00
William Casarin
dbba0e1bb0 android: fix remaining keyboard issues
Fixes: https://github.com/damus-io/notedeck/issues/896
Fixes: https://github.com/damus-io/notedeck/issues/894
Fixes: https://github.com/damus-io/notedeck/issues/895
Fixes: https://github.com/damus-io/notedeck/issues/893
Signed-off-by: William Casarin <jb55@jb55.com>
2025-06-30 14:47:57 -07:00
William Casarin
400050f3fb Merge remote-tracking branches 'github/pr/877' and 'github/pr/885'
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
Fernando López Guevara (2):
      fix(content): handle case where notes are not loaded
      feat(app_images): add module to manage static app image assets
2025-06-25 10:30:24 -07:00
William Casarin
5010d3662d thread: move comment to the correct place
Signed-off-by: William Casarin <jb55@jb55.com>
2025-06-25 10:29:33 -07:00
William Casarin
a0ac4b16ad nostrdb: bump to v0.7.0
includes replay fix

Signed-off-by: William Casarin <jb55@jb55.com>
2025-06-25 10:29:33 -07:00
Fernando López Guevara
36667bc024 feat(app_images): add module to manage static app image assets 2025-06-25 09:53:31 -07:00
Fernando López Guevara
c6dbb0e856 fix(content): handle case where notes are not loaded 2025-06-24 09:15:52 -07:00
William Casarin
48f17f91b8 log: make some routing logs into debug logs
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-06-24 08:31:25 -07:00
William Casarin
ca5ecb3777 Merge multiple hashtags in a column
Fernando López Guevara (1):
      hashtag-column: allow multiple hashtags

William Casarin (2):
      hashtag: improve sanitization function
2025-06-24 08:30:18 -07:00
William Casarin
b67a2ddc31 hashtag: improve sanitization function
We don't want punctuation in hashtags

Signed-off-by: William Casarin <jb55@jb55.com>
2025-06-24 08:29:37 -07:00
Fernando López Guevara
f214e97382 hashtag-column: allow multiple hashtags
Changelog-Changed: Allow multiple hashtags in hashtag columns
2025-06-24 08:16:10 -07:00
William Casarin
5c31bf16c8 Merge remote-tracking branch 'github/pr/899' 2025-06-23 13:48:21 -07:00
kernelkind
86d68e786a threads: fix other replies not rendering in presence of one muted
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-06-23 16:32:34 -04:00
kernelkind
589a8a904c fix log messages
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-06-23 16:32:28 -04:00
William Casarin
75fd22d8ed thread: selected thread notes should be... selectable
We couldn't select text on thread notes before,
now we can

Signed-off-by: William Casarin <jb55@jb55.com>
2025-06-23 13:00:08 -07:00
William Casarin
15b4978d47 deps: switch to damus-io egui-nav
thanks kernel!

Signed-off-by: William Casarin <jb55@jb55.com>
2025-06-23 11:27:04 -07:00
William Casarin
7ba81d0761 Merge Threads by kernel
kernelkind (16):
      add `NoteId` hashbrown `Equivalent` impl
      unknowns: use unowned noteid instead of owned
      tmp: upgrade `egui-nav` to use `ReturnType`
      add `ThreadSubs` for managing local & remote subscriptions
      add threads impl
      add overlay conception to `Router`
      add overlay to `RouterAction`
      ui: add `hline_with_width`
      note: refactor to use action composition & reduce nesting
      add pfp bounding box to `NoteResponse`
      add unread note indicator option to `NoteView`
      thread UI
      add preview flag to `NoteAction`
      add `NotesOpenResult`
      integrate new threads conception
      only deserialize first route in each column
2025-06-23 10:52:00 -07:00
kernelkind
b7d6e3b2f1 only deserialize first route in each column
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-06-22 16:03:13 -04:00
kernelkind
d560e84eab integrate new threads conception
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-06-22 16:03:13 -04:00
kernelkind
f6753bae97 add NotesOpenResult
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-06-22 16:03:13 -04:00
kernelkind
87b4b5fc70 add preview flag to NoteAction
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-06-22 16:03:09 -04:00
kernelkind
b3569e90d6 thread UI
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-06-22 16:01:20 -04:00
kernelkind
51476772c4 add unread note indicator option to NoteView
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-06-22 16:00:42 -04:00
kernelkind
ea91f582ed add pfp bounding box to NoteResponse
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-06-22 16:00:40 -04:00
kernelkind
b7bab1d29f note: refactor to use action composition & reduce nesting
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-06-22 16:00:37 -04:00
kernelkind
c3b8823f72 ui: add hline_with_width
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-06-22 16:00:35 -04:00
kernelkind
41c2c048a8 add overlay to RouterAction
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-06-22 16:00:33 -04:00
kernelkind
e0dd09dd5f add overlay conception to Router
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-06-22 16:00:29 -04:00
kernelkind
cdcca0ba35 add threads impl
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-06-22 16:00:26 -04:00
kernelkind
3c31e1a651 add ThreadSubs for managing local & remote subscriptions
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-06-22 16:00:23 -04:00
kernelkind
faa40bb616 tmp: upgrade egui-nav to use ReturnType
remove when damus-io/egui-nav merges

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-06-22 16:00:18 -04:00
kernelkind
a77fe6ca00 unknowns: use unowned noteid instead of owned
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-06-18 14:39:05 -04:00
kernelkind
6da10c4faf add NoteId hashbrown Equivalent impl
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-06-18 14:38:26 -04:00
valkuros@gmail.com
2bd824bc0a Changed line 683 from Persisted to temp per Minor bug #888 2025-06-17 21:48:38 -04:00
William Casarin
cc5a888b89 Merge 'Initial android support'
This gets android into a somewhat usable state.

Still news a few follow ups.

William Casarin (9):
      nix: add $ANDROID_JAR helper to shell
      add input context menu helper
      thread: enable selectable text in threads
      universe: add full tabs
      android: fix build
      dave: initial android fixes
      android: arboard clipboard support
      android: add initial ci
      Merge 'Initial android support'
2025-06-17 13:20:06 -07:00
William Casarin
012ff9d53d android: add initial ci
Signed-off-by: William Casarin <jb55@jb55.com>
2025-06-17 13:15:40 -07:00
William Casarin
c8e861812b android: arboard clipboard support
Signed-off-by: William Casarin <jb55@jb55.com>
2025-06-16 16:11:51 -07:00
William Casarin
be9406da7b dave: initial android fixes 2025-06-16 16:11:51 -07:00
William Casarin
505083998d android: fix build
wip android keyboard fixes

wip 4.0.0 game-activity

Signed-off-by: William Casarin <jb55@jb55.com>
2025-06-16 16:11:51 -07:00
William Casarin
7b558f8f58 universe: add full tabs
so we can monitor replies as well

Signed-off-by: William Casarin <jb55@jb55.com>
2025-06-16 16:11:51 -07:00
William Casarin
5e1e45184b thread: enable selectable text in threads
This avoids some of the nested thread loading,
but we can fix that next

Changelog-Changed: Made text in threads selectable
Signed-off-by: William Casarin <jb55@jb55.com>
2025-06-16 16:11:51 -07:00
William Casarin
9033383a29 add input context menu helper
We are going to want this in more places

Signed-off-by: William Casarin <jb55@jb55.com>
2025-06-16 16:11:15 -07:00
William Casarin
c6045279dd nix: add $ANDROID_JAR helper to shell
So we can easily print JNI signatures via javap

javap -classpath "$ANDROID_JAR" -s android.content.ClipboardManager

This can be used to call java code with JNI

Signed-off-by: William Casarin <jb55@jb55.com>
2025-06-16 16:09:03 -07:00
William Casarin
e8d240df42 toolbar: process actions
Signed-off-by: William Casarin <jb55@jb55.com>
2025-06-07 10:19:13 -07:00
William Casarin
0ea1a92ea7 chrome: hook up toolbar actions
We will implement execution of these actions in the
upcoming commits!

stay tuned

Signed-off-by: William Casarin <jb55@jb55.com>
2025-06-07 10:19:13 -07:00
William Casarin
0eec6881fc Initial tab bar 2025-06-07 10:19:13 -07:00
William Casarin
bcd9c61d46 chrome: extract more non-methods 2025-06-07 10:19:13 -07:00
William Casarin
65928bcdbb chrome: extract method to function
We don't need anything from Chrome in this function, so we can just
extract it to a top-level function
2025-06-07 10:19:13 -07:00
Fernando López Guevara
e6c8231579 fix(search): make input background gray in light mode 2025-06-06 13:40:02 -03:00
William Casarin
6812a0e6ae dave: add chrome toggle button
We were missing this, which meant we could get stuck in
dave
2025-06-05 15:39:18 -07:00
William Casarin
b139af475e dave: small cleanup 2025-06-05 14:37:03 -07:00
William Casarin
e87b6f1905 chrome: collapsible side panel
This implements the initial logic that makes the side panel collapsible.

Since we don't have a proper hamburger control, we do the same thing we
do on iOS for now.
2025-06-05 12:01:55 -07:00
William Casarin
5cb0911d7e log: less verbose unknown id logging 2025-06-05 11:59:51 -07:00
William Casarin
b186458fec nix: emulator
This expression was incorrect...
2025-06-05 11:59:51 -07:00
alltheseas
d6b44d1836 Update README.md with deepwiki badge (#875)
* Update README.md with deepwiki badge

Added deepwiki badge

* Update README.md rearranged deepwiki badge

moved deepwiki badge to top
2025-06-04 08:51:55 -07:00
William Casarin
f380c24649 fix note response regression
Fixes: 7d916805bc ("note: cleanup wide/standard implementation")
2025-06-03 11:26:18 -07:00
William Casarin
771537a4f6 android: hover post button when narrow
Signed-off-by: William Casarin <jb55@jb55.com>
2025-06-03 10:29:38 -07:00
William Casarin
7d916805bc note: cleanup wide/standard implementation
Since this function was getting too big
2025-06-03 10:29:38 -07:00
William Casarin
5ef77efebb scroll: simple fix
Instead of a complicated min scroll distance implementation,
we simply disable drag to scroll on carousel to fix vertical
scrolling on android
2025-06-03 10:29:38 -07:00
William Casarin
81a9ddbebc Merge remote-tracking branches 'github/pr/87{0,1,2}'
Merge a few bug fixes and lint issues

Fernando López Guevara (1):
      fix: skip blurring for user's own images

William Casarin (3):
      clippy: fix large enum.

kernelkind (1):
      bugfix: txn failed
2025-06-02 10:34:45 -07:00
Fernando López Guevara
f3f5026719 fix: skip blurring for user's own images 2025-06-02 12:36:42 -03:00
kernelkind
91c9cfc34f bugfix: txn failed
`ERROR notedeck_columns::timeline: setup_new_timeline:
database error: Transaction failed`

Reproduce by creating column, deleting it, then trying to create
it again. Before this fix, it was blank. Now it displays correctly

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-06-01 17:56:22 -04:00
William Casarin
0a675dfff0 clippy: fix large enum.
Signed-off-by: William Casarin <jb55@jb55.com>
2025-06-01 17:56:18 -04:00
William Casarin
1c3b172e21 clippy: fix large enum.
Signed-off-by: William Casarin <jb55@jb55.com>
2025-05-31 16:33:18 -07:00
William Casarin
d9b1de9d2c Merge remote-tracking branches 'github/pr/869' and 'github/pr/868'
Merge some misc changes from Fernando

Fernando López Guevara (2):
      feat(column): add tooltip on remove column button
      feat(hashtag-column): handle new hashtag on Enter key press
2025-05-31 16:01:02 -07:00
William Casarin
fc51ddb438 Merge remote-tracking branches 'github/pr/864' and 'github/pr/866' 2025-05-31 15:53:36 -07:00
Fernando López Guevara
3972f5f2ab feat(hashtag-column): handle new hashtag on Enter key press 2025-05-27 16:53:56 -03:00
Fernando López Guevara
269ffee857 feat(column): add tooltip on remove column button 2025-05-27 14:44:11 -03:00
kernelkind
2d55c8fb06 add search improvements
- '@' symbol brings up mention picker
- search for npub1, note1, and hashtags work

closes: https://github.com/damus-io/notedeck/issues/83
closes: https://github.com/damus-io/notedeck/issues/85

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-26 16:52:19 -04:00
kernelkind
9387fe4973 stop error log spam
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-26 16:32:21 -04:00
kernelkind
58b15d99d7 add SearchType
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-26 16:32:18 -04:00
kernelkind
64d3a0842e add NoteId::from_bech method
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-25 18:26:44 -04:00
kernelkind
a1ac0cd2c8 appease clippy
not sure why this warning is only now showing up

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-25 18:25:34 -04:00
kernelkind
db5e10656d set variable for scroll offset
necessary to maintain scroll positions across popup & Nav

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-22 20:33:20 -04:00
kernelkind
3cb2dd88b6 use popup sheet for CustomZapView
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-22 20:33:18 -04:00
kernelkind
c36a22828d use router action
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-22 20:33:15 -04:00
kernelkind
a44667ef1a nav: move process nav response to own method
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-22 20:33:12 -04:00
kernelkind
f452a9010b nav: move action processing to own method
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-22 20:33:10 -04:00
kernelkind
08a720b860 add SingletonRouter
used for popup

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-22 20:33:06 -04:00
kernelkind
99aa50c120 TMP: use new egui-nav with popup
replace with damus-io/egui-nav when merged

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-22 20:32:33 -04:00
William Casarin
ee85b754dd Fix text wrapping issues
Mentions were getting wrapped in a horizontal, which breaks the outer
horizontal_wrapped in note contents. When this breaks, it seems to be
breaking subsequent wrapping in notes.

Remove the horizontal to the remaining text wrapping issues!

Changelog-Fixed: Fix text wrapping glitches
Fixes: https://github.com/damus-io/notedeck/issues/33
Signed-off-by: William Casarin <jb55@jb55.com>
2025-05-20 18:09:15 -07:00
William Casarin
e75eb5ffd5 clippy: quick lint fix 2025-05-19 15:28:58 -07:00
William Casarin
163abe891a Merge remote-tracking branch 'pr/862' 2025-05-19 15:25:56 -07:00
kernelkind
5598cc8ba0 use CustomZapView
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-17 14:41:48 -04:00
kernelkind
a9a819f742 add CustomZapView
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-17 14:41:26 -04:00
kernelkind
68b5c32e7f method to get current default zap amount
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-17 14:30:21 -04:00
kernelkind
16e2c9d5b0 make styled button toggleable
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-17 14:30:19 -04:00
kernelkind
d2158a6482 display name should wrap
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-17 14:30:15 -04:00
kernelkind
54c0fdb563 don't show zap button if no wallet
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-17 14:29:43 -04:00
kernelkind
98cb082fb4 hotfix: can login again
adds fallback pubkey as account and selects it when there are
no accounts

closes: https://github.com/damus-io/notedeck/issues/855

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-16 15:50:31 -04:00
William Casarin
86d2a9e2e7 clippy: fix lint related to iterator
warning: called `Iterator::last` on a `DoubleEndedIterator`; this will
needlessly iterate the entire iterator
   --> crates/notedeck/src/urls.rs:262:43
    |
262 |             if let Some(file_name) = path.last() {
    |                                           ^^^^^^ help: try: `next_back()`
    |

Signed-off-by: William Casarin <jb55@jb55.com>
2025-05-14 09:56:06 -07:00
William Casarin
c469a0ff22 timeline: show media on universe timeline
Now that we have blurred images from people you don't
follow, we can enable this again

Signed-off-by: William Casarin <jb55@jb55.com>
2025-05-14 09:53:55 -07:00
William Casarin
54308c807e Merge blurhash support
kernelkind (22):
      add `trust_media_from_pk2` method
      add hashbrown
      introduce & use `JobPool`
      introduce JobsCache
      add blurhash dependency
      introduce blur
      note: remove unnecessary derive macros from `NoteAction`
      propagate `JobsCache`
      `ImagePulseTint` -> `PulseAlpha`
      images: move fetch to fn
      add `TexturesCache`
      images: make `MediaCache` hold `MediaCacheType`
      images: make promise payload optional to take easily
      post: unnest
      notedeck_ui: move carousel to `note/media.rs`
      note media: only show full screen when loaded
      note media: unnest full screen media
      pass `NoteAction` by value instead of reference
      propagate `Images` to actionbar
      add one shot error message
      make `Widget` impl `ProfilePic` mutably
      implement blurring
2025-05-14 09:27:39 -07:00
William Casarin
b48b1e4813 release: changelog
Signed-off-by: William Casarin <jb55@jb55.com>
2025-05-05 13:54:33 -07:00
kernelkind
b2abe495ca implement blurring
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-04 12:57:57 -04:00
kernelkind
7d2112b472 make Widget impl ProfilePic mutably
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-04 12:57:54 -04:00
kernelkind
640bf742c0 add one shot error message
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-04 12:57:51 -04:00
kernelkind
929099c15f propagate Images to actionbar
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-04 12:57:48 -04:00
kernelkind
e7c3755a08 pass NoteAction by value instead of reference
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-04 12:57:45 -04:00
kernelkind
953496fc74 note media: unnest full screen media
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-04 12:57:43 -04:00
kernelkind
01636786be note media: only show full screen when loaded
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-04 12:56:48 -04:00
kernelkind
379d6c0307 notedeck_ui: move carousel to note/media.rs
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-04 12:56:45 -04:00
kernelkind
258ac3de29 post: unnest
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-04 12:56:42 -04:00
kernelkind
def9de0dc0 images: make promise payload optional to take easily
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-04 12:56:38 -04:00
kernelkind
d204db4b29 images: make MediaCache hold MediaCacheType
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-04 12:56:34 -04:00
kernelkind
7f01f3623d add TexturesCache
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-04 12:56:29 -04:00
kernelkind
faec75e1b6 images: move fetch to fn
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-04 12:56:26 -04:00
kernelkind
a4ec0982d2 ImagePulseTint -> PulseAlpha
make it more generic to pulse alpha values, not necessarily image
tints

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-04 12:56:22 -04:00
kernelkind
a29277d263 propagate JobsCache
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-04 12:55:53 -04:00
kernelkind
e6212e5d17 note: remove unnecessary derive macros from NoteAction
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-04 12:54:38 -04:00
kernelkind
b9e2daf47a introduce blur
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-04 12:54:34 -04:00
kernelkind
d227eb6551 add blurhash dependency
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-04 12:54:30 -04:00
kernelkind
badf3070c8 introduce JobsCache
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-04 12:54:26 -04:00
kernelkind
5cdf3698d2 introduce & use JobPool
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-04 12:54:23 -04:00
kernelkind
7bb871d377 add hashbrown
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-04 12:54:18 -04:00
kernelkind
e453c742de add trust_media_from_pk2 method
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-05-04 12:53:49 -04:00
William Casarin
b072c93964 Release Notedeck Beta v0.4.0
Signed-off-by: William Casarin <jb55@jb55.com>
2025-05-03 14:42:32 -07:00
William Casarin
bdd0ef4c5c ui: fix a bunch of missing hover pointers
let's try to keep on top of these

Signed-off-by: William Casarin <jb55@jb55.com>
2025-05-01 20:50:56 -07:00
William Casarin
8b7914e395 chrome: fix theme persistence
Fixes: #832
Signed-off-by: William Casarin <jb55@jb55.com>
2025-05-01 20:27:08 -07:00
William Casarin
a94cbb2dc0 dave: hide media in dave note previews
it bugs out sometimes

Signed-off-by: William Casarin <jb55@jb55.com>
2025-05-01 20:21:42 -07:00
William Casarin
2539dead1e dave: nudge avatar when you click
Signed-off-by: William Casarin <jb55@jb55.com>
2025-05-01 20:21:30 -07:00
William Casarin
3eb9e30e8f dave: fix sidebar click
Fixes: #837
Signed-off-by: William Casarin <jb55@jb55.com>
2025-05-01 19:54:02 -07:00
William Casarin
514e5748b8 dave: add trial mode
Fixes: #827
Signed-off-by: William Casarin <jb55@jb55.com>
2025-05-01 19:37:45 -07:00
William Casarin
6bbc20471a dave: include anonymous user identifier in api call
- don't include users pubkey

This could be used to associate requests with real users,
rendering the anonymized user_id pointless

TODO: Implement a new tool call that lets dave ask for your pubkey

Fixes: #834
Fixes: #836
Signed-off-by: William Casarin <jb55@jb55.com>
2025-05-01 19:33:33 -07:00
William Casarin
093189b019 ui: make post replies selectable
I wanted to copy a quote from something I was replying to, I couldn't
now I can

Fixes: #835
Signed-off-by: William Casarin <jb55@jb55.com>
2025-05-01 17:28:59 -07:00
William Casarin
dbfc2804f1 chrome: switch from ALPHA to BETA
Fixes: #828
Signed-off-by: William Casarin <jb55@jb55.com>
2025-05-01 17:23:14 -07:00
William Casarin
5bae19fe00 mention: show username instead of display_name
Fixes: #833
Signed-off-by: William Casarin <jb55@jb55.com>
2025-05-01 17:23:08 -07:00
William Casarin
10a2459da2 windows: don't show terminal window
Looks like this got accidentally commented out in an android build

Signed-off-by: William Casarin <jb55@jb55.com>
2025-05-01 12:03:36 -07:00
William Casarin
a5f4290acf columns: never truncate notes you're replying to
So you can see everything

Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-30 11:00:37 -07:00
William Casarin
b83c5f7de5 columns: remove spamming info logs about writing to cache
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-30 11:00:19 -07:00
William Casarin
1931eb6558 dave: fix image in readme 2025-04-30 08:49:22 -07:00
William Casarin
1668b3701c dave: add screenshot to readme 2025-04-30 08:44:42 -07:00
kernelkind
a38c682d78 use default zap amount for zap
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-23 13:53:32 -04:00
kernelkind
c456432015 ui: show default zap amount in wallet view
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-23 13:53:32 -04:00
kernelkind
fcec3b4c8e accounts: check if selected account has wallet
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-23 13:53:32 -04:00
kernelkind
e6d9de2b99 wallet: helper method to get current wallet
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-23 13:53:32 -04:00
kernelkind
9013a2e067 propagate DefaultZapState to wallet ui
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-23 13:53:32 -04:00
kernelkind
f16e63cf3b use ZapWallet
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-23 13:53:32 -04:00
kernelkind
61943aa6c7 introduce ZapWallet
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-23 13:53:32 -04:00
kernelkind
4b608cef5f add default zap
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-23 13:53:32 -04:00
kernelkind
015e7790d0 move WalletState to UI
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-23 13:53:32 -04:00
kernelkind
9e21518e4b Wallet token parser shouldn't parse all
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-23 13:53:32 -04:00
kernelkind
200ef58912 UserAccount use builder pattern
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-23 13:53:32 -04:00
kernelkind
5bddf83655 extend ZapAction
going to need amounts for configurable zaps

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-23 13:53:20 -04:00
kernelkind
0adaafd523 remove unnecessary #[allow(dead_code)]
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-23 13:50:57 -04:00
William Casarin
3dccdf2bad chrome: use actual columns noteaction executor
there is code duplication here and it is causing bugs
2025-04-22 19:07:25 -07:00
William Casarin
e8a1233174 dave: bubble note actions to chrome
This allows chrome to pass note actions to other apps
2025-04-22 18:42:12 -07:00
William Casarin
4cedea9fdb dave: more flexible env config
With this I was able to get openrouter working:

DAVE_API_KEY=$OPENROUTER_API_KEY
DAVE_ENDPOINT=https://openrouter.ai/api/v1
DAVE_MODEL="google/gemini-2.0-flash-001"
RUST_LOG=async_openai=debug,notedeck_dave=debug

./target/release/notedeck
2025-04-22 16:46:38 -07:00
William Casarin
5c0874ab85 dave: give present notes a proper tool response
so the ai know we actually did something
2025-04-22 16:20:26 -07:00
William Casarin
56f5151739 dave: return tool errors back to the ai
So that it can correct itself
2025-04-22 16:05:54 -07:00
William Casarin
9692b6b9fe dave: add query rendering, fix author queries 2025-04-22 10:50:58 -07:00
William Casarin
5c8fba220c ui: add ProfilePic::from_profile_or_default
This is yet another helper, I really need to clean this
ui widget up in terms of its possible constructors...
2025-04-22 10:50:58 -07:00
William Casarin
c4084a1fb5 ui: add note truncation
Truncate notes by default. We still need a show more button though
2025-04-22 10:50:58 -07:00
William Casarin
e4658df847 name: display_name before name in NostrName
This is technically more currect. name is more of a username for
tagging.
2025-04-22 10:47:14 -07:00
William Casarin
ba4198eeec enostr: rename to_bech to npub
a bit more clear as to what this is
2025-04-22 10:46:51 -07:00
William Casarin
88aa78dc03 dave: ensure system prompt is included when reset 2025-04-21 17:27:38 -07:00
William Casarin
d4681801e8 dave: add new chat button 2025-04-21 17:10:03 -07:00
William Casarin
9ec5f1efa2 docs: improve top-level docs
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-21 13:36:36 -07:00
William Casarin
405b62c15a docs: add notedeck_chrome docs
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-21 13:31:47 -07:00
William Casarin
f7e47dedee docs: add notedeck_columns readme
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-21 13:26:02 -07:00
William Casarin
5f6a69b4b3 docs: add notedeck docs
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-21 13:21:45 -07:00
William Casarin
82c0bc0718 docs: add tokenator docs
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-21 13:16:18 -07:00
William Casarin
310a835b27 docs: remove test hallucination
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-21 13:12:29 -07:00
William Casarin
d617b688f1 docs: add some ui-related guides
generated using code2prompt + claude 3.7 sonnet

Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-21 13:10:20 -07:00
William Casarin
0d51e25ab0 dave: improve docs with ai
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-21 12:48:33 -07:00
William Casarin
6601747eb4 dave: add readme
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-21 12:26:50 -07:00
William Casarin
2fdb36475a dave: add a few docs
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-21 12:11:21 -07:00
William Casarin
04a11fd45d dave: cleanly separate ui from logic
This is a good demo of how easy it is to build a notedeck app,
so let's detangle the ui from logic to showcase this

Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-21 12:05:27 -07:00
William Casarin
5811a5f4e6 dave: improve multi-note display 2025-04-20 09:05:02 -07:00
William Casarin
a33aad1f62 note: fix from_hex crash on bad note ids 2025-04-19 19:34:12 -07:00
William Casarin
f496d4b8c4 dave: initial note rendering
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-18 17:03:59 -07:00
William Casarin
43310b271e ci: bump ubuntu runner
Since our current runner was deprecated

Link: https://github.com/actions/runner-images/issues/11101
2025-04-17 14:13:55 -07:00
William Casarin
d04fc892a7 dave: constrain power for now
we will focus on more specific tools instead
2025-04-17 12:53:31 -07:00
William Casarin
8af80d7d10 ui: move note and profile rendering to notedeck_ui
We want to render notes in other apps like dave, so lets move
our note rendering to notedeck_ui. We rework NoteAction so it doesn't
have anything specific to notedeck_columns

Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-17 12:34:43 -07:00
William Casarin
e4bae57619 refactor: ocd unnecessary pass by value
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-17 08:39:47 -07:00
William Casarin
81ef677bf2 refactor: nevernest get_display_name
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-17 08:37:14 -07:00
William Casarin
8472a9b643 log: silence gif log
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-17 07:47:11 -07:00
William Casarin
7836bde868 dave: fix input box
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-17 00:16:55 -07:00
William Casarin
fbdc2527ca dave: give up on plaintext formatting
its so heavily trained to use markdown, lets just use that

Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-17 00:16:30 -07:00
William Casarin
d30e4c53ee post: fix bug where send shortcut send unfocused inputs
Fixes: https://github.com/damus-io/notedeck/issues/810
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-16 23:52:34 -07:00
William Casarin
b50bc2e988 dave: refactor a bit
pulling tokens isn't really a part of rendering,
so let's pull that out

Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-16 22:41:41 -07:00
William Casarin
bf18eb4e69 refactor: extract input_ui into its own function
too many things happening in the ui function

Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-16 22:29:20 -07:00
William Casarin
cc03f24920 refactor: move input buttons ui into its own fn
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-16 22:25:44 -07:00
William Casarin
aa0c1012db misc: driveby fixes
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-16 22:20:00 -07:00
William Casarin
4a0e2fa347 dave: tweak prompt
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-16 20:35:39 -07:00
kernelkind
ae0a74d383 pulse pending zap button
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-16 14:14:48 -04:00
kernelkind
45fe192f75 introduce ImagePulseTint
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-16 13:34:55 -04:00
kernelkind
bd78be1659 move error out of AnyZapState
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-16 13:34:51 -04:00
William Casarin
4260d3e9da nostrdb: fix on windows
had this on an older version for some reason
2025-04-15 08:53:30 -07:00
kernelkind
2a2c177300 Fix flaky test_zap_event
Closes: #808
Co-authored-by: William Casarin <jb55@jb55.com>
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-15 08:42:05 -07:00
William Casarin
b228411b8d columns: remove thread warning
yes threads suck, but this message is also annoying
2025-04-15 08:24:24 -07:00
William Casarin
66377351b3 ui: add some margin to chrome sidebar
Looks a bit better
2025-04-15 08:24:04 -07:00
William Casarin
4f0d96679d previews: disable for now
we don't use these much and it slows compile time
2025-04-15 08:20:25 -07:00
William Casarin
87794fae33 chrome: fix wallet button
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 16:37:22 -07:00
William Casarin
e5c3bb4fe9 dave: fix bugs
fixed some bugs i introduced during the refactor

Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 16:13:40 -07:00
William Casarin
39e2accbce multicast: broadcast context
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 16:02:50 -07:00
William Casarin
50dec5b5d5 context: implement note broadcasting
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 16:02:43 -07:00
William Casarin
956c557851 dave: only search non-replies
I think this makes the most sense for most queries

Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 16:02:33 -07:00
William Casarin
f1e359a5c3 dave: update to custom filters nostrdb
We will use this for filtering replies from most
requests

Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 15:59:11 -07:00
William Casarin
2ed561579f dave: add a few tool docs
So that readers of this code can actually figure out
what these types actually mean.

Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 12:45:19 -07:00
William Casarin
be47a692f6 dave: remove old file
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 12:38:36 -07:00
William Casarin
d6c065694a dave: organize
move more things into their own modules

Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 12:37:25 -07:00
William Casarin
2a9c5c7848 dave: reorganize ModelConfig
start to clean up the lib.rs a bit

Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:53:52 -07:00
William Casarin
b8c5423edd dave: don't make dave active yet
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:30:58 -07:00
William Casarin
4ca7bcec6d lint: fix clippy and fmt issues
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:30:24 -07:00
William Casarin
b4d1265283 dave: tweak prompt
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:30:24 -07:00
William Casarin
e6676a202a log: changed urlcache log to debug
its more of a debug log

Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:30:24 -07:00
William Casarin
47e942be28 dave: fix ollama config if enabled
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:30:24 -07:00
William Casarin
403b0f7696 chrome: fix support route
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:30:24 -07:00
William Casarin
72312179d4 chrome: fix settings view
restore some chrome panel actions

Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:30:24 -07:00
William Casarin
6dc68436e9 dave: improve query tool
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:30:24 -07:00
William Casarin
0294d2d1c8 dave: remove default property value and add to description
openai doesn't seem to support this

Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:30:24 -07:00
William Casarin
686d8c6185 dave: add a bit of spacing, fix sned
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:30:24 -07:00
William Casarin
633cba8331 dave: introduce model config
so you can switch between openai and ollama models

Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:30:24 -07:00
William Casarin
366827d335 dave: tweak search tool to include limit arg
So that dave can return single notes

Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:30:24 -07:00
William Casarin
418e08541d notedeck: include frame history
for debugging.

Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:30:22 -07:00
William Casarin
f36390d8f8 icons: add new_message icon
This is used for created new dave sessions

Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:29:03 -07:00
William Casarin
7c58dc019b dave: extract search_call ui
A bit cleaner

Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:29:03 -07:00
William Casarin
fcd7c261bb chrome: initial action handling
still need settings and account nav

Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:29:03 -07:00
William Casarin
c6a7a50f81 dave: improve design
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:29:03 -07:00
William Casarin
f412b1ac7b dave: better initial rotation
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:29:03 -07:00
William Casarin
b8e2a16e3b dave: give dave a new home in the sidebar
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:29:03 -07:00
William Casarin
627c3ba9b3 assets: update columns app icon
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:29:03 -07:00
William Casarin
9c9b4199f5 ui crate and chrome sidebar
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:29:01 -07:00
William Casarin
415a052602 assets: add columns app icon
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:17:06 -07:00
William Casarin
6e751aa20a dave: fix android build 2025-04-14 11:17:06 -07:00
William Casarin
4469918fd2 dave: prepare for android 2025-04-14 11:17:06 -07:00
William Casarin
cb7a3adacf dave: move quaternion to its own file 2025-04-14 11:16:43 -07:00
William Casarin
31aae7f315 dave: auto-reply, initial avatar anim
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:16:43 -07:00
William Casarin
80f02d829a clippy fixes
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:16:43 -07:00
William Casarin
0b4807f62d dave: tools working even better
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:16:42 -07:00
William Casarin
4dfb013d6a dave: toolcall parsing
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:16:13 -07:00
William Casarin
6e2c4cb695 dave: tweak prompt
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:16:13 -07:00
William Casarin
56534af698 dave: use local llama for testing for now
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:16:13 -07:00
William Casarin
89b96aeab3 dave: remove shader since we do it inline now
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:16:13 -07:00
William Casarin
e7241353bb dave: add background to user messages
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:16:13 -07:00
William Casarin
c3bdde59a9 dave: fix prompt and token concat 2025-04-14 11:16:13 -07:00
William Casarin
d3d6a0c805 dave: only re-render dave if he's moving
we can be smarter about re-rendering in the future.
we really only need to re-render when he's moving
2025-04-14 11:16:13 -07:00
William Casarin
32f7d484f8 dave: rotation tweaks
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:16:13 -07:00
William Casarin
343f2ce410 dave: cube lighting
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:16:13 -07:00
William Casarin
968d9bc245 dave is alive
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:16:12 -07:00
William Casarin
a701275460 nostrdb: only use 2 ingester threads
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-14 11:15:10 -07:00
kernelkind
0c87d02fe0 fix zaps networking tests
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-13 17:19:03 -04:00
William Casarin
f0763b1278 zaps: fix invalid zaps
p tags needs to be the zap target
2025-04-12 15:39:07 -07:00
kernelkind
c512cb046f process zaps in Notedeck
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-08 22:50:00 -04:00
kernelkind
cfbd601196 note zap button
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-08 22:50:00 -04:00
kernelkind
5917bc16fd propagate current account
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-08 22:50:00 -04:00
kernelkind
18ea05db0a use Zaps
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-08 22:50:00 -04:00
kernelkind
cbf281dcc1 introduce Zaps
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-08 22:50:00 -04:00
kernelkind
fd2299f5f0 add hashbrown
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-08 22:50:00 -04:00
kernelkind
a7da4d6a11 add Zap
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-08 22:50:00 -04:00
kernelkind
38fb05475d fetch zap invoice
closes: https://github.com/damus-io/notedeck/issues/128

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-08 22:50:00 -04:00
kernelkind
1cf7e9e3d1 wallet side panel button
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-08 22:50:00 -04:00
kernelkind
ebec367809 wallet route
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-08 22:50:00 -04:00
kernelkind
c2fbcaa5eb add wallet ui
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-08 22:50:00 -04:00
kernelkind
1ce530faec add human_format dep
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-08 22:50:00 -04:00
kernelkind
ee8c1e41df move sized_button into ui/widgets as styled_button
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-08 22:50:00 -04:00
kernelkind
13428af006 move close_button to ui/widgets.rs as x_button
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-08 22:50:00 -04:00
kernelkind
944c9863f5 process wallet action
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-08 22:50:00 -04:00
kernelkind
c3655e033b use UserAccount for account storage
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-08 22:50:00 -04:00
kernelkind
dbe71bbb80 add get wallet method 2025-04-08 22:50:00 -04:00
kernelkind
6b45843103 add Wallet to UserAccount
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-08 22:49:53 -04:00
kernelkind
0bcd84166d integrate global wallet into app
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-08 20:25:13 -04:00
kernelkind
c77246c231 accounts: update & optimised find
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-08 20:25:10 -04:00
kernelkind
b1215f1932 wallet
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-08 20:25:10 -04:00
kernelkind
4522920939 introduce TokenHandler
used for saving anything `TokenSerializable` to disk

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-08 20:25:07 -04:00
kernelkind
31b3316d9c add tokio dep to notedeck package
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-04 18:53:25 -04:00
kernelkind
7213c1b7eb add nwc dependency
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-04-04 18:52:43 -04:00
kernelkind
79ac3b0d14 token serialize user account
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-03-29 20:02:32 -04:00
William Casarin
499bac5ca4 fix again 2025-03-23 18:49:04 -07:00
William Casarin
3cc46b8a7d attempt macos fix 2025-03-23 18:45:14 -07:00
William Casarin
a2a119ec5c nostrdb: add author_kind index 2025-03-23 13:00:20 -07:00
William Casarin
23c93e1028 perf: reduce timeline overscan
I think I did this for image preloading, but it renders more things than
we need.
2025-03-23 11:30:18 -07:00
William Casarin
a21a3c079c theme: fix window styles
the headers are way too big
2025-03-23 11:30:18 -07:00
William Casarin
54deb2dd88 switch to profiling crates
This switches to the profiling crate for compatible
profiling between rust libraries.

To enable:

$ cargo build --release --features puffin

Feel free to experiment with other profiling backends
as well! Would be great to get tracy working.
2025-03-23 11:30:18 -07:00
William Casarin
7b9e6f180c disable large scale unknown id detection
its slow
2025-03-23 09:40:46 -07:00
kernelkind
6003ef5aec FileKeyStorage -> AccountStorage
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-03-22 00:06:49 -04:00
kernelkind
d9f92ef54f serialize UserAccount
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-03-22 00:06:49 -04:00
kernelkind
ad90a9565a canonize UserAccount
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-03-22 00:06:49 -04:00
kernelkind
675a223b11 migrate to tokenator key storage impl
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-03-22 00:06:49 -04:00
kernelkind
0bd486a8f4 serialize Keypair using tokenator
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-03-22 00:06:48 -04:00
kernelkind
1e05fa551d simplify key storage
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-03-22 00:06:31 -04:00
kernelkind
69b651bbc5 remove security framework storage
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-03-22 00:06:31 -04:00
William Casarin
d1f38c3d19 Merge right click paste #507
jglad (1):
      #507 add right click paste in search
2025-03-21 16:46:49 -07:00
William Casarin
26b58683b8 feat: integrate nostrdb relay indexing
- Upgrade `nostrdb` to v0.6.1 with relay metadata support
- Switch to `nostr::RelayUrl` for typed relay URLs
- Use `process_event_with()` to pass relay info during ingestion
- Update `Relay`, `RelayPool`, and unknown ID logic accordingly

This enables richer indexing with relay provenance in events.

Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-21 16:20:37 -07:00
jglad
318f96e37e #507 add right click paste in search
Signed-off-by: Jakub Gladysz <jakub.gladysz@protonmail.com>
2025-03-21 21:49:45 +01:00
William Casarin
a7f34a9dc7 Merge bump to 0.31.1
commit 2d801408b2
Author: William Casarin <jb55@jb55.com>
Date:   Mon Mar 17 18:05:01 2025 -0700

    egui: bump to 0.31.1
2025-03-18 09:41:25 -07:00
William Casarin
4d98b996ba Merge add padding to relay view
commit fddddba618
Author: jglad <jakub.gladysz1@gmail.com>
Date:   Tue Mar 18 09:29:22 2025 +0100

    #761 add padding to relay view
2025-03-18 09:38:07 -07:00
jglad
fddddba618 #761 add padding to relay view 2025-03-18 09:29:22 +01:00
William Casarin
2d801408b2 egui: bump to 0.31.1 2025-03-17 19:45:26 -07:00
William Casarin
beece0eb95 filter: fix memory leak in nostrdb Filter
Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-17 16:44:51 -07:00
William Casarin
adb3359bd8 debug: fix debug crash when adding columns
Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-17 16:44:51 -07:00
William Casarin
0b22ca345c column: inline some things because why not 2025-03-17 16:44:51 -07:00
William Casarin
fdd202741a Merge linux package fixes from ken 2025-03-14 12:52:24 -07:00
William Casarin
71f6d3014a Merge fullscreen images from jglad
jglad (3):
      #716 add full screen images
      #716 move goto button one level down
      #716 store full size img, add zoom & pan
2025-03-13 10:31:01 -07:00
jglad
a124187db6 #716 store full size img, add zoom & pan 2025-03-11 21:47:52 +01:00
William Casarin
c93c2242b1 ui: fix deprecated rounding routines in search ui
Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-11 11:00:03 -07:00
William Casarin
2e991a9aa5 fix a few compile issues after rebase
Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-11 10:55:15 -07:00
William Casarin
8467de2b5d android: attempt initial keyboard visibility fix
This isn't the right approach, but I keep it here as a reminder
of what we need to do next

Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-11 10:53:15 -07:00
William Casarin
33f570678d android: switch to android-activity out of path
Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-11 10:53:15 -07:00
William Casarin
bd85233120 android: capture current keyboard height
expose a new virtual_keyboard_height function under notedeck::platform::android

which gets the current height of the virtual keyboard. We can use this
to tranlate the view out of the way

Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-11 10:53:13 -07:00
William Casarin
09ad354d24 android: add push configs to readme
This wasn't documented, let's fix that

Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-11 10:52:29 -07:00
William Casarin
0a2a3f2bff android: improve make commands
Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-11 10:52:29 -07:00
William Casarin
1dec07afe8 android: expand logs
Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-11 10:52:29 -07:00
William Casarin
a168a38760 android: misc testing
Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-11 10:52:29 -07:00
Ken Sedgwick
93c97cf769 android: need argv0 placeholders because unneeded arg detection 2025-03-11 10:52:29 -07:00
Ken Sedgwick
c10e84b10d fixed egui::Frame::NONE references 2025-03-11 10:52:28 -07:00
Ken Sedgwick
2f4d9442f0 allow deprecated round_rect_to_pixels 2025-03-11 10:52:00 -07:00
Ken Sedgwick
27f4acea1c WIP: use modified version of egui 2025-03-11 10:52:00 -07:00
William Casarin
51457a0260 android: update to latest winit/egui/android-activity
so we can start fixing this shit

Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-11 10:51:56 -07:00
Ken Sedgwick
267f3c4527 update android default config push instructions 2025-03-11 10:47:33 -07:00
William Casarin
da9b2bcd46 android: 0.30.0 game activity
still no text input, at least it's not crashing

Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-11 10:47:32 -07:00
William Casarin
b33346a25d update to egui 0.30.0 2025-03-11 10:47:32 -07:00
William Casarin
b21e39dea9 android: get GameActivity to launch
For some reason there are no touch inputs though

Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-11 10:47:32 -07:00
William Casarin
9ce2b4da2c remove extra crap from manifest
still not launching, thought this might have been why. nope

Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-11 10:47:32 -07:00
jglad
02ec025096 #716 move goto button one level down 2025-03-10 18:02:43 +01:00
kernelkind
a9f473e3c9 introduce NoteContext
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-03-08 14:57:32 -05:00
jglad
d629ea893a #716 add full screen images 2025-03-08 17:57:32 +01:00
William Casarin
d85c6043b7 search: auto-focus search field on navigate
I'm going to add a search changelog on this commit since I forgot
to do so previously.

Fixes: https://linear.app/damus/issue/DECK-538/auto-focus-search-field-on-search-view
Changelog-Added: Added fulltext search ui
Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-07 16:02:35 -08:00
kernelkind
8e0e42a1f3 fix note content rects
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-03-07 18:04:12 -05:00
kernelkind
e7113b17a8 fix note context menu placement inside rect
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-03-07 18:04:12 -05:00
kernelkind
e2be2ddd58 fix context selection responses
closes: https://github.com/damus-io/notedeck/issues/574

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-03-07 18:04:12 -05:00
William Casarin
63f8790380 FIX BUILD YET AGAIN
because i'm retarded

Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-07 14:06:07 -08:00
William Casarin
e72a3f11fe Merge Cmd-Enter binding 2025-03-07 14:01:36 -08:00
William Casarin
e92e78126f windows: fix build due to outdated nostrdb-rs bindings
Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-07 13:59:34 -08:00
William Casarin
1953496019 search: hook up nav actions
Fixes: https://linear.app/damus/issue/DECK-537/hook-up-search-query-view-responses
Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-07 13:24:52 -08:00
William Casarin
c2545d17e7 route: add Search route and hook up SearchView
Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-07 13:24:52 -08:00
William Casarin
9edc9bf4a5 ui: add SearchView and SearchQueryState
Introduce a new view for searching for notes.

Fixes: https://linear.app/damus/issue/DECK-510/initial-search-query-view
Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-07 13:24:52 -08:00
William Casarin
5fde3277a1 notedeck: add debouncer util
I wanted this separate from the timed serializer so I could use it for
other things

Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-07 13:24:52 -08:00
William Casarin
d19e4b1d2b search: improve search column header
Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-07 13:24:52 -08:00
William Casarin
f7c1a39bc1 args: add search column argument
Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-07 13:24:52 -08:00
William Casarin
e09df3e7c3 timeline: add nip50 search timelines
Fixes: https://github.com/damus-io/notedeck/issues/456
Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-07 13:24:41 -08:00
William Casarin
62a1571dea search: show icon again
Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-07 12:58:42 -08:00
William Casarin
23285e7d76 nevernest some note posting code
Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-07 12:58:42 -08:00
William Casarin
873b0e0dcc nav: ocd updates
Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-07 12:58:42 -08:00
William Casarin
4365839242 Revert driller
This reverts commit cec49c83bd.

Revert "update NoteContentsDriller to NoteContext"

This reverts commit 65bd6a65f9.

Revert "introduce the driller"

This reverts commit 95d618e7fe.
2025-03-07 12:53:34 -08:00
William Casarin
50cf75b8bc lint: fix lint issue
Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-07 12:36:39 -08:00
William Casarin
cec49c83bd fix formatting
Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-07 12:36:26 -08:00
William Casarin
65bd6a65f9 update NoteContentsDriller to NoteContext
Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-07 12:30:53 -08:00
kernelkind
95d618e7fe introduce the driller
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-03-07 12:07:24 -05:00
kernelkind
035aa20790 remove redudant arg
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-03-07 11:58:39 -05:00
Ethan Tuttle
0d251bda9f support Cmd+Enter for posting on macOS
Add support for Command key (macOS) in addition to Ctrl key for submitting posts via keyboard shortcut
2025-03-06 21:56:44 -05:00
Ken Sedgwick
a1f72fa852 manually specify ubuntu libc dependency 2025-03-05 17:44:53 -08:00
kernelkind
e3eab0dfa8 user can explicitly close mention hints
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-03-02 15:05:48 -05:00
kernelkind
66b35c5026 add button for closing mention hints
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-03-02 15:05:48 -05:00
kernelkind
e37c14c9eb fix search results rect bounds
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-03-02 15:05:48 -05:00
kernelkind
83caa9f814 exit mention on double space
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-03-02 15:05:48 -05:00
kernelkind
ea4217d4c8 only show mention hints if prev char is whitespace or at first char
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-03-02 15:05:44 -05:00
William Casarin
d3bae69465 Merge avoid duplicate crates by rex4539 #746 2025-02-28 15:26:22 -08:00
William Casarin
95affa2245 ui: fix bounciness when loading pfps
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-27 10:37:07 -08:00
William Casarin
94e31ff715 add_column: add a bit of padding between title and desc
Co-authored-by: Grok3
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-26 19:48:04 -08:00
William Casarin
9713503d9e add_column: use weak color for descriptions
This matches the figma

Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-26 19:41:08 -08:00
William Casarin
cee8ab792c algo: fix algo feed icon
temporary placeholder, but at least its less ugly

Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-26 19:32:19 -08:00
kernelkind
7ca7dd156b fix video links not showing
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-26 16:09:39 -05:00
William Casarin
b4cb44a3d5 gif: don't allow retries
seems to be spamming

Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-26 12:49:32 -08:00
William Casarin
a524bbd5a4 Merge GIF support by kernel
kernelkind (20):
      use bincode
      update ehttp to 0.5.0
      introduce UrlMimes
      use mime_guess
      add SupportedMimeType
      rename ImageCache -> MediaCache
      Use TexturedImage in MediaCache
      render MediaCache method
      move MediaCache rendering to render_media_cache call
      support multiple media cache files
      introduce Images
      render Images method
      migrate to using Images instead of MediaCache directly
      URL mime hosted completeness
      handle UrlCache io
      introduce gif animation
      handle gif state
      integrate gifs
      use SupportedMimeType for media_upload
      render gif in PostView
2025-02-26 12:29:39 -08:00
kernelkind
615e27c1de fix mention crash
closes: https://github.com/damus-io/notedeck/issues/747

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-25 17:49:19 -05:00
kernelkind
9d88ba1415 render gif in PostView
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-25 16:49:00 -05:00
kernelkind
e5fc461a79 use SupportedMimeType for media_upload
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-25 16:49:00 -05:00
kernelkind
490dedfaf1 integrate gifs
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-25 16:49:00 -05:00
kernelkind
d1c7a5a239 handle gif state
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-25 16:49:00 -05:00
kernelkind
3e79f92291 introduce gif animation
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-25 16:49:00 -05:00
kernelkind
0461a98d5d handle UrlCache io
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-25 16:49:00 -05:00
kernelkind
9592452757 URL mime hosted completeness
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-25 16:49:00 -05:00
kernelkind
33fdf647e3 migrate to using Images instead of MediaCache directly
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-25 16:49:00 -05:00
kernelkind
75a352a86f render Images method
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-25 16:49:00 -05:00
kernelkind
fc9e1ff7f6 introduce Images
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-25 16:49:00 -05:00
kernelkind
888a933e56 support multiple media cache files
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-25 16:49:00 -05:00
kernelkind
7c2b4775f1 move MediaCache rendering to render_media_cache call
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-25 16:49:00 -05:00
kernelkind
32b3e2110d render MediaCache method
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-25 16:49:00 -05:00
kernelkind
594ea0b42d Use TexturedImage in MediaCache
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-25 16:49:00 -05:00
kernelkind
4f4a0feb8c rename ImageCache -> MediaCache
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-25 16:49:00 -05:00
kernelkind
bf68eb3ea8 add SupportedMimeType
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-25 16:49:00 -05:00
kernelkind
5791b0c5b1 use mime_guess
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-25 16:49:00 -05:00
kernelkind
4cd80c10b1 introduce UrlMimes
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-25 16:49:00 -05:00
kernelkind
fa9e318e41 update ehttp to 0.5.0
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-25 16:49:00 -05:00
kernelkind
9466c10875 use bincode
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-25 16:49:00 -05:00
Dimitris Apostolou
cc5941e919 avoid duplicate crates 2025-02-24 22:06:37 +02:00
William Casarin
660b7cc8b7 feat: add --no-media flag to disable media display
- Introduced a new "no_media" boolean in ColumnsArgs to capture the
  --no-media flag.

- Updated NoteOptions to include a setting for hiding media, configured
  from parsed arguments.

- Refactored Damus to consolidate note options (textmode, scramble, and
  no-media) into a single NoteOptions field.

- Modified navigation UI rendering to pass the unified note_options.

This change allows users to disable media display via the --no-media flag.

Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-22 14:39:33 -08:00
William Casarin
bd352f76d4 feat: add scramble flag for development text scrambling
This commit introduces a new scramble option to help reduce distractions
during development by scrambling text using rot13. When enabled via the
new `--scramble` flag, text displayed in various views is transformed,
making it easier to focus on layout and behavior without reading the
actual content.

App & Args Updates

  - Added a `scramble: bool` field to the main application state (in `app.rs`).

  - Extended argument parsing (in `args.rs`) to recognize the `--scramble` flag.

NoteOptions Enhancement

  - Introduced a new bit flag `scramble_text` in `NoteOptions` with
    corresponding setter/getter methods.

UI Adjustments

  - Propagated the scramble flag through note rendering functions across
    navigation, timeline, and note view modules.

  - Updated several UI components (e.g., in `nav.rs`, `route.rs`, and
    `contents.rs`) to accept and apply the new note options.

Rot13 Implementation

  - Implemented a helper function (`rot13`) to scramble text
    conditionally when the scramble option is enabled.

This feature is intended for development builds only, offering a way to
obscure text content during UI tweaks and testing.

Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-22 14:30:38 -08:00
William Casarin
32c7f83bd7 Merge hide nsec in account panel
jglad (2):
      fix compilation
      hide nsec in account panel
2025-02-21 12:13:35 -08:00
Ken Sedgwick
dfa4b24b7d check message length before prefix comparisons 2025-02-20 16:04:29 -08:00
Ken Sedgwick
22f9c32121 fix EOSE parsing to handle extra whitespace 2025-02-20 12:57:31 -08:00
Ken Sedgwick
fe7f0a3976 fix OK message parser to include last message component 2025-02-20 12:57:31 -08:00
Ken Sedgwick
80f4360005 fix event error return when decoding fails 2025-02-20 12:57:30 -08:00
Ken Sedgwick
84e0546e69 improve relay message parsing unit tests 2025-02-20 12:57:26 -08:00
Ken Sedgwick
d39b2706e0 add diagnostic string to DecodeFailed 2025-02-20 11:54:04 -08:00
jglad
030e76c046 hide nsec in account panel 2025-02-13 19:46:59 +01:00
jglad
346e705f36 fix compilation 2025-02-12 21:54:26 +01:00
William Casarin
d82d7fd00d Merge relay debug view fixes & more strict args #711
Ken Sedgwick (5):
      drive-by compiler warning fixes
      drive-by clippy fixes
      add derive Debug to some things
      panic on unknown CLI arguments
      move RelayDebugView to notedeck crate and restore --relay-debug

William Casarin (4):
      clippy: fix lint
      args: skip creation of vec
      nix: fix android build

Link: https://github.com/damus-io/notedeck/pull/711
2025-02-10 17:03:23 -08:00
William Casarin
353a3c87c3 nix: fix android build
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-10 17:01:57 -08:00
William Casarin
6fb720e0c5 args: skip creation of vec
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-10 16:57:50 -08:00
William Casarin
44181e24db clippy: fix lint
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-10 16:57:18 -08:00
William Casarin
7f0d0106d9 Merge remote-tracking branch 'github/pr/724' 2025-02-10 16:52:56 -08:00
William Casarin
56f19b155d changelog: add unreleased section
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-10 16:52:05 -08:00
William Casarin
c4b56a48af Merge ctrl-enter to post
Ethan Tuttle (1):
      feat: ctrl+enter when creating a new note, sends the note, the same way clicking the "Post Now" button.
2025-02-10 16:46:24 -08:00
William Casarin
d0be18c80e clippy: fix enum too large issue
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-10 16:33:58 -08:00
William Casarin
194fa68641 profilesearch: add some padding
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-10 16:08:19 -08:00
William Casarin
a95bc6ad5e egui: replace with damus-io repo
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-10 15:57:50 -08:00
Ethan Tuttle
8aaaa336e2 feat: ctrl+enter when creating a new note, sends the note, the same way clicking the "Post Now" button.
This button combination is common enough in "power user" apps for multiline input that I think this is a good default and could likely be configurable in the future.
2025-02-08 21:48:00 -05:00
jglad
4aefe1f1fe refactor 2025-02-08 10:59:15 +01:00
jglad
8588600a2c fix: handle missing file [#715] 2025-02-08 10:52:37 +01:00
kernelkind
0e21611645 cache LayoutJob
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-07 15:58:57 -05:00
kernelkind
bc8ed2c642 color mentions in PostView
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-07 15:58:57 -05:00
kernelkind
a3e975d133 implement TextBuffer -> PostBuffer downcasting
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-07 15:58:57 -05:00
kernelkind
07c6b27493 use updated TextEdit::layouter in egui
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-07 15:58:57 -05:00
kernelkind
b9501ad572 add mention tags to post note
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-07 15:58:57 -05:00
kernelkind
c0662798a2 add PostView mentions UI
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-07 15:58:57 -05:00
kernelkind
e7ada80876 mentions logic
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-07 15:58:57 -05:00
kernelkind
48933c2488 use dev dep pretty assertions
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-07 15:58:57 -05:00
kernelkind
c375146658 add SearchResultsView impl
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-07 15:58:57 -05:00
kernelkind
c1c4c1cc7a supply inner_rect for PostView
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-07 15:58:57 -05:00
William Casarin
8c49e6e5f6 Merge explicitly activate and deactivate account relay/muted list #678
Ken Sedgwick (4):
      drive-by compiler warning fixes
      improve debug logging, comments, and variable names for clarity
      explicitly activate and deactivate account relay list subs
      explicitly activate and deactivate account muted list subs
2025-02-07 11:07:17 -08:00
William Casarin
0d22debb05 Merge multiple image uploading
kernelkind (1):
      allow multiple media uploads per selection
2025-02-06 16:51:01 -08:00
William Casarin
36dc28451a Merge fix file logging #718
kernelkind (1):
      fix file logging
2025-02-06 14:00:55 -08:00
kernelkind
0aa70239fe remove # char if user inserted it
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-06 16:50:25 -05:00
kernelkind
d916021179 fix file logging
closes: https://github.com/damus-io/notedeck/issues/572

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-06 16:38:54 -05:00
kernelkind
7efb31c145 allow multiple media uploads per selection
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-06 15:55:53 -05:00
Ken Sedgwick
091c638eb1 move RelayDebugView to notedeck crate and restore --relay-debug 2025-02-06 12:25:37 -08:00
Ken Sedgwick
480f98eda4 panic on unknown CLI arguments
Currently silently ignores which is not helpful ...
2025-02-06 12:25:31 -08:00
William Casarin
c32a42f9b9 clippy: fix naming lint
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-06 12:17:48 -08:00
Ken Sedgwick
201cfb2db1 add derive Debug to some things 2025-02-06 10:32:50 -08:00
Ken Sedgwick
2ddc53faa5 drive-by clippy fixes 2025-02-06 10:32:49 -08:00
Ken Sedgwick
345324a2f6 drive-by compiler warning fixes 2025-02-06 10:32:49 -08:00
Ken Sedgwick
482313f883 add relay hints to Mention::{Profile,Event} and UnknownIds 2025-02-06 10:08:01 -08:00
Ken Sedgwick
f0588a7f6b drive-by compiler warning fixes 2025-02-06 10:08:00 -08:00
Ken Sedgwick
38da6c9eaf explicitly activate and deactivate account muted list subs
This is the same treatment to muted as applied to relay lists in #678
2025-02-06 10:02:06 -08:00
Ken Sedgwick
9ce4c2891e explicitly activate and deactivate account relay list subs
- ensures only one active at a time
- stops leaking relay list subs
2025-02-06 10:02:05 -08:00
Ken Sedgwick
2fcfed4dd5 improve debug logging, comments, and variable names for clarity 2025-02-06 10:02:04 -08:00
Ken Sedgwick
e8c0b903a8 drive-by compiler warning fixes 2025-02-06 10:02:04 -08:00
William Casarin
fd030f5b5c Merge rewrite deck serialization, timeline cache, add algo timelines #712
William Casarin (19):
      algos: introduce last_n_per_pubkey_from_tags
      wip algo timelines
      Initial token parser combinator
      token_parser: unify parsing and serialization
      token_serializer: introduce TokenWriter
      token_parser: simplify AddColumnRoute serialization
      tokens: add a more advanced tokens parser
      tokens: add AccountsRoute token serializer
      tokens: add PubkeySource and ListKinds token serializer
      tokens: add TimelineRoute token serializer
      tokens: initial Route token serializer
      add tokenator crate
      note_id: add hex helpers for root notes
      tokens: add token serialization for AlgoTimeline
      tokens: add token serialization for TimelineKind
      tokens: switch over to using token serialization
      Switch to unified timeline cache via TimelineKinds
      hashtags: click hashtags to open them
2025-02-05 18:46:16 -08:00
William Casarin
ac10c7e5b2 hashtags: click hashtags to open them
Fixes: https://github.com/damus-io/notedeck/issues/695
Fixes: https://github.com/damus-io/notedeck/issues/713
Changelog-Added: Add ability to click hashtags
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-05 18:43:09 -08:00
William Casarin
0cc1d8a600 Switch to unified timeline cache via TimelineKinds
This is a fairly large rewrite which unifies our threads, timelines and
profiles. Now all timelines have a MultiSubscriber, and can be added
and removed to columns just like Threads and Profiles.

Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-05 18:30:45 -08:00
William Casarin
ae85f2dd34 version: bump to 0.3.1
Just a binary fix for ubuntu

Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-04 11:51:16 -08:00
William Casarin
74801098f3 ci: upload artifacts step
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-04 11:18:21 -08:00
William Casarin
d46e526a45 tokens: switch over to using token serialization
This removes all of the old serialization code

Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-04 08:08:08 -08:00
William Casarin
5ba06986db tokens: add token serialization for TimelineKind
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-04 08:08:08 -08:00
William Casarin
d108df86b4 tokens: add token serialization for AlgoTimeline
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-04 08:08:08 -08:00
William Casarin
d85b610cf4 note_id: add hex helpers for root notes
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-04 08:08:08 -08:00
William Casarin
ed455f7ea4 add tokenator crate
also remove a lot of the "advanced" token parsing style
which was a bit too verbose for my tastes

Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-04 08:08:08 -08:00
William Casarin
29491cca05 tokens: initial Route token serializer
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-04 08:08:08 -08:00
William Casarin
4e87ed7065 tokens: add TimelineRoute token serializer
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-04 08:08:08 -08:00
William Casarin
70a39ca69c tokens: add PubkeySource and ListKinds token serializer
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-04 08:08:08 -08:00
William Casarin
6b57401e14 tokens: add AccountsRoute token serializer
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-04 08:08:08 -08:00
William Casarin
00ef3082f3 tokens: add a more advanced tokens parser
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-04 08:08:08 -08:00
William Casarin
efa5b7e32f token_parser: simplify AddColumnRoute serialization
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-04 08:08:08 -08:00
William Casarin
4f89d95aef token_serializer: introduce TokenWriter
This simplifies token serialization

Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-04 08:08:08 -08:00
William Casarin
61b3a92792 token_parser: unify parsing and serialization
This reduces the number of things we have to update in our token parser
and serializer. For payloads, we we have to handle the payload cases
different, but we now have a structure that can deal with that
efficiently.

Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-04 08:08:08 -08:00
William Casarin
005ecd740d Initial token parser combinator
In an attempt to make our deck serializer more localized,
comprehensible, and less error-prone, we introduce a new parser
combinator based around string tokens.

This replaces the Selection-based intermediary types so that we have a
more direct serialization style.
2025-02-04 08:08:08 -08:00
William Casarin
662755550f wip algo timelines
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-04 08:08:08 -08:00
William Casarin
7afe3b7d7c algos: introduce last_n_per_pubkey_from_tags
This function creates filters for the base our first algo in Damus:

Called "last N note per pubkey". I don't have a better name for it.

This function generates a query in the form:

[
  {"authors": ["author_a"], "limit": 1, "kinds": [1]
, {"authors": ["author_b"], "limit": 1, "kinds": [1]
, {"authors": ["author_c"], "limit": 1, "kinds": [1]
, {"authors": ["author_c"], "limit": 1, "kinds": [1]
  ...
]

Due to an unfortunate restriction currently in nostrdb and strfry, we
can only do about 16 to 20 of these at any given time. I have made
this limit configurable in strfry[1]. I just need to do the same in
nostrdb now.

[1] https://github.com/hoytech/strfry/pull/133

Changelog-Added: Add last_n_per_pubkey_from_tags algo function
2025-02-04 08:08:08 -08:00
William Casarin
35dbe812b2 ci: run on older ubuntu
From guythatsits:

> Not sure if this is helpful but the reason it isn't installing in any
of the debian based is the libc6 dependency(requires 2.39, these are
both on 2.35). Looks like popos alpha apparently includes the updated
library. Had the same issue on just a barebone debian system as well.

By default, GitHub Actions uses the latest Ubuntu runner (e.g.,
ubuntu-latest), which has glibc 2.39+. Instead, we can explicitly use
ubuntu-20.04, which has glibc 2.31.

Reported-by: guythatsits
Fixes: https://github.com/damus-io/notedeck/issues/706
Changelog-Fixed: Fixed issue running binary on older debian distros
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-04 06:48:52 -08:00
William Casarin
9dd33d5c5b pfp: 4.0 stroke, add border_stroke method
This reduces code duplication, and makes the border a bit cleaner
so that it blends into the panel color

Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-03 20:24:51 -08:00
William Casarin
b35c7fc0ee theme: refactor dark theme logic to use is_oled
This actually has no behavioral change, but is more
logically correct if we ever end up updating these functions

Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-03 19:53:09 -08:00
William Casarin
96481a47f3 pfp: remove border except for profile
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-03 19:45:32 -08:00
William Casarin
635c9770de Merge 'add border behind pfp' #597
Hello new contributor!

jglad (4):
      #597 add border behind pfp
      replace with full circle border
      make optional
      fix formatting
2025-02-03 19:34:10 -08:00
kernelkind
623b4617d2 move login help text below TextEdit
closes: https://github.com/damus-io/notedeck/issues/687

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-03 15:30:32 -05:00
William Casarin
f8f3676450 clippy fixes
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-03 11:38:55 -08:00
William Casarin
8f4daa5e89 nix: don't shell zenity on macos
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-03 11:37:10 -08:00
kernelkind
2b7d66e7ae add deck icon hover tooltip with deck name
closes: https://github.com/damus-io/notedeck/issues/691

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-02-03 14:32:39 -05:00
Derek Ross
baf1dc0d7e fix: change word verification to identification 2025-01-31 19:52:17 -05:00
Derek Ross
582a43e9f4 fix: updated NIP-05 verification to Nostr address 2025-01-31 19:47:59 -05:00
William Casarin
69b7581a53 Notedeck Alpha 2 Release - v0.3
Thanks again to kernel and ken, as well as some new contributors:
kieran, daniel saxton for our alpha 2 release!

Added
=====

- Clicking a mention now opens profile page (William Casarin)
- Note previews when hovering reply descriptions (William Casarin)
- Media uploads (kernelkind)
- Profile editing (kernelkind)
- Add hashtags to posts (Daniel Saxton)
- Enhanced command-line interface for user interactions (Ken Sedgwick)
- Various Android updates and compatibility improvements (Ken Sedgwick, William Casarin)
- Debug features for user relay-list and mute list synchronization (Ken Sedgwick)

Changed
=======

- Add confirmation when deleting columns (kernelkind)
- Enhance Android build and performance (Ken Sedgwick)
- Image cache handling using sha256 hash (kieran)
- Introduction of decks_cache and improvements (kernelkind)
- Migrated to egui v0.29.1 (William Casarin)
- Only show column delete button when not navigating (William Casarin)
- Show profile pictures in column headers (William Casarin)
- Show usernames in user columns (William Casarin)
- Switch to only notes & replies on some tabs (William Casarin)
- Tombstone muted notes (Ken)
- Pointer interactions enhancements in UI (William Casarin)
- Persistent theme setup across sessions (kernelkind)
- Increased ping intervals for network performance (William Casarin)
- Nostrdb update for async support (Ken Sedgwick)

Fixed
=====

- Fix GIT_COMMIT_HASH compilation issue (William Casarin)
- Fix avatar alignment in profile previews (William Casarin)
- Fix broken quote repost hitbox (William Casarin)
- Fix crash when navigating in debug mode (William Casarin)
- Fix long delays when reconnecting (William Casarin)
- Fix repost button size (William Casarin)
- Fixed since kind filters (kernelkind)
- Clippy warnings resolved (Dimitris Apostolou)

Refactoring & Improvements
==========================

- Numerous internal structural improvements and modularization (William Casarin, Ken Sedgwick)

Signed-off-by: William Casarin <jb55@jb55.com>
2025-01-31 10:31:07 -08:00
jglad
cd72cc36e8 fix formatting 2025-01-31 08:41:25 +01:00
jglad
803f427f77 make optional 2025-01-30 19:14:02 +01:00
jglad
a0f2521bdd replace with full circle border 2025-01-29 18:19:42 +01:00
jglad
a70817743a #597 add border behind pfp 2025-01-28 18:10:21 +01:00
William Casarin
5d4548d3f7 Switch to GameActivity and gradle build
Fixes: https://github.com/damus-io/notedeck/issues/189
Fixes: https://github.com/damus-io/notedeck/issues/190
Signed-off-by: William Casarin <jb55@jb55.com>
2025-01-27 17:31:28 -08:00
William Casarin
478603e16e contacts: disable hashtag follows for now
People are spamming hashtags with AI CP. Let's disable this until we
at least have image blurring.

Alternatively we could only show the link for people you don't follow.

Signed-off-by: William Casarin <jb55@jb55.com>
2025-01-27 12:30:22 -08:00
William Casarin
d0265a5dcb nix: add zenity
needed for file selector
2025-01-25 17:14:27 -08:00
William Casarin
6dd0e5207e Merge image uploading from kernel
kernelkind (8):
      upload media button
      get file binary
      import base64
      notedeck_columns: use sha2 & base64
      use rfd for desktop file selection
      add utils for uploading media
      draft fields for media upload feat
      ui: user can upload images
2025-01-25 16:17:11 -08:00
William Casarin
0c3db9a31e Merge additional account relay list improvements from Ken
Ken Sedgwick (1):
      additional account relay list improvements
2025-01-25 16:16:03 -08:00
William Casarin
8ad9ad20ba Merge clippy fixes from Dimitris
Dimitris Apostolou (1):
      Fix clippy warnings
2025-01-25 16:15:13 -08:00
William Casarin
8a87791594 morenotes: show pointer on hover
all clickable things should show pointers

Cc: kernel
2025-01-25 16:06:32 -08:00
William Casarin
181be70c0f networking: increase ping interval
some relays are really slow to respond on this for some reason
2025-01-25 16:05:56 -08:00
kernelkind
5b0068e6cb add more notes indicator
closes: https://github.com/damus-io/notedeck/issues/72

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-25 17:48:34 -05:00
kernelkind
7abf1c9c15 ui: user can upload images
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-24 15:43:48 -05:00
kernelkind
1091bd0cdf draft fields for media upload feat
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-24 15:43:48 -05:00
kernelkind
b1aaeecdc2 add utils for uploading media
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-24 15:43:48 -05:00
kernelkind
5aa1c982cd use rfd for desktop file selection
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-24 15:43:48 -05:00
kernelkind
b5a782a06e notedeck_columns: use sha2 & base64
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-24 15:43:48 -05:00
kernelkind
2bce115b21 import base64
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-24 15:43:48 -05:00
kernelkind
d96be829cd get file binary
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-24 15:43:48 -05:00
kernelkind
1a0e232176 upload media button
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-24 15:43:48 -05:00
Dimitris Apostolou
2c8f6298b8 Fix clippy warnings
Signed-off-by: Dimitris Apostolou <dimitris.apostolou@icloud.com>
2025-01-24 19:32:24 +02:00
Ken Sedgwick
1e0801f54b additional account relay list improvements
- Use the current selected account only to determine desired
  relays. Previously the desired relay list was determined from the
  union of all accounts.
- Update the relay configuration immediately when the user switches accounts.
- Delete relays from the account (instead of the relay pool
  directly). This results in the relay being removed in the pool as
  well, but is persisted correctly.
2025-01-23 16:07:49 -08:00
William Casarin
2cbae68a7f Merge remote-tracking branches 'github/pr/657' and 'github/pr/658' 2025-01-22 16:04:45 -08:00
Ken Sedgwick
94a1d78114 publish NIP-65 relay lists 2025-01-22 15:16:08 -08:00
Ken Sedgwick
3278d3ba16 upgrade url string to RelaySpec for [read|write] markers
I think RelaySpec wants to move to enostr so the RelayPool can support
read and write relays ...
2025-01-22 12:43:05 -08:00
Ken Sedgwick
fe3e2dad14 add Accounts::add_advertised_relay 2025-01-21 16:03:44 -08:00
Ken Sedgwick
e436be400e add add relay GUI 2025-01-21 12:21:13 -08:00
Ken Sedgwick
366ca24ac1 drive-by clippy fixes 2025-01-21 10:18:37 -08:00
kieran
0249b5ab04 Always update accounts 2025-01-21 10:29:22 +00:00
kieran
01066bfb3d export enostr / nostrdb 2025-01-21 09:43:54 +00:00
William Casarin
835851ee52 Merge remote-tracking branch 'pr/656' 2025-01-20 15:51:03 -08:00
kieran
86e68b1c29 move Notedeck to notedeck crate 2025-01-20 23:25:31 +00:00
kieran
c401f4c484 note-ref: derive hash 2025-01-20 20:33:49 +00:00
William Casarin
43637f52bb Merge a few fixes from kernel #652,#649
kernelkind (2):
      log nip05 error
      fix persist deck author profile bug
2025-01-20 10:44:30 -08:00
greenart7c3
2901ba8227 Fix side panel color when using light theme 2025-01-20 15:11:03 -03:00
William Casarin
4b542c0a74 switch to TimelineCache
Signed-off-by: William Casarin <jb55@jb55.com>
2025-01-19 14:18:59 -08:00
William Casarin
e52ba5937f debug: log when adding notes to start
Signed-off-by: William Casarin <jb55@jb55.com>
2025-01-19 12:58:21 -08:00
William Casarin
a94ebc3603 mutes: hide logs
these are getting spammed on each frame. not ideal

Signed-off-by: William Casarin <jb55@jb55.com>
2025-01-19 12:43:03 -08:00
William Casarin
9a48b12e36 enostr: introduce PubkeyRef
This will be used for typesafe and copy-free pubkey references

Signed-off-by: William Casarin <jb55@jb55.com>
2025-01-19 12:43:03 -08:00
William Casarin
f9a09ea2be note: introduce RootNoteId
RootNoteId is simply a newtype for specifying an ID that is a root note
id (in the sense of a nip10 note)

This makes it more clear at the type level when referring to note ids

Signed-off-by: William Casarin <jb55@jb55.com>
2025-01-19 12:43:03 -08:00
kernelkind
a585704fb6 fix persist deck author profile bug
closes: https://github.com/damus-io/notedeck/issues/651

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-18 16:01:46 -05:00
kernelkind
35427170da log nip05 error
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-17 15:41:01 -05:00
William Casarin
9a4c5e394d envrc: update vrod's npub for testing
Signed-off-by: William Casarin <jb55@jb55.com>
2025-01-17 10:01:59 -08:00
William Casarin
b45a63a529 persistent: dont nuke decks when using cli columns
Sometimes the commandline is nice for loading individual threads you
want to lookup (we don't have a UI for this yet). If you don't currently
choose a different data directory, then your decks cache gets nuked when
doing this.

Change the logic so that deck persistence is disabled when using CLI
columns. This stops notedeck from nuking your default decks.

Signed-off-by: William Casarin <jb55@jb55.com>
2025-01-17 09:32:48 -08:00
William Casarin
e4732f5112 grip: fix double frame border 2025-01-15 14:00:08 -08:00
William Casarin
8fbe954cf3 adjust context menu/grip circle sizes
also adjust grip position so that it is more right
2025-01-15 13:48:53 -08:00
William Casarin
3b68e285fb grip: show pointer cursor on grip 2025-01-15 13:48:33 -08:00
kernelkind
5043f00eb3 update colors
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-15 16:09:29 -05:00
kernelkind
ec7de41cc3 toggle move tooltip on button press
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-15 16:09:29 -05:00
kernelkind
23d65898aa use replace move icon with grab
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-15 16:09:29 -05:00
kernelkind
1914fafc68 integrate column moving
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-15 16:09:29 -05:00
kernelkind
10d45d6cc3 move column
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-15 16:09:29 -05:00
kernelkind
1d6da3ba0d move columns ui
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-15 16:09:29 -05:00
William Casarin
6647e7dc3f Merge Hashtag parsing
Daniel Saxton (5):
      Add t tags for hashtags
      Use HashSet, lowercase, and add emoji tests
      Add test and format
      Fix emoji hashtags
      Handle punctuation better

Link: https://github.com/damus-io/notedeck/pull/592
2025-01-14 10:16:12 -08:00
kernelkind
ba8ac18de7 integrate ZoomHandler
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-13 18:48:29 -05:00
kernelkind
3fafda34b4 introduce ZoomHandler
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-13 18:48:16 -05:00
kernelkind
205d627f99 use TimedSerializer in AppSizeHandler
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-13 18:47:20 -05:00
kernelkind
a24a089e87 extract timing from AppSizeHandler to TimedSerializer
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-13 18:46:16 -05:00
William Casarin
28af9c2272 Merge 'image-cache: use sha256 hash of url for key'
Link: https://github.com/damus-io/notedeck/pull/631
2025-01-10 07:13:50 -08:00
William Casarin
f050de6b7a build: fix missing GIT_COMMIT_HASH in some cases
Some people try to build without git, this adds a fallback so that
compilation still works in those cases.

Changelog-Fixed: Fix GIT_COMMIT_HASH compilation issue
Fixes: https://github.com/damus-io/notedeck/issues/634
Fixes: https://github.com/damus-io/notedeck/issues/633
2025-01-10 06:33:39 -08:00
William Casarin
d97dcb147d fixed reconnect duration
Changelog-Fixes: Fix long delays when reconnecting
2025-01-10 06:08:07 -08:00
kieran
1a744d8e3b image-cache: remove expect 2025-01-10 09:31:04 +00:00
kieran
eaa9b3ae4c image-cache: migrate storage 2025-01-09 20:40:35 +00:00
kieran
06417ff69e image-cache: use sha256 hash of url for key 2025-01-07 12:00:46 +00:00
William Casarin
e08e30f912 mutes: simplify mutefun and don't render tombstone yet 2025-01-04 16:19:41 -08:00
William Casarin
c7d3aae856 nav: slow down nav animation a bit
you care barely see it
2025-01-04 16:01:24 -08:00
William Casarin
4fd8ac377e multicast: remove rejoin debug message
it's spammy
2025-01-04 15:20:01 -08:00
William Casarin
212c296da5 Merge tombstone muted notes #606
Changelog-Changed: Tombstone muted notes
2025-01-04 14:16:16 -08:00
William Casarin
16b20568da Merge relay debug view
Fix a few conflicts
2025-01-04 13:54:29 -08:00
William Casarin
e1187c372f Merge profiling editing #625
Changelog-Added: Added profile editing
2025-01-04 13:18:31 -08:00
William Casarin
5f21d32d96 android: fix android logging 2025-01-04 13:14:05 -08:00
kernelkind
05ab1179e6 remove ProfileState from cache once sent
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-04 14:05:42 -05:00
kernelkind
6645d4880f integrate EditProfileView
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-04 13:42:52 -05:00
William Casarin
fe6206c546 Note multicasting
This is an initial implementation of note multicast, which sends posted
notes to other notedecks on the same network.

This came about after I nerd sniped myself thinking about p2p nostr on
local networks[1]

You can test this exclusively without joining any other relays by
passing -r multicast on the command line.

[1] https://damus.io/note1j50pseqwma38g3aqrsnhvld0m0ysdgppw6fjnvvcj0haeulgswgq80lpca

Signed-off-by: William Casarin <jb55@jb55.com>
2025-01-04 10:37:11 -08:00
William Casarin
f5afdd04a6 chrome: dont parse args twice
Signed-off-by: William Casarin <jb55@jb55.com>
2025-01-04 10:34:46 -08:00
kernelkind
3384d2af14 new profile responses
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-03 18:39:35 -05:00
kernelkind
4ea17f8920 holder for ProfileState
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-03 18:39:35 -05:00
kernelkind
5fd9f32ba7 prelim fns for edit profiles
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-03 18:39:35 -05:00
kernelkind
b1a84788ff move show_profile to its own fn
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-03 18:39:35 -05:00
kernelkind
4baa7b2ef3 use preview
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-03 18:39:35 -05:00
kernelkind
eac24ac982 get bolded font helper
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-03 18:39:35 -05:00
kernelkind
d6f81991ab refactor banner
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-03 18:39:35 -05:00
kernelkind
a1236692e5 profile edit UI
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-03 18:39:35 -05:00
kernelkind
a1520fec7e edit profile button
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-03 18:39:35 -05:00
kernelkind
45d07cc432 profile view improvements
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-03 18:39:35 -05:00
kernelkind
a7cfe9bd37 refactor DisplayName -> NostrName
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-03 18:39:35 -05:00
kernelkind
df82e08041 remove unused code
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-03 18:39:35 -05:00
kernelkind
a99dad7e9a profile body
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-03 18:39:35 -05:00
kernelkind
2dde3034a1 refactor profile
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-03 18:39:35 -05:00
Ken Sedgwick
e193e00539 Mute rendering instead of ingress
Previous approach was to keep muted content from getting inserted.

Instead, this version alters it's display.  This makes toggling mutes
on and off externally much more stable (the display changes but we
don't have to rebuild content trees)

For now muted content is collapsed to a red "Muted" tombstone w/ a reason.
2025-01-02 15:21:14 -08:00
Ken Sedgwick
2d7de8fdc0 Add debug-widget-callstack and debug-interactive-widgets features
- they need to be separate, both on at once is too much

    --features debug-widget-callstack
      Show callstack for the current widget on hover if all modifier keys
      are pressed down

    --features debug-interactive-widgets
      Show an overlay on all interactive widgets

    Notes:
    - debug-widget-callstack asserts `egui:callstack` feature when enabled
    - Only works in debug builds, compile error w/ release builds
2025-01-02 14:14:29 -08:00
kernelkind
efa0bfcca1 integrate RelayDebugView
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-01 15:57:08 -05:00
kernelkind
d88036ecba add relay_debug arg
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-01 15:57:08 -05:00
kernelkind
7d9679e05c add egui window
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-01 15:57:08 -05:00
kernelkind
83411fdef0 RelayDebugView
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-01 15:57:08 -05:00
kernelkind
54dfa0c945 integrate SubsDebug into RelayPool
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-01 15:57:08 -05:00
kernelkind
612c89118f add subs_debug
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-01-01 15:57:08 -05:00
William Casarin
eeab1666e7 query: fix since filter on kind queries
Before kind queries with since filters wasn't working. Now it does.

Changelog-Fixed: Fixed since kind filters
2024-12-30 11:41:30 -08:00
William Casarin
f569f948af nixos: fix wayland
Suggested-by: @duck1123
Fixes: #609
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-24 21:32:03 -08:00
Daniel Saxton
bc7a3c8927 Handle punctuation better 2024-12-24 19:14:46 -06:00
kernelkind
55cc8e4f1d Delete column confirmation
Changelog-Changed: Add confirmation when deleting columns
Closes: https://github.com/damus-io/notedeck/pull/608
Fixes: https://github.com/damus-io/notedeck/issues/549
Signed-off-by: kernelkind <kernelkind@gmail.com>
2024-12-24 16:28:26 -08:00
kernelkind
588bb8c5b2 use hashtag icon in hashtag col header
Signed-off-by: kernelkind <kernelkind@gmail.com>
2024-12-23 16:15:08 -05:00
kernelkind
d7e7c75b89 use hashtag icon
closes https://github.com/damus-io/notedeck/issues/490

Signed-off-by: kernelkind <kernelkind@gmail.com>
2024-12-23 15:37:56 -05:00
William Casarin
6d1d28c84b lockfile: update
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-20 15:40:19 -08:00
William Casarin
fcac49a0a5 previews: run previews as notedeck apps
This allows ./preview to be a notedeck app runner. I am currently
using it for the ProfilePic app (which will because notedeck_viz)

Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-20 15:39:26 -08:00
William Casarin
475314da75 columns: navigate back when switching account
Fixes: https://github.com/damus-io/notedeck/issues/600
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-20 13:54:20 -08:00
William Casarin
40c5dbf418 timeline: only show one tab in hashtag timeline 2024-12-19 09:52:56 -08:00
William Casarin
ca988165cc column: show pointer button on hover 2024-12-19 09:08:52 -08:00
William Casarin
8025be823a ui: customizable tabs per column view
This reduces the number of choices the user needs to make. Some of these
filters were redundant anyways. This also saves memory.

Universe: Notes
Notificaitons: Notes & Replies
Everything else: Notes, Notes & Replies

Changelog-Changed: Simplified tab selections on some columns
Fixes: https://github.com/damus-io/notedeck/issues/517
2024-12-19 08:48:07 -08:00
William Casarin
cb2330abac refactor: move reply_desc into its own file
it's grown up enough now to deserve that at least
2024-12-19 07:58:43 -08:00
William Casarin
5449d6ceb5 note: options: streamline bit macro
Include has method in the bit note options macro
2024-12-19 07:49:56 -08:00
William Casarin
ef8d5b73ee columns: remove dead code 2024-12-19 07:31:40 -08:00
William Casarin
6fa6a5733e timeline: auto-add yourself to your home timeline
This is the most intuitive, and damus iOS does the same thing. You
have to follow yourself, sorry. Otherwise you won't see your posts
when you post which is confusing.

Fixes: https://github.com/damus-io/notedeck/issues/509
2024-12-19 07:21:15 -08:00
Daniel Saxton
7916961bf4 Fix emoji hashtags 2024-12-18 19:07:31 -06:00
Daniel Saxton
659ce458e0 Add test and format 2024-12-18 18:53:39 -06:00
William Casarin
09d6568ef9 ui: make reply description mentions clickable
Small oversight from previous changes
2024-12-18 14:55:11 -08:00
William Casarin
11274ac4df nav: make back nav faster
Changed the egui-nav spring function so its now so slow near the
end of the aniation.

Fixes: https://github.com/damus-io/notedeck/issues/595
2024-12-18 13:04:09 -08:00
William Casarin
f693bb54c1 fix weird crash with missing timeline
My timeline wen't missing and then I started crashing here..

bizarre
2024-12-18 13:03:51 -08:00
Daniel Saxton
f6e0ec7f79 Use HashSet, lowercase, and add emoji tests 2024-12-18 15:02:22 -06:00
kernelkind
785d102e80 show profile preview for external pubkeys
Closes: https://github.com/damus-io/notedeck/pull/589
Signed-off-by: kernelkind <kernelkind@gmail.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-18 11:05:08 -08:00
kernelkind
544a41e695 helper method for FontId
Signed-off-by: kernelkind <kernelkind@gmail.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-18 11:05:03 -08:00
kernelkind
3295124915 prepare AcquireKeyState for add column extern UI
Signed-off-by: kernelkind <kernelkind@gmail.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-18 11:04:53 -08:00
Daniel Saxton
7b21d3895d Add t tags for hashtags 2024-12-18 10:46:43 -06:00
William Casarin
f748b8b34a profile: fix avatar alignment in profile previews
Changelog-Fixed: Fix avatar alignment in profile previews
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-17 14:43:46 -08:00
William Casarin
4967f64bb6 ui: show note previews when hovering reply descriptions
Preview: https://cdn.jb55.com/s/bef26a2caf09e952.png
Demo: https://cdn.jb55.com/s/hover-preview-2.mp4

Changelog-Added: Show note previews when hovering reply descriptions
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-17 14:43:46 -08:00
William Casarin
5a241d730e mentions: open profile page when clicking a user mention
Fixes: https://github.com/damus-io/notedeck/issues/588
Changelog-Added: Clicking a mention now opens profile page
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-17 12:33:10 -08:00
William Casarin
80982059fc ui: fix repost button size
triggering ocd

Changelog-Fixed: Fix repost button size
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-17 12:32:58 -08:00
William Casarin
59dec0c066 ui: show cursor when hovering pfp
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-17 12:21:40 -08:00
William Casarin
ed5b1c4cf4 mention: change ?? to @???
More consistent with the other label

Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-17 12:21:40 -08:00
William Casarin
49fe7ae5c7 ui: add show_pointer
For showing the cursor when hovering over a clickable thing. We need
this in more places.

Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-17 11:52:28 -08:00
William Casarin
482a3cb818 columns: move from Cow<'static, str> to ColumnTitle<'a>
This further deliminates our column titles to those that are simple,
and to those that require additional information from the database.

This allows us to avoid creating many transactions pointlessly if we
don't need to.

Changelog-Changed: Show usernames in user columns
2024-12-17 10:20:59 -08:00
William Casarin
47e0b0ed52 nostrdb: update to fix sub memleak 2024-12-17 09:19:35 -08:00
kernelkind
69a6bf3664 column: add individual column
A column for following a single user

Closes: https://github.com/damus-io/notedeck/pull/583
Signed-off-by: kernelkind <kernelkind@gmail.com>
2024-12-17 09:19:16 -08:00
kernelkind
5b4c7a6371 deps: ignore packages dir
Closes: https://github.com/damus-io/notedeck/pull/583
Signed-off-by: kernelkind <kernelkind@gmail.com>
2024-12-17 09:13:36 -08:00
Ken Sedgwick
0c29c89909 need mutable ndb reference to unsubscribe
Closes: https://github.com/damus-io/notedeck/pull/584
2024-12-17 08:57:06 -08:00
Ken Sedgwick
926a3f80f4 ndb.get_notekey_by_id now returns NoteKey
Closes: https://github.com/damus-io/notedeck/pull/584
2024-12-17 08:57:04 -08:00
Ken Sedgwick
e1a55c6532 update nostrdb-rs for async stream support
Closes: https://github.com/damus-io/notedeck/pull/584
2024-12-17 08:56:48 -08:00
Ken Sedgwick
7da98b3c5c fix android build with cargo update num_enum@0.7.3
Compiling num_enum v0.7.3
error[E0463]: can't find crate for `num_enum_derive`

Closes: https://github.com/damus-io/notedeck/pull/584
2024-12-17 08:56:45 -08:00
Ken Sedgwick
553a88d574 android: use more app top margin for android
A more refined solution would query the android environment for the
system bar height ...

Closes: https://github.com/damus-io/notedeck/pull/585
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-17 08:52:39 -08:00
William Casarin
18226a35ff android: fix build
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-14 01:51:56 -08:00
William Casarin
1e0228e396 Fix notes note updating in profile view
Fixes: https://github.com/damus-io/notedeck/issues/576
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-14 00:06:33 -08:00
William Casarin
e5ab8d5b9c nostrdb: update to fix profile queries
before profile queries were not working at the database level,
because there was no note_pubkey or note_pubkey_kind index. Now there
is! So profiles should be much faster to query now, and will actually
return results.

There still appears to be an issue with the profile NotesHolder which
is preventing it from updating, via the logic in poll_notes_into_view.
The original Timeline version of this function works fine, but it looks
like the NotesHolder one is broken.

Going to work on refactoring the notes holder next to fix.

Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-13 23:35:08 -08:00
William Casarin
ab829b45fc enostr: update ewebsock
This was using an ancient version of rustls, which in turn included
an old version of ring, which was the real reason of the windows
compile issues (i think)

Cc: kernelkind <kernelkind@gmail.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-13 11:01:09 -08:00
William Casarin
516bbb6bc6 deb: add name so package works again
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-13 11:01:09 -08:00
William Casarin
5fda025206 android: change apk name to Notedeck
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-13 10:45:47 -08:00
William Casarin
0302a228f8 rpm: fix rpm build
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-13 10:45:47 -08:00
William Casarin
0cb400efe3 osx: update bundle name
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-13 10:13:57 -08:00
William Casarin
6da23a1374 windows: amd64 installer
This adds windows amd64 support via a dynamic build step that configures
the inno installer for the correct architecture

Changelog-Added: Add amd64 support for Windows build
Closes: https://github.com/damus-io/notedeck/issues/506
2024-12-13 10:07:47 -08:00
William Casarin
c874606af5 tests: add --testrunner flag so that column tests dont fail on startup
We added a startup panic to prevent users from running as debug mode,
our tests are also hitting this. Add a new --testrunner flag which
skips this check. We want this separate from the --debug flag so that
the tests have a more consistent runtime environment.

Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-13 10:01:53 -08:00
William Casarin
08a5ed1076 lockfile: update
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-13 10:01:53 -08:00
kernelkind
13a406b9cd deps: remove reqwest
This was preventing us from building on windows amd

Closes: https://github.com/damus-io/notedeck/pull/567
Signed-off-by: kernelkind <kernelkind@gmail.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-13 10:01:26 -08:00
kernelkind
4bfa26fd8c Revert "move login logic from promise to async fns"
This reverts commit baaa7cc05d.

Closes: https://github.com/damus-io/notedeck/pull/567
2024-12-13 09:37:47 -08:00
William Casarin
c3bbc6b977 android: fix issues due to rearchitecture 2024-12-13 09:36:10 -08:00
Ken Sedgwick
8b80096290 android: misc fixes for android
Closes: https://github.com/damus-io/notedeck/pull/568
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-13 08:19:39 -08:00
William Casarin
ec755493d9 Introducing Damus Notedeck: a nostr browser
This splits notedeck into:

- notedeck
- notedeck_chrome
- notedeck_columns

The `notedeck` crate is the library that `notedeck_chrome` and
`notedeck_columns`, use. It contains common functionality related to
notedeck apps such as the NoteCache, ImageCache, etc.

The `notedeck_chrome` crate is the binary and ui chrome. It is
responsible for managing themes, user accounts, signing, data paths,
nostrdb, image caches etc. It will eventually have its own ui which has
yet to be determined.  For now it just manages the browser data, which
is passed to apps via a new struct called `AppContext`.

`notedeck_columns` is our columns app, with less responsibility now that
more things are handled by `notedeck_chrome`

There is still much work left to do before this is a proper browser:

- process isolation
- sandboxing
- etc

This is the beginning of a new era! We're just getting started.

Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-12 20:08:55 -08:00
William Casarin
aa14fb092d note: add copy note json
very handy
2024-12-11 16:12:25 -08:00
William Casarin
a429ff689c theme: fallback theme should be dark
this default was deeply cursed
2024-12-11 15:20:47 -08:00
kernelkind
9e67f9dc8c theme: persist across app close
Signed-off-by: kernelkind <kernelkind@gmail.com>
2024-12-11 16:56:14 -05:00
kernelkind
2ce845c1fc log: only show notedeck logs
Closes: https://github.com/damus-io/notedeck/pull/563
Signed-off-by: kernelkind <kernelkind@gmail.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-11 12:10:06 -08:00
William Casarin
97006d4d6f test: patch up some broken enostr tests
didn't fix them all though. apparently this test suite was
not running
2024-12-11 11:24:29 -08:00
William Casarin
74c5f0c748 split notedeck into crates
This splits notedeck into crates, separating the browser chrome and
individual apps:

* notedeck: binary file, browser chrome
* notedeck_columns: our columns app
* enostr: same as before

We still need to do more work to cleanly separate the chrome apis
from the app apis. Soon I will create notedeck-notebook to see what
makes sense to be shared between the apps.

Some obvious ones that come to mind:

1. ImageCache

We will likely want to move this to the notedeck crate, as most apps
will want some kind of image cache. In web browsers, web pages do not
need to worry about this, so we will likely have to do something similar

2. Ndb

Since NdbRef is threadsafe and Ndb is an Arc<NdbRef>, it can be safely
copied to each app. This will simplify things. In the future we might
want to create an abstraction over this? Maybe each app shouldn't have
access to the same database... we assume the data in DBs are all public
anyways, but if we have unwrapped giftwraps that could be a problem.

3. RelayPool / Subscription Manager

The browser should probably maintain these. Then apps can use ken's
high level subscription manager api and not have to worry about
connection pool details

4. Accounts

Accounts and key management should be handled by the chrome. Apps should
only have a simple signer interface.

That's all for now, just something to think about!

Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-11 11:24:29 -08:00
William Casarin
10cbdf15f0 remove queries
not used anymore
2024-12-11 11:24:29 -08:00
William Casarin
d36487e28f caution: don't crash on unknown keyword
Fixes: 34f0c3b0ce ("serialization for DecksCache")
2024-12-11 11:24:29 -08:00
kernelkind
06c9023e30 fix edit deck bug
Signed-off-by: kernelkind <kernelkind@gmail.com>
2024-12-11 12:06:09 -05:00
kernelkind
c08b5a6662 ui: use text color for glyphs
Signed-off-by: kernelkind <kernelkind@gmail.com>
2024-12-10 15:10:44 -05:00
kernelkind
e2b8f4e0cc columns.json migration integration
Signed-off-by: kernelkind <kernelkind@gmail.com>
2024-12-10 15:03:01 -05:00
kernelkind
dbddb3a3f2 add columns.json -> DecksCache migration
Signed-off-by: kernelkind <kernelkind@gmail.com>
2024-12-10 15:03:01 -05:00
kernelkind
213332ee71 allow DeckAuthor source for timeline
Signed-off-by: kernelkind <kernelkind@gmail.com>
2024-12-10 15:03:01 -05:00
kernelkind
0138186a00 remove unnecesary serializations
Signed-off-by: kernelkind <kernelkind@gmail.com>
2024-12-10 15:03:01 -05:00
kernelkind
72c44bdf2d use new serialization
Signed-off-by: kernelkind <kernelkind@gmail.com>
2024-12-10 15:03:01 -05:00
kernelkind
34f0c3b0ce serialization for DecksCache
Signed-off-by: kernelkind <kernelkind@gmail.com>
2024-12-10 15:02:57 -05:00
kernelkind
4cd3515a78 add decks UI to side panel
Signed-off-by: kernelkind <kernelkind@gmail.com>
2024-12-10 13:51:46 -05:00
kernelkind
56a8ba30f3 deck actions
Signed-off-by: kernelkind <kernelkind@gmail.com>
2024-12-10 13:51:45 -05:00
kernelkind
845a983592 new column constructor
Signed-off-by: kernelkind <kernelkind@gmail.com>
2024-12-10 13:51:18 -05:00
kernelkind
94598bedf5 introduce decks_cache
Signed-off-by: kernelkind <kernelkind@gmail.com>
2024-12-10 12:59:52 -05:00
kernelkind
deb08a5a9d decks structs: remove unnecesssary unwraps
Signed-off-by: kernelkind <kernelkind@gmail.com>
2024-12-10 12:56:02 -05:00
kernelkind
a24bd4cc43 fix crash on AccountsView
It appears that Context::set_style doesn't keep the style changes from
the Damus constructor to the update method, but
`Context::all_styles_mut` does

Closes: https://github.com/damus-io/notedeck/issues/555
Closes: https://github.com/damus-io/notedeck/pull/560
Signed-off-by: kernelkind <kernelkind@gmail.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-09 15:31:20 -08:00
Ken Sedgwick
35138cd951 android: update to winit 0.30.5 2024-12-09 10:40:04 -08:00
Ken Sedgwick
66dbbf7c03 egui: update deprecated calls to use UiBuilder instead 2024-12-09 10:39:35 -08:00
Ken Sedgwick
e31ce5d879 android: Undo the workspace stuff because android builds don't like 2024-12-09 10:39:00 -08:00
William Casarin
32caaee642 Revert "introduce decks_cache"
This was causing a crash when switching accounts

This reverts commit 69e93b0ebf.
2024-12-09 09:05:05 -08:00
William Casarin
1b31557b03 prevent users from running as debug
We give a friendly message now. If you need to run as debug,
use `cargo run -- --debug` or `./target/debug/notedeck --debug`

We also remove the callstack feature because it doesn't seem
like you need it for widget callstacks.

Fixes: aafddf5acb ("debug: add crate features which enable egui DebugOptions")
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-07 20:56:14 -08:00
William Casarin
720230ca55 fix profiler
I updated puffin to egui v0.29.1 and now it works

Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-07 19:53:32 -08:00
William Casarin
323d1bcd2c Migrate to egui v0.29.1
Not too many breaking changes. I updated egui-nav and egui-tabs as well.

Fixes: https://github.com/damus-io/notedeck/issues/315
Changelog-Fixed: Fixed crash when navigating in debug mode
Changelog-Changed: Migrated to egui v0.29.1
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-07 16:40:43 -08:00
William Casarin
2543978ffe github: unify fmt and clippy step
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-06 10:58:14 -08:00
William Casarin
c309894be8 deck: remove experimental feature
compiler was giving me errors

Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-06 10:57:49 -08:00
William Casarin
a90645d475 simplify prev function
Thanks chatgpt, I thought this was more verbose than it could have been.

Changelog-None:
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-06 10:46:20 -08:00
kernelkind
69e93b0ebf introduce decks_cache
Signed-off-by: kernelkind <kernelkind@gmail.com>
2024-12-06 10:46:20 -08:00
kernelkind
35613f2e74 ConfigureDeck & EditDeck user interfaces
`./preview ConfigureDeckView`
`./preview EditDeckView`

Signed-off-by: kernelkind <kernelkind@gmail.com>
2024-12-05 20:46:14 -05:00
kernelkind
83fe173ba3 appearance fixes
- query for emoji fonts
- add more font sizes

Signed-off-by: kernelkind <kernelkind@gmail.com>
2024-12-05 20:45:29 -05:00
kernelkind
40a96f1df2 decks structs
introduces `DecksCache`, `Decks`, and `Deck`

Signed-off-by: kernelkind <kernelkind@gmail.com>
2024-12-05 20:45:29 -05:00
William Casarin
cea433f69a column: reduce bounciness when navigating
Also right align profile pics when navigating

Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-05 14:02:18 -08:00
William Casarin
d06e790cd2 column: only show delete button when not navigating
This is pretty confusing otherwise

Changelog-Changed: Only show column delete button when not navigating
Fixes: https://github.com/damus-io/notedeck/issues/548
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-05 13:30:24 -08:00
William Casarin
edd71c1a2a column: improve nav style
Show back label, switch back to chevron design

Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-05 13:24:00 -08:00
William Casarin
8444047aa6 column: switch to profile pictures in header
We also switch away from manual layout to centered cross-alignment.

Changelog-Changed: Show profile pictures in column headers
Fixes: https://github.com/damus-io/notedeck/issues/12
Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-05 12:51:02 -08:00
William Casarin
713d9d7bb5 column: use simplified column string titles for now
even though we will replace this soon, it is still technically
more correct than Timeline(1), etc
2024-12-05 10:59:23 -08:00
William Casarin
093cf8c720 column: switch to simplified strings for column headers
This uses less allocations, and once we switch to profile pictures in
the header the old way won't be needed

Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-05 10:59:00 -08:00
William Casarin
be3edc02a4 nav: refactor title rendering for flexibility
Updated navigation to use a custom title renderer for more flexible
rendering of navigation titles. This change decouples the rendering
logic from predefined formats, enabling dynamic title compositions
based on application context and data.

This includes:
- Refactoring `NavResponse` to introduce `NotedeckNavResponse` for
  handling unified navigation response data.
- Adding `NavTitle` in `ui/column/header.rs` to handle rendering
  of navigation titles and profile images dynamically.
- Updating route and timeline logic to support new rendering pipeline.
- Replacing hardcoded title rendering with data-driven approaches.

Benefits:
- Simplifies navigation handling by consolidating title and action
  management.
- Improves scalability for new navigation features without modifying
  core logic.
- Enhances visual customization capabilities.

Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-05 10:58:59 -08:00
William Casarin
cf773a90fd anim: smoothly animate delete button from 0 size
This is a much cleaner animation

Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-05 10:57:21 -08:00
373 changed files with 52460 additions and 16272 deletions

9
.envrc
View File

@@ -1,6 +1,7 @@
# set to false if you don't care to include android stuff
export use_android=true
export android_emulator=false
export ANDROID_DIR=crates/notedeck_chrome/android
use nix --arg use_android $use_android --arg android_emulator $android_emulator
@@ -13,6 +14,8 @@ source scripts/macos_build_secrets.sh || :
export PATH=$PATH:$HOME/.cargo/bin
export JB55=32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245
export JACK=npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m
export VROD=npub1h50pnxqw9jg7dhr906fvy4mze2yzawf895jhnc3p7qmljdugm6gsrurqev
export JEFFG=npub1zuuajd7u3sx8xu92yav9jwxpr839cs0kc3q6t56vd5u9q033xmhsk6c2uc
export OLLAMA_HOST=http://ollama.jb55.com
# simple todo reminders
export TODO_FILE=TODO
2>/dev/null todo.sh ls || :

View File

@@ -22,8 +22,5 @@ jobs:
if: ${{ inputs.additional-setup != '' }}
run: ${{ inputs.additional-setup }}
- name: Rust cache
uses: Swatinem/rust-cache@v2
- name: Run Tests (Native Only)
run: cargo test

View File

@@ -10,31 +10,45 @@ on:
- "*"
jobs:
fmt:
name: Rustfmt
runs-on: ubuntu-latest
lint:
name: Rustfmt + Clippy
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- run: cargo fmt --all -- --check
components: rustfmt,clippy
- run: |
cargo fmt --all -- --check
cargo clippy
clippy:
name: Clippy
runs-on: ubuntu-latest
android:
name: Check (android)
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- run: cargo clippy -- -D warnings
components: rustfmt,clippy
- name: Setup Java JDK
uses: actions/setup-java@v4.5.0
with:
java-version: '17'
distribution: 'temurin'
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Add android rust target
run: rustup target add aarch64-linux-android
- name: Install Cargo NDK
run: cargo install cargo-ndk
- name: Run tests
run: make jni-check
linux-test:
name: Test (Linux)
uses: ./.github/workflows/build-and-test.yml
with:
os: ubuntu-latest
os: ubuntu-22.04
additional-setup: |
sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libssl-dev
@@ -52,7 +66,7 @@ jobs:
packaging:
name: rpm/deb
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
needs: linux-test
if: github.ref_name == 'master' || github.ref_name == 'ci'
@@ -76,9 +90,6 @@ jobs:
fi
cargo install cargo-generate-rpm cargo-deb
- name: Rust cache
uses: Swatinem/rust-cache@v2
- name: Build Cross (${{ matrix.arch }})
if: matrix.arch != runner.arch
run: cargo build --release --target=${{ matrix.arch }}-unknown-linux-gnu
@@ -89,19 +100,19 @@ jobs:
- name: Build RPM (Cross)
if: matrix.arch != runner.arch
run: cargo generate-rpm --target=${{ matrix.arch }}-unknown-linux-gnu
run: cargo generate-rpm -p crates/notedeck_chrome --target=${{ matrix.arch }}-unknown-linux-gnu
- name: Build RPM
if: matrix.arch == runner.arch
run: cargo generate-rpm
run: cargo generate-rpm -p crates/notedeck_chrome
- name: Build deb (Cross)
if: matrix.arch != runner.arch
run: cargo deb --target=${{ matrix.arch }}-unknown-linux-gnu
run: cargo deb -p notedeck_chrome --target=${{ matrix.arch }}-unknown-linux-gnu
- name: Build deb
if: matrix.arch == runner.arch
run: cargo deb
run: cargo deb -p notedeck_chrome
- name: Upload RPM
uses: actions/upload-artifact@v4
@@ -178,10 +189,15 @@ jobs:
path: packages/notedeck-${{ matrix.arch }}.dmg
windows-installer:
name: Build Windows Installer (x86_64)
name: Windows Installer
runs-on: windows-latest
needs: windows-test
if: github.ref_name == 'master' || github.ref_name == 'ci'
strategy:
fail-fast: false
matrix:
arch: [x86_64, aarch64]
steps:
# Checkout the repository
- name: Checkout Code
@@ -203,19 +219,91 @@ jobs:
- name: Install Inno Setup
run: choco install innosetup --no-progress --yes
# Set up Rust toolchain
- name: Install Rust toolchain
run: rustup target add ${{ matrix.arch }}-pc-windows-msvc
# Build
- name: Build
shell: pwsh
run: |
$target = "${{ matrix.arch }}-pc-windows-msvc"
Write-Output "Building for target: $target"
cargo build --release --target=$target
# Generate ISS Script
- name: Generate Inno Setup Script
shell: pwsh
run: |
$arch = "${{ matrix.arch }}"
$issContent = @"
[Setup]
AppName=Damus Notedeck
AppVersion=0.1
DefaultDirName={pf}\Notedeck
DefaultGroupName=Damus Notedeck
OutputDir=..\packages\$arch
OutputBaseFilename=DamusNotedeckInstaller
Compression=lzma
SolidCompression=yes
[Files]
Source: "..\target\$arch-pc-windows-msvc\release\notedeck.exe"; DestDir: "{app}"; Flags: ignoreversion
[Icons]
Name: "{group}\Damus Notedeck"; Filename: "{app}\notedeck.exe"
[Run]
Filename: "{app}\notedeck.exe"; Description: "Launch Damus Notedeck"; Flags: nowait postinstall skipifsilent
"@
Set-Content -Path "scripts/windows-installer-$arch.iss" -Value $issContent
# Build Installer
- name: Run Inno Setup Script
run: |
& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" "scripts\windows-installer.iss"
# List outputs
- name: List Inno Script outputs
run: dir packages
& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" "scripts\windows-installer-${{ matrix.arch }}.iss"
# Move output
- name: Move Inno Script outputs to architecture-specific folder
run: |
New-Item -ItemType Directory -Force -Path packages\${{ matrix.arch }}
Move-Item -Path packages\${{ matrix.arch }}\DamusNotedeckInstaller.exe -Destination packages\${{ matrix.arch }}\DamusNotedeckInstaller.exe
# Upload the installer as an artifact
- name: Upload Installer
uses: actions/upload-artifact@v4
with:
name: DamusNotedeckInstaller.exe
path: packages\DamusNotedeckInstaller.exe
name: DamusNotedeckInstaller-${{ matrix.arch }}.exe
path: packages\${{ matrix.arch }}\DamusNotedeckInstaller.exe
upload-artifacts:
name: Upload Artifacts to Server
runs-on: ubuntu-22.04
needs: [packaging, macos-dmg, windows-installer]
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/ci'
steps:
- name: Download all Artifacts
uses: actions/download-artifact@v4
- name: Setup SSH and Upload
run: |
eval "$(ssh-agent -s)"
mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo "${{ secrets.DEPLOY_SFTP_KEY }}" | tr -d '\r' | ssh-add -
echo "${{ secrets.DEPLOY_IP }} ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEN65pj1cNMqlf96jZLr1i9+mnHIN4jjRPPTDix6sRnt" >> ~/.ssh/known_hosts
ls -la /home/runner/work/notedeck/notedeck/notedeck-x86_64.rpm
export ARTIFACTS=/home/runner/work/notedeck/notedeck
sftp ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_IP }} <<EOF
cd upload/artifacts
put $ARTIFACTS/notedeck-x86_64.rpm/*
put $ARTIFACTS/notedeck-x86_64.deb/*
put $ARTIFACTS/notedeck-x86_64.dmg/*
put $ARTIFACTS/notedeck-aarch64.rpm/*
put $ARTIFACTS/notedeck-aarch64.deb/*
put $ARTIFACTS/notedeck-aarch64.dmg/*
put $ARTIFACTS/DamusNotedeckInstaller-x86_64.exe/*
put $ARTIFACTS/DamusNotedeckInstaller-aarch64.exe/*
bye
EOF

24
.gitignore vendored
View File

@@ -1,17 +1,25 @@
.build-result
.DS_Store
.buildcmd
perf.data
perf.data.old
TODO.bak
android-config.json
logcat.txt
build.log
rusty-tags.vi
crates/notedeck_chrome/android/app/build
.privenv
*.so
*.swp
*.jar
target
.gradle
queries/damus-notifs.json
.git
cache
/dist
/packages
.direnv/
src/camera.rs
scripts/macos_build_secrets.sh
*.patch
*.txt
/tags
*.mdb
.zed
.lsp
.idea
local.properties

1
.rgignore Normal file
View File

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

68
CHANGELOG.md Normal file
View File

@@ -0,0 +1,68 @@
# Notedeck Beta - v0.4 - 2025-05-05
# Added
- Dave nostr ai assistant app
- GIFs!
- Fulltext note search
- Add full screen images, add zoom & pan
- Zaps! NWC/ Wallet ui
- Introduce last note per pubkey feed (experimental)
- Allow multiple media uploads per selection
- Major Android improvements (still wip)
- Added notedeck app sidebar
- User Tagging
- Note truncation
- Local network note broadcast, broadcast notes to other notedeck notes while you're offline
- Mute list support (reading)
- Relay list support
- Ctrl-enter to send notes
- Added relay indexing (relay columns soon)
- Click hashtags to open hashtag timeline
# Fixed
- Fix timelines sometimes not updating (stale feeds)
- Fix ui bounciness when loading profile pictures
- Fix unselectable post replies
# Notedeck Alpha 2 - v0.3 - 2025-01-31
## Added
- Clicking a mention now opens profile page (William Casarin)
- Note previews when hovering reply descriptions (William Casarin)
- Media uploads (kernelkind)
- Profile editing (kernelkind)
- Add hashtags to posts (Daniel Saxton)
- Enhanced command-line interface for user interactions (Ken Sedgwick)
- Various Android updates and compatibility improvements (Ken Sedgwick, William Casarin)
- Debug features for user relay-list and mute list synchronization (Ken Sedgwick)
## Changed
- Add confirmation when deleting columns (kernelkind)
- Enhance Android build and performance (Ken Sedgwick)
- Image cache handling using sha256 hash (kieran)
- Introduction of decks_cache and improvements (kernelkind)
- Migrated to egui v0.29.1 (William Casarin)
- Only show column delete button when not navigating (William Casarin)
- Show profile pictures in column headers (William Casarin)
- Show usernames in user columns (William Casarin)
- Switch to only notes & replies on some tabs (William Casarin)
- Tombstone muted notes (Ken)
- Pointer interactions enhancements in UI (William Casarin)
- Persistent theme setup across sessions (kernelkind)
- Increased ping intervals for network performance (William Casarin)
- Nostrdb update for async support (Ken Sedgwick)
## Fixed
- Fix GIT_COMMIT_HASH compilation issue (William Casarin)
- Fix avatar alignment in profile previews (William Casarin)
- Fix broken quote repost hitbox (William Casarin)
- Fix crash when navigating in debug mode (William Casarin)
- Fix long delays when reconnecting (William Casarin)
- Fix repost button size (William Casarin)
- Fixed since kind filters (kernelkind)
- Clippy warnings resolved (Dimitris Apostolou)
## Refactoring & Improvements
- Numerous internal structural improvements and modularization (William Casarin, Ken Sedgwick)

5036
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,74 +1,92 @@
[package]
name = "notedeck"
version = "0.2.0"
authors = ["William Casarin <jb55@jb55.com>"]
edition = "2021"
default-run = "notedeck"
#rust-version = "1.60"
license = "GPLv3"
description = "A multiplatform nostr client"
[workspace]
resolver = "2"
package.version = "0.5.9"
members = [
"crates/notedeck",
"crates/notedeck_chrome",
"crates/notedeck_columns",
"crates/notedeck_dave",
"crates/notedeck_notebook",
"crates/notedeck_ui",
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["lib", "cdylib"]
"crates/enostr", "crates/tokenator", "crates/notedeck_dave", "crates/notedeck_ui",
]
[workspace.dependencies]
egui = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", features = ["serde"] }
eframe = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "eframe", default-features = false, features = [ "wgpu", "wayland", "x11", "android-native-activity" ] }
egui_extras = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "egui_extras", features = ["all_loaders"] }
nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "71154e4100775f6932ee517da4350c433ba14ec7" }
[dependencies]
#egui-android = { git = "https://github.com/jb55/egui-android.git" }
egui = { workspace = true }
eframe = { workspace = true }
egui_extras = { workspace = true }
ehttp = "0.2.0"
egui_tabs = { git = "https://github.com/damus-io/egui-tabs", branch = "egui-0.28" }
egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "fd0900bdff4be35709372e921f2b49f68b261469" }
egui_virtual_list = { git = "https://github.com/jb55/hello_egui", branch = "egui-0.28", package = "egui_virtual_list" }
reqwest = { version = "0.12.4", default-features = false, features = [ "rustls-tls-native-roots" ] }
image = { version = "0.25", features = ["jpeg", "png", "webp"] }
log = "0.4.17"
poll-promise = { version = "0.3.0", features = ["tokio"] }
serde_derive = "1"
serde = { version = "1", features = ["derive"] } # You only need this if you want app persistence
tracing = "0.1.40"
#wasm-bindgen = "0.2.83"
nostrdb = { workspace = true }
#nostrdb = { path = "/Users/jb55/dev/github/damus-io/nostrdb-rs" }
#nostrdb = "0.3.4"
enostr = { path = "enostr" }
serde_json = "1.0.89"
env_logger = "0.10.0"
puffin_egui = { version = "0.27.0", optional = true }
puffin = { version = "0.19.0", optional = true }
hex = "0.4.3"
opener = "0.8.2"
base32 = "0.4.0"
base64 = "0.22.1"
rmpv = "1.3.0"
bech32 = { version = "0.11", default-features = false }
bitflags = "2.5.0"
dirs = "5.0.1"
eframe = { version = "0.31.1", default-features = false, features = [ "wgpu", "wayland", "x11", "android-game-activity" ] }
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 = "3c67eb6298edbff36d46546897cfac33df4f04db" }
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" }
ehttp = "0.5.0"
enostr = { path = "crates/enostr" }
ewebsock = { version = "0.2.0", features = ["tls"] }
fluent = "0.17.0"
fluent-resmgr = "0.0.8"
fluent-langneg = "0.13"
hex = "0.4.3"
image = { version = "0.25", features = ["jpeg", "png", "webp"] }
indexmap = "2.6.0"
log = "0.4.17"
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 = "2b2e5e43c019b80b98f1db6a03a1b88ca699bfa3" }
#nostrdb = "0.6.1"
notedeck = { path = "crates/notedeck" }
notedeck_chrome = { path = "crates/notedeck_chrome" }
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"
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" }
serde = { version = "1", features = ["derive"] } # You only need this if you want app persistence
serde_derive = "1"
serde_json = "1.0.89"
strum = "0.26"
strum_macros = "0.26"
bitflags = "2.5.0"
uuid = { version = "1.10.0", features = ["v4"] }
indexmap = "2.6.0"
dirs = "5.0.1"
thiserror = "2.0.7"
tokio = { version = "1.16", features = ["macros", "rt-multi-thread", "fs"] }
tracing = { version = "0.1.40", features = ["log"] }
tracing-appender = "0.2.3"
urlencoding = "2.1.3"
open = "5.3.0"
url = "2.5"
[dev-dependencies]
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tempfile = "3.13.0"
unic-langid = { version = "0.9.6", features = ["macros"] }
sys-locale = "0.3"
url = "2.5.2"
urlencoding = "2.1.3"
uuid = { version = "1.10.0", features = ["v4"] }
sha2 = "0.10.8"
bincode = "1.3.3"
mime_guess = "2.0.5"
pretty_assertions = "1.4.1"
jni = "0.21.1"
profiling = "1.0"
lightning-invoice = "0.33.1"
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"
[target.'cfg(target_os = "macos")'.dependencies]
security-framework = "2.11.0"
[features]
default = []
profiling = ["puffin", "puffin_egui", "eframe/puffin"]
debug-widget-callstack = ["egui/callstack"]
debug-interactive-widgets = []
[profile.small]
inherits = 'release'
@@ -78,72 +96,23 @@ codegen-units = 1 # Reduce number of codegen units to increase optimizations
panic = 'abort' # Abort on panic
strip = true # Strip symbols from binary*
# web:
[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.6"
tracing-wasm = "0.2"
wasm-bindgen-futures = "0.4"
# native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "1.16", features = ["macros", "rt-multi-thread", "fs"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.11.1"
android-activity = { version = "0.4", features = [ "native-activity" ] }
#winit = "0.28.6"
winit = { version = "0.29", features = [ "android-native-activity" ] }
#winit = { git="https://github.com/rust-windowing/winit.git", rev = "2a58b785fed2a3746f7c7eebce95bce67ddfd27c", features = ["android-native-activity"] }
[package.metadata.bundle]
identifier = "com.damus.notedeck"
icon = ["assets/app_icon.icns"]
[package.metadata.android]
package = "com.damus.app"
apk_name = "damus"
#assets = "assets"
[[package.metadata.android.uses_feature]]
name = "android.hardware.vulkan.level"
required = true
version = 1
[[package.metadata.android.uses_permission]]
name = "android.permission.WRITE_EXTERNAL_STORAGE"
max_sdk_version = 18
[[package.metadata.android.uses_permission]]
name = "android.permission.READ_EXTERNAL_STORAGE"
max_sdk_version = 18
[package.metadata.android.signing.release]
path = "damus.keystore"
keystore_password = "damuskeystore"
[[package.metadata.android.uses_permission]]
name = "android.permission.INTERNET"
[package.metadata.android.application]
label = "Damus"
[package.metadata.generate-rpm]
assets = [
{ source = "target/release/notedeck", dest = "/usr/bin/notedeck", mode = "755" },
]
[[bin]]
name = "notedeck"
path = "src/bin/notedeck.rs"
[[bin]]
name = "ui_preview"
path = "src/ui_preview/main.rs"
[patch.crates-io]
egui = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb" }
eframe = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "eframe" }
emath = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "emath" }
egui_extras = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "egui_extras" }
#egui = { path = "/home/jb55/dev/github/emilk/egui/crates/egui" }
#eframe = { path = "/home/jb55/dev/github/emilk/egui/crates/eframe" }
#egui-winit = { path = "/home/jb55/dev/github/emilk/egui/crates/egui-winit" }
#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 = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
eframe = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
egui-winit = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
egui-wgpu = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
egui_extras = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
epaint = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
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 = { path = "/home/jb55/dev/github/rust-windowing/winit" }
android-activity = { git = "https://github.com/damus-io/android-activity", rev = "a8948332c7c551303d32eb26a59d0abd676e47a5" }
#android-activity = { path = "/home/jb55/dev/github/rust-mobile/android-activity/android-activity" }

View File

@@ -1,8 +1,30 @@
.DEFAULT_GOAL := check
.PHONY: fake
all:
ANDROID_DIR := crates/notedeck_chrome/android
check:
cargo check
tags: fake
find . -type d -name target -prune -o -type f -name '*.rs' -print | xargs ctags
rusty-tags vi
.PHONY: fake
jni: fake
cargo ndk --target arm64-v8a -o $(ANDROID_DIR)/app/src/main/jniLibs/ build --profile release
jni-check: fake
cargo ndk --target arm64-v8a check
apk: jni
cd $(ANDROID_DIR) && ./gradlew build
gradle:
cd $(ANDROID_DIR) && ./gradlew build
push-android-config:
adb push android-config.json /sdcard/Android/data/com.damus.notedeck/files/android-config.json
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

186
README.md
View File

@@ -1,96 +1,148 @@
# Damus Notedeck
# Notedeck
[![CI](https://github.com/damus-io/notedeck/actions/workflows/rust.yml/badge.svg)](https://github.com/damus-io/notedeck/actions/workflows/rust.yml)
[![CI](https://github.com/damus-io/notedeck/actions/workflows/rust.yml/badge.svg)](https://github.com/damus-io/notedeck/actions/workflows/rust.yml)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/damus-io/notedeck)
A multiplatform nostr client. Works on android and desktop
A modern, multiplatform Nostr client built with Rust. Notedeck provides a feature-rich experience for interacting with the Nostr protocol on both desktop and Android platforms.
The desktop client is called notedeck:
<p align="center">
<img src="https://cdn.jb55.com/s/6130555f03db55b2.png" alt="Notedeck Desktop Screenshot" width="700">
</p>
![notedeck](https://cdn.jb55.com/s/6130555f03db55b2.png)
## ✨ Features
## Android
- **Multi-column Layout**: TweetDeck-style interface for viewing different Nostr content
- **Dave AI Assistant**: AI-powered assistant that can search and analyze Nostr content
- **Profile Management**: View and edit Nostr profiles
- **Media Support**: View and upload images with GIF support
- **Lightning Integration**: Zap (tip) content creators with Bitcoin Lightning
- **Cross-platform**: Works on desktop (Linux, macOS, Windows) and Android
Look it actually runs on android!
## 📱 Mobile Support
<img src="https://cdn.jb55.com/s/bebeeadf7001fae1.png" height="500px" />
Notedeck runs smoothly on Android devices with a responsive interface:
## Usage
<p align="center">
<img src="https://cdn.jb55.com/s/bebeeadf7001fae1.png" alt="Notedeck Android Screenshot" height="500px">
</p>
## 🏗️ Project Structure
```
notedeck
├── crates
│ ├── notedeck - Core library with shared functionality
│ ├── notedeck_chrome - UI container and navigation framework
│ ├── notedeck_columns - TweetDeck-style column interface
│ ├── notedeck_dave - AI assistant for Nostr
│ ├── notedeck_ui - Shared UI components
│ └── tokenator - String token parsing library
```
## 🚀 Getting Started
### Desktop
To run on desktop platforms:
```bash
$ ./target/release/notedeck
# Development build
cargo run -- --debug
# Release build
cargo run --release
```
# Developer Setup
### Android
## Desktop (Linux/MacOS, Windows?)
If you're running debian-based machine like Ubuntu or ElementaryOS, all you need is to install [rustup] and run `sudo apt install build-essential`.
For Android devices:
```bash
$ cargo run --release
# Install required target
rustup target add aarch64-linux-android
# Build and install on connected device
cargo apk run --release -p notedeck_chrome
```
## Android
### Android Emulator
The dev shell should also have all of the android-sdk dependencies needed for development, but you still need the `aarch64-linux-android` rustup target installed:
1. Install [Android Studio](https://developer.android.com/studio)
2. Open 'Device Manager' and create a device with API level `34` and ABI `arm64-v8a`
3. Start the emulator
4. Run: `cargo apk run --release -p notedeck_chrome`
```
$ rustup target add aarch64-linux-android
```
## 🧪 Development
To run on a real device, just type:
### Android Configuration
```bash
$ cargo apk run --release
```
Customize Android views for testing:
## Android Emulator
1. Copy `example-android-config.json` to `android-config.json`
2. Run `make push-android-config` to deploy to your device
- Install [Android Studio](https://developer.android.com/studio)
- Open 'Device Manager' in Android Studio
- Add a new device with API level `34` and ABI `arm64-v8a` (even though the app uses 30, the 30 emulator can't find the vulkan adapter, but 34 works fine)
- Start up the emulator
while the emulator is running, run:
```bash
cargo apk run --release
```
The app should appear on the emulator
[direnv]: https://direnv.net/
## Previews
You can preview individual widgets and views by running the preview script:
```bash
./preview RelayView
./preview ProfilePreview
# ... etc
```
When adding new previews you need to implement the Preview trait for your
view/widget and then add it to the `src/ui_preview/main.rs` bin:
```rust
previews!(runner, name,
RelayView,
AccountLoginView,
ProfilePreview,
);
```
## Contributing
Configure the developer environment:
### Setting Up Developer Environment
```bash
./scripts/dev_setup.sh
```
This will add the pre-commit hook to your local repository to suggest proper formatting before commits.
This adds pre-commit hooks for proper code formatting.
[rustup]: https://rustup.rs/
## 📚 Documentation
Detailed developer documentation is available in each crate:
- [Notedeck Core](./crates/notedeck/DEVELOPER.md)
- [Notedeck Chrome](./crates/notedeck_chrome/DEVELOPER.md)
- [Notedeck Columns](./crates/notedeck_columns/DEVELOPER.md)
- [Dave AI Assistant](./crates/notedeck_dave/docs/README.md)
- [UI Components](./crates/notedeck_ui/docs/components.md)
## 🔄 Release Status
Notedeck is currently in **BETA** status. For the latest changes, see the [CHANGELOG](./CHANGELOG.md).
## Future
Notedeck allows for app development built on top of the performant, built specifically for nostr database [nostrdb][nostrdb]. An example app written on notedeck is [Dave](./crates/notedeck_dave)
Building on notedeck dev documentation is also on the roadmap.
## 🤝 Contributing
### Developers
Contributions are welcome! Please check the developer documentation and follow these guidelines:
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
### Translators
Help us bring Notedeck to non-English speakers!
Request to join the Notedeck translations team through [Crowdin](https://crowdin.com/project/notedeck).
If you do not have a Crowdin account, sign up for one.
If you do not see your language, please request it in Crowdin.
## 🔒 Security
For security issues, please refer to our [Security Policy](./SECURITY.md).
## 📄 License
This project is licensed under the GPL - see license information in individual crates.
## 👥 Authors
- William Casarin <jb55@jb55.com>
- kernelkind <kernelkind@gmail.com>
- And [contributors](https://github.com/damus-io/notedeck/graphs/contributors)
[nostrdb]: https://github.com/damus-io/nostrdb

0
TODO Normal file
View File

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

186
assets/damusbg.svg Normal file
View File

@@ -0,0 +1,186 @@
<?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="svg5"
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
sodipodi:docname="damus-bg.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:blackoutopacity="0.0"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.5946522"
inkscape:cx="407.8014"
inkscape:cy="491.88416"
inkscape:window-width="1296"
inkscape:window-height="916"
inkscape:window-x="222"
inkscape:window-y="38"
inkscape:window-maximized="0"
inkscape:current-layer="svg5"
inkscape:showpageshadow="2"
inkscape:deskcolor="#d1d1d1" />
<defs
id="defs2">
<linearGradient
inkscape:collect="always"
id="linearGradient39361">
<stop
style="stop-color:#0de8ff;stop-opacity:0.78082192;"
offset="0"
id="stop39357" />
<stop
style="stop-color:#d600fc;stop-opacity:0.95433789;"
offset="1"
id="stop39359" />
</linearGradient>
<inkscape:path-effect
effect="bspline"
id="path-effect255"
is_visible="true"
lpeversion="1"
weight="33.333333"
steps="2"
helper_size="0"
apply_no_weight="true"
apply_with_weight="true"
only_selected="false" />
<linearGradient
inkscape:collect="always"
id="linearGradient2119">
<stop
style="stop-color:#1c55ff;stop-opacity:1;"
offset="0"
id="stop2115" />
<stop
style="stop-color:#7f35ab;stop-opacity:1;"
offset="0.5"
id="stop2123" />
<stop
style="stop-color:#ff0bd6;stop-opacity:1;"
offset="1"
id="stop2117" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient2119"
id="linearGradient2121"
x1="10.067794"
y1="248.81357"
x2="246.56145"
y2="7.1864405"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient39361"
id="linearGradient39367"
x1="62.104473"
y1="128.78963"
x2="208.25758"
y2="128.78963"
gradientUnits="userSpaceOnUse" />
</defs>
<g
inkscape:label="Background"
inkscape:groupmode="layer"
id="layer1"
sodipodi:insensitive="true"
style="display:inline">
<rect
style="fill:url(#linearGradient2121);fill-opacity:1;stroke-width:0.264583"
id="rect61"
width="256"
height="256"
x="-5.3875166e-08"
y="-1.0775033e-07"
ry="0"
inkscape:label="Gradient"
sodipodi:insensitive="true" />
</g>
<g
id="g407"
inkscape:label="Logo"
style="display:none">
<g
id="layer2"
inkscape:label="LogoStroke"
style="display:inline">
<path
style="fill:url(#linearGradient39367);fill-opacity:1;stroke:#ffffff;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 101.1429,213.87373 C 67.104473,239.1681 67.104473,42.67112 67.104473,42.67112 135.18122,57.58146 203.25844,72.491904 203.25758,105.24181 c -8.6e-4,32.74991 -68.07625,83.33755 -102.11468,108.63192 z"
id="path253" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Poly">
<path
style="fill:#ffffff;fill-opacity:0.325424;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 67.32839,76.766948 112.00424,99.41949 100.04873,52.226693 Z"
id="path4648" />
<path
style="fill:#ffffff;fill-opacity:0.274576;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 111.45696,98.998695 107.00758,142.60261 70.077729,105.67276 Z"
id="path9299" />
<path
style="fill:#ffffff;fill-opacity:0.379661;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 111.01202,99.221164 29.14343,-37.15232 25.80641,39.377006 z"
id="path9301" />
<path
style="fill:#ffffff;fill-opacity:0.447458;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 111.45696,99.443631 57.17452,55.172309 -2.89209,-53.17009 z"
id="path9368" />
<path
style="fill:#ffffff;fill-opacity:0.20678;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 106.78511,142.38015 62.06884,12.68073 -57.17452,-55.617249 z"
id="path9370" />
<path
style="fill:#ffffff;fill-opacity:0.244068;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 106.78511,142.38015 -28.47603,32.9254 62.51378,7.56395 z"
id="path9372" />
<path
style="fill:#ffffff;fill-opacity:0.216949;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 165.96186,101.44585 195.7727,125.02756 182.64703,78.754017 Z"
id="path9374" />
</g>
<g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="Vertices">
<circle
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path27764"
cx="106.86934"
cy="142.38014"
r="2.0022209" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle28773"
cx="111.54119"
cy="99.221161"
r="2.0022209" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle29091"
cx="165.90784"
cy="101.36163"
r="2.0022209" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

186
assets/damusfg.svg Normal file
View File

@@ -0,0 +1,186 @@
<?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="svg5"
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
sodipodi:docname="damusfg.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:blackoutopacity="0.0"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.5946522"
inkscape:cx="407.8014"
inkscape:cy="491.88416"
inkscape:window-width="1296"
inkscape:window-height="916"
inkscape:window-x="222"
inkscape:window-y="38"
inkscape:window-maximized="0"
inkscape:current-layer="svg5"
inkscape:showpageshadow="2"
inkscape:deskcolor="#d1d1d1" />
<defs
id="defs2">
<linearGradient
inkscape:collect="always"
id="linearGradient39361">
<stop
style="stop-color:#0de8ff;stop-opacity:0.78082192;"
offset="0"
id="stop39357" />
<stop
style="stop-color:#d600fc;stop-opacity:0.95433789;"
offset="1"
id="stop39359" />
</linearGradient>
<inkscape:path-effect
effect="bspline"
id="path-effect255"
is_visible="true"
lpeversion="1"
weight="33.333333"
steps="2"
helper_size="0"
apply_no_weight="true"
apply_with_weight="true"
only_selected="false" />
<linearGradient
inkscape:collect="always"
id="linearGradient2119">
<stop
style="stop-color:#1c55ff;stop-opacity:1;"
offset="0"
id="stop2115" />
<stop
style="stop-color:#7f35ab;stop-opacity:1;"
offset="0.5"
id="stop2123" />
<stop
style="stop-color:#ff0bd6;stop-opacity:1;"
offset="1"
id="stop2117" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient2119"
id="linearGradient2121"
x1="10.067794"
y1="248.81357"
x2="246.56145"
y2="7.1864405"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient39361"
id="linearGradient39367"
x1="62.104473"
y1="128.78963"
x2="208.25758"
y2="128.78963"
gradientUnits="userSpaceOnUse" />
</defs>
<g
inkscape:label="Background"
inkscape:groupmode="layer"
id="layer1"
sodipodi:insensitive="true"
style="display:none">
<rect
style="fill:url(#linearGradient2121);fill-opacity:1;stroke-width:0.264583"
id="rect61"
width="256"
height="256"
x="-5.3875166e-08"
y="-1.0775033e-07"
ry="0"
inkscape:label="Gradient"
sodipodi:insensitive="true" />
</g>
<g
id="g407"
inkscape:label="Logo"
transform="matrix(0.61641471,0,0,0.61641471,51.853453,49.401806)">
<g
id="layer2"
inkscape:label="LogoStroke"
style="display:inline">
<path
style="fill:url(#linearGradient39367);fill-opacity:1;stroke:#ffffff;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 101.1429,213.87373 C 67.104473,239.1681 67.104473,42.67112 67.104473,42.67112 135.18122,57.58146 203.25844,72.491904 203.25758,105.24181 c -8.6e-4,32.74991 -68.07625,83.33755 -102.11468,108.63192 z"
id="path253" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Poly">
<path
style="fill:#ffffff;fill-opacity:0.325424;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 67.32839,76.766948 112.00424,99.41949 100.04873,52.226693 Z"
id="path4648" />
<path
style="fill:#ffffff;fill-opacity:0.274576;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 111.45696,98.998695 107.00758,142.60261 70.077729,105.67276 Z"
id="path9299" />
<path
style="fill:#ffffff;fill-opacity:0.379661;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 111.01202,99.221164 29.14343,-37.15232 25.80641,39.377006 z"
id="path9301" />
<path
style="fill:#ffffff;fill-opacity:0.447458;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 111.45696,99.443631 57.17452,55.172309 -2.89209,-53.17009 z"
id="path9368" />
<path
style="fill:#ffffff;fill-opacity:0.20678;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 106.78511,142.38015 62.06884,12.68073 -57.17452,-55.617249 z"
id="path9370" />
<path
style="fill:#ffffff;fill-opacity:0.244068;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 106.78511,142.38015 -28.47603,32.9254 62.51378,7.56395 z"
id="path9372" />
<path
style="fill:#ffffff;fill-opacity:0.216949;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 165.96186,101.44585 195.7727,125.02756 182.64703,78.754017 Z"
id="path9374" />
</g>
<g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="Vertices">
<circle
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path27764"
cx="106.86934"
cy="142.38014"
r="2.0022209" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle28773"
cx="111.54119"
cy="99.221161"
r="2.0022209" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle29091"
cx="165.90784"
cy="101.36163"
r="2.0022209" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
assets/icons/accounts.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
assets/icons/algo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

12
assets/icons/algo.svg Normal file
View File

@@ -0,0 +1,12 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="16" fill="#2C2C2C"/>
<g clip-path="url(#clip0_3568_3937)">
<path opacity="0.12" d="M15.9998 21.3334C18.9454 21.3334 21.3332 18.9456 21.3332 16.0001C21.3332 13.0546 18.9454 10.6667 15.9998 10.6667C13.0543 10.6667 10.6665 13.0546 10.6665 16.0001C10.6665 18.9456 13.0543 21.3334 15.9998 21.3334Z" fill="white"/>
<path d="M21.3335 15.9999C21.3335 18.9455 18.9457 21.3333 16.0002 21.3333M21.3335 15.9999C21.3335 13.0544 18.9457 10.6666 16.0002 10.6666M21.3335 15.9999H10.6668M16.0002 21.3333C13.0546 21.3333 10.6668 18.9455 10.6668 15.9999M16.0002 21.3333C17.3342 19.8728 18.0927 17.9775 18.1339 15.9999C18.0927 14.0223 17.3342 12.127 16.0002 10.6666M16.0002 21.3333C14.6661 19.8728 13.9084 17.9775 13.8672 15.9999C13.9084 14.0223 14.6661 12.127 16.0002 10.6666M16.0002 10.6666C13.0546 10.6666 10.6668 13.0544 10.6668 15.9999M12.0002 21.3333C12.0002 22.0697 11.4032 22.6666 10.6668 22.6666C9.93045 22.6666 9.3335 22.0697 9.3335 21.3333C9.3335 20.5969 9.93045 19.9999 10.6668 19.9999C11.4032 19.9999 12.0002 20.5969 12.0002 21.3333ZM22.6668 21.3333C22.6668 22.0697 22.0699 22.6666 21.3335 22.6666C20.5971 22.6666 20.0002 22.0697 20.0002 21.3333C20.0002 20.5969 20.5971 19.9999 21.3335 19.9999C22.0699 19.9999 22.6668 20.5969 22.6668 21.3333ZM12.0002 10.6666C12.0002 11.403 11.4032 11.9999 10.6668 11.9999C9.93045 11.9999 9.3335 11.403 9.3335 10.6666C9.3335 9.93021 9.93045 9.33325 10.6668 9.33325C11.4032 9.33325 12.0002 9.93021 12.0002 10.6666ZM22.6668 10.6666C22.6668 11.403 22.0699 11.9999 21.3335 11.9999C20.5971 11.9999 20.0002 11.403 20.0002 10.6666C20.0002 9.93021 20.5971 9.33325 21.3335 9.33325C22.0699 9.33325 22.6668 9.93021 22.6668 10.6666Z" stroke="white" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_3568_3937">
<rect width="16" height="16" fill="white" transform="translate(8 8)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

9
assets/icons/columns.svg Normal file
View File

@@ -0,0 +1,9 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M68 144.8C68 117.917 68 104.476 73.2317 94.2085C77.8336 85.1767 85.1767 77.8336 94.2085 73.2317C104.476 68 117.917 68 144.8 68H367.2C394.083 68 407.524 68 417.792 73.2317C426.823 77.8336 434.166 85.1767 438.768 94.2085C444 104.476 444 117.917 444 144.8V367.2C444 394.083 444 407.524 438.768 417.792C434.166 426.823 426.823 434.166 417.792 438.768C407.524 444 394.083 444 367.2 444H144.8C117.917 444 104.476 444 94.2085 438.768C85.1767 434.166 77.8336 426.823 73.2317 417.792C68 407.524 68 394.083 68 367.2V144.8ZM88 139.2C88 121.278 88 112.317 91.4878 105.472C94.5557 99.4511 99.4511 94.5557 105.472 91.4878C112.317 88 121.278 88 139.2 88H188C199.201 88 204.802 88 209.08 90.1799C212.843 92.0973 215.903 95.1569 217.82 98.9202C220 103.198 220 108.799 220 120V392C220 403.201 220 408.802 217.82 413.08C215.903 416.843 212.843 419.903 209.08 421.82C204.802 424 199.201 424 188 424H139.2C121.278 424 112.317 424 105.472 420.512C99.4511 417.444 94.5557 412.549 91.4878 406.528C88 399.683 88 390.722 88 372.8V139.2ZM242.18 98.9202C240 103.198 240 108.799 240 120V392C240 403.201 240 408.802 242.18 413.08C244.097 416.843 247.157 419.903 250.92 421.82C255.198 424 260.799 424 272 424H295C306.201 424 311.802 424 316.08 421.82C319.843 419.903 322.903 416.843 324.82 413.08C327 408.802 327 403.201 327 392V120C327 108.799 327 103.198 324.82 98.9202C322.903 95.1569 319.843 92.0973 316.08 90.1799C311.802 88 306.201 88 295 88H272C260.799 88 255.198 88 250.92 90.1799C247.157 92.0973 244.097 95.1569 242.18 98.9202Z" fill="url(#paint0_linear_19_1273)"/>
<defs>
<linearGradient id="paint0_linear_19_1273" x1="444" y1="444" x2="-5.21356" y2="206.447" gradientUnits="userSpaceOnUse">
<stop stop-color="#DACAA0"/>
<stop offset="1" stop-color="#8C93D7"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
assets/icons/columns_80.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 719 B

BIN
assets/icons/eye-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
assets/icons/eye-light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -0,0 +1,3 @@
<svg width="14" height="16" viewBox="0 0 14 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.32844 1.4159C8.36511 1.12223 8.20384 0.839518 7.93237 0.721671C7.66091 0.603828 7.34424 0.679064 7.15478 0.906418L1.20178 8.04995C1.0989 8.17335 0.994695 8.29835 0.918828 8.40822C0.847082 8.51208 0.716075 8.71635 0.712028 8.98475C0.707388 9.29208 0.844335 9.58449 1.08338 9.77762C1.2922 9.94635 1.53297 9.97649 1.65871 9.98788C1.79166 9.99995 1.95438 9.99988 2.11504 9.99988H6.24504L5.67204 14.5838C5.63533 14.8775 5.79664 15.1602 6.06811 15.2781C6.33958 15.3959 6.65624 15.3207 6.84571 15.0933L12.7987 7.94975C12.9016 7.82635 13.0058 7.70135 13.0816 7.59149C13.1534 7.48762 13.2844 7.28335 13.2884 7.01495C13.293 6.70762 13.1561 6.41525 12.9171 6.22207C12.7082 6.05333 12.4675 6.02321 12.3418 6.01183C12.2088 5.99979 12.046 5.99982 11.8854 5.99985L7.75544 5.99986L8.32844 1.41588V1.4159Z" fill="#FFB757"/>
</svg>

After

Width:  |  Height:  |  Size: 922 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.12" d="M6 14V8H10V14" fill="white"/>
<path d="M5.99992 14V9.06667C5.99992 8.69327 5.99992 8.5066 6.07259 8.364C6.1365 8.23853 6.23849 8.1366 6.36392 8.07267C6.50654 8 6.69325 8 7.06659 8H8.93325C9.30665 8 9.49332 8 9.63592 8.07267C9.76139 8.1366 9.86332 8.23853 9.92725 8.364C9.99992 8.5066 9.99992 8.69327 9.99992 9.06667V14M1.33325 6.33333L7.35992 1.81333C7.58945 1.64121 7.70419 1.55514 7.83019 1.52196C7.94145 1.49268 8.05838 1.49268 8.16965 1.52197C8.29565 1.55514 8.41038 1.64121 8.63992 1.81333L14.6666 6.33333M2.66659 5.33333V11.8667C2.66659 12.6134 2.66659 12.9868 2.81191 13.272C2.93975 13.5229 3.14372 13.7269 3.3946 13.8547C3.67982 14 4.05318 14 4.79992 14H11.1999C11.9467 14 12.3201 14 12.6053 13.8547C12.8561 13.7269 13.0601 13.5229 13.1879 13.272C13.3333 12.9868 13.3333 12.6134 13.3333 11.8667V5.33333L9.27992 2.29333C8.82092 1.94907 8.59138 1.77695 8.33932 1.71059C8.11685 1.65203 7.88298 1.65203 7.66052 1.71059C7.40845 1.77695 7.17892 1.94907 6.71992 2.29333L2.66659 5.33333Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
assets/icons/key_4x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
assets/icons/links_4x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="#8a8a8a" xmlns="http://www.w3.org/2000/svg" class="icon-xl-heavy"><path d="M15.6729 3.91287C16.8918 2.69392 18.8682 2.69392 20.0871 3.91287C21.3061 5.13182 21.3061 7.10813 20.0871 8.32708L14.1499 14.2643C13.3849 15.0293 12.3925 15.5255 11.3215 15.6785L9.14142 15.9899C8.82983 16.0344 8.51546 15.9297 8.29289 15.7071C8.07033 15.4845 7.96554 15.1701 8.01005 14.8586L8.32149 12.6785C8.47449 11.6075 8.97072 10.615 9.7357 9.85006L15.6729 3.91287ZM18.6729 5.32708C18.235 4.88918 17.525 4.88918 17.0871 5.32708L11.1499 11.2643C10.6909 11.7233 10.3932 12.3187 10.3014 12.9613L10.1785 13.8215L11.0386 13.6986C11.6812 13.6068 12.2767 13.3091 12.7357 12.8501L18.6729 6.91287C19.1108 6.47497 19.1108 5.76499 18.6729 5.32708ZM11 3.99929C11.0004 4.55157 10.5531 4.99963 10.0008 5.00007C9.00227 5.00084 8.29769 5.00827 7.74651 5.06064C7.20685 5.11191 6.88488 5.20117 6.63803 5.32695C6.07354 5.61457 5.6146 6.07351 5.32698 6.63799C5.19279 6.90135 5.10062 7.24904 5.05118 7.8542C5.00078 8.47105 5 9.26336 5 10.4V13.6C5 14.7366 5.00078 15.5289 5.05118 16.1457C5.10062 16.7509 5.19279 17.0986 5.32698 17.3619C5.6146 17.9264 6.07354 18.3854 6.63803 18.673C6.90138 18.8072 7.24907 18.8993 7.85424 18.9488C8.47108 18.9992 9.26339 19 10.4 19H13.6C14.7366 19 15.5289 18.9992 16.1458 18.9488C16.7509 18.8993 17.0986 18.8072 17.362 18.673C17.9265 18.3854 18.3854 17.9264 18.673 17.3619C18.7988 17.1151 18.8881 16.7931 18.9393 16.2535C18.9917 15.7023 18.9991 14.9977 18.9999 13.9992C19.0003 13.4469 19.4484 12.9995 20.0007 13C20.553 13.0004 21.0003 13.4485 20.9999 14.0007C20.9991 14.9789 20.9932 15.7808 20.9304 16.4426C20.8664 17.116 20.7385 17.7136 20.455 18.2699C19.9757 19.2107 19.2108 19.9756 18.27 20.455C17.6777 20.7568 17.0375 20.8826 16.3086 20.9421C15.6008 21 14.7266 21 13.6428 21H10.3572C9.27339 21 8.39925 21 7.69138 20.9421C6.96253 20.8826 6.32234 20.7568 5.73005 20.455C4.78924 19.9756 4.02433 19.2107 3.54497 18.2699C3.24318 17.6776 3.11737 17.0374 3.05782 16.3086C2.99998 15.6007 2.99999 14.7266 3 13.6428V10.3572C2.99999 9.27337 2.99998 8.39922 3.05782 7.69134C3.11737 6.96249 3.24318 6.3223 3.54497 5.73001C4.02433 4.7892 4.78924 4.0243 5.73005 3.54493C6.28633 3.26149 6.88399 3.13358 7.55735 3.06961C8.21919 3.00673 9.02103 3.00083 9.99922 3.00007C10.5515 2.99964 10.9996 3.447 11 3.99929Z" fill="#8a8a8a"></path></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.12" d="M12.0001 5.33337C12.0001 4.27251 11.5787 3.25509 10.8286 2.50495C10.0784 1.7548 9.06095 1.33337 8.00008 1.33337C6.93922 1.33337 5.92182 1.7548 5.17167 2.50495C4.42152 3.25509 4.0001 4.27251 4.0001 5.33337C4.0001 7.39351 3.48041 8.80404 2.89987 9.73697C2.41018 10.524 2.16534 10.9174 2.17431 11.0272C2.18426 11.1488 2.21 11.1951 2.30794 11.2678C2.3964 11.3334 2.79516 11.3334 3.59266 11.3334H12.4075C13.205 11.3334 13.6038 11.3334 13.6922 11.2678C13.7902 11.1951 13.8159 11.1488 13.8259 11.0272C13.8349 10.9174 13.59 10.524 13.1003 9.73697C12.5197 8.80404 12.0001 7.39351 12.0001 5.33337Z" fill="white"/>
<path d="M9.33342 14H6.66675M12.0001 5.33337C12.0001 4.27251 11.5787 3.25509 10.8286 2.50495C10.0784 1.7548 9.06095 1.33337 8.00008 1.33337C6.93922 1.33337 5.92182 1.7548 5.17167 2.50495C4.42152 3.25509 4.0001 4.27251 4.0001 5.33337C4.0001 7.39351 3.48041 8.80404 2.89987 9.73697C2.41018 10.524 2.16534 10.9174 2.17431 11.0272C2.18426 11.1488 2.21 11.1951 2.30794 11.2678C2.3964 11.3334 2.79516 11.3334 3.59266 11.3334H12.4075C13.205 11.3334 13.6038 11.3334 13.6922 11.2678C13.7902 11.1951 13.8159 11.1488 13.8259 11.0272C13.8349 10.9174 13.59 10.524 13.1003 9.73697C12.5197 8.80404 12.0001 7.39351 12.0001 5.33337Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

11
assets/icons/sparkle.svg Normal file
View File

@@ -0,0 +1,11 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_11_352)">
<path opacity="1.0" d="M8.66663 2L9.82276 5.00591C10.0108 5.49473 10.1048 5.73914 10.251 5.94473C10.3805 6.12693 10.5397 6.28613 10.7219 6.41569C10.9275 6.56187 11.1719 6.65587 11.6607 6.84387L14.6666 8L11.6607 9.15613C11.1719 9.34413 10.9275 9.43813 10.7219 9.58433C10.5397 9.71387 10.3805 9.87307 10.251 10.0553C10.1048 10.2609 10.0108 10.5053 9.82276 10.9941L8.66663 14L7.51049 10.9941C7.32249 10.5053 7.22849 10.2609 7.08229 10.0553C6.95276 9.87307 6.79356 9.71387 6.61135 9.58433C6.40577 9.43813 6.16135 9.34413 5.67253 9.15613L2.66663 8L5.67253 6.84387C6.16135 6.65587 6.40577 6.56187 6.61135 6.41569C6.79356 6.28613 6.95276 6.12693 7.08229 5.94473C7.22849 5.73914 7.32249 5.49473 7.51049 5.00591L8.66663 2Z" fill="white"/>
<path d="M3.00004 14.6667V11.3334M3.00004 4.66671V1.33337M1.33337 3.00004H4.66671M1.33337 13H4.66671M8.66671 2.00004L7.51057 5.00595C7.32257 5.49477 7.22857 5.73918 7.08237 5.94477C6.95284 6.12697 6.79364 6.28617 6.61143 6.41573C6.40585 6.56191 6.16143 6.65591 5.67261 6.84391L2.66671 8.00004L5.67261 9.15617C6.16143 9.34417 6.40585 9.43817 6.61143 9.58437C6.79364 9.71391 6.95284 9.87311 7.08237 10.0553C7.22857 10.2609 7.32257 10.5053 7.51057 10.9941L8.66671 14L9.82284 10.9941C10.0108 10.5053 10.1048 10.2609 10.251 10.0553C10.3806 9.87311 10.5398 9.71391 10.722 9.58437C10.9276 9.43817 11.172 9.34417 11.6608 9.15617L14.6667 8.00004L11.6608 6.84391C11.172 6.65591 10.9276 6.56191 10.722 6.41573C10.5398 6.28617 10.3806 6.12697 10.251 5.94477C10.1048 5.73918 10.0108 5.49477 9.82284 5.00595L8.66671 2.00004Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_11_352">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.12" d="M11.8667 14C12.6134 14 12.9868 14 13.272 13.8547C13.5229 13.7269 13.7269 13.5229 13.8547 13.272C14 12.9868 14 12.6134 14 11.8667V7.46671C14 6.71997 14 6.3466 13.8547 6.06139C13.7269 5.81051 13.5229 5.60653 13.272 5.4787C12.9868 5.33337 12.6134 5.33337 11.8667 5.33337H4.13333C3.3866 5.33337 3.01323 5.33337 2.72801 5.4787C2.47713 5.60653 2.27315 5.8105 2.14533 6.06139C2 6.3466 2 6.71997 2 7.46671V11.8667C2 12.6134 2 12.9868 2.14533 13.272C2.27315 13.5229 2.47713 13.7269 2.72801 13.8547C3.01323 14 3.38659 14 4.13333 14H11.8667Z" fill="white"/>
<path d="M10.6667 5.33322V3.00032C10.6667 2.44583 10.6667 2.16858 10.5499 1.9982C10.4478 1.84934 10.2897 1.74821 10.1119 1.71794C9.9082 1.68329 9.65647 1.79947 9.153 2.03183L3.23934 4.76121C2.79034 4.96845 2.56583 5.07207 2.40141 5.23277C2.25604 5.37483 2.14508 5.54825 2.077 5.73977C2 5.95641 2 6.20367 2 6.6982V9.99987M11 9.66653H11.0067M2 7.46653V11.8665C2 12.6133 2 12.9867 2.14533 13.2719C2.27315 13.5227 2.47713 13.7267 2.72801 13.8545C3.01323 13.9999 3.38659 13.9999 4.13333 13.9999H11.8667C12.6134 13.9999 12.9868 13.9999 13.272 13.8545C13.5229 13.7267 13.7269 13.5227 13.8547 13.2719C14 12.9867 14 12.6133 14 11.8665V7.46653C14 6.7198 14 6.34645 13.8547 6.06123C13.7269 5.81035 13.5229 5.60637 13.272 5.47855C12.9868 5.33322 12.6134 5.33322 11.8667 5.33322H4.13333C3.3866 5.33322 3.01323 5.33322 2.72801 5.47854C2.47713 5.60637 2.27315 5.81035 2.14533 6.06123C2 6.34645 2 6.7198 2 7.46653ZM11.3333 9.66653C11.3333 9.85067 11.1841 9.99987 11 9.99987C10.8159 9.99987 10.6667 9.85067 10.6667 9.66653C10.6667 9.48247 10.8159 9.3332 11 9.3332C11.1841 9.3332 11.3333 9.48247 11.3333 9.66653Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
assets/icons/zap_4x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 866 B

19
assets/mkicons Executable file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
MIPMAP="../crates/notedeck_chrome/android/app/src/main/res/mipmap-"
function mkicon() {
local name="$1"
echo "making icon $name"
mkdir -p "${MIPMAP}/{l,m,h,xh,xxh,xxxh}dpi"
inkscape "$name".svg -w 36 -h 36 -o ${MIPMAP}ldpi/"$name".png &
inkscape "$name".svg -w 48 -h 48 -o ${MIPMAP}mdpi/"$name".png &
inkscape "$name".svg -w 72 -h 72 -o ${MIPMAP}hdpi/"$name".png &
inkscape "$name".svg -w 96 -h 96 -o ${MIPMAP}xhdpi/"$name".png &
inkscape "$name".svg -w 144 -h 144 -o ${MIPMAP}xxhdpi/"$name".png &
inkscape "$name".svg -w 192 -h 192 -o ${MIPMAP}xxxhdpi/"$name".png &
wait
}
mkicon "damusfg"
mkicon "damusbg"

View File

@@ -0,0 +1,370 @@
# 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 = Über mich
# Column title for account management
Accounts_f018 = Konten
# Button label to add a relay
Add_269d = Hinzufügen
# Label for add column button
Add_47df = Hinzufügen
# Button label to add a different wallet
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = Eine andere Wallet hinzufügen, die nur für dieses Konto verwendet wird
# Error message for missing wallet
Add_a_wallet_to_continue_d170 = Wallet hinzufügen um fortzufahren
# Button label to add a new account
Add_account_1cfc = Konto hinzufügen
# Column title for adding new account
Add_Account_d06c = Konto hinzufügen
# Column title for adding algorithm column
Add_Algo_Column_0d75 = Algorithmus-Spalte hinzufügen
# Column title for adding new column
Add_Column_c764 = Spalte hinzufügen
# Column title for adding new deck
Add_Deck_fabf = Deck hinzufügen
# Column title for adding external notifications column
Add_External_Notifications_Column_41ae = Externe Benachrichtigungsspalte hinzufügen
# Column title for adding hashtag column
Add_Hashtag_Column_ebf4 = Hashtag-Spalte hinzufügen
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = Letzte Notizen-Spalte hinzufügen
# Column title for adding notifications column
Add_Notifications_Column_79f8 = Benachrichtigungs-Spalte hinzufügen
# Button label to add a relay
Add_relay_269d = Relay hinzufügen
# Button label to add a wallet
Add_Wallet_d1be = Wallet hinzufügen
# Title for algorithmic feeds column
Algo_2452 = Algorithmus
# Description for algorithmic feeds column
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
# Button to send message to Dave AI assistant
Ask_b7f4 = Fragen
# Placeholder text for Dave AI input field
Ask_dave_anything_33d1 = Frage Dave etwas...
# Profile banner URL field label
Banner_52ef = Banner
# Beta version label
BETA_8e5d = BETA
# Broadcast the note to all connected relays
Broadcast_fe43 = Senden
# Broadcast the note only to local network relays
Broadcast_Local_7e50 = Lokal senden
# Button label to cancel an action
Cancel_ed3b = Abbrechen
# Hover text for editable zap amount
Click_to_edit_0414 = Zum Bearbeiten anklicken
# Column title for note composition
Compose_Note_c094 = Notiz erstellen
# Button label to confirm an action
Confirm_f8a6 = Bestätigen
# Status label for connected relay
Connected_f8cc = Verbunden
# Status label for connecting relay
Connecting_6b7e = Verbinde...
# Title for contact list column
Contact_List_f85a = Kontaktliste
# Column title for contact lists
Contacts_7533 = Kontakte
# Column title for last notes per contact
Contacts__last_notes_3f84 = Kontakte (letzte Notizen)
# Button label to copy logs
Copy_a688 = Kopieren
# Button to copy media link to clipboard
Copy_Link_dc7c = Link kopieren
# Copy the unique note identifier to clipboard
Copy_Note_ID_6b45 = Notiz-ID kopieren
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = Notiz-JSON kopieren
# Copy the author's public key to clipboard
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 }Tg.
# Relative time in hours
count_h_3ecb = { $count }Std.
# Relative time in minutes
count_m_b41e = { $count }Min.
# Relative time in months
count_mo_7aba = { $count }Mon.
# Relative time in seconds
count_s_aa26 = { $count }Sek.
# Relative time in weeks
count_w_7468 = { $count }Wo.
# Relative time in years
count_y_9408 = { $count }J.
# Button to create a new account
Create_Account_6994 = Konto erstellen
# Button label to create a new deck
Create_Deck_16b7 = Deck erstellen
# Column title for custom timelines
Custom_a69e = Benutzerdefiniert
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = Zap-Betrag anpassen
# Column title for support page
Damus_Support_27c0 = Damus Support
# Label for deck name input field
Deck_name_cd32 = Deck-Name
# Label for decks section in side panel
DECKS_1fad = DECKS
# Label for default zap amount input
Default_amount_per_zap_399d = Standardbetrag pro Zap:
# Name of the default deck feed
Default_Deck_fcca = Standard-Deck
# Button label to delete a deck
Delete_Deck_bb29 = Deck löschen
# Tooltip for deleting a column
Delete_this_column_8d5a = Diese Spalte löschen
# Button label to delete a wallet
Delete_Wallet_d1d4 = Wallet löschen
# Profile display name field label
Display_name_f9d9 = Anzeigename
# Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" wird zur Identifikation verwendet
# Column title for editing deck
Edit_Deck_4018 = Deck bearbeiten
# Button label to edit a deck
Edit_Deck_fd93 = Deck bearbeiten
# Button label to edit user profile
Edit_Profile_49e6 = Profil bearbeiten
# Column title for profile editing
Edit_Profile_8ad4 = Profil bearbeiten
# Placeholder for hashtag input field
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = Gewünschte Hashtags hier eingeben (für mehrere, durch Leerzeichen trennen)
# Placeholder for relay input field
Enter_the_relay_here_1c8b = Relay hier eingeben
# Hint text to prompt entering the user's public key.
Enter_the_user_s_key__npub__hex__nip05__here_650c = Hier den Benutzerschlüssel (npub, hex, nip05) eingeben...
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
Enter_your_key_0fca = Gib deinen Schlüssel ein
# 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 =
Gib deinen öffentlichen Schlüssel (npub), eine Nostr-Adresse (z.B. {$address}) oder deinen privaten Schlüssel (nsec) ein.
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
# Title for hashtags column
Hashtags_f8e0 = Hashtags
# Title for Home column
Home_8c19 = Startseite
# Label for deck icon selection
Icon_b0ab = Symbol
# Title for individual user column
Individual_b776 = Individuell
# Error message for invalid zap amount
Invalid_amount_6630 = Ungültiger Betrag
# Error message for invalid key input
Invalid_key_4726 = Ungültiger Schlüssel
# Error message for invalid Nostr Wallet Connect URI
Invalid_NWC_URI_031b = Ungültige 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 = Behalte den Überblick über deine Notizen & Antworten
# Title for last note per user column
Last_Note_per_User_17ad = Letzte Notiz pro Profil
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = Lightning-Netzwerkadresse (lud16)
# Login page title
Login_9eef = Anmelden
# Login button text
Login_now___let_s_do_this_5630 = Jetzt anmelden — auf geht's!
# Text shown on blurred media from unfollowed users
Media_from_someone_you_don_t_follow_5611 = Medien von einem Profil, dem du nicht folgst
# Tooltip for moving a column
Moves_this_column_to_another_position_0d4b = Verschiebt diese Spalte an eine andere Position
# Title for the user's deck
My_Deck_4ac5 = Mein Deck
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = Neu bei Nostr?
# NIP-05 identity field label
Nostr_address__NIP-05_identity_74a2 = Nostr-Adresse (NIP-05-Identität)
# Default username when profile is not available
nostrich_df29 = Nostrich
# Status label for disconnected relay
Not_Connected_6292 = Nicht verbunden
# Link text for note references
note_cad6 = Notiz
# Beta product warning message
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck ist ein Beta-Produkt. Erwarte Fehler und kontaktiere uns, wenn Probleme oder Fehler auftreten.
# Filter label for notes only view
Notes_03fb = Notizen
# Label for notes-only filter
Notes_60d2 = Notizen
# Filter label for notes and replies view
Notes___Replies_1ec2 = Notizen & Antworten
# Label for notes and replies filter
Notes___Replies_6e3b = Notizen & Antworten
# Column title for notifications
Notifications_d673 = Benachrichtigungen
# Title for notifications column
Notifications_ef56 = Benachrichtigungen
# Relative time for very recent events (less than 3 seconds)
now_2181 = Jetzt
# 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
# 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
Please_create_a_name_for_the_deck_38e7 = Bitte erstelle einen Namen für das Deck.
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Bitte erstelle einen Namen für das Deck und wähle ein Symbol aus.
# Error message for missing deck icon
Please_select_an_icon_655b = Bitte wählen ein Symbol aus.
# Button label to post a note
Post_now_8a49 = Jetzt veröffentlichen
# 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 = Drücke die Schaltfläche unten, um deine neuesten Protokolle in die Zwischenablage deines Systems zu kopieren. Dann füge sie in deine E-Mail ein.
# Profile picture URL field label
Profile_picture_81ff = Profilbild
# Column title for quote composition
Quote_475c = Zitat
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = Zitat von unbekannter Notiz
# Label for read-only profile mode
Read_only_82ff = Nur Lesezugriff
# Column title for relay management
Relays_9d89 = Relays
# Label for relay list section
Relays_ad5e = Relays
# Column title for reply composition
Reply_3bf1 = Antwort
# Hover text for reply button
Reply_to_this_note_f5de = Auf diese Notiz antworten
# Error message when reply note cannot be found
Reply_to_unknown_note_4401 = Antwort auf unbekannte Notiz
# Fallback template for replying to user
replying_to__user_15ab = Antwort an { $user }
# Template for replying to user in unknown thread
replying_to__user__in_someone_s_thread_e148 = Antwort an { $user } im Beitrag von jemandem
# Template for replying to note in different user's thread
replying_to__user__s__note__in__thread_user__s__thread_daa8 = Antwort auf { $user }'s { $note } in { $thread_user }'s { $thread }
# Template for replying to user's note
replying_to__user__s__note_ccba = Antwort auf { $user }'s { $note }
# Template for replying to root thread
replying_to__user__s__thread_444d = Antwort auf { $user }'s { $thread }
# Fallback text when reply note is not found
replying_to_a_note_e0bc = Antwort auf eine Notiz
# Hover text for repost button
Repost_this_note_8e56 = Diese Notiz teilen
# Label for reposted notes
Reposted_61c8 = Teilen
# Heading for support section
Running_into_a_bug_1796 = Ein Fehler aufgetreten?
# 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 = Speichern
# Button label to save profile changes
Save_changes_00db = Änderungen speichern
# Column title for search page
Search_c573 = Suche
# Placeholder for search notes input field
Search_notes_42a6 = Notizen suchen...
# Search in progress message
Searching_for___query_5d18 = Suche nach '{ $query }'
# Description for Home column
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 label to send a zap
Send_1ea4 = Senden
# 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
Sign_out_337b = Abmelden
# Title for someone else's notes column
Someone_else_s_Notes_7e5f = Notizen anderer Profile
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Mitteilungen anderer Profile
# 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
Stay_up_to_date_with_a_certain_hashtag_88e3 = Mit einem bestimmten Hashtag auf dem Laufenden bleiben
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = Bleibe auf dem Laufenden mit Benachrichtigungen und Erwähnungen
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = Bleib auf dem Laufenden bei den Notizen & Antworten anderer
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = Bleib bei den Benachrichtigungen und Erwähnungen anderer auf dem Laufenden
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = Bleib bei den Notizen & Antworten eines anderen auf dem Laufenden
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = Bleib bei deinen Benachrichtigungen und Erwähnungen auf dem Laufenden
# Step 1 label in support instructions
Step_1_8656 = Schritt 1
# Step 2 label in support instructions
Step_2_d08d = Schritt 2
# 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
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Zum Dunkelmodus wechseln
# Hover text for light mode toggle button
Switch_to_light_mode_72ce = Zum Hellmodus wechseln
# Button text to load blurred media
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!
# Column title for note thread view
Thread_0f20 = Unterhaltung
# Link text for thread references
thread_ad1f = Unterhaltung
# Title for universe column
Universe_e01e = Weltraum
# Column title for universe feed
Universe_ffaa = Weltraum
# Checkbox label for using wallet only for current account
Use_this_wallet_for_the_current_account_only_61dc = Diese Wallet nur für das aktuelle Konto verwenden
# Username and domain identification message
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
# Column title for wallet management
Wallet_5e50 = Wallet
# Hint for deck name input field
We_recommend_short_names_083e = Wir empfehlen kurze Namen
# Profile website field label
Website_7980 = Website
# Placeholder for note input field
Write_a_banger_note_here_bad2 = Schreib hier eine richtig coole Notiz...
# Placeholder text for key input field
Your_key_here_81bd = Dein Schlüssel hier...
# Title for your notes column
Your_Notes_f6db = Deine Notizen
# Title for your notifications column
Your_Notifications_080d = Deine Benachrichtigungen
# Heading for zap (tip) action
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = Zappe diese Notiz
# Pluralized strings
# Search results count
Got__count__results_for___query_85fb =
{ $count ->
[one] { $count } Ergebnis für '{ $query } gefunden'
*[other] { $count } Ergebnisse für '{ $query } gefunden'
}

View File

@@ -0,0 +1,602 @@
# 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 = About
# Column title for account management
Accounts_f018 = Accounts
# Button label to add a relay
Add_269d = Add
# Label for add column button
Add_47df = Add
# Button label to add a different wallet
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = Add a different wallet that will only be used for this account
# Error message for missing wallet
Add_a_wallet_to_continue_d170 = Add a wallet to continue
# Button label to add a new account
Add_account_1cfc = Add account
# Column title for adding new account
Add_Account_d06c = Add Account
# Column title for adding algorithm column
Add_Algo_Column_0d75 = Add Algo Column
# Column title for adding new column
Add_Column_c764 = Add Column
# Column title for adding new deck
Add_Deck_fabf = Add Deck
# Column title for adding external notifications column
Add_External_Notifications_Column_41ae = Add External Notifications Column
# Column title for adding hashtag column
Add_Hashtag_Column_ebf4 = Add Hashtag Column
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = Add Last Notes Column
# Column title for adding notifications column
Add_Notifications_Column_79f8 = Add Notifications Column
# Button label to add a relay
Add_relay_269d = Add relay
# Button label to add a wallet
Add_Wallet_d1be = Add Wallet
# Title for algorithmic feeds column
Algo_2452 = Algo
# Description for algorithmic feeds column
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Algorithmic feeds to aid in note discovery
# 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
# Placeholder text for Dave AI input field
Ask_dave_anything_33d1 = Ask dave anything...
# Profile banner URL field label
Banner_52ef = Banner
# Beta version label
BETA_8e5d = BETA
# Option in settings section to show the source client label at the bottom of the note
Bottom_33c8 = Bottom
# Broadcast the note to all connected relays
Broadcast_fe43 = Broadcast
# Broadcast the note only to local network relays
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
# Status label for connected relay
Connected_f8cc = Connected
# Status label for connecting relay
Connecting_6b7e = Connecting...
# Title for contact list column
Contact_List_f85a = Contact List
# Column title for contact lists
Contacts_7533 = Contacts
# Column title for last notes per contact
Contacts__last_notes_3f84 = Contacts (last notes)
# Button label to copy logs
Copy_a688 = Copy
# Button to copy media link to clipboard
Copy_Link_dc7c = Copy Link
# Copy the unique note identifier to clipboard
Copy_Note_ID_6b45 = Copy Note ID
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = Copy Note JSON
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = Copy Pubkey
# Copy the text content of the note to clipboard
Copy_Text_f81c = Copy Text
# 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}mo
# Relative time in seconds
count_s_aa26 = {$count}s
# Relative time in weeks
count_w_7468 = {$count}w
# Relative time in years
count_y_9408 = {$count}y
# Button to create a new account
Create_Account_6994 = Create Account
# Button label to create a new deck
Create_Deck_16b7 = Create Deck
# Column title for custom timelines
Custom_a69e = Custom
# Column title for zap amount customization
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
# Label for decks section in side panel
DECKS_1fad = DECKS
# Label for default zap amount input
Default_amount_per_zap_399d = Default amount per zap:
# Name of the default deck feed
Default_Deck_fcca = Default Deck
# Button label to delete a deck
Delete_Deck_bb29 = Delete Deck
# Tooltip for deleting a column
Delete_this_column_8d5a = Delete this column
# Button label to delete a wallet
Delete_Wallet_d1d4 = Delete Wallet
# Profile display name field label
Display_name_f9d9 = Display name
# Domain identification message
domain___will_be_used_for_identification_b67e = "{$domain}" will be used for identification
# Column title for editing deck
Edit_Deck_4018 = Edit Deck
# Button label to edit a deck
Edit_Deck_fd93 = Edit Deck
# Button label to edit user profile
Edit_Profile_49e6 = Edit Profile
# Column title for profile editing
Edit_Profile_8ad4 = Edit Profile
# Placeholder for hashtag input field
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = Enter the desired hashtags here (for multiple space-separated)
# Placeholder for relay input field
Enter_the_relay_here_1c8b = Enter the relay here
# Hint text to prompt entering the user's public key.
Enter_the_user_s_key__npub__hex__nip05__here_650c = Enter the user's key (npub, hex, nip05) here...
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
Enter_your_key_0fca = Enter your key
# 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 = 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.
# Label for find user button
Find_User_bd12 = Find User
# Title for hashtags column
Hashtags_f8e0 = Hashtags
# Option in settings section to hide the source client label in note display
Hide_281d = Hide
# Title for Home column
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
# Error message for invalid zap amount
Invalid_amount_6630 = Invalid amount
# Error message for invalid key input
Invalid_key_4726 = Invalid key.
# Error message for invalid Nostr Wallet Connect URI
Invalid_NWC_URI_031b = Invalid 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 = Keep track of your notes & replies
# 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)
# Login page title
Login_9eef = Login
# Login button text
Login_now___let_s_do_this_5630 = Login now — let's do this!
# Text shown on blurred media from unfollowed users
Media_from_someone_you_don_t_follow_5611 = Media from someone you don't follow
# Tooltip for moving a column
Moves_this_column_to_another_position_0d4b = Moves this column to another position
# Title for the user's deck
My_Deck_4ac5 = My Deck
# 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?
# NIP-05 identity field label
Nostr_address__NIP-05_identity_74a2 = Nostr address (NIP-05 identity)
# Default username when profile is not available
nostrich_df29 = nostrich
# Status label for disconnected relay
Not_Connected_6292 = Not Connected
# Link text for note references
note_cad6 = note
# Beta product warning message
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck is a beta product. Expect bugs and contact us when you run into issues.
# Filter label for notes only view
Notes_03fb = Notes
# Label for notes-only filter
Notes_60d2 = Notes
# Filter label for notes and replies view
Notes___Replies_1ec2 = Notes & Replies
# Label for notes and replies filter
Notes___Replies_6e3b = Notes & Replies
# Column title for notifications
Notifications_d673 = Notifications
# Title for notifications column
Notifications_ef56 = Notifications
# Relative time for very recent events (less than 3 seconds)
now_2181 = now
# 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...
# Error message for missing deck name
Please_create_a_name_for_the_deck_38e7 = Please create a name for the deck.
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Please create a name for the deck and select an icon.
# Error message for missing deck icon
Please_select_an_icon_655b = Please select an icon.
# Button label to post a note
Post_now_8a49 = Post now
# 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 = Press the button below to copy your most recent logs to your system's clipboard. Then paste it into your email.
# Profile picture URL field label
Profile_picture_81ff = Profile picture
# Column title for quote composition
Quote_475c = Quote
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = Quote of unknown note
# Label for read-only profile mode
Read_only_82ff = Read only
# Column title for relay management
Relays_9d89 = Relays
# Label for relay list section
Relays_ad5e = Relays
# Column title for reply composition
Reply_3bf1 = Reply
# Hover text for reply button
Reply_to_this_note_f5de = Reply to this note
# Error message when reply note cannot be found
Reply_to_unknown_note_4401 = Reply to unknown note
# Fallback template for replying to user
replying_to__user_15ab = replying to {$user}
# Template for replying to user in unknown thread
replying_to__user__in_someone_s_thread_e148 = replying to {$user} in someone's thread
# Template for replying to note in different user's thread
replying_to__user__s__note__in__thread_user__s__thread_daa8 = replying to {$user}'s {$note} in {$thread_user}'s {$thread}
# Template for replying to user's note
replying_to__user__s__note_ccba = replying to {$user}'s {$note}
# Template for replying to root thread
replying_to__user__s__thread_444d = replying to {$user}'s {$thread}
# Fallback text when reply note is not found
replying_to_a_note_e0bc = replying to a note
# Hover text for repost button
Repost_this_note_8e56 = Repost this note
# Label for reposted notes
Reposted_61c8 = Reposted
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Reset
# Heading for support section
Running_into_a_bug_1796 = Running into a 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 = Save
# Button label to save profile changes
Save_changes_00db = Save changes
# Column title for search page
Search_c573 = Search
# Placeholder for search notes input field
Search_notes_42a6 = Search notes...
# Search in progress message
Searching_for___query_5d18 = Searching for '{$query}'
# 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 label to send a zap
Send_1ea4 = Send
# Column title for app settings
Settings_7a4f = Settings
# Label for Show source client, others settings section
Show_source_client_9e31 = Show source client
# 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
# Button label to sign out of account
Sign_out_337b = Sign out
# Title for someone else's notes column
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
# 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
# Description for hashtags column
Stay_up_to_date_with_a_certain_hashtag_88e3 = Stay up to date with a certain hashtag
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = Stay up to date with notifications and mentions
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = Stay up to date with someone else's notes & replies
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = Stay up to date with someone else's notifications and mentions
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = Stay up to date with someone's notes & replies
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = Stay up to date with your notifications and mentions
# Step 1 label in support instructions
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
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Switch to dark mode
# Hover text for light mode toggle button
Switch_to_light_mode_72ce = Switch to light mode
# Button text to load blurred media
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
# Link text for thread references
thread_ad1f = thread
# Option in settings section to show the source client label at the top of the note
Top_6aeb = Top
# Title for universe column
Universe_e01e = Universe
# Column title for universe feed
Universe_ffaa = Universe
# Checkbox label for using wallet only for current account
Use_this_wallet_for_the_current_account_only_61dc = Use this wallet for the current account only
# Username and domain identification message
username___at___domain___will_be_used_for_identification_a4fd = "{$username}" at "{$domain}" will be used for identification
# 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
# Hint for deck name input field
We_recommend_short_names_083e = We recommend short names
# Profile website field label
Website_7980 = Website
# Placeholder for note input field
Write_a_banger_note_here_bad2 = Write a banger note here...
# Placeholder text for key input field
Your_key_here_81bd = Your key here...
# Title for your notes column
Your_Notes_f6db = Your Notes
# Title for your notifications column
Your_Notifications_080d = Your Notifications
# Heading for zap (tip) action
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
Got__count__results_for___query_85fb =
{ $count ->
[one] Got {$count} result for '{$query}'
*[other] Got {$count} results for '{$query}'
}

View File

@@ -0,0 +1,602 @@
# 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 = {"["}Àbóút{"]"}
# Column title for account management
Accounts_f018 = {"["}Àççóúñts{"]"}
# Button label to add a relay
Add_269d = {"["}Àdd{"]"}
# Label for add column button
Add_47df = {"["}Àdd{"]"}
# Button label to add a different wallet
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = {"["}Àdd à dífféréñt wàllét thàt wíll óñly bé úséd fór thís àççóúñt{"]"}
# Error message for missing wallet
Add_a_wallet_to_continue_d170 = {"["}Àdd à wàllét tó çóñtíñúé{"]"}
# Button label to add a new account
Add_account_1cfc = {"["}Àdd àççóúñt{"]"}
# Column title for adding new account
Add_Account_d06c = {"["}Àdd Àççóúñt{"]"}
# Column title for adding algorithm column
Add_Algo_Column_0d75 = {"["}Àdd Àlgó Çólúmñ{"]"}
# Column title for adding new column
Add_Column_c764 = {"["}Àdd Çólúmñ{"]"}
# Column title for adding new deck
Add_Deck_fabf = {"["}Àdd Déçk{"]"}
# Column title for adding external notifications column
Add_External_Notifications_Column_41ae = {"["}Àdd Éxtérñàl Ñótífíçàtíóñs Çólúmñ{"]"}
# Column title for adding hashtag column
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ñ{"]"}
# Column title for adding notifications column
Add_Notifications_Column_79f8 = {"["}Àdd Ñótífíçàtíóñs Çólúmñ{"]"}
# Button label to add a relay
Add_relay_269d = {"["}Àdd rélày{"]"}
# Button label to add a wallet
Add_Wallet_d1be = {"["}Àdd Wàllét{"]"}
# Title for algorithmic feeds column
Algo_2452 = {"["}Àlgó{"]"}
# Description for algorithmic feeds column
Algorithmic_feeds_to_aid_in_note_discovery_d344 = {"["}Àlgóríthmíç fééds tó àíd íñ ñóté dísçóvéry{"]"}
# 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{"]"}
# Placeholder text for Dave AI input field
Ask_dave_anything_33d1 = {"["}Àsk dàvé àñythíñg...{"]"}
# Profile banner URL field label
Banner_52ef = {"["}Bàññér{"]"}
# Beta version label
BETA_8e5d = {"["}BÉTÀ{"]"}
# Option in settings section to show the source client label at the bottom of the note
Bottom_33c8 = {"["}Bóttóm{"]"}
# Broadcast the note to all connected relays
Broadcast_fe43 = {"["}Bróàdçàst{"]"}
# Broadcast the note only to local network relays
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{"]"}
# Status label for connected relay
Connected_f8cc = {"["}Çóññéçtéd{"]"}
# Status label for connecting relay
Connecting_6b7e = {"["}Çóññéçtíñg...{"]"}
# Title for contact list column
Contact_List_f85a = {"["}Çóñtàçt Líst{"]"}
# Column title for contact lists
Contacts_7533 = {"["}Çóñtàçts{"]"}
# Column title for last notes per contact
Contacts__last_notes_3f84 = {"["}Çóñtàçts (làst ñótés){"]"}
# Button label to copy logs
Copy_a688 = {"["}Çópy{"]"}
# Button to copy media link to clipboard
Copy_Link_dc7c = {"["}Çópy Líñk{"]"}
# Copy the unique note identifier to clipboard
Copy_Note_ID_6b45 = {"["}Çópy Ñóté ÍD{"]"}
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = {"["}Çópy Ñóté JSÓÑ{"]"}
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = {"["}Çópy Púbkéy{"]"}
# Copy the text content of the note to clipboard
Copy_Text_f81c = {"["}Çópy Téxt{"]"}
# 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ó{"]"}
# Relative time in seconds
count_s_aa26 = {"["}{$count}s{"]"}
# Relative time in weeks
count_w_7468 = {"["}{$count}w{"]"}
# Relative time in years
count_y_9408 = {"["}{$count}y{"]"}
# Button to create a new account
Create_Account_6994 = {"["}Çréàté Àççóúñt{"]"}
# Button label to create a new deck
Create_Deck_16b7 = {"["}Çréàté Déçk{"]"}
# Column title for custom timelines
Custom_a69e = {"["}Çústóm{"]"}
# Column title for zap amount customization
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é{"]"}
# Label for decks section in side panel
DECKS_1fad = {"["}DÉÇKS{"]"}
# Label for default zap amount input
Default_amount_per_zap_399d = {"["}Défàúlt àmóúñt pér zàp:{"]"}
# Name of the default deck feed
Default_Deck_fcca = {"["}Défàúlt Déçk{"]"}
# Button label to delete a deck
Delete_Deck_bb29 = {"["}Délété Déçk{"]"}
# Tooltip for deleting a column
Delete_this_column_8d5a = {"["}Délété thís çólúmñ{"]"}
# Button label to delete a wallet
Delete_Wallet_d1d4 = {"["}Délété Wàllét{"]"}
# Profile display name field label
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íóñ{"]"}
# Column title for editing deck
Edit_Deck_4018 = {"["}Édít Déçk{"]"}
# Button label to edit a deck
Edit_Deck_fd93 = {"["}Édít Déçk{"]"}
# Button label to edit user profile
Edit_Profile_49e6 = {"["}Édít Prófílé{"]"}
# Column title for profile editing
Edit_Profile_8ad4 = {"["}Édít Prófílé{"]"}
# Placeholder for hashtag input field
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = {"["}Éñtér thé désíréd hàshtàgs héré (fór múltíplé spàçé-sépàràtéd){"]"}
# Placeholder for relay input field
Enter_the_relay_here_1c8b = {"["}Éñtér thé rélày héré{"]"}
# Hint text to prompt entering the user's public key.
Enter_the_user_s_key__npub__hex__nip05__here_650c = {"["}Éñtér thé úsér's kéy (ñpúb, héx, ñíp05) héré...{"]"}
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
Enter_your_key_0fca = {"["}Éñtér yóúr kéy{"]"}
# 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 = {"["}Éñtér yóúr públíç kéy (ñpúb), ñóstr àddréss (é.g. {$address}), ór prívàté kéy (ñséç). Yóú múst éñtér yóúr prívàté kéy tó bé àblé tó póst, réply, étç.{"]"}
# Label for find user button
Find_User_bd12 = {"["}Fíñd Úsér{"]"}
# Title for hashtags column
Hashtags_f8e0 = {"["}Hàshtàgs{"]"}
# Option in settings section to hide the source client label in note display
Hide_281d = {"["}Hídé{"]"}
# Title for Home column
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{"]"}
# Error message for invalid zap amount
Invalid_amount_6630 = {"["}Íñvàlíd àmóúñt{"]"}
# Error message for invalid key input
Invalid_key_4726 = {"["}Íñvàlíd kéy.{"]"}
# Error message for invalid Nostr Wallet Connect URI
Invalid_NWC_URI_031b = {"["}Íñvàlíd ÑWÇ ÚRÍ{"]"}
# 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 = {"["}Kéép tràçk óf yóúr ñótés & réplíés{"]"}
# 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){"]"}
# Login page title
Login_9eef = {"["}Lógíñ{"]"}
# Login button text
Login_now___let_s_do_this_5630 = {"["}Lógíñ ñów — lét's dó thís!{"]"}
# Text shown on blurred media from unfollowed users
Media_from_someone_you_don_t_follow_5611 = {"["}Médíà fróm sóméóñé yóú dóñ't fóllów{"]"}
# Tooltip for moving a column
Moves_this_column_to_another_position_0d4b = {"["}Móvés thís çólúmñ tó àñóthér pósítíóñ{"]"}
# Title for the user's deck
My_Deck_4ac5 = {"["}My Déçk{"]"}
# 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?{"]"}
# NIP-05 identity field label
Nostr_address__NIP-05_identity_74a2 = {"["}Ñóstr àddréss (ÑÍP-05 ídéñtíty){"]"}
# Default username when profile is not available
nostrich_df29 = {"["}ñóstríçh{"]"}
# Status label for disconnected relay
Not_Connected_6292 = {"["}Ñót Çóññéçtéd{"]"}
# Link text for note references
note_cad6 = {"["}ñóté{"]"}
# Beta product warning message
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = {"["}Ñótédéçk ís à bétà pródúçt. Éxpéçt búgs àñd çóñtàçt ús whéñ yóú rúñ íñtó íssúés.{"]"}
# Filter label for notes only view
Notes_03fb = {"["}Ñótés{"]"}
# Label for notes-only filter
Notes_60d2 = {"["}Ñótés{"]"}
# Filter label for notes and replies view
Notes___Replies_1ec2 = {"["}Ñótés & Réplíés{"]"}
# Label for notes and replies filter
Notes___Replies_6e3b = {"["}Ñótés & Réplíés{"]"}
# Column title for notifications
Notifications_d673 = {"["}Ñótífíçàtíóñs{"]"}
# Title for notifications column
Notifications_ef56 = {"["}Ñótífíçàtíóñs{"]"}
# Relative time for very recent events (less than 3 seconds)
now_2181 = {"["}ñów{"]"}
# 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é...{"]"}
# Error message for missing deck name
Please_create_a_name_for_the_deck_38e7 = {"["}Pléàsé çréàté à ñàmé fór thé déçk.{"]"}
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = {"["}Pléàsé çréàté à ñàmé fór thé déçk àñd séléçt àñ íçóñ.{"]"}
# Error message for missing deck icon
Please_select_an_icon_655b = {"["}Pléàsé séléçt àñ íçóñ.{"]"}
# Button label to post a note
Post_now_8a49 = {"["}Póst ñów{"]"}
# 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 = {"["}Préss thé búttóñ bélów tó çópy yóúr móst réçéñt lógs tó yóúr systém's çlípbóàrd. Théñ pàsté ít íñtó yóúr émàíl.{"]"}
# Profile picture URL field label
Profile_picture_81ff = {"["}Prófílé píçtúré{"]"}
# Column title for quote composition
Quote_475c = {"["}Qúóté{"]"}
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = {"["}Qúóté óf úñkñówñ ñóté{"]"}
# Label for read-only profile mode
Read_only_82ff = {"["}Réàd óñly{"]"}
# Column title for relay management
Relays_9d89 = {"["}Rélàys{"]"}
# Label for relay list section
Relays_ad5e = {"["}Rélàys{"]"}
# Column title for reply composition
Reply_3bf1 = {"["}Réply{"]"}
# Hover text for reply button
Reply_to_this_note_f5de = {"["}Réply tó thís ñóté{"]"}
# Error message when reply note cannot be found
Reply_to_unknown_note_4401 = {"["}Réply tó úñkñówñ ñóté{"]"}
# Fallback template for replying to user
replying_to__user_15ab = {"["}réplyíñg tó {$user}{"]"}
# Template for replying to user in unknown thread
replying_to__user__in_someone_s_thread_e148 = {"["}réplyíñg tó {$user} íñ sóméóñé's thréàd{"]"}
# Template for replying to note in different user's thread
replying_to__user__s__note__in__thread_user__s__thread_daa8 = {"["}réplyíñg tó {$user}'s {$note} íñ {$thread_user}'s {$thread}{"]"}
# Template for replying to user's note
replying_to__user__s__note_ccba = {"["}réplyíñg tó {$user}'s {$note}{"]"}
# Template for replying to root thread
replying_to__user__s__thread_444d = {"["}réplyíñg tó {$user}'s {$thread}{"]"}
# Fallback text when reply note is not found
replying_to_a_note_e0bc = {"["}réplyíñg tó à ñóté{"]"}
# Hover text for repost button
Repost_this_note_8e56 = {"["}Répóst thís ñóté{"]"}
# Label for reposted notes
Reposted_61c8 = {"["}Répóstéd{"]"}
# 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?{"]"}
# Label for satoshis (Bitcoin unit) for custom zap amount input field
SATS_45d7 = {"["}SÀTS{"]"}
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
sats_e5ec = {"["}sàts{"]"}
# Button to save default zap amount
Save_6f7c = {"["}Sàvé{"]"}
# Button label to save profile changes
Save_changes_00db = {"["}Sàvé çhàñgés{"]"}
# Column title for search page
Search_c573 = {"["}Séàrçh{"]"}
# Placeholder for search notes input field
Search_notes_42a6 = {"["}Séàrçh ñótés...{"]"}
# Search in progress message
Searching_for___query_5d18 = {"["}Séàrçhíñg fór '{$query}'{"]"}
# 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 label to send a zap
Send_1ea4 = {"["}Séñd{"]"}
# Column title for app settings
Settings_7a4f = {"["}Séttíñgs{"]"}
# Label for Show source client, others settings section
Show_source_client_9e31 = {"["}Shów sóúrçé çlíéñt{"]"}
# 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{"]"}
# Button label to sign out of account
Sign_out_337b = {"["}Sígñ óút{"]"}
# Title for someone else's notes column
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{"]"}
# 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{"]"}
# Description for hashtags column
Stay_up_to_date_with_a_certain_hashtag_88e3 = {"["}Stày úp tó dàté wíth à çértàíñ hàshtàg{"]"}
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = {"["}Stày úp tó dàté wíth ñótífíçàtíóñs àñd méñtíóñs{"]"}
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = {"["}Stày úp tó dàté wíth sóméóñé élsé's ñótés & réplíés{"]"}
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = {"["}Stày úp tó dàté wíth sóméóñé élsé's ñótífíçàtíóñs àñd méñtíóñs{"]"}
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = {"["}Stày úp tó dàté wíth sóméóñé's ñótés & réplíés{"]"}
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = {"["}Stày úp tó dàté wíth yóúr ñótífíçàtíóñs àñd méñtíóñs{"]"}
# Step 1 label in support instructions
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{"]"}
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = {"["}Swítçh tó dàrk módé{"]"}
# Hover text for light mode toggle button
Switch_to_light_mode_72ce = {"["}Swítçh tó líght módé{"]"}
# Button text to load blurred media
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{"]"}
# Link text for thread references
thread_ad1f = {"["}thréàd{"]"}
# Option in settings section to show the source client label at the top of the note
Top_6aeb = {"["}Tóp{"]"}
# Title for universe column
Universe_e01e = {"["}Úñívérsé{"]"}
# Column title for universe feed
Universe_ffaa = {"["}Úñívérsé{"]"}
# Checkbox label for using wallet only for current account
Use_this_wallet_for_the_current_account_only_61dc = {"["}Úsé thís wàllét fór thé çúrréñt àççóúñt óñly{"]"}
# Username and domain identification message
username___at___domain___will_be_used_for_identification_a4fd = {"["}"{$username}" àt "{$domain}" wíll bé úséd fór ídéñtífíçàtíóñ{"]"}
# 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{"]"}
# Hint for deck name input field
We_recommend_short_names_083e = {"["}Wé réçómméñd shórt ñàmés{"]"}
# Profile website field label
Website_7980 = {"["}Wébsíté{"]"}
# Placeholder for note input field
Write_a_banger_note_here_bad2 = {"["}Wríté à bàñgér ñóté héré...{"]"}
# Placeholder text for key input field
Your_key_here_81bd = {"["}Yóúr kéy héré...{"]"}
# Title for your notes column
Your_Notes_f6db = {"["}Yóúr Ñótés{"]"}
# Title for your notifications column
Your_Notifications_080d = {"["}Yóúr Ñótífíçàtíóñs{"]"}
# Heading for zap (tip) action
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
Got__count__results_for___query_85fb =
{ $count ->
[one] {"["}Gót {$count} résúlt fór '{$query}'{"]"}
*[other] {"["}Gót {$count} résúlts fór '{$query}'{"]"}
}

View File

@@ -0,0 +1,368 @@
# 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 = Información
# Column title for account management
Accounts_f018 = Cuentas
# Button label to add a relay
Add_269d = Agregar
# Label for add column button
Add_47df = Agregar
# Button label to add a different wallet
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = Agregar una billetera diferente que solo se utilizará para esta cuenta
# Error message for missing wallet
Add_a_wallet_to_continue_d170 = Agregar una billetera para continuar
# Button label to add a new account
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 = 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
# 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 hashtags
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = Agregar columna de últimas notas
# Column title for adding notifications column
Add_Notifications_Column_79f8 = Agregar columna de notificaciones
# Button label to add a relay
Add_relay_269d = Agregar relé
# Button label to add a wallet
Add_Wallet_d1be = Agregar billetera
# Title for algorithmic feeds column
Algo_2452 = Algo
# Description for algorithmic feeds column
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
# Button to send message to Dave AI assistant
Ask_b7f4 = Preguntar
# Placeholder text for Dave AI input field
Ask_dave_anything_33d1 = Pregúntale cualquier cosa a Dave...
# Profile banner URL field label
Banner_52ef = Banner
# Beta version label
BETA_8e5d = BETA
# Broadcast the note to all connected relays
Broadcast_fe43 = Transmitir
# Broadcast the note only to local network relays
Broadcast_Local_7e50 = Transmitir localmente
# Button label to cancel an action
Cancel_ed3b = Cancelar
# Hover text for editable zap amount
Click_to_edit_0414 = Haz clic para editar
# Column title for note composition
Compose_Note_c094 = Redactar nota
# Button label to confirm an action
Confirm_f8a6 = Confirmar
# Status label for connected relay
Connected_f8cc = Conectado
# Status label for connecting relay
Connecting_6b7e = Conectando...
# 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 enlace
# Copy the unique note identifier to clipboard
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 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 = Crear cuenta
# Button label to create a new 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 cantidad de zap
# Column title for support page
Damus_Support_27c0 = Ayuda de Damus
# Label for deck name input field
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 = 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
# Tooltip for deleting a column
Delete_this_column_8d5a = Eliminar esta columna
# Button label to delete a wallet
Delete_Wallet_d1d4 = Eliminar billetera
# Profile display name field label
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
# Button label to edit a deck
Edit_Deck_fd93 = Editar deck
# 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 = Ingresa aquí los hashtags deseados (si son varios, sepáralos con un espacio)
# Placeholder for relay input field
Enter_the_relay_here_1c8b = Ingresa el relé aquí
# Hint text to prompt entering the user's public key.
Enter_the_user_s_key__npub__hex__nip05__here_650c = Ingresa la clave del usuario (npub, hex, nip05) aquí...
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
Enter_your_key_0fca = Ingresa tu clave
# 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 = 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
# Title for hashtags column
Hashtags_f8e0 = Hashtags
# Title for Home column
Home_8c19 = Inicio
# Label for deck icon selection
Icon_b0ab = Ícono
# Title for individual user column
Individual_b776 = Individual
# Error message for invalid zap amount
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
Invalid_NWC_URI_031b = NWC URI no válido
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
k_100K_686c = 100.000
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
k_10K_f7e6 = 10.000
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
k_20K_4977 = 20.000
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
k_50K_c2dc = 50.000
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
k_5K_f7e6 = 5.000
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = Haz seguimiento de tus notas y respuestas
# Title for last note per user column
Last_Note_per_User_17ad = Última nota por usuario
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = Dirección de la red Lightning (lud16)
# Login page title
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 = 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
# 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
Nostr_address__NIP-05_identity_74a2 = Dirección de Nostr (identidad NIP-05)
# Default username when profile is not available
nostrich_df29 = nostrich
# Status label for disconnected relay
Not_Connected_6292 = No 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 es un producto en fase beta. Es posible que haya errores, así que ponte en contacto con nosotros si tienes algún problema.
# 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 y respuestas
# Label for notes and replies filter
Notes___Replies_6e3b = Notas y respuestas
# Column title for notifications
Notifications_d673 = Notificaciones
# Title for notifications column
Notifications_ef56 = Notificaciones
# Relative time for very recent events (less than 3 seconds)
now_2181 = ahora
# 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
# Placeholder text for NWC URI input
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 ícono.
# Error message for missing deck icon
Please_select_an_icon_655b = Selecciona un ícono.
# Button label to post a note
Post_now_8a49 = Publicar ahora
# 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 = Presiona el siguiente botón para copiar los registros más recientes al portapapeles del sistema. A continuación, pégalos en tu correo electrónico.
# Profile picture URL field label
Profile_picture_81ff = Imagen de perfil
# Column title for quote composition
Quote_475c = Citar
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = Cita de nota desconocida
# Label for read-only profile mode
Read_only_82ff = Solo lectura
# Column title for relay management
Relays_9d89 = Relés
# Label for relay list section
Relays_ad5e = Relés
# Column title for reply composition
Reply_3bf1 = Respuesta
# 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 desconocida
# Fallback template for replying to 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
replying_to__user__s__note__in__thread_user__s__thread_daa8 = respondiendo a { $note } de { $user } en { $thread } de { $thread_user }
# Template for replying to user's note
replying_to__user__s__note_ccba = respondiendo a { $note } de { $user }
# Template for replying to root thread
replying_to__user__s__thread_444d = respondiendo a { $thread } de { $user }
# Fallback text when reply note is not found
replying_to_a_note_e0bc = respondiendo a una nota
# Hover text for repost button
Repost_this_note_8e56 = Volver a publicar esta nota
# Label for reposted notes
Reposted_61c8 = Publicadas de nuevo
# Heading for support section
Running_into_a_bug_1796 = ¿Encontraste un error?
# 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 cambios
# Column title for search page
Search_c573 = Búsqueda
# Placeholder for search notes input field
Search_notes_42a6 = Buscar notas...
# Search in progress message
Searching_for___query_5d18 = Buscando '{ $query }'
# Description for Home column
See_notes_from_your_contacts_ac16 = Ver notas de tus contactos
# Description for universe column
See_the_whole_nostr_universe_7694 = Ver todo el universo de nostr
# Button label to send a zap
Send_1ea4 = Enviar
# 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
Sign_out_337b = Cerrar sesión
# Title for someone else's notes column
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
# 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
Stay_up_to_date_with_a_certain_hashtag_88e3 = Mantente al día con un hashtag específico
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = Mantente al día con notificaciones y menciones
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = Mantente al día con las notas y respuestas de otra persona
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = Mantente al día con las notificaciones y menciones de otra persona
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = Mantente al día con las notas y respuestas de alguien
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = Mantente al día con tus notificaciones y menciones
# Step 1 label in support instructions
Step_1_8656 = Paso 1
# Step 2 label in support instructions
Step_2_d08d = Paso 2
# 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
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Cambiar a modo oscuro
# Hover text for light mode toggle button
Switch_to_light_mode_72ce = Cambiar a modo claro
# Button text to load blurred media
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!
# Column title for note thread view
Thread_0f20 = Conversación
# Link text for thread references
thread_ad1f = conversación
# 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 billetera solo para la cuenta actual
# Username and domain identification message
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
# Column title for wallet management
Wallet_5e50 = Billetera
# Hint for deck name input field
We_recommend_short_names_083e = Recomendamos nombres cortos
# Profile website field label
Website_7980 = Sitio web
# Placeholder for note input field
Write_a_banger_note_here_bad2 = Escribe aquí una nota impactante...
# Placeholder text for key input field
Your_key_here_81bd = Tu clave aquí...
# Title for your notes column
Your_Notes_f6db = Tus notas
# Title for your notifications column
Your_Notifications_080d = Tus notificaciones
# Heading for zap (tip) action
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = Enviar un zap a esta nota
# Pluralized strings
# Search results count
Got__count__results_for___query_85fb =
{ $count ->
[uno] Obtuvo { $count } resultado para '{ $query }'
*[otro] Obtuvo { $count } resultados para '{ $query }'
}

View File

@@ -0,0 +1,368 @@
# 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 = Información
# Column title for account management
Accounts_f018 = Cuentas
# Button label to add a relay
Add_269d = Añadir
# Label for add column button
Add_47df = Añadir
# Button label to add a different wallet
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = Añadir un monedero diferente que solo se utilizará para esta cuenta
# Error message for missing wallet
Add_a_wallet_to_continue_d170 = Añadir un monedero para continuar
# Button label to add a new account
Add_account_1cfc = Añadir cuenta
# Column title for adding new account
Add_Account_d06c = Añadir cuenta
# Column title for adding algorithm column
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
# 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 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
Add_Notifications_Column_79f8 = Añadir columna de notificaciones
# Button label to add a relay
Add_relay_269d = Añadir relé
# Button label to add a wallet
Add_Wallet_d1be = Añadir monedero
# Title for algorithmic feeds column
Algo_2452 = Algo
# Description for algorithmic feeds column
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
# Button to send message to Dave AI assistant
Ask_b7f4 = Preguntar
# Placeholder text for Dave AI input field
Ask_dave_anything_33d1 = Pregúntale cualquier cosa a Dave...
# Profile banner URL field label
Banner_52ef = Banner
# Beta version label
BETA_8e5d = BETA
# Broadcast the note to all connected relays
Broadcast_fe43 = Transmitir
# Broadcast the note only to local network relays
Broadcast_Local_7e50 = Transmitir localmente
# Button label to cancel an action
Cancel_ed3b = Cancelar
# Hover text for editable zap amount
Click_to_edit_0414 = Haz clic para editar
# Column title for note composition
Compose_Note_c094 = Redactar nota
# Button label to confirm an action
Confirm_f8a6 = Confirmar
# Status label for connected relay
Connected_f8cc = Conectado
# Status label for connecting relay
Connecting_6b7e = Conectando...
# 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 enlace
# Copy the unique note identifier to clipboard
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 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 = Crear cuenta
# Button label to create a new 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 cantidad de zap
# Column title for support page
Damus_Support_27c0 = Ayuda de Damus
# Label for deck name input field
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 = 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
# Tooltip for deleting a column
Delete_this_column_8d5a = Eliminar esta columna
# Button label to delete a wallet
Delete_Wallet_d1d4 = Eliminar monedero
# Profile display name field label
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
# Button label to edit a deck
Edit_Deck_fd93 = Editar deck
# 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 = Ingresa aquí los hashtags deseados (si son varios, sepáralos con un espacio)
# Placeholder for relay input field
Enter_the_relay_here_1c8b = Ingresa el relé aquí
# Hint text to prompt entering the user's public key.
Enter_the_user_s_key__npub__hex__nip05__here_650c = Ingresa la clave del usuario (npub, hex, nip05) aquí...
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
Enter_your_key_0fca = Ingresa tu clave
# 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 = 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
# Title for hashtags column
Hashtags_f8e0 = Hashtags
# Title for Home column
Home_8c19 = Inicio
# Label for deck icon selection
Icon_b0ab = Icono
# Title for individual user column
Individual_b776 = Individual
# Error message for invalid zap amount
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
Invalid_NWC_URI_031b = NWC URI no válido
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
k_100K_686c = 100.000
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
k_10K_f7e6 = 10.000
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
k_20K_4977 = 20.000
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
k_50K_c2dc = 50.000
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
k_5K_f7e6 = 5.000
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = Haz seguimiento de tus notas y respuestas
# Title for last note per user column
Last_Note_per_User_17ad = Última nota por usuario
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = Dirección de la red Lightning (lud16)
# Login page title
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 = 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
# 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
Nostr_address__NIP-05_identity_74a2 = Dirección de Nostr (identidad NIP-05)
# Default username when profile is not available
nostrich_df29 = nostrich
# Status label for disconnected relay
Not_Connected_6292 = No 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 es un producto en fase beta. Es posible que haya errores, así que ponte en contacto con nosotros si tienes algún problema.
# 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 y respuestas
# Label for notes and replies filter
Notes___Replies_6e3b = Notas y respuestas
# Column title for notifications
Notifications_d673 = Notificaciones
# Title for notifications column
Notifications_ef56 = Notificaciones
# Relative time for very recent events (less than 3 seconds)
now_2181 = ahora
# 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
# Placeholder text for NWC URI input
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.
# Error message for missing deck icon
Please_select_an_icon_655b = Selecciona un icono.
# Button label to post a note
Post_now_8a49 = Publicar ahora
# 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 = Presiona el siguiente botón para copiar los registros más recientes al portapapeles del sistema. A continuación, pégalos en tu correo electrónico.
# Profile picture URL field label
Profile_picture_81ff = Imagen de perfil
# Column title for quote composition
Quote_475c = Citar
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = Cita de nota desconocida
# Label for read-only profile mode
Read_only_82ff = Solo lectura
# Column title for relay management
Relays_9d89 = Relés
# Label for relay list section
Relays_ad5e = Relés
# Column title for reply composition
Reply_3bf1 = Respuesta
# 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 desconocida
# Fallback template for replying to 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
replying_to__user__s__note__in__thread_user__s__thread_daa8 = respondiendo a { $note } de { $user } en { $thread } de { $thread_user }
# Template for replying to user's note
replying_to__user__s__note_ccba = respondiendo a { $note } de { $user }
# Template for replying to root thread
replying_to__user__s__thread_444d = respondiendo a { $thread } de { $user }
# Fallback text when reply note is not found
replying_to_a_note_e0bc = respondiendo a una nota
# Hover text for repost button
Repost_this_note_8e56 = Volver a publicar esta nota
# Label for reposted notes
Reposted_61c8 = Publicadas de nuevo
# Heading for support section
Running_into_a_bug_1796 = ¿Has encontrado un error?
# 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 cambios
# Column title for search page
Search_c573 = Búsqueda
# Placeholder for search notes input field
Search_notes_42a6 = Buscar notas...
# Search in progress message
Searching_for___query_5d18 = Buscando '{ $query }'
# Description for Home column
See_notes_from_your_contacts_ac16 = Ver notas de tus contactos
# Description for universe column
See_the_whole_nostr_universe_7694 = Ver todo el universo de nostr
# Button label to send a zap
Send_1ea4 = Enviar
# 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
Sign_out_337b = Cerrar sesión
# Title for someone else's notes column
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
# 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
Stay_up_to_date_with_a_certain_hashtag_88e3 = Mantente al día con un hashtag específico
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = Mantente al día con notificaciones y menciones
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = Mantente al día con las notas y respuestas de otra persona
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = Mantente al día con las notificaciones y menciones de otra persona
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = Mantente al día con las notas y respuestas de alguien
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = Mantente al día con tus notificaciones y menciones
# Step 1 label in support instructions
Step_1_8656 = Paso 1
# Step 2 label in support instructions
Step_2_d08d = Paso 2
# 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
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Cambiar a modo oscuro
# Hover text for light mode toggle button
Switch_to_light_mode_72ce = Cambiar a modo claro
# Button text to load blurred media
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!
# Column title for note thread view
Thread_0f20 = Conversación
# Link text for thread references
thread_ad1f = conversación
# 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 este monedero solo para la cuenta actual
# Username and domain identification message
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
# Column title for wallet management
Wallet_5e50 = Monedero
# Hint for deck name input field
We_recommend_short_names_083e = Recomendamos nombres cortos
# Profile website field label
Website_7980 = Sitio web
# Placeholder for note input field
Write_a_banger_note_here_bad2 = Escribe aquí una nota impactante...
# Placeholder text for key input field
Your_key_here_81bd = Tu clave aquí...
# Title for your notes column
Your_Notes_f6db = Tus notas
# Title for your notifications column
Your_Notifications_080d = Tus notificaciones
# Heading for zap (tip) action
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = Enviar un zap a esta nota
# Pluralized strings
# Search results count
Got__count__results_for___query_85fb =
{ $count ->
[uno] Obtuvo { $count } resultado para '{ $query }'
*[otro] Obtuvo { $count } resultados para '{ $query }'
}

View File

@@ -0,0 +1,408 @@
# 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 = A propos
# Column title for account management
Accounts_f018 = Comptes
# Button label to add a relay
Add_269d = Ajouter
# Label for add column button
Add_47df = Ajouter
# Button label to add a different wallet
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = Ajouter un portefeuille différent qui ne sera utilisé que pour ce compte
# Error message for missing wallet
Add_a_wallet_to_continue_d170 = Ajouter un portefeuille pour continuer
# Button label to add a new account
Add_account_1cfc = Ajouter un compte
# Column title for adding new account
Add_Account_d06c = Ajouter un compte
# Column title for adding algorithm column
Add_Algo_Column_0d75 = Ajouter une colonne Algo
# Column title for adding new column
Add_Column_c764 = Ajouter une colonne
# Column title for adding new deck
Add_Deck_fabf = Ajouter un deck
# Column title for adding external notifications column
Add_External_Notifications_Column_41ae = Ajouter une colonne pour les notifications externes
# Column title for adding hashtag column
Add_Hashtag_Column_ebf4 = Ajouter une colonne Hashtag
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = Ajouter une colonne pour les dernières notes
# Column title for adding notifications column
Add_Notifications_Column_79f8 = Ajouter une colonne pour les notifications
# Button label to add a relay
Add_relay_269d = Ajouter un relai
# Button label to add a wallet
Add_Wallet_d1be = Ajouter un portefeuille
# Title for algorithmic feeds column
Algo_2452 = Algo
# Description for algorithmic feeds column
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
Ask_dave_anything_33d1 = Demandez à Dave n'importe quoi...
# Profile banner URL field label
Banner_52ef = Bannière
# Beta version label
BETA_8e5d = BETA
# Option in settings section to show the source client label at the bottom of the note
Bottom_33c8 = En bas
# Broadcast the note to all connected relays
Broadcast_fe43 = Diffusion
# Broadcast the note only to local network relays
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
Connected_f8cc = Connecté
# Status label for connecting relay
Connecting_6b7e = Connexion...
# Title for contact list column
Contact_List_f85a = Liste de contacts
# Column title for contact lists
Contacts_7533 = Contacts
# Column title for last notes per contact
Contacts__last_notes_3f84 = Contacts (dernières notes)
# Button label to copy logs
Copy_a688 = Copier
# Button to copy media link to clipboard
Copy_Link_dc7c = Copier le lien
# Copy the unique note identifier to clipboard
Copy_Note_ID_6b45 = Copier l'ID de la note
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = Copier le JSON de la note
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = Copier la Pubkey
# Copy the text content of the note to clipboard
Copy_Text_f81c = Copier le texte
# Relative time in days
count_d_b9be = { $count }j
# Relative time in hours
count_h_3ecb = { $count }h
# Relative time in minutes
count_m_b41e = { $count }min
# Relative time in months
count_mo_7aba = { $count }m
# 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 = Créer un compte
# Button label to create a new deck
Create_Deck_16b7 = Créer un deck
# Column title for custom timelines
Custom_a69e = Personnaliser
# Column title for zap amount customization
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
DECKS_1fad = DECKS
# Label for default zap amount input
Default_amount_per_zap_399d = Montant par défaut pour un Zap :
# Name of the default deck feed
Default_Deck_fcca = Deck par défaut
# Button label to delete a deck
Delete_Deck_bb29 = Supprimer le deck
# Tooltip for deleting a column
Delete_this_column_8d5a = Supprimer cette colonne
# Button label to delete a wallet
Delete_Wallet_d1d4 = Supprimer le portefeuille
# Profile display name field label
Display_name_f9d9 = Nom d'utilisateur
# Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" sera utilisé pour l'identification
# Column title for editing deck
Edit_Deck_4018 = Modifier le deck
# Button label to edit a deck
Edit_Deck_fd93 = Modifier le deck
# Button label to edit user profile
Edit_Profile_49e6 = Modifier le profil
# Column title for profile editing
Edit_Profile_8ad4 = Modifier le profil
# Placeholder for hashtag input field
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = Entrez les hashtags souhaités ici (séparez-les avec un espace)
# Placeholder for relay input field
Enter_the_relay_here_1c8b = Entrer un relai ici
# Hint text to prompt entering the user's public key.
Enter_the_user_s_key__npub__hex__nip05__here_650c = Entrer ici la clé de l'utilisateur (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 = Entrez votre clé
# 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 = 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
# Title for hashtags column
Hashtags_f8e0 = Hashtags
# Option in settings section to hide the source client label in note display
Hide_281d = Masquer
# 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
Invalid_amount_6630 = Montant invalide
# Error message for invalid key input
Invalid_key_4726 = Clé non valide.
# Error message for invalid Nostr Wallet Connect URI
Invalid_NWC_URI_031b = Invalide 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 = 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
Login_9eef = Se connecter
# Login button text
Login_now___let_s_do_this_5630 = Se connecter maintenant - c'est parti !
# Text shown on blurred media from unfollowed users
Media_from_someone_you_don_t_follow_5611 = Média d'une personne que vous ne suivez pas
# Tooltip for moving a column
Moves_this_column_to_another_position_0d4b = Déplace cette colonne vers une autre position
# Title for the user's deck
My_Deck_4ac5 = Mon deck
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = Nouveau sur Nostr ?
# NIP-05 identity field label
Nostr_address__NIP-05_identity_74a2 = Adresse Nostr (NIP-05 identité)
# Default username when profile is not available
nostrich_df29 = nostrich
# Status label for disconnected relay
Not_Connected_6292 = Non connecté
# Link text for note references
note_cad6 = note
# Beta product warning message
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck est un produit en phase beta. Attendez-vous à des bugs et contactez-nous si vous rencontrez des problèmes.
# Filter label for notes only view
Notes_03fb = Notes
# Label for notes-only filter
Notes_60d2 = Notes
# Filter label for notes and replies view
Notes___Replies_1ec2 = Notes & Réponses
# Label for notes and replies filter
Notes___Replies_6e3b = Notes & Réponses
# Column title for notifications
Notifications_d673 = Notifications
# Title for notifications column
Notifications_ef56 = Notifications
# Relative time for very recent events (less than 3 seconds)
now_2181 = maintenant
# 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
Please_create_a_name_for_the_deck_38e7 = Veuillez créer un nom pour le deck.
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Veuillez créer un nom pour le deck et sélectionner une icône.
# Error message for missing deck icon
Please_select_an_icon_655b = Veuillez choisir une icône.
# Button label to post a note
Post_now_8a49 = Publier maintenant
# 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 = Cliquez sur le bouton ci-dessous pour copier vos données les plus récentes dans le presse-papiers de votre système. Collez-les ensuite dans votre courrier électronique.
# Profile picture URL field label
Profile_picture_81ff = Photo de profil
# Column title for quote composition
Quote_475c = Citation
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = Citation d'une note inconnue
# Label for read-only profile mode
Read_only_82ff = En lecture seule
# Column title for relay management
Relays_9d89 = Relais
# Label for relay list section
Relays_ad5e = Relais
# Column title for reply composition
Reply_3bf1 = Répondre
# Hover text for reply button
Reply_to_this_note_f5de = Répondre à cette note
# Error message when reply note cannot be found
Reply_to_unknown_note_4401 = Répondre à la note inconnue
# Fallback template for replying to user
replying_to__user_15ab = répondre à { $user }
# Template for replying to user in unknown thread
replying_to__user__in_someone_s_thread_e148 = répondre à { $user } dans le fil de discussion
# Template for replying to note in different user's thread
replying_to__user__s__note__in__thread_user__s__thread_daa8 = répondre à la { $note } de { $user } dans le { $thread } sur le { $thread_user }
# Template for replying to user's note
replying_to__user__s__note_ccba = répondre à la { $note } de { $user }
# Template for replying to root thread
replying_to__user__s__thread_444d = répondre dans le { $thread } de { $user }
# Fallback text when reply note is not found
replying_to_a_note_e0bc = répondre à une note
# Hover text for repost button
Repost_this_note_8e56 = Republier cette note
# Label for reposted notes
Reposted_61c8 = Republier
# 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
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 = Enregistrer
# Button label to save profile changes
Save_changes_00db = Enregistrer les modifications
# Column title for search page
Search_c573 = Rechercher
# Placeholder for search notes input field
Search_notes_42a6 = Rechercher des notes...
# Search in progress message
Searching_for___query_5d18 = Recherche par '{ $query }'
# Description for Home column
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 label to send a zap
Send_1ea4 = Envoyer
# Column title for app settings
Settings_7a4f = Paramètres
# Label for Show source client, others settings section
Show_source_client_9e31 = Afficher le client source
# 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
Sign_out_337b = Se déconnecter
# Title for someone else's notes column
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
# 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
Stay_up_to_date_with_a_certain_hashtag_88e3 = Restez informé sur un hashtag
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = Restez informé avec les notifications et les mentions
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = Restez informé des notes et des réponses de quelqu'un d'autre
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = Restez informé des notifications et mentions de quelqu'un d'autre
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = Restez informé des notes et réponses de quelqu'un
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = Restez informé pour vos notifications et mentions
# Step 1 label in support instructions
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
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Passer en mode sombre
# Hover text for light mode toggle button
Switch_to_light_mode_72ce = Passer en mode clair
# Button text to load blurred media
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
thread_ad1f = fil
# Option in settings section to show the source client label at the top of the note
Top_6aeb = En haut
# Title for universe column
Universe_e01e = Universel
# Column title for universe feed
Universe_ffaa = Universel
# Checkbox label for using wallet only for current account
Use_this_wallet_for_the_current_account_only_61dc = Utiliser ce portefeuille pour le compte actuel
# Username and domain identification message
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
We_recommend_short_names_083e = Nous recommandons des noms courts
# Profile website field label
Website_7980 = Site web
# Placeholder for note input field
Write_a_banger_note_here_bad2 = Écrivez une note banger ici...
# Placeholder text for key input field
Your_key_here_81bd = Votre clé ici...
# Title for your notes column
Your_Notes_f6db = Vos Notes
# Title for your notifications column
Your_Notifications_080d = Vos notifications
# Heading for zap (tip) action
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
# Search results count
Got__count__results_for___query_85fb =
{ $count ->
[one] A obtenu { $count } pour '{ $query }'
*[other] A obtenu { $count } pour '{ $query }'
}

View File

@@ -0,0 +1,408 @@
# 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
# Option in settings section to show the source client label at the bottom of the note
Bottom_33c8 = Abaixo
# 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
# Title for hashtags column
Hashtags_f8e0 = #
# Option in settings section to hide the source client label in note display
Hide_281d = Ocultar
# 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
# 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 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 label to send a zap
Send_1ea4 = Enviar
# Column title for app settings
Settings_7a4f = Configurações
# Label for Show source client, others settings section
Show_source_client_9e31 = Mostrar cliente de origem
# 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
# 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
# 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
# Option in settings section to show the source client label at the top of the note
Top_6aeb = Topo
# 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,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 = เพิ่ม Deck
# 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 = ถามเดฟได้ทุกเรื่อง...
# Profile banner URL field label
Banner_52ef = ภาพปก
# Beta version label
BETA_8e5d = เบต้า
# Option in settings section to show the source client label at the bottom of the note
Bottom_33c8 = ด้านล่าง
# 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 = คัดลอก Pubkey
# Copy the text content of the note to clipboard
Copy_Text_f81c = คัดลอกข้อความ
# 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 }mo
# Relative time in seconds
count_s_aa26 = { $count }s
# Relative time in weeks
count_w_7468 = { $count }w
# Relative time in years
count_y_9408 = { $count }y
# Button to create a new account
Create_Account_6994 = สร้างบัญชี
# Button label to create a new deck
Create_Deck_16b7 = สร้าง Deck
# 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 = ชื่อ Deck
# Label for decks section in side panel
DECKS_1fad = DECKS
# Label for default zap amount input
Default_amount_per_zap_399d = ยอด Zap เริ่มต้น
# Name of the default deck feed
Default_Deck_fcca = Deck หลัก
# Button label to delete a deck
Delete_Deck_bb29 = ลบ Deck
# 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 = แก้ไข Deck
# Button label to edit a deck
Edit_Deck_fd93 = แก้ไข Deck
# 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 = ค้นหาผู้ใช้
# Title for hashtags column
Hashtags_f8e0 = แฮชแท็ก
# Option in settings section to hide the source client label in note display
Hide_281d = ซ่อน
# 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 = ที่อยู่ Lightning Network (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 = Deck ของฉัน
# 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 = เมื่อสักครู่
# 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 = กรุณาตั้งชื่อ Deck
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = กรุณาตั้งชื่อ Deck และเลือกไอคอน
# 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 = ตอบกลับโน้ต { $note } ของ { $user } ในเธรด { $thread } ของ { $thread_user }
# 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 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 = ท่องจักรวาล Nostr ทั้งหมด
# Button label to send a zap
Send_1ea4 = ส่ง
# Column title for app settings
Settings_7a4f = การตั้งค่า
# Label for Show source client, others settings section
Show_source_client_9e31 = แสดงไคลเอนต์ต้นทาง
# 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 = การแจ้งเตือนของผู้อื่น
# 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 = ติดตามโน้ตของผู้อื่น
# 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 = ช่วงทดลองใช้ผู้ช่วย 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
thread_ad1f = เธรด
# Option in settings section to show the source client label at the top of the note
Top_6aeb = ด้านบน
# 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 = "{ $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
We_recommend_short_names_083e = เราแนะนำให้ใช้ชื่อสั้นๆ
# Profile website field label
Website_7980 = เว็บไซต์
# 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,409 @@
# 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 = BETA
# Option in settings section to show the source client label at the bottom of the note
Bottom_33c8 = 底部
# 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 = 自定义打闪金额
# 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
DECKS_1fad = 仪表板
# Label for default zap amount input
Default_amount_per_zap_399d = 打闪默认金额:
# 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 = 查找用户
# Title for hashtags column
Hashtags_f8e0 = 标签
# Option in settings section to hide the source client label in note display
Hide_281d = 隐藏
# 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 = 10万
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
k_10K_f7e6 = 1万
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
k_20K_4977 = 2万
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
k_50K_c2dc = 5万
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
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
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 = nostr 用户
# 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 = 刚刚
# 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
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 = 正在回复在{ $thread_user }的{ $thread }中的{ $user }的{ $note }
# 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 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 = 聪
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
sats_e5ec = 聪
# 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 = 查看整个 nostr 宇宙
# Button label to send a zap
Send_1ea4 = 发送
# Column title for app settings
Settings_7a4f = 设置
# Label for Show source client, others settings section
Show_source_client_9e31 = 显示来源客户端
# 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 = 其他人的通知
# 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 = 第一步
# 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 = 订阅某人的笔记
# 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 助手试用期已经结束 :(。感谢测试!可打闪付款的 Dave 即将来临!
# Label for theme, Appearance settings section
Theme_4aac = 主题:
# Column title for note thread view
Thread_0f20 = 帖子
# Link text for thread references
thread_ad1f = 帖子
# Option in settings section to show the source client label at the top of the note
Top_6aeb = 顶部
# 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 = "{ $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
We_recommend_short_names_083e = 我们推荐使用简短的名称
# Profile website field label
Website_7980 = 网站
# 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 = 打闪
# Hover text for zap button
Zap_this_note_42b2 = 打闪此笔记
# 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,409 @@
# 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 = 測試版
# Option in settings section to show the source client label at the bottom of the note
Bottom_33c8 = 底部
# 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 = 自訂打閃金額
# 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
DECKS_1fad = 儀表板
# Label for default zap amount input
Default_amount_per_zap_399d = 默認打閃金額:
# 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 = 查找用戶
# Title for hashtags column
Hashtags_f8e0 = 標籤
# Option in settings section to hide the source client label in note display
Hide_281d = 隱藏
# 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 = 10萬
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
k_10K_f7e6 = 1萬
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
k_20K_4977 = 2萬
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
k_50K_c2dc = 5萬
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
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
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 = nostr 用戶
# 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 = 剛剛
# 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
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 = 正在回覆在{ $thread_user }的{ $thread }中的{ $user }的{ $note }
# 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 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 = 聰
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
sats_e5ec = 聰
# 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 = 查看整個 nostr 宇宙
# Button label to send a zap
Send_1ea4 = 發送
# Column title for app settings
Settings_7a4f = 設置
# Label for Show source client, others settings section
Show_source_client_9e31 = 顯示來源客戶端
# 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 = 其他人的通知
# 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 = 第一步
# 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 = 訂閱某人的筆記
# 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 助手試用期已經結束 :(。感謝測試!可打閃付款的 Dave 即將來臨!
# Label for theme, Appearance settings section
Theme_4aac = 主題:
# Column title for note thread view
Thread_0f20 = 串文
# Link text for thread references
thread_ad1f = 串文
# Option in settings section to show the source client label at the top of the note
Top_6aeb = 頂部
# 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 = "{ $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
We_recommend_short_names_083e = 我們推薦使用簡短的名稱
# Profile website field label
Website_7980 = 網站
# 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 = 打閃
# Hover text for zap button
Zap_this_note_42b2 = 打閃此筆記
# 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

@@ -1,10 +0,0 @@
use std::process::Command;
fn main() {
if let Ok(output) = Command::new("git").args(["rev-parse", "HEAD"]).output() {
if output.status.success() {
let hash = String::from_utf8_lossy(&output.stdout);
println!("cargo:rustc-env=GIT_COMMIT_HASH={}", hash.trim());
}
}
}

23
crates/enostr/Cargo.toml Normal file
View File

@@ -0,0 +1,23 @@
[package]
name = "enostr"
version = "0.3.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
ewebsock = { version = "0.8.0", features = ["tls"] }
serde_derive = { workspace = true }
serde = { workspace = true, features = ["derive"] } # You only need this if you want app persistence
serde_json = { workspace = true }
nostr = { workspace = true }
bech32 = { workspace = true }
nostrdb = { workspace = true }
hex = { workspace = true }
tracing = { workspace = true }
thiserror = { workspace = true }
url = { workspace = true }
mio = { workspace = true }
tokio = { workspace = true }
tokenator = { workspace = true }
hashbrown = { workspace = true }

View File

@@ -1,13 +1,22 @@
use crate::{Error, Note};
use nostrdb::Filter;
use crate::Error;
use nostrdb::{Filter, Note};
use serde_json::json;
#[derive(Debug, Clone)]
pub struct EventClientMessage {
pub note_json: String,
}
impl EventClientMessage {
pub fn to_json(&self) -> String {
format!("[\"EVENT\", {}]", self.note_json)
}
}
/// Messages sent by clients, received by relays
#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum ClientMessage {
Event {
note: Note,
},
Event(EventClientMessage),
Req {
sub_id: String,
filters: Vec<Filter>,
@@ -19,12 +28,14 @@ pub enum ClientMessage {
}
impl ClientMessage {
pub fn event(note: Note) -> Self {
ClientMessage::Event { note }
pub fn event(note: &Note) -> Result<Self, Error> {
Ok(ClientMessage::Event(EventClientMessage {
note_json: note.json()?,
}))
}
pub fn raw(raw: String) -> Self {
ClientMessage::Raw(raw)
pub fn event_json(note_json: String) -> Result<Self, Error> {
Ok(ClientMessage::Event(EventClientMessage { note_json }))
}
pub fn req(sub_id: String, filters: Vec<Filter>) -> Self {
@@ -37,14 +48,14 @@ impl ClientMessage {
pub fn to_json(&self) -> Result<String, Error> {
Ok(match self {
Self::Event { note } => json!(["EVENT", note]).to_string(),
Self::Event(ecm) => ecm.to_json(),
Self::Raw(raw) => raw.clone(),
Self::Req { sub_id, filters } => {
if filters.is_empty() {
format!("[\"REQ\",\"{}\",{{ }}]", sub_id)
format!("[\"REQ\",\"{sub_id}\",{{ }}]")
} else if filters.len() == 1 {
let filters_json_str = filters[0].json()?;
format!("[\"REQ\",\"{}\",{}]", sub_id, filters_json_str)
format!("[\"REQ\",\"{sub_id}\",{filters_json_str}]")
} else {
let filters_json_str: Result<Vec<String>, Error> = filters
.iter()

View File

@@ -0,0 +1,3 @@
mod message;
pub use message::{ClientMessage, EventClientMessage};

View File

@@ -0,0 +1,61 @@
//use nostr::prelude::secp256k1;
use std::array::TryFromSliceError;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("message is empty")]
Empty,
#[error("decoding failed: {0}")]
DecodeFailed(String),
#[error("hex decoding failed")]
HexDecodeFailed,
#[error("invalid bech32")]
InvalidBech32,
#[error("invalid byte size")]
InvalidByteSize,
#[error("invalid signature")]
InvalidSignature,
#[error("invalid public key")]
InvalidPublicKey,
#[error("invalid relay url")]
InvalidRelayUrl,
// Secp(secp256k1::Error),
#[error("json error: {0}")]
Json(#[from] serde_json::Error),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("nostrdb error: {0}")]
Nostrdb(#[from] nostrdb::Error),
#[error("{0}")]
Generic(String),
}
impl From<String> for Error {
fn from(s: String) -> Self {
Error::Generic(s)
}
}
impl From<TryFromSliceError> for Error {
fn from(_e: TryFromSliceError) -> Self {
Error::InvalidByteSize
}
}
impl From<hex::FromHexError> for Error {
fn from(_e: hex::FromHexError) -> Self {
Error::HexDecodeFailed
}
}

View File

@@ -0,0 +1,291 @@
use nostr::nips::nip19::FromBech32;
use nostr::nips::nip19::ToBech32;
use nostr::nips::nip49::EncryptedSecretKey;
use serde::Deserialize;
use serde::Serialize;
use tokenator::ParseError;
use tokenator::TokenParser;
use tokenator::TokenSerializable;
use crate::Pubkey;
use crate::SecretKey;
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct Keypair {
pub pubkey: Pubkey,
pub secret_key: Option<SecretKey>,
}
pub struct KeypairUnowned<'a> {
pub pubkey: &'a Pubkey,
pub secret_key: Option<&'a SecretKey>,
}
impl<'a> From<&'a Keypair> for KeypairUnowned<'a> {
fn from(value: &'a Keypair) -> Self {
Self {
pubkey: &value.pubkey,
secret_key: value.secret_key.as_ref(),
}
}
}
impl Keypair {
pub fn from_secret(secret_key: SecretKey) -> Self {
let cloned_secret_key = secret_key.clone();
let nostr_keys = nostr::Keys::new(secret_key);
Keypair {
pubkey: Pubkey::new(nostr_keys.public_key().to_bytes()),
secret_key: Some(cloned_secret_key),
}
}
pub fn new(pubkey: Pubkey, secret_key: Option<SecretKey>) -> Self {
Keypair { pubkey, secret_key }
}
pub fn only_pubkey(pubkey: Pubkey) -> Self {
Keypair {
pubkey,
secret_key: None,
}
}
pub fn to_full(&self) -> Option<FilledKeypair<'_>> {
self.secret_key.as_ref().map(|secret_key| FilledKeypair {
pubkey: &self.pubkey,
secret_key,
})
}
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct FullKeypair {
pub pubkey: Pubkey,
pub secret_key: SecretKey,
}
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub struct FilledKeypair<'a> {
pub pubkey: &'a Pubkey,
pub secret_key: &'a SecretKey,
}
impl<'a> FilledKeypair<'a> {
pub fn new(pubkey: &'a Pubkey, secret_key: &'a SecretKey) -> Self {
FilledKeypair { pubkey, secret_key }
}
pub fn to_full(&self) -> FullKeypair {
FullKeypair {
pubkey: self.pubkey.to_owned(),
secret_key: self.secret_key.to_owned(),
}
}
}
impl<'a> From<&'a FilledKeypair<'a>> for KeypairUnowned<'a> {
fn from(value: &'a FilledKeypair<'a>) -> Self {
Self {
pubkey: value.pubkey,
secret_key: Some(value.secret_key),
}
}
}
impl FullKeypair {
pub fn new(pubkey: Pubkey, secret_key: SecretKey) -> Self {
FullKeypair { pubkey, secret_key }
}
pub fn to_filled(&self) -> FilledKeypair<'_> {
FilledKeypair::new(&self.pubkey, &self.secret_key)
}
pub fn generate() -> Self {
let mut rng = nostr::secp256k1::rand::rngs::OsRng;
let (secret_key, _) = &nostr::SECP256K1.generate_keypair(&mut rng);
let (xopk, _) = secret_key.x_only_public_key(&nostr::SECP256K1);
let secret_key = nostr::SecretKey::from(*secret_key);
FullKeypair {
pubkey: Pubkey::new(xopk.serialize()),
secret_key,
}
}
pub fn to_keypair(self) -> Keypair {
Keypair {
pubkey: self.pubkey,
secret_key: Some(self.secret_key),
}
}
}
impl std::fmt::Display for Keypair {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Keypair:\n\tpublic: {}\n\tsecret: {}",
self.pubkey,
match self.secret_key {
Some(_) => "Some(<hidden>)",
None => "None",
}
)
}
}
impl std::fmt::Display for FullKeypair {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Keypair:\n\tpublic: {}\n\tsecret: <hidden>", self.pubkey)
}
}
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct SerializableKeypair {
pub pubkey: Pubkey,
pub encrypted_secret_key: Option<EncryptedSecretKey>,
}
impl SerializableKeypair {
pub fn from_keypair(kp: &Keypair, pass: &str, log_n: u8) -> Self {
Self {
pubkey: kp.pubkey,
encrypted_secret_key: kp.secret_key.clone().and_then(|s| {
EncryptedSecretKey::new(&s, pass, log_n, nostr::nips::nip49::KeySecurity::Weak).ok()
}),
}
}
pub fn to_keypair(&self, pass: &str) -> Keypair {
Keypair::new(
self.pubkey,
self.encrypted_secret_key
.and_then(|e| e.to_secret_key(pass).ok()),
)
}
}
impl TokenSerializable for Pubkey {
fn parse_from_tokens<'a>(parser: &mut TokenParser<'a>) -> Result<Self, ParseError<'a>> {
parser.parse_token(PUBKEY_TOKEN)?;
let raw = parser.pull_token()?;
let pubkey =
Pubkey::try_from_bech32_string(raw, true).map_err(|_| ParseError::DecodeFailed)?;
Ok(pubkey)
}
fn serialize_tokens(&self, writer: &mut tokenator::TokenWriter) {
writer.write_token(PUBKEY_TOKEN);
let Some(bech) = self.npub() else {
tracing::error!("Could not convert pubkey to bech: {}", self.hex());
return;
};
writer.write_token(&bech);
}
}
impl TokenSerializable for Keypair {
fn parse_from_tokens<'a>(parser: &mut TokenParser<'a>) -> Result<Self, ParseError<'a>> {
TokenParser::alt(
parser,
&[
|p| Ok(Keypair::only_pubkey(Pubkey::parse_from_tokens(p)?)),
|p| Ok(Keypair::from_secret(parse_seckey(p)?)),
],
)
}
fn serialize_tokens(&self, writer: &mut tokenator::TokenWriter) {
if let Some(seckey) = &self.secret_key {
writer.write_token(ESECKEY_TOKEN);
let maybe_eseckey = EncryptedSecretKey::new(
seckey,
ESECKEY_PASS,
7,
nostr::nips::nip49::KeySecurity::Unknown,
);
let Ok(eseckey) = maybe_eseckey else {
tracing::error!("Could not convert seckey to EncryptedSecretKey");
return;
};
let Ok(serialized) = eseckey.to_bech32() else {
tracing::error!("Could not serialize ncryptsec");
return;
};
writer.write_token(&serialized);
} else {
self.pubkey.serialize_tokens(writer);
}
}
}
const ESECKEY_TOKEN: &str = "eseckey";
const ESECKEY_PASS: &str = "notedeck";
const PUBKEY_TOKEN: &str = "pubkey";
fn parse_seckey<'a>(parser: &mut TokenParser<'a>) -> Result<SecretKey, ParseError<'a>> {
parser.parse_token(ESECKEY_TOKEN)?;
let raw = parser.pull_token()?;
let eseckey = EncryptedSecretKey::from_bech32(raw).map_err(|_| ParseError::DecodeFailed)?;
let seckey = eseckey
.to_secret_key(ESECKEY_PASS)
.map_err(|_| ParseError::DecodeFailed)?;
Ok(seckey)
}
#[cfg(test)]
mod tests {
use tokenator::{TokenParser, TokenSerializable, TokenWriter};
use super::{FullKeypair, Keypair};
#[test]
fn test_token_eseckey_serialize_deserialize() {
let kp = FullKeypair::generate();
let mut writer = TokenWriter::new("\t");
kp.clone().to_keypair().serialize_tokens(&mut writer);
let serialized = writer.str();
let data = &serialized.split("\t").collect::<Vec<&str>>();
let mut parser = TokenParser::new(data);
let m_new_kp = Keypair::parse_from_tokens(&mut parser);
assert!(m_new_kp.is_ok());
let new_kp = m_new_kp.unwrap();
assert_eq!(kp, new_kp.to_full().unwrap().to_full());
}
#[test]
fn test_token_pubkey_serialize_deserialize() {
let kp = Keypair::only_pubkey(FullKeypair::generate().pubkey);
let mut writer = TokenWriter::new("\t");
kp.clone().serialize_tokens(&mut writer);
let serialized = writer.str();
let data = &serialized.split("\t").collect::<Vec<&str>>();
let mut parser = TokenParser::new(data);
let m_new_kp = Keypair::parse_from_tokens(&mut parser);
assert!(m_new_kp.is_ok());
let new_kp = m_new_kp.unwrap();
assert_eq!(kp, new_kp);
}
}

View File

@@ -7,17 +7,18 @@ mod profile;
mod pubkey;
mod relay;
pub use client::ClientMessage;
pub use client::{ClientMessage, EventClientMessage};
pub use error::Error;
pub use ewebsock;
pub use filter::Filter;
pub use keypair::{FilledKeypair, FullKeypair, Keypair, SerializableKeypair};
pub use keypair::{FilledKeypair, FullKeypair, Keypair, KeypairUnowned, SerializableKeypair};
pub use nostr::SecretKey;
pub use note::{Note, NoteId};
pub use profile::Profile;
pub use pubkey::Pubkey;
pub use profile::ProfileState;
pub use pubkey::{Pubkey, PubkeyRef};
pub use relay::message::{RelayEvent, RelayMessage};
pub use relay::pool::{PoolEvent, RelayPool};
pub use relay::pool::{PoolEvent, PoolRelay, RelayPool};
pub use relay::subs_debug::{OwnedRelayEvent, RelayLogEvent, SubsDebug, TransferStats};
pub use relay::{Relay, RelayStatus};
pub type Result<T> = std::result::Result<T, error::Error>;

View File

@@ -9,11 +9,11 @@ pub struct NoteId([u8; 32]);
impl fmt::Debug for NoteId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.hex())
write!(f, "NoteId({})", self.hex())
}
}
static HRP_NOTE: nostr::bech32::Hrp = nostr::bech32::Hrp::parse_unchecked("note");
static HRP_NOTE: bech32::Hrp = bech32::Hrp::parse_unchecked("note");
impl NoteId {
pub fn new(bytes: [u8; 32]) -> Self {
@@ -29,12 +29,22 @@ impl NoteId {
}
pub fn from_hex(hex_str: &str) -> Result<Self, Error> {
let evid = NoteId(hex::decode(hex_str)?.as_slice().try_into().unwrap());
let evid = NoteId(hex::decode(hex_str)?.as_slice().try_into()?);
Ok(evid)
}
pub fn to_bech(&self) -> Option<String> {
nostr::bech32::encode::<nostr::bech32::Bech32>(HRP_NOTE, &self.0).ok()
bech32::encode::<bech32::Bech32>(HRP_NOTE, &self.0).ok()
}
pub fn from_bech(bech: &str) -> Option<Self> {
let (hrp, data) = bech32::decode(bech).ok()?;
if hrp != HRP_NOTE {
return None;
}
Some(NoteId::new(data.try_into().ok()?))
}
}
@@ -133,3 +143,9 @@ impl<'de> Deserialize<'de> for NoteId {
NoteId::from_hex(&s).map_err(serde::de::Error::custom)
}
}
impl hashbrown::Equivalent<NoteId> for &[u8; 32] {
fn equivalent(&self, key: &NoteId) -> bool {
self.as_slice() == key.bytes()
}
}

View File

@@ -0,0 +1,110 @@
use serde_json::{Map, Value};
#[derive(Debug, Clone)]
pub struct ProfileState(Value);
impl Default for ProfileState {
fn default() -> Self {
ProfileState::new(Map::default())
}
}
impl ProfileState {
pub fn new(value: Map<String, Value>) -> Self {
Self(Value::Object(value))
}
pub fn get_str(&self, name: &str) -> Option<&str> {
self.0.get(name).and_then(|v| v.as_str())
}
pub fn values_mut(&mut self) -> &mut Map<String, Value> {
self.0.as_object_mut().unwrap()
}
/// Insert or overwrite an existing value with a string
pub fn str_mut(&mut self, name: &str) -> &mut String {
let val = self
.values_mut()
.entry(name)
.or_insert(Value::String("".to_string()));
// if its not a string, make it one. this will overrwrite
// the old value, so be careful
if !val.is_string() {
*val = Value::String("".to_string());
}
match val {
Value::String(s) => s,
// SAFETY: we replace it above, so its impossible to be something
// other than a string
_ => panic!("impossible"),
}
}
pub fn value(&self) -> &Value {
&self.0
}
pub fn to_json(&self) -> String {
// SAFETY: serializing a value should be irrefutable
serde_json::to_string(self.value()).unwrap()
}
#[inline]
pub fn name(&self) -> Option<&str> {
self.get_str("name")
}
#[inline]
pub fn banner(&self) -> Option<&str> {
self.get_str("name")
}
#[inline]
pub fn display_name(&self) -> Option<&str> {
self.get_str("display_name")
}
#[inline]
pub fn lud06(&self) -> Option<&str> {
self.get_str("lud06")
}
#[inline]
pub fn nip05(&self) -> Option<&str> {
self.get_str("nip05")
}
#[inline]
pub fn lud16(&self) -> Option<&str> {
self.get_str("lud16")
}
#[inline]
pub fn about(&self) -> Option<&str> {
self.get_str("about")
}
#[inline]
pub fn picture(&self) -> Option<&str> {
self.get_str("picture")
}
#[inline]
pub fn website(&self) -> Option<&str> {
self.get_str("website")
}
pub fn from_note_contents(contents: &str) -> Self {
let json = serde_json::from_str(contents);
let data = if let Ok(Value::Object(data)) = json {
data
} else {
Map::new()
};
Self::new(data)
}
}

View File

@@ -1,7 +1,7 @@
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::Error;
use nostr::bech32::Hrp;
use std::borrow::Borrow;
use std::fmt;
use std::ops::Deref;
use tracing::debug;
@@ -9,7 +9,34 @@ use tracing::debug;
#[derive(Eq, PartialEq, Clone, Copy, Hash, Ord, PartialOrd)]
pub struct Pubkey([u8; 32]);
static HRP_NPUB: Hrp = Hrp::parse_unchecked("npub");
#[derive(Eq, PartialEq, Clone, Copy, Hash, Ord, PartialOrd)]
pub struct PubkeyRef<'a>(&'a [u8; 32]);
static HRP_NPUB: bech32::Hrp = bech32::Hrp::parse_unchecked("npub");
impl Borrow<[u8; 32]> for PubkeyRef<'_> {
fn borrow(&self) -> &[u8; 32] {
self.0
}
}
impl<'a> PubkeyRef<'a> {
pub fn new(bytes: &'a [u8; 32]) -> Self {
Self(bytes)
}
pub fn bytes(&self) -> &[u8; 32] {
self.0
}
pub fn to_owned(&self) -> Pubkey {
Pubkey::new(*self.bytes())
}
pub fn hex(&self) -> String {
hex::encode(self.bytes())
}
}
impl Deref for Pubkey {
type Target = [u8; 32];
@@ -19,6 +46,12 @@ impl Deref for Pubkey {
}
}
impl Borrow<[u8; 32]> for Pubkey {
fn borrow(&self) -> &[u8; 32] {
&self.0
}
}
impl Pubkey {
pub fn new(data: [u8; 32]) -> Self {
Self(data)
@@ -32,6 +65,10 @@ impl Pubkey {
&self.0
}
pub fn as_ref(&self) -> PubkeyRef<'_> {
PubkeyRef(self.bytes())
}
pub fn parse(s: &str) -> Result<Self, Error> {
match Pubkey::from_hex(s) {
Ok(pk) => Ok(pk),
@@ -58,7 +95,7 @@ impl Pubkey {
}
pub fn try_from_bech32_string(s: &str, verify: bool) -> Result<Self, Error> {
let data = match nostr::bech32::decode(s) {
let data = match bech32::decode(s) {
Ok(res) => Ok(res),
Err(_) => Err(Error::InvalidBech32),
}?;
@@ -78,8 +115,8 @@ impl Pubkey {
}
}
pub fn to_bech(&self) -> Option<String> {
nostr::bech32::encode::<nostr::bech32::Bech32>(HRP_NPUB, &self.0).ok()
pub fn npub(&self) -> Option<String> {
bech32::encode::<bech32::Bech32>(HRP_NPUB, &self.0).ok()
}
}
@@ -89,6 +126,12 @@ impl fmt::Display for Pubkey {
}
}
impl fmt::Debug for PubkeyRef<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.hex())
}
}
impl fmt::Debug for Pubkey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.hex())
@@ -123,3 +166,9 @@ impl<'de> Deserialize<'de> for Pubkey {
Pubkey::from_hex(&s).map_err(serde::de::Error::custom)
}
}
impl hashbrown::Equivalent<Pubkey> for &[u8; 32] {
fn equivalent(&self, key: &Pubkey) -> bool {
self.as_slice() == key.bytes()
}
}

View File

@@ -8,6 +8,10 @@ pub struct CommandResult<'a> {
message: &'a str,
}
pub fn calculate_command_result_size(result: &CommandResult) -> usize {
std::mem::size_of_val(result) + result.event_id.len() + result.message.len()
}
#[derive(Debug, Eq, PartialEq)]
pub enum RelayMessage<'a> {
OK(CommandResult<'a>),
@@ -74,9 +78,14 @@ impl<'a> RelayMessage<'a> {
return Err(Error::Empty);
}
// make sure we can inspect the begning of the message below ...
if msg.len() < 12 {
return Err(Error::DecodeFailed("message too short".into()));
}
// Notice
// Relay response format: ["NOTICE", <message>]
if &msg[0..=9] == "[\"NOTICE\"," {
if msg.len() >= 12 && &msg[0..=9] == "[\"NOTICE\"," {
// TODO: there could be more than one space, whatever
let start = if msg.as_bytes().get(10).copied() == Some(b' ') {
12
@@ -99,7 +108,7 @@ impl<'a> RelayMessage<'a> {
let subid = &msg[start..subid_end].trim().trim_matches('"');
return Ok(Self::event(msg, subid));
} else {
return Ok(Self::event(msg, "fixme"));
return Err(Error::DecodeFailed("Invalid EVENT format".into()));
}
}
@@ -107,18 +116,28 @@ impl<'a> RelayMessage<'a> {
// Relay response format: ["EOSE", <subscription_id>]
if &msg[0..=7] == "[\"EOSE\"," {
let start = if msg.as_bytes().get(8).copied() == Some(b' ') {
10
10 // Skip space after the comma
} else {
9
9 // Start immediately after the comma
};
let end = msg.len() - 2;
return Ok(Self::eose(&msg[start..end]));
// Use rfind to locate the last quote
if let Some(end_bracket_index) = msg.rfind(']') {
let end = end_bracket_index - 1; // Account for space before bracket
if start < end {
// Trim subscription id and remove extra spaces and quotes
let subid = &msg[start..end].trim().trim_matches('"').trim();
return Ok(RelayMessage::eose(subid));
}
}
return Err(Error::DecodeFailed(
"Invalid subscription ID or format".into(),
));
}
// OK (NIP-20)
// Relay response format: ["OK",<event_id>, <true|false>, <message>]
if &msg[0..=5] == "[\"OK\"," {
// TODO: fix this
if &msg[0..=5] == "[\"OK\"," && msg.len() >= 78 {
let event_id = &msg[7..71];
let booly = &msg[73..77];
let status: bool = if booly == "true" {
@@ -126,13 +145,14 @@ impl<'a> RelayMessage<'a> {
} else if booly == "false" {
false
} else {
return Err(Error::DecodeFailed);
return Err(Error::DecodeFailed("bad boolean value".into()));
};
return Ok(Self::ok(event_id, status, "fixme"));
let message_start = msg.rfind(',').unwrap() + 1;
let message = &msg[message_start..msg.len() - 2].trim().trim_matches('"');
return Ok(Self::ok(event_id, status, message));
}
Err(Error::DecodeFailed)
Err(Error::DecodeFailed("unrecognized message type".into()))
}
}
@@ -141,39 +161,115 @@ mod tests {
use super::*;
#[test]
fn test_handle_valid_notice() -> Result<()> {
let valid_notice_msg = r#"["NOTICE","Invalid event format!"]"#;
let handled_valid_notice_msg = RelayMessage::notice("Invalid event format!".to_string());
assert_eq!(
RelayMessage::from_json(valid_notice_msg)?,
handled_valid_notice_msg
);
fn test_handle_various_messages() -> Result<()> {
let tests = vec![
// Valid cases
(
// shortest valid message
r#"["EOSE","x"]"#,
Ok(RelayMessage::eose("x")),
),
(
// also very short
r#"["NOTICE",""]"#,
Ok(RelayMessage::notice("")),
),
(
r#"["NOTICE","Invalid event format!"]"#,
Ok(RelayMessage::notice("Invalid event format!")),
),
(
r#"["EVENT", "random_string", {"id":"example","content":"test"}]"#,
Ok(RelayMessage::event(
r#"["EVENT", "random_string", {"id":"example","content":"test"}]"#,
"random_string",
)),
),
(
r#"["EOSE","random-subscription-id"]"#,
Ok(RelayMessage::eose("random-subscription-id")),
),
(
r#"["EOSE", "random-subscription-id"]"#,
Ok(RelayMessage::eose("random-subscription-id")),
),
(
r#"["EOSE", "random-subscription-id" ]"#,
Ok(RelayMessage::eose("random-subscription-id")),
),
(
r#"["OK","b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30",true,"pow: difficulty 25>=24"]"#,
Ok(RelayMessage::ok(
"b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30",
true,
"pow: difficulty 25>=24",
)),
),
// Invalid cases
(
r#"["EVENT","random_string"]"#,
Err(Error::DecodeFailed("Invalid EVENT format".into())),
),
(
r#"["EOSE"]"#,
Err(Error::DecodeFailed("message too short".into())),
),
(
r#"["NOTICE"]"#,
Err(Error::DecodeFailed("message too short".into())),
),
(
r#"["NOTICE": 404]"#,
Err(Error::DecodeFailed("unrecognized message type".into())),
),
(
r#"["OK","event_id"]"#,
Err(Error::DecodeFailed("unrecognized message type".into())),
),
(
r#"["OK","b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30"]"#,
Err(Error::DecodeFailed("unrecognized message type".into())),
),
(
r#"["OK","b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30",hello,""]"#,
Err(Error::DecodeFailed("bad boolean value".into())),
),
(
r#"["OK","b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30",hello,404]"#,
Err(Error::DecodeFailed("bad boolean value".into())),
),
];
for (input, expected) in tests {
match expected {
Ok(expected_msg) => {
let result = RelayMessage::from_json(input);
assert_eq!(
result?, expected_msg,
"Expected {:?} for input: {}",
expected_msg, input
);
}
Err(expected_err) => {
let result = RelayMessage::from_json(input);
assert!(
matches!(result, Err(ref e) if *e.to_string() == expected_err.to_string()),
"Expected error {:?} for input: {}, but got: {:?}",
expected_err,
input,
result
);
}
}
}
Ok(())
}
#[test]
fn test_handle_invalid_notice() {
//Missing content
let invalid_notice_msg = r#"["NOTICE"]"#;
//The content is not string
let invalid_notice_msg_content = r#"["NOTICE": 404]"#;
assert_eq!(
RelayMessage::from_json(invalid_notice_msg).unwrap_err(),
Error::DecodeFailed
);
assert_eq!(
RelayMessage::from_json(invalid_notice_msg_content).unwrap_err(),
Error::DecodeFailed
);
}
/*
#[test]
fn test_handle_valid_event() -> Result<()> {
use tracing::debug;
env_logger::init();
let valid_event_msg = r#"["EVENT", "random_string", {"id":"70b10f70c1318967eddf12527799411b1a9780ad9c43858f5e5fcd45486a13a5","pubkey":"379e863e8357163b5bce5d2688dc4f1dcc2d505222fb8d74db600f30535dfdfe","created_at":1612809991,"kind":1,"tags":[],"content":"test","sig":"273a9cd5d11455590f4359500bccb7a89428262b96b3ea87a756b770964472f8c3e87f5d5e64d8d2e859a71462a3f477b554565c4f2f326cb01dd7620db71502"}]"#;
let id = "70b10f70c1318967eddf12527799411b1a9780ad9c43858f5e5fcd45486a13a5";
@@ -184,15 +280,17 @@ mod tests {
let content = "test";
let sig = "273a9cd5d11455590f4359500bccb7a89428262b96b3ea87a756b770964472f8c3e87f5d5e64d8d2e859a71462a3f477b554565c4f2f326cb01dd7620db71502";
let handled_event = Event::new_dummy(id, pubkey, created_at, kind, tags, content, sig);
let handled_event = Note::new_dummy(id, pubkey, created_at, kind, tags, content, sig).expect("ev");
debug!("event {:?}", handled_event);
let msg = RelayMessage::from_json(valid_event_msg);
let msg = RelayMessage::from_json(valid_event_msg).expect("valid json");
debug!("msg {:?}", msg);
let note_json = serde_json::to_string(&handled_event).expect("json ev");
assert_eq!(
msg?,
RelayMessage::event(handled_event?, "random_string".to_string())
msg,
RelayMessage::event(&note_json, "random_string")
);
Ok(())
@@ -205,49 +303,40 @@ mod tests {
//Event JSON with incomplete content
let invalid_event_msg_content = r#"["EVENT","random_string",{"id":"70b10f70c1318967eddf12527799411b1a9780ad9c43858f5e5fcd45486a13a5","pubkey":"379e863e8357163b5bce5d2688dc4f1dcc2d505222fb8d74db600f30535dfdfe"}]"#;
assert_eq!(
assert!(matches!(
RelayMessage::from_json(invalid_event_msg).unwrap_err(),
Error::DecodeFailed
);
));
assert_eq!(
assert!(matches!(
RelayMessage::from_json(invalid_event_msg_content).unwrap_err(),
Error::DecodeFailed
);
));
}
*/
#[test]
fn test_handle_valid_eose() -> Result<()> {
let valid_eose_msg = r#"["EOSE","random-subscription-id"]"#;
let handled_valid_eose_msg = RelayMessage::eose("random-subscription-id".to_string());
assert_eq!(
RelayMessage::from_json(valid_eose_msg)?,
handled_valid_eose_msg
);
Ok(())
}
// TODO: fix these tests
/*
#[test]
fn test_handle_invalid_eose() {
// Missing subscription ID
assert_eq!(
assert!(matches!(
RelayMessage::from_json(r#"["EOSE"]"#).unwrap_err(),
Error::DecodeFailed
);
));
// The subscription ID is not string
assert_eq!(
assert!(matches!(
RelayMessage::from_json(r#"["EOSE",404]"#).unwrap_err(),
Error::DecodeFailed
);
));
}
#[test]
fn test_handle_valid_ok() -> Result<()> {
let valid_ok_msg = r#"["OK","b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30",true,"pow: difficulty 25>=24"]"#;
let handled_valid_ok_msg = RelayMessage::ok(
"b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30".to_string(),
"b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30",
true,
"pow: difficulty 25>=24".into(),
);
@@ -256,27 +345,5 @@ mod tests {
Ok(())
}
#[test]
fn test_handle_invalid_ok() {
// Missing params
assert_eq!(
RelayMessage::from_json(
r#"["OK","b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30"]"#
)
.unwrap_err(),
Error::DecodeFailed
);
// Invalid status
assert_eq!(
RelayMessage::from_json(r#"["OK","b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30",hello,""]"#).unwrap_err(),
Error::DecodeFailed
);
// Invalid message
assert_eq!(
RelayMessage::from_json(r#"["OK","b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30",hello,404]"#).unwrap_err(),
Error::DecodeFailed
);
}
*/
}

View File

@@ -0,0 +1,225 @@
use ewebsock::{Options, WsEvent, WsMessage, WsReceiver, WsSender};
use mio::net::UdpSocket;
use std::io;
use std::net::IpAddr;
use std::net::{SocketAddr, SocketAddrV4};
use std::time::{Duration, Instant};
use crate::{ClientMessage, EventClientMessage, Result};
use std::fmt;
use std::hash::{Hash, Hasher};
use std::net::Ipv4Addr;
use tracing::{debug, error};
pub mod message;
pub mod pool;
pub mod subs_debug;
#[derive(Debug, Copy, Clone)]
pub enum RelayStatus {
Connected,
Connecting,
Disconnected,
}
pub struct MulticastRelay {
last_join: Instant,
status: RelayStatus,
address: SocketAddrV4,
socket: UdpSocket,
interface: Ipv4Addr,
}
impl MulticastRelay {
pub fn new(address: SocketAddrV4, socket: UdpSocket, interface: Ipv4Addr) -> Self {
let last_join = Instant::now();
let status = RelayStatus::Connected;
MulticastRelay {
status,
address,
socket,
interface,
last_join,
}
}
/// Multicast seems to fail every 260 seconds. We force a rejoin every 200 seconds or
/// so to ensure we are always in the group
pub fn rejoin(&mut self) -> Result<()> {
self.last_join = Instant::now();
self.status = RelayStatus::Disconnected;
self.socket
.leave_multicast_v4(self.address.ip(), &self.interface)?;
self.socket
.join_multicast_v4(self.address.ip(), &self.interface)?;
self.status = RelayStatus::Connected;
Ok(())
}
pub fn should_rejoin(&self) -> bool {
(Instant::now() - self.last_join) >= Duration::from_secs(200)
}
pub fn try_recv(&self) -> Option<WsEvent> {
let mut buffer = [0u8; 65535];
// Read the size header
match self.socket.recv_from(&mut buffer) {
Ok((size, src)) => {
let parsed_size = u32::from_be_bytes(buffer[0..4].try_into().ok()?) as usize;
debug!("multicast: read size {} from start of header", size - 4);
if size != parsed_size + 4 {
error!(
"multicast: partial data received: expected {}, got {}",
parsed_size, size
);
return None;
}
let text = String::from_utf8_lossy(&buffer[4..size]);
debug!("multicast: received {} bytes from {}: {}", size, src, &text);
Some(WsEvent::Message(WsMessage::Text(text.to_string())))
}
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
// No data available, continue
None
}
Err(e) => {
error!("multicast: error receiving data: {}", e);
None
}
}
}
pub fn send(&self, msg: &EventClientMessage) -> Result<()> {
let json = msg.to_json();
let len = json.len();
debug!("writing to multicast relay");
let mut buf: Vec<u8> = Vec::with_capacity(4 + len);
// Write the length of the message as 4 bytes (big-endian)
buf.extend_from_slice(&(len as u32).to_be_bytes());
// Append the JSON message bytes
buf.extend_from_slice(json.as_bytes());
self.socket.send_to(&buf, SocketAddr::V4(self.address))?;
Ok(())
}
}
pub fn setup_multicast_relay(
wakeup: impl Fn() + Send + Sync + Clone + 'static,
) -> Result<MulticastRelay> {
use mio::{Events, Interest, Poll, Token};
let port = 9797;
let address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), port);
let multicast_ip = Ipv4Addr::new(239, 19, 88, 1);
let mut socket = UdpSocket::bind(address)?;
let interface = Ipv4Addr::UNSPECIFIED;
let multicast_address = SocketAddrV4::new(multicast_ip, port);
socket.join_multicast_v4(&multicast_ip, &interface)?;
let mut poll = Poll::new()?;
poll.registry().register(
&mut socket,
Token(0),
Interest::READABLE | Interest::WRITABLE,
)?;
// wakeup our render thread when we have new stuff on the socket
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))) {
error!("multicast socket poll error: {err}. ending multicast poller.");
return;
}
wakeup();
std::thread::yield_now();
}
});
Ok(MulticastRelay::new(multicast_address, socket, interface))
}
pub struct Relay {
pub url: nostr::RelayUrl,
pub status: RelayStatus,
pub sender: WsSender,
pub receiver: WsReceiver,
}
impl fmt::Debug for Relay {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Relay")
.field("url", &self.url)
.field("status", &self.status)
.finish()
}
}
impl Hash for Relay {
fn hash<H: Hasher>(&self, state: &mut H) {
// Hashes the Relay by hashing the URL
self.url.hash(state);
}
}
impl PartialEq for Relay {
fn eq(&self, other: &Self) -> bool {
self.url == other.url
}
}
impl Eq for Relay {}
impl Relay {
pub fn new(url: nostr::RelayUrl, wakeup: impl Fn() + Send + Sync + 'static) -> Result<Self> {
let status = RelayStatus::Connecting;
let (sender, receiver) =
ewebsock::connect_with_wakeup(url.as_str(), Options::default(), wakeup)?;
Ok(Self {
url,
sender,
receiver,
status,
})
}
pub fn send(&mut self, msg: &ClientMessage) {
let json = match msg.to_json() {
Ok(json) => {
debug!("sending {} to {}", json, self.url);
json
}
Err(e) => {
error!("error serializing json for filter: {e}");
return;
}
};
let txt = WsMessage::Text(json);
self.sender.send(txt);
}
pub fn connect(&mut self, wakeup: impl Fn() + Send + Sync + 'static) -> Result<()> {
let (sender, receiver) =
ewebsock::connect_with_wakeup(self.url.as_str(), Options::default(), wakeup)?;
self.status = RelayStatus::Connecting;
self.sender = sender;
self.receiver = receiver;
Ok(())
}
pub fn ping(&mut self) {
let msg = WsMessage::Ping(vec![]);
self.sender.send(msg);
}
}

View File

@@ -0,0 +1,411 @@
use crate::relay::{setup_multicast_relay, MulticastRelay, Relay, RelayStatus};
use crate::{ClientMessage, Error, Result};
use nostrdb::Filter;
use std::collections::BTreeSet;
use std::time::{Duration, Instant};
use url::Url;
#[cfg(not(target_arch = "wasm32"))]
use ewebsock::{WsEvent, WsMessage};
#[cfg(not(target_arch = "wasm32"))]
use tracing::{debug, error};
use super::subs_debug::SubsDebug;
#[derive(Debug)]
pub struct PoolEvent<'a> {
pub relay: &'a str,
pub event: ewebsock::WsEvent,
}
impl PoolEvent<'_> {
pub fn into_owned(self) -> PoolEventBuf {
PoolEventBuf {
relay: self.relay.to_owned(),
event: self.event,
}
}
}
pub struct PoolEventBuf {
pub relay: String,
pub event: ewebsock::WsEvent,
}
pub enum PoolRelay {
Websocket(WebsocketRelay),
Multicast(MulticastRelay),
}
pub struct WebsocketRelay {
pub relay: Relay,
pub last_ping: Instant,
pub last_connect_attempt: Instant,
pub retry_connect_after: Duration,
}
impl PoolRelay {
pub fn url(&self) -> &str {
match self {
Self::Websocket(wsr) => wsr.relay.url.as_str(),
Self::Multicast(_wsr) => "multicast",
}
}
pub fn set_status(&mut self, status: RelayStatus) {
match self {
Self::Websocket(wsr) => {
wsr.relay.status = status;
}
Self::Multicast(_mcr) => {}
}
}
pub fn try_recv(&self) -> Option<WsEvent> {
match self {
Self::Websocket(recvr) => recvr.relay.receiver.try_recv(),
Self::Multicast(recvr) => recvr.try_recv(),
}
}
pub fn status(&self) -> RelayStatus {
match self {
Self::Websocket(wsr) => wsr.relay.status,
Self::Multicast(mcr) => mcr.status,
}
}
pub fn send(&mut self, msg: &ClientMessage) -> Result<()> {
match self {
Self::Websocket(wsr) => {
wsr.relay.send(msg);
Ok(())
}
Self::Multicast(mcr) => {
// we only send event client messages at the moment
if let ClientMessage::Event(ecm) = msg {
mcr.send(ecm)?;
}
Ok(())
}
}
}
pub fn subscribe(&mut self, subid: String, filter: Vec<Filter>) -> Result<()> {
self.send(&ClientMessage::req(subid, filter))
}
pub fn websocket(relay: Relay) -> Self {
Self::Websocket(WebsocketRelay::new(relay))
}
pub fn multicast(wakeup: impl Fn() + Send + Sync + Clone + 'static) -> Result<Self> {
Ok(Self::Multicast(setup_multicast_relay(wakeup)?))
}
}
impl WebsocketRelay {
pub fn new(relay: Relay) -> Self {
Self {
relay,
last_ping: Instant::now(),
last_connect_attempt: Instant::now(),
retry_connect_after: Self::initial_reconnect_duration(),
}
}
pub fn initial_reconnect_duration() -> Duration {
Duration::from_secs(5)
}
}
pub struct RelayPool {
pub relays: Vec<PoolRelay>,
pub ping_rate: Duration,
pub debug: Option<SubsDebug>,
}
impl Default for RelayPool {
fn default() -> Self {
RelayPool::new()
}
}
impl RelayPool {
// Constructs a new, empty RelayPool.
pub fn new() -> RelayPool {
RelayPool {
relays: vec![],
ping_rate: Duration::from_secs(45),
debug: None,
}
}
pub fn add_multicast_relay(
&mut self,
wakeup: impl Fn() + Send + Sync + Clone + 'static,
) -> Result<()> {
let multicast_relay = PoolRelay::multicast(wakeup)?;
self.relays.push(multicast_relay);
Ok(())
}
pub fn use_debug(&mut self) {
self.debug = Some(SubsDebug::default());
}
pub fn ping_rate(&mut self, duration: Duration) -> &mut Self {
self.ping_rate = duration;
self
}
pub fn has(&self, url: &str) -> bool {
for relay in &self.relays {
if relay.url() == url {
return true;
}
}
false
}
pub fn urls(&self) -> BTreeSet<String> {
self.relays
.iter()
.map(|pool_relay| pool_relay.url().to_string())
.collect()
}
pub fn send(&mut self, cmd: &ClientMessage) {
for relay in &mut self.relays {
if let Some(debug) = &mut self.debug {
debug.send_cmd(relay.url().to_owned(), cmd);
}
if let Err(err) = relay.send(cmd) {
error!("error sending {:?} to {}: {err}", cmd, relay.url());
}
}
}
pub fn unsubscribe(&mut self, subid: String) {
for relay in &mut self.relays {
let cmd = ClientMessage::close(subid.clone());
if let Some(debug) = &mut self.debug {
debug.send_cmd(relay.url().to_owned(), &cmd);
}
if let Err(err) = relay.send(&cmd) {
error!(
"error unsubscribing from {} on {}: {err}",
&subid,
relay.url()
);
}
}
}
pub fn subscribe(&mut self, subid: String, filter: Vec<Filter>) {
for relay in &mut self.relays {
if let Some(debug) = &mut self.debug {
debug.send_cmd(
relay.url().to_owned(),
&ClientMessage::req(subid.clone(), filter.clone()),
);
}
if let Err(err) = relay.send(&ClientMessage::req(subid.clone(), filter.clone())) {
error!("error subscribing to {}: {err}", relay.url());
}
}
}
/// Keep relay connectiongs alive by pinging relays that haven't been
/// pinged in awhile. Adjust ping rate with [`ping_rate`].
pub fn keepalive_ping(&mut self, wakeup: impl Fn() + Send + Sync + Clone + 'static) {
for relay in &mut self.relays {
let now = std::time::Instant::now();
match relay {
PoolRelay::Multicast(_) => {}
PoolRelay::Websocket(relay) => {
match relay.relay.status {
RelayStatus::Disconnected => {
let reconnect_at =
relay.last_connect_attempt + relay.retry_connect_after;
if now > reconnect_at {
relay.last_connect_attempt = now;
let next_duration = Duration::from_millis(3000);
debug!(
"bumping reconnect duration from {:?} to {:?} and retrying connect",
relay.retry_connect_after, next_duration
);
relay.retry_connect_after = next_duration;
if let Err(err) = relay.relay.connect(wakeup.clone()) {
error!("error connecting to relay: {}", err);
}
} else {
// let's wait a bit before we try again
}
}
RelayStatus::Connected => {
relay.retry_connect_after =
WebsocketRelay::initial_reconnect_duration();
let should_ping = now - relay.last_ping > self.ping_rate;
if should_ping {
debug!("pinging {}", relay.relay.url);
relay.relay.ping();
relay.last_ping = Instant::now();
}
}
RelayStatus::Connecting => {
// cool story bro
}
}
}
}
}
}
pub fn send_to(&mut self, cmd: &ClientMessage, relay_url: &str) {
for relay in &mut self.relays {
if relay.url() == relay_url {
if let Some(debug) = &mut self.debug {
debug.send_cmd(relay.url().to_owned(), cmd);
}
if let Err(err) = relay.send(cmd) {
error!("send_to err: {err}");
}
return;
}
}
}
/// check whether a relay url is valid to add
pub fn is_valid_url(&self, url: &str) -> bool {
if url.is_empty() {
return false;
}
let url = match Url::parse(url) {
Ok(parsed_url) => parsed_url.to_string(),
Err(_err) => {
// debug!("bad relay url \"{}\": {:?}", url, err);
return false;
}
};
if self.has(&url) {
return false;
}
true
}
// Adds a websocket url to the RelayPool.
pub fn add_url(
&mut self,
url: String,
wakeup: impl Fn() + Send + Sync + Clone + 'static,
) -> Result<()> {
let url = Self::canonicalize_url(url);
// Check if the URL already exists in the pool.
if self.has(&url) {
return Ok(());
}
let relay = Relay::new(
nostr::RelayUrl::parse(url).map_err(|_| Error::InvalidRelayUrl)?,
wakeup,
)?;
let pool_relay = PoolRelay::websocket(relay);
self.relays.push(pool_relay);
Ok(())
}
pub fn add_urls(
&mut self,
urls: BTreeSet<String>,
wakeup: impl Fn() + Send + Sync + Clone + 'static,
) -> Result<()> {
for url in urls {
self.add_url(url, wakeup.clone())?;
}
Ok(())
}
pub fn remove_urls(&mut self, urls: &BTreeSet<String>) {
self.relays
.retain(|pool_relay| !urls.contains(pool_relay.url()));
}
// standardize the format (ie, trailing slashes)
fn canonicalize_url(url: String) -> String {
match Url::parse(&url) {
Ok(parsed_url) => parsed_url.to_string(),
Err(_) => url, // If parsing fails, return the original URL.
}
}
/// Attempts to receive a pool event from a list of relays. The
/// function searches each relay in the list in order, attempting to
/// receive a message from each. If a message is received, return it.
/// If no message is received from any relays, None is returned.
pub fn try_recv(&mut self) -> Option<PoolEvent<'_>> {
for relay in &mut self.relays {
if let PoolRelay::Multicast(mcr) = relay {
// try rejoin on multicast
if mcr.should_rejoin() {
if let Err(err) = mcr.rejoin() {
error!("multicast: rejoin error: {err}");
}
}
}
if let Some(event) = relay.try_recv() {
match &event {
WsEvent::Opened => {
relay.set_status(RelayStatus::Connected);
}
WsEvent::Closed => {
relay.set_status(RelayStatus::Disconnected);
}
WsEvent::Error(err) => {
error!("{:?}", err);
relay.set_status(RelayStatus::Disconnected);
}
WsEvent::Message(ev) => {
// let's just handle pongs here.
// We only need to do this natively.
#[cfg(not(target_arch = "wasm32"))]
if let WsMessage::Ping(ref bs) = ev {
debug!("pong {}", relay.url());
match relay {
PoolRelay::Websocket(wsr) => {
wsr.relay.sender.send(WsMessage::Pong(bs.to_owned()));
}
PoolRelay::Multicast(_mcr) => {}
}
}
}
}
if let Some(debug) = &mut self.debug {
debug.receive_cmd(relay.url().to_owned(), (&event).into());
}
let pool_event = PoolEvent {
event,
relay: relay.url(),
};
return Some(pool_event);
}
}
None
}
}

View File

@@ -0,0 +1,267 @@
use std::{collections::HashMap, mem, time::SystemTime};
use ewebsock::WsMessage;
use nostrdb::Filter;
use crate::{ClientMessage, Error, RelayEvent, RelayMessage};
use super::message::calculate_command_result_size;
type RelayId = String;
type SubId = String;
pub struct SubsDebug {
data: HashMap<RelayId, RelayStats>,
time_incd: SystemTime,
pub relay_events_selection: Option<RelayId>,
}
#[derive(Default)]
pub struct RelayStats {
pub count: TransferStats,
pub events: Vec<RelayLogEvent>,
pub sub_data: HashMap<SubId, SubStats>,
}
#[derive(Clone)]
pub enum RelayLogEvent {
Send(ClientMessage),
Recieve(OwnedRelayEvent),
}
#[derive(Clone)]
pub enum OwnedRelayEvent {
Opened,
Closed,
Other(String),
Error(String),
Message(String),
}
impl From<RelayEvent<'_>> for OwnedRelayEvent {
fn from(value: RelayEvent<'_>) -> Self {
match value {
RelayEvent::Opened => OwnedRelayEvent::Opened,
RelayEvent::Closed => OwnedRelayEvent::Closed,
RelayEvent::Other(ws_message) => {
let ws_str = match ws_message {
WsMessage::Binary(_) => "Binary".to_owned(),
WsMessage::Text(t) => format!("Text:{t}"),
WsMessage::Unknown(u) => format!("Unknown:{u}"),
WsMessage::Ping(_) => "Ping".to_owned(),
WsMessage::Pong(_) => "Pong".to_owned(),
};
OwnedRelayEvent::Other(ws_str)
}
RelayEvent::Error(error) => OwnedRelayEvent::Error(error.to_string()),
RelayEvent::Message(relay_message) => {
let relay_msg = match relay_message {
RelayMessage::OK(_) => "OK".to_owned(),
RelayMessage::Eose(s) => format!("EOSE:{s}"),
RelayMessage::Event(_, s) => format!("EVENT:{s}"),
RelayMessage::Notice(s) => format!("NOTICE:{s}"),
};
OwnedRelayEvent::Message(relay_msg)
}
}
}
}
#[derive(PartialEq, Eq, Hash, Clone)]
pub struct RelaySub {
pub(crate) subid: String,
pub(crate) filter: String,
}
#[derive(Default)]
pub struct SubStats {
pub filter: String,
pub count: TransferStats,
}
#[derive(Default)]
pub struct TransferStats {
pub up_total: usize,
pub down_total: usize,
// 1 sec < last tick < 2 sec
pub up_sec_prior: usize,
pub down_sec_prior: usize,
// < 1 sec since last tick
up_sec_cur: usize,
down_sec_cur: usize,
}
impl Default for SubsDebug {
fn default() -> Self {
Self {
data: Default::default(),
time_incd: SystemTime::now(),
relay_events_selection: None,
}
}
}
impl SubsDebug {
pub fn get_data(&self) -> &HashMap<RelayId, RelayStats> {
&self.data
}
pub(crate) fn send_cmd(&mut self, relay: String, cmd: &ClientMessage) {
let data = self.data.entry(relay).or_default();
let msg_num_bytes = calculate_client_message_size(cmd);
match cmd {
ClientMessage::Req { sub_id, filters } => {
data.sub_data.insert(
sub_id.to_string(),
SubStats {
filter: filters_to_string(filters),
count: Default::default(),
},
);
}
ClientMessage::Close { sub_id } => {
data.sub_data.remove(sub_id);
}
_ => {}
}
data.count.up_sec_cur += msg_num_bytes;
data.events.push(RelayLogEvent::Send(cmd.clone()));
}
pub(crate) fn receive_cmd(&mut self, relay: String, cmd: RelayEvent) {
let data = self.data.entry(relay).or_default();
let msg_num_bytes = calculate_relay_event_size(&cmd);
if let RelayEvent::Message(RelayMessage::Event(sid, _)) = cmd {
if let Some(sub_data) = data.sub_data.get_mut(sid) {
let c = &mut sub_data.count;
c.down_sec_cur += msg_num_bytes;
}
};
data.count.down_sec_cur += msg_num_bytes;
data.events.push(RelayLogEvent::Recieve(cmd.into()));
}
pub fn try_increment_stats(&mut self) {
let cur_time = SystemTime::now();
if let Ok(dur) = cur_time.duration_since(self.time_incd) {
if dur.as_secs() >= 1 {
self.time_incd = cur_time;
self.internal_inc_stats();
}
}
}
fn internal_inc_stats(&mut self) {
for relay_data in self.data.values_mut() {
let c = &mut relay_data.count;
inc_data_count(c);
for sub in relay_data.sub_data.values_mut() {
inc_data_count(&mut sub.count);
}
}
}
}
fn inc_data_count(c: &mut TransferStats) {
c.up_total += c.up_sec_cur;
c.up_sec_prior = c.up_sec_cur;
c.down_total += c.down_sec_cur;
c.down_sec_prior = c.down_sec_cur;
c.up_sec_cur = 0;
c.down_sec_cur = 0;
}
fn calculate_client_message_size(message: &ClientMessage) -> usize {
match message {
ClientMessage::Event(note) => note.note_json.len() + 10, // 10 is ["EVENT",]
ClientMessage::Req { sub_id, filters } => {
mem::size_of_val(message)
+ mem::size_of_val(sub_id)
+ sub_id.len()
+ filters.iter().map(mem::size_of_val).sum::<usize>()
}
ClientMessage::Close { sub_id } => {
mem::size_of_val(message) + mem::size_of_val(sub_id) + sub_id.len()
}
ClientMessage::Raw(data) => mem::size_of_val(message) + data.len(),
}
}
fn calculate_relay_event_size(event: &RelayEvent<'_>) -> usize {
let base_size = mem::size_of_val(event); // Size of the enum on the stack
let variant_size = match event {
RelayEvent::Opened | RelayEvent::Closed => 0, // No additional data
RelayEvent::Other(ws_message) => calculate_ws_message_size(ws_message),
RelayEvent::Error(error) => calculate_error_size(error),
RelayEvent::Message(message) => calculate_relay_message_size(message),
};
base_size + variant_size
}
fn calculate_ws_message_size(message: &WsMessage) -> usize {
match message {
WsMessage::Binary(vec) | WsMessage::Ping(vec) | WsMessage::Pong(vec) => {
mem::size_of_val(message) + vec.len()
}
WsMessage::Text(string) | WsMessage::Unknown(string) => {
mem::size_of_val(message) + string.len()
}
}
}
fn calculate_error_size(error: &Error) -> usize {
match error {
Error::Empty
| Error::HexDecodeFailed
| Error::InvalidBech32
| Error::InvalidByteSize
| Error::InvalidSignature
| Error::InvalidRelayUrl
| Error::Io(_)
| Error::InvalidPublicKey => mem::size_of_val(error), // No heap usage
Error::DecodeFailed(string) => mem::size_of_val(error) + string.len(),
Error::Json(json_err) => mem::size_of_val(error) + json_err.to_string().len(),
Error::Nostrdb(nostrdb_err) => mem::size_of_val(error) + nostrdb_err.to_string().len(),
Error::Generic(string) => mem::size_of_val(error) + string.len(),
}
}
fn calculate_relay_message_size(message: &RelayMessage) -> usize {
match message {
RelayMessage::OK(result) => calculate_command_result_size(result),
RelayMessage::Eose(str_ref)
| RelayMessage::Event(str_ref, _)
| RelayMessage::Notice(str_ref) => mem::size_of_val(message) + str_ref.len(),
}
}
fn filters_to_string(f: &Vec<Filter>) -> String {
let mut cur_str = String::new();
for filter in f {
if let Ok(json) = filter.json() {
if !cur_str.is_empty() {
cur_str.push_str(", ");
}
cur_str.push_str(&json);
}
}
cur_str
}

View File

@@ -0,0 +1,62 @@
[package]
name = "notedeck"
version = { workspace = true }
edition = "2021"
description = "The APIs and data structures used by notedeck apps"
[dependencies]
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 }
poll-promise = { workspace = true }
tracing = { workspace = true }
uuid = { workspace = true }
serde_json = { workspace = true }
serde = { workspace = true }
hex = { workspace = true }
thiserror = { workspace = true }
puffin = { workspace = true, optional = true }
puffin_egui = { workspace = true, optional = true }
sha2 = { workspace = true }
bincode = { workspace = true }
ehttp = {workspace = true }
mime_guess = { workspace = true }
egui-winit = { workspace = true }
tokenator = { workspace = true }
profiling = { workspace = true }
nwc = { workspace = true }
tokio = { workspace = true }
bech32 = { workspace = true }
lightning-invoice = { workspace = true }
secp256k1 = { workspace = true }
hashbrown = { workspace = true }
fluent = { workspace = true }
fluent-resmgr = { workspace = true }
fluent-langneg = { workspace = true }
unic-langid = { workspace = true }
sys-locale = { workspace = true }
once_cell = { workspace = true }
md5 = { workspace = true }
bitflags = { workspace = true }
regex = "1"
[dev-dependencies]
tempfile = { workspace = true }
tokio = { workspace = true }
[target.'cfg(target_os = "android")'.dependencies]
jni = { workspace = true }
[features]
puffin = ["puffin_egui", "dep:puffin"]

View File

@@ -0,0 +1,396 @@
# Notedeck Developer Documentation
This document provides technical details and guidance for developers working with the Notedeck crate.
## Architecture Overview
Notedeck is built around a modular architecture that separates concerns into distinct components:
### Core Components
1. **App Framework (`app.rs`)**
- `Notedeck` - The main application framework that ties everything together
- `App` - The trait that specific applications must implement
2. **Data Layer**
- `Ndb` - NostrDB database for efficient storage and querying
- `NoteCache` - In-memory cache for expensive-to-compute note data like nip10 structure
- `Images` - Image and GIF cache management
3. **Network Layer**
- `RelayPool` - Manages connections to Nostr relays
- `UnknownIds` - Tracks and resolves unknown profiles and notes
4. **User Accounts**
- `Accounts` - Manages user keypairs and account information
- `AccountStorage` - Handles persistent storage of account data
5. **Wallet Integration**
- `Wallet` - Lightning wallet integration
- `Zaps` - Handles Nostr zap functionality
6. **UI Components**
- `NotedeckTextStyle` - Text styling utilities
- `ColorTheme` - Theme management
- Various UI helpers
7. **Localization System**
- `LocalizationManager` - Core localization functionality
- `LocalizationContext` - Thread-safe context for sharing localization
- Fluent-based translation system
## Key Concepts
### Note Context and Actions
Notes have associated context and actions that define how users can interact with them:
```rust
pub enum NoteAction {
Reply(NoteId), // Reply to a note
Quote(NoteId), // Quote a note
Hashtag(String), // Click on a hashtag
Profile(Pubkey), // View a profile
Note(NoteId), // View a note
Context(ContextSelection), // Context menu options
Zap(ZapAction), // Zap (tip) interaction
}
```
### Relay Management
Notedeck handles relays through the `RelaySpec` structure, which implements NIP-65 functionality for marking relays as read or write.
### Filtering and Subscriptions
The `FilterState` enum manages the state of subscriptions to Nostr relays:
```rust
pub enum FilterState {
NeedsRemote(Vec<Filter>),
FetchingRemote(UnifiedSubscription),
GotRemote(Subscription),
Ready(Vec<Filter>),
Broken(FilterError),
}
```
## Development Workflow
### Setting Up Your Environment
1. Clone the repository
2. Build with `cargo build`
3. Test with `cargo test`
### Creating a New Notedeck App
1. Import the notedeck crate
2. Implement the `App` trait
3. Use the `Notedeck` struct as your application framework
Example:
```rust
use notedeck::{App, Notedeck, AppContext};
struct MyNostrApp {
// Your app-specific state
}
impl App for MyNostrApp {
fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) {
// Your app's UI and logic here
}
}
fn main() {
let notedeck = Notedeck::new(...).app(MyNostrApp { /* ... */ });
// Run your app
}
```
### Working with Notes
Notes are the core data structure in Nostr. Here's how to work with them:
```rust
// Get a note by ID
let txn = Transaction::new(&ndb).expect("txn");
if let Ok(note) = ndb.get_note_by_id(&txn, note_id.bytes()) {
// Process the note
}
// Create a cached note
let cached_note = note_cache.cached_note_or_insert(note_key, &note);
```
### Adding Account Management
Account management is handled through the `Accounts` struct:
```rust
// Add a new account
let action = accounts.add_account(keypair);
action.process_action(&mut unknown_ids, &ndb, &txn);
// Get the current account
if let Some(account) = accounts.get_selected_account() {
// Use the account
}
```
## Advanced Topics
### Zaps Implementation
Notedeck implements the zap (tipping) functionality according to the Nostr protocol:
1. Creates a zap request note (kind 9734)
2. Fetches a Lightning invoice via LNURL or LUD-16
3. Pays the invoice using a connected wallet
4. Tracks the zap state
### Image Caching
The image caching system efficiently manages images and animated GIFs:
1. Downloads images from URLs
2. Stores them in a local cache
3. Handles conversion between formats
4. Manages memory usage
### Persistent Storage
Notedeck provides several persistence mechanisms:
- `AccountStorage` - For user accounts
- `TimedSerializer` - For settings that need to be saved after a delay
- Various handlers for specific settings (zoom, theme, app size)
### Localization System
Notedeck includes a comprehensive internationalization system built on the [Fluent](https://projectfluent.org/) translation framework. The system is designed for performance and developer experience.
#### Architecture
The localization system consists of several key components:
1. **LocalizationManager** - Core functionality for managing locales and translations
2. **LocalizationContext** - Thread-safe context for sharing localization across the application
3. **Fluent Resources** - Translation files in `.ftl` format stored in `assets/translations/`
#### Key Features
- **Efficient Caching**: Parsed Fluent resources and formatted strings are cached for performance
- **Thread Safety**: Uses `RwLock` for safe concurrent access
- **Dynamic Locale Switching**: Change languages at runtime without restarting
- **Argument Support**: Localized strings can include dynamic arguments
- **Development Tools**: Pseudolocale support for testing UI layout
#### Using the tr! and tr_plural! Macros
The `tr!` and `tr_plural!` macros are the primary way to use localization in Notedeck code. They provide a convenient, type-safe interface for getting localized strings.
##### The tr! Macro
```rust
use notedeck::tr;
// Simple string with comment
let welcome = tr!("Welcome to Notedeck!", "Main welcome message");
let cancel = tr!("Cancel", "Button label to cancel an action");
// String with parameters
let greeting = tr!("Hello, {name}!", "Greeting message", name="Alice");
// Multiple parameters
let message = tr!(
"Welcome {name} to {app}!",
"Welcome message with app name",
name="Alice",
app="Notedeck"
);
// In UI components
ui.button(tr!("Reply to {user}", "Reply button text", user="alice@example.com"));
```
##### The tr_plural! Macro
Use tr_plural! when there can be multiple variations of the same string depending on
some numeric count.
Not all languages follow the same pluralization rules
```rust
use notedeck::tr_plural;
// Simple pluralization
let count = 5;
let message = tr_plural!(
"You have {count} note", // Singular form
"You have {count} notes", // Plural form
"Note count message", // Comment
count // Count value
);
// With additional parameters
let user = "Alice";
let message = tr_plural!(
"{user} has {count} note", // Singular
"{user} has {count} notes", // Plural
"User note count message", // Comment
count, // Count
user=user // Additional parameter
);
```
##### Key Features
- **Automatic Key Normalization**: Converts messages and comments into valid FTL keys
- **Fallback Handling**: Falls back to original message if translation not found
- **Parameter Interpolation**: Automatically handles named parameters
- **Comment Context**: Provides context for translators
##### Best Practices
1. **Always Include Comments**: Comments provide context for translators
```rust
// Good
tr!("Add", "Button label to add something")
// Bad
tr!("Add", "")
```
2. **Use Descriptive Comments**: Make comments specific and helpful
```rust
// Good
tr!("Reply", "Button to reply to a note")
// Bad
tr!("Reply", "Reply")
```
3. **Consistent Parameter Names**: Use consistent parameter names across related strings
```rust
// Consistent
tr!("Follow {user}", "Follow button", user="alice")
tr!("Unfollow {user}", "Unfollow button", user="alice")
```
4. **Always use tr_plural! for plural strings**: Not all languages follow English pluralization rules
```rust
// Good
// Each language can have more (or less) than just two pluralization forms.
// Let the translators and the localization system help you figure that out implicitly.
let message = tr_plural!(
"You have {count} note", // Singular form
"You have {count} notes", // Plural form
"Note count message", // Comment
count // Count value
);
// Bad
// Not all languages follow pluralization rules of English.
// Some languages can have more (or less) than two variations!
if count == 1 {
tr!("You have 1 note", "Note count message")
} else {
tr!("You have {count} notes", "Note count message")
}
```
#### Translation File Format
Translation files use the [Fluent](https://projectfluent.org/) format (`.ftl`).
Developers should never create their own `.ftl` files. Whenever user-facing strings are changed in code, run `python3 scripts/export_source_strings.py`. This script will generate `assets/translations/en-US/main.ftl` and `assets/translations/en-XA/main.ftl`. The format of the files look like the following:
```ftl
# Simple string
welcome_message = Welcome to Notedeck!
# String with arguments
welcome_user = Welcome {$name}!
# String with pluralization
note_count = {$count ->
[1] One note
*[other] {$count} notes
}
```
#### Adding New Languages
TODO
#### Development with Pseudolocale (en-XA)
For testing that all user-facing strings are going through the localization system and that the
UI layout renders well with different language translations, enable the pseudolocale:
```bash
cargo run -- --debug --locale en-XA
```
The pseudolocale (`en-XA`) transforms English text in a way that is still readable but makes adjustments obvious enough that they are different from the original text (such as replacing English letters with accented equivalents), helping identify potential UI layout issues once it gets translated
to other languages.
Example transformations:
- "Add relay" → "[Àdd rélày]"
- "Cancel" → "[Çàñçél]"
- "Confirm" → "[Çóñfírm]"
#### Performance Considerations
- **Resource Caching**: Parsed Fluent resources are cached per locale
- **String Caching**: Simple strings (without arguments) are cached for repeated access
- **Cache Management**: Caches are automatically cleared when switching locales
- **Memory Limits**: String cache size can be limited to prevent memory growth
#### Testing Localization
The localization system includes comprehensive tests:
```bash
# Run localization tests
cargo test i18n
```
## Troubleshooting
### Common Issues
1. **Relay Connection Issues**
- Check network connectivity
- Verify relay URLs are correct
- Look for relay debug messages
2. **Database Errors**
- Ensure the database path is writable
- Check for database corruption
- Increase map size if needed
3. **Performance Issues**
- Monitor the frame history
- Check for large image caches
- Consider reducing the number of active subscriptions
4. **Localization Issues**
- Verify translation files exist in the correct directory structure
- Check that locale codes are valid (e.g., `en-US`, `es-ES`)
- Ensure FTL files are properly formatted
- Look for missing translation keys in logs
## Contributing
When contributing to Notedeck:
1. Follow the existing code style
2. Add tests for new functionality
3. Update documentation as needed
4. Keep performance in mind, especially for mobile targets
5. For UI changes, test with pseudolocale enabled
6. When adding new strings, ensure they are properly localized

30
crates/notedeck/README.md Normal file
View File

@@ -0,0 +1,30 @@
# Notedeck
Notedeck is a shared Rust library that provides the core functionality for building Nostr client applications. It serves as the foundation for various Notedeck applications like notedeck_chrome, notedeck_columns, and notedeck_dave.
## Overview
The Notedeck crate implements common data types, utilities, and logic used across all Notedeck applications. It provides a unified interface for interacting with the Nostr protocol, managing accounts, handling note data, and rendering UI components.
Key features include:
- **Nostr Protocol Integration**: Connect to relays, subscribe to events, publish notes
- **Account Management**: Handle user accounts, keypairs, and profiles
- **Note Handling**: Cache and process notes efficiently
- **UI Components**: Common UI elements and styles
- **Image Caching**: Efficient image and GIF caching system
- **Wallet Integration**: Lightning wallet support with zaps functionality
- **Theme Support**: Customizable themes and styles
- **Storage**: Persistent storage for settings and data
## Applications
This crate serves as the foundation for several Notedeck applications:
- **notedeck_chrome** - The browser chrome, manages a toolbar for switching between different clients
- **notedeck_columns** - A column-based Nostr client interface
- **notedeck_dave** - A nostr ai assistant
## License
GPLv2

View File

@@ -0,0 +1,526 @@
use uuid::Uuid;
use crate::account::cache::AccountCache;
use crate::account::contacts::Contacts;
use crate::account::mute::AccountMutedData;
use crate::account::relay::{
modify_advertised_relays, update_relay_configuration, AccountRelayData, RelayAction,
RelayDefaults,
};
use crate::storage::AccountStorageWriter;
use crate::user_account::UserAccountSerializable;
use crate::{
AccountStorage, MuteFun, SingleUnkIdAction, UnifiedSubscription, UnknownIds, UserAccount,
ZapWallet,
};
use enostr::{ClientMessage, FilledKeypair, Keypair, Pubkey, RelayPool};
use nostrdb::{Ndb, Note, Transaction};
// TODO: remove this
use std::sync::Arc;
/// The interface for managing the user's accounts.
/// Represents all user-facing operations related to account management.
pub struct Accounts {
pub cache: AccountCache,
storage_writer: Option<AccountStorageWriter>,
relay_defaults: RelayDefaults,
subs: AccountSubs,
}
impl Accounts {
#[allow(clippy::too_many_arguments)]
pub fn new(
key_store: Option<AccountStorage>,
forced_relays: Vec<String>,
fallback: Pubkey,
ndb: &mut Ndb,
txn: &Transaction,
pool: &mut RelayPool,
ctx: &egui::Context,
unknown_ids: &mut UnknownIds,
) -> Self {
let (mut cache, unknown_id) = AccountCache::new(UserAccount::new(
Keypair::only_pubkey(fallback),
AccountData::new(fallback.bytes()),
));
unknown_id.process_action(unknown_ids, ndb, txn);
let mut storage_writer = None;
if let Some(keystore) = key_store {
let (reader, writer) = keystore.rw();
match reader.get_accounts() {
Ok(accounts) => {
for account in accounts {
add_account_from_storage(&mut cache, account).process_action(
unknown_ids,
ndb,
txn,
)
}
}
Err(e) => {
tracing::error!("could not get keys: {e}");
}
}
if let Some(selected) = reader.get_selected_key().ok().flatten() {
cache.select(selected);
}
storage_writer = Some(writer);
};
let relay_defaults = RelayDefaults::new(forced_relays);
let selected = cache.selected_mut();
let selected_data = &mut selected.data;
selected_data.query(ndb, txn);
let subs = {
AccountSubs::new(
ndb,
pool,
&relay_defaults,
&selected.key.pubkey,
selected_data,
create_wakeup(ctx),
)
};
Accounts {
cache,
storage_writer,
relay_defaults,
subs,
}
}
pub fn remove_account(
&mut self,
pk: &Pubkey,
ndb: &mut Ndb,
pool: &mut RelayPool,
ctx: &egui::Context,
) -> bool {
let Some(resp) = self.cache.remove(pk) else {
return false;
};
if pk != self.cache.fallback() {
if let Some(key_store) = &self.storage_writer {
if let Err(e) = key_store.remove_key(&resp.deleted) {
tracing::error!("Could not remove account {pk}: {e}");
}
}
}
if let Some(swap_to) = resp.swap_to {
let txn = Transaction::new(ndb).expect("txn");
self.select_account_internal(&swap_to, ndb, &txn, pool, ctx);
}
true
}
pub fn contains_full_kp(&self, pubkey: &enostr::Pubkey) -> bool {
self.cache
.get(pubkey)
.is_some_and(|u| u.key.secret_key.is_some())
}
#[must_use = "UnknownIdAction's must be handled. Use .process_unknown_id_action()"]
pub fn add_account(&mut self, kp: Keypair) -> Option<AddAccountResponse> {
let acc = if let Some(acc) = self.cache.get_mut(&kp.pubkey) {
if kp.secret_key.is_none() || acc.key.secret_key.is_some() {
tracing::info!("Already have account, not adding");
return None;
}
acc.key = kp.clone();
AccType::Acc(&*acc)
} else {
let new_account_data = AccountData::new(kp.pubkey.bytes());
AccType::Entry(
self.cache
.add(UserAccount::new(kp.clone(), new_account_data)),
)
};
if let Some(key_store) = &self.storage_writer {
if let Err(e) = key_store.write_account(&acc.get_acc().into()) {
tracing::error!("Could not add key for {:?}: {e}", kp.pubkey);
}
}
Some(AddAccountResponse {
switch_to: kp.pubkey,
unk_id_action: SingleUnkIdAction::pubkey(kp.pubkey),
})
}
/// Update the `UserAccount` via callback and save the result to disk.
/// return true if the update was successful
pub fn update_current_account(&mut self, update: impl FnOnce(&mut UserAccount)) -> bool {
let cur_account = self.get_selected_account_mut();
update(cur_account);
let cur_acc = self.get_selected_account();
let Some(key_store) = &self.storage_writer else {
return false;
};
if let Err(err) = key_store.write_account(&cur_acc.into()) {
tracing::error!("Could not add account {:?} to storage: {err}", cur_acc.key);
return false;
}
true
}
pub fn selected_filled(&self) -> Option<FilledKeypair<'_>> {
self.get_selected_account().key.to_full()
}
/// Get the selected account's pubkey as bytes. Common operation so
/// we make it a helper here.
pub fn selected_account_pubkey_bytes(&self) -> &[u8; 32] {
self.get_selected_account().key.pubkey.bytes()
}
pub fn selected_account_pubkey(&self) -> &Pubkey {
&self.get_selected_account().key.pubkey
}
pub fn get_selected_account(&self) -> &UserAccount {
self.cache.selected()
}
pub fn selected_account_has_wallet(&self) -> bool {
self.get_selected_account().wallet.is_some()
}
fn get_selected_account_mut(&mut self) -> &mut UserAccount {
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()
}
fn get_selected_account_data(&self) -> &AccountData {
&self.cache.selected().data
}
pub fn select_account(
&mut self,
pk_to_select: &Pubkey,
ndb: &mut Ndb,
txn: &Transaction,
pool: &mut RelayPool,
ctx: &egui::Context,
) {
if !self.cache.select(*pk_to_select) {
return;
}
self.select_account_internal(pk_to_select, ndb, txn, pool, ctx);
}
/// Have already selected in `AccountCache`, updating other things
fn select_account_internal(
&mut self,
pk_to_select: &Pubkey,
ndb: &mut Ndb,
txn: &Transaction,
pool: &mut RelayPool,
ctx: &egui::Context,
) {
if let Some(key_store) = &self.storage_writer {
if let Err(e) = key_store.select_key(Some(*pk_to_select)) {
tracing::error!("Could not select key {:?}: {e}", pk_to_select);
}
}
self.get_selected_account_mut().data.query(ndb, txn);
self.subs.swap_to(
ndb,
pool,
&self.relay_defaults,
pk_to_select,
&self.cache.selected().data,
create_wakeup(ctx),
);
}
pub fn mutefun(&self) -> Box<MuteFun> {
let account_data = self.get_selected_account_data();
let muted = Arc::clone(&account_data.muted.muted);
Box::new(move |note: &Note, thread: &[u8; 32]| muted.is_muted(note, thread))
}
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
pool.send_to(
&ClientMessage::req(
self.subs.relay.remote.clone(),
vec![data.relay.filter.clone()],
),
relay_url,
);
// send the active account's muted subscription
pool.send_to(
&ClientMessage::req(
self.subs.mute.remote.clone(),
vec![data.muted.filter.clone()],
),
relay_url,
);
pool.send_to(
&ClientMessage::req(
self.subs.contacts.remote.clone(),
vec![data.contacts.filter.clone()],
),
relay_url,
);
}
pub fn update(&mut self, ndb: &mut Ndb, pool: &mut RelayPool, ctx: &egui::Context) {
// IMPORTANT - This function is called in the UI update loop,
// make sure it is fast when idle
let Some(update) = self
.cache
.selected_mut()
.data
.poll_for_updates(ndb, &self.subs)
else {
return;
};
match update {
// If needed, update the relay configuration
AccountDataUpdate::Relay => {
let acc = self.cache.selected();
update_relay_configuration(
pool,
&self.relay_defaults,
&acc.key.pubkey,
&acc.data.relay,
create_wakeup(ctx),
);
}
}
}
pub fn get_full<'a>(&'a self, pubkey: &Pubkey) -> Option<FilledKeypair<'a>> {
self.cache.get(pubkey).and_then(|r| r.key.to_full())
}
pub fn process_relay_action(
&mut self,
ctx: &egui::Context,
pool: &mut RelayPool,
action: RelayAction,
) {
let acc = self.cache.selected_mut();
modify_advertised_relays(&acc.key, action, pool, &self.relay_defaults, &mut acc.data);
update_relay_configuration(
pool,
&self.relay_defaults,
&acc.key.pubkey,
&acc.data.relay,
create_wakeup(ctx),
);
}
pub fn get_subs(&self) -> &AccountSubs {
&self.subs
}
}
enum AccType<'a> {
Entry(hashbrown::hash_map::OccupiedEntry<'a, Pubkey, UserAccount>),
Acc(&'a UserAccount),
}
impl<'a> AccType<'a> {
fn get_acc(&'a self) -> &'a UserAccount {
match self {
AccType::Entry(occupied_entry) => occupied_entry.get(),
AccType::Acc(user_account) => user_account,
}
}
}
fn create_wakeup(ctx: &egui::Context) -> impl Fn() + Send + Sync + Clone + 'static {
let ctx = ctx.clone();
move || {
ctx.request_repaint();
}
}
fn add_account_from_storage(
cache: &mut AccountCache,
user_account_serializable: UserAccountSerializable,
) -> SingleUnkIdAction {
let Some(acc) = get_acc_from_storage(user_account_serializable) else {
return SingleUnkIdAction::NoAction;
};
let pk = acc.key.pubkey;
cache.add(acc);
SingleUnkIdAction::pubkey(pk)
}
fn get_acc_from_storage(user_account_serializable: UserAccountSerializable) -> Option<UserAccount> {
let keypair = user_account_serializable.key;
let new_account_data = AccountData::new(keypair.pubkey.bytes());
let mut wallet = None;
if let Some(wallet_s) = user_account_serializable.wallet {
let m_wallet: Result<crate::ZapWallet, crate::Error> = wallet_s.into();
match m_wallet {
Ok(w) => wallet = Some(w),
Err(e) => {
tracing::error!("Problem creating wallet from disk: {e}");
}
};
}
Some(UserAccount {
key: keypair,
wallet,
data: new_account_data,
})
}
#[derive(Clone)]
pub struct AccountData {
pub(crate) relay: AccountRelayData,
pub(crate) muted: AccountMutedData,
pub contacts: Contacts,
}
impl AccountData {
pub fn new(pubkey: &[u8; 32]) -> Self {
Self {
relay: AccountRelayData::new(pubkey),
muted: AccountMutedData::new(pubkey),
contacts: Contacts::new(pubkey),
}
}
pub(super) fn poll_for_updates(
&mut self,
ndb: &Ndb,
subs: &AccountSubs,
) -> Option<AccountDataUpdate> {
let txn = Transaction::new(ndb).expect("txn");
let mut resp = None;
if self.relay.poll_for_updates(ndb, &txn, subs.relay.local) {
resp = Some(AccountDataUpdate::Relay);
}
self.muted.poll_for_updates(ndb, &txn, subs.mute.local);
self.contacts
.poll_for_updates(ndb, &txn, subs.contacts.local);
resp
}
/// Note: query should be called as close to the subscription as possible
pub(super) fn query(&mut self, ndb: &Ndb, txn: &Transaction) {
self.relay.query(ndb, txn);
self.muted.query(ndb, txn);
self.contacts.query(ndb, txn);
}
}
pub(super) enum AccountDataUpdate {
Relay,
}
pub struct AddAccountResponse {
pub switch_to: Pubkey,
pub unk_id_action: SingleUnkIdAction,
}
pub struct AccountSubs {
relay: UnifiedSubscription,
mute: UnifiedSubscription,
pub contacts: UnifiedSubscription,
}
impl AccountSubs {
pub(super) fn new(
ndb: &mut Ndb,
pool: &mut RelayPool,
relay_defaults: &RelayDefaults,
pk: &Pubkey,
data: &AccountData,
wakeup: impl Fn() + Send + Sync + Clone + 'static,
) -> Self {
let relay = subscribe(ndb, pool, &data.relay.filter);
let mute = subscribe(ndb, pool, &data.muted.filter);
let contacts = subscribe(ndb, pool, &data.contacts.filter);
update_relay_configuration(pool, relay_defaults, pk, &data.relay, wakeup);
Self {
relay,
mute,
contacts,
}
}
pub(super) fn swap_to(
&mut self,
ndb: &mut Ndb,
pool: &mut RelayPool,
relay_defaults: &RelayDefaults,
pk: &Pubkey,
new_selection_data: &AccountData,
wakeup: impl Fn() + Send + Sync + Clone + 'static,
) {
unsubscribe(ndb, pool, &self.relay);
unsubscribe(ndb, pool, &self.mute);
unsubscribe(ndb, pool, &self.contacts);
*self = AccountSubs::new(ndb, pool, relay_defaults, pk, new_selection_data, wakeup);
}
}
fn subscribe(ndb: &Ndb, pool: &mut RelayPool, filter: &nostrdb::Filter) -> UnifiedSubscription {
let filters = vec![filter.clone()];
let sub = ndb
.subscribe(&filters)
.expect("ndb relay list subscription");
// remote subscription
let subid = Uuid::new_v4().to_string();
pool.subscribe(subid.clone(), filters);
UnifiedSubscription {
local: sub,
remote: subid,
}
}
fn unsubscribe(ndb: &mut Ndb, pool: &mut RelayPool, sub: &UnifiedSubscription) {
pool.unsubscribe(sub.remote.clone());
// local subscription
ndb.unsubscribe(sub.local)
.expect("ndb relay list unsubscribe");
}

View File

@@ -0,0 +1,126 @@
use enostr::Pubkey;
use hashbrown::{hash_map::OccupiedEntry, HashMap};
use crate::{SingleUnkIdAction, UserAccount};
pub struct AccountCache {
selected: Pubkey,
fallback: Pubkey,
fallback_account: UserAccount,
// never empty at rest
accounts: HashMap<Pubkey, UserAccount>,
}
impl AccountCache {
pub(super) fn new(fallback: UserAccount) -> (Self, SingleUnkIdAction) {
let mut accounts = HashMap::with_capacity(1);
let pk = fallback.key.pubkey;
accounts.insert(pk, fallback.clone());
(
Self {
selected: pk,
fallback: pk,
fallback_account: fallback,
accounts,
},
SingleUnkIdAction::pubkey(pk),
)
}
pub fn get(&self, pk: &Pubkey) -> Option<&UserAccount> {
self.accounts.get(pk)
}
pub fn get_bytes(&self, pk: &[u8; 32]) -> Option<&UserAccount> {
self.accounts.get(pk)
}
pub(super) fn get_mut(&mut self, pk: &Pubkey) -> Option<&mut UserAccount> {
self.accounts.get_mut(pk)
}
pub(super) fn add<'a>(
&'a mut self,
account: UserAccount,
) -> OccupiedEntry<'a, Pubkey, UserAccount> {
let pk = account.key.pubkey;
self.accounts.entry(pk).insert(account)
}
pub(super) fn remove(&mut self, pk: &Pubkey) -> Option<AccountDeletionResponse> {
if *pk == self.fallback && self.accounts.len() == 1 {
// no point in removing it since it'll just get re-added anyway
return None;
}
let removed = self.accounts.remove(pk)?;
if self.accounts.is_empty() {
self.accounts
.insert(self.fallback, self.fallback_account.clone());
}
if self.selected == *pk {
// TODO(kernelkind): choose next better
let (next, _) = self
.accounts
.iter()
.next()
.expect("accounts can never be empty");
self.selected = *next;
return Some(AccountDeletionResponse {
deleted: removed.key,
swap_to: Some(*next),
});
}
Some(AccountDeletionResponse {
deleted: removed.key,
swap_to: None,
})
}
/// guarenteed that all selected exist in accounts
pub(super) fn select(&mut self, pk: Pubkey) -> bool {
if !self.accounts.contains_key(&pk) {
return false;
}
self.selected = pk;
true
}
pub fn selected(&self) -> &UserAccount {
self.accounts
.get(&self.selected)
.expect("guarenteed that selected exists in accounts")
}
pub(super) fn selected_mut(&mut self) -> &mut UserAccount {
self.accounts
.get_mut(&self.selected)
.expect("guarenteed that selected exists in accounts")
}
pub fn fallback(&self) -> &Pubkey {
&self.fallback
}
}
impl<'a> IntoIterator for &'a AccountCache {
type Item = (&'a Pubkey, &'a UserAccount);
type IntoIter = hashbrown::hash_map::Iter<'a, Pubkey, UserAccount>;
fn into_iter(self) -> Self::IntoIter {
self.accounts.iter()
}
}
pub struct AccountDeletionResponse {
pub deleted: enostr::Keypair,
pub swap_to: Option<Pubkey>,
}

View File

@@ -0,0 +1,167 @@
use std::collections::HashSet;
use enostr::Pubkey;
use nostrdb::{Filter, Ndb, Note, NoteKey, Subscription, Transaction};
#[derive(Clone)]
pub struct Contacts {
pub filter: Filter,
pub(super) state: ContactState,
}
#[derive(Clone)]
pub enum ContactState {
Unreceived,
Received {
contacts: HashSet<Pubkey>,
note_key: NoteKey,
timestamp: u64,
},
}
#[derive(Eq, PartialEq, Debug, Clone, Copy)]
pub enum IsFollowing {
/// We don't have the contact list, so we don't know
Unknown,
/// We are follow
Yes,
No,
}
impl Contacts {
pub fn new(pubkey: &[u8; 32]) -> Self {
let filter = Filter::new().authors([pubkey]).kinds([3]).limit(1).build();
Self {
filter,
state: ContactState::Unreceived,
}
}
pub(super) fn query(&mut self, ndb: &Ndb, txn: &Transaction) {
let binding = ndb
.query(txn, &[self.filter.clone()], 1)
.expect("query user relays results");
let Some(res) = binding.first() else {
return;
};
update_state(&mut self.state, &res.note, res.note_key);
}
pub fn is_following(&self, other_pubkey: &[u8; 32]) -> IsFollowing {
match &self.state {
ContactState::Unreceived => IsFollowing::Unknown,
ContactState::Received {
contacts,
note_key: _,
timestamp: _,
} => {
if contacts.contains(other_pubkey) {
IsFollowing::Yes
} else {
IsFollowing::No
}
}
}
}
pub(super) fn poll_for_updates(&mut self, ndb: &Ndb, txn: &Transaction, sub: Subscription) {
let nks = ndb.poll_for_notes(sub, 1);
let Some(key) = nks.first() else {
return;
};
let note = match ndb.get_note_by_key(txn, *key) {
Ok(note) => note,
Err(e) => {
tracing::error!("Could not find note at key {:?}: {e}", key);
return;
}
};
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);
}
pub fn get_state(&self) -> &ContactState {
&self.state
}
}
fn update_state(state: &mut ContactState, note: &Note, key: NoteKey) {
match state {
ContactState::Unreceived => {
*state = ContactState::Received {
contacts: get_contacts_owned(note),
note_key: key,
timestamp: note.created_at(),
};
}
ContactState::Received {
contacts,
note_key,
timestamp,
} => {
update_contacts(contacts, note);
*note_key = key;
*timestamp = note.created_at();
}
};
}
fn get_contacts<'a>(note: &Note<'a>) -> HashSet<&'a [u8; 32]> {
let mut contacts = HashSet::with_capacity(note.tags().count().into());
for tag in note.tags() {
if tag.count() < 2 {
continue;
}
let Some("p") = tag.get_str(0) else {
continue;
};
let Some(cur_id) = tag.get_id(1) else {
continue;
};
contacts.insert(cur_id);
}
contacts
}
fn get_contacts_owned(note: &Note<'_>) -> HashSet<Pubkey> {
get_contacts(note)
.iter()
.map(|p| Pubkey::new(**p))
.collect()
}
fn update_contacts(cur: &mut HashSet<Pubkey>, new: &Note<'_>) {
let new_contacts = get_contacts(new);
cur.retain(|pk| new_contacts.contains(pk.bytes()));
new_contacts.iter().for_each(|c| {
if !cur.contains(*c) {
cur.insert(Pubkey::new(**c));
}
});
}

View File

@@ -0,0 +1,12 @@
pub mod accounts;
pub mod cache;
pub mod contacts;
pub mod mute;
pub mod relay;
pub const FALLBACK_PUBKEY: fn() -> enostr::Pubkey = || {
enostr::Pubkey::new([
170, 115, 48, 129, 228, 240, 247, 157, 212, 48, 35, 216, 152, 50, 101, 89, 63, 43, 65, 169,
136, 103, 28, 252, 239, 63, 72, 155, 145, 173, 147, 254,
])
};

View File

@@ -0,0 +1,99 @@
use std::sync::Arc;
use nostrdb::{Filter, Ndb, NoteKey, Subscription, Transaction};
use tracing::{debug, error};
use crate::Muted;
#[derive(Clone)]
pub(crate) struct AccountMutedData {
pub filter: Filter,
pub muted: Arc<Muted>,
}
impl AccountMutedData {
pub fn new(pubkey: &[u8; 32]) -> Self {
// Construct a filter for the user's NIP-51 muted list
let filter = Filter::new()
.authors([pubkey])
.kinds([10000])
.limit(1)
.build();
AccountMutedData {
filter,
muted: Arc::new(Muted::default()),
}
}
pub(super) fn query(&mut self, ndb: &Ndb, txn: &Transaction) {
// Query the ndb immediately to see if the user's muted list is already there
let lim = self
.filter
.limit()
.unwrap_or(crate::filter::default_limit()) as i32;
let nks = ndb
.query(txn, &[self.filter.clone()], lim)
.expect("query user muted results")
.iter()
.map(|qr| qr.note_key)
.collect::<Vec<NoteKey>>();
let muted = Self::harvest_nip51_muted(ndb, txn, &nks);
debug!("initial muted {:?}", muted);
self.muted = Arc::new(muted);
}
pub(crate) fn harvest_nip51_muted(ndb: &Ndb, txn: &Transaction, nks: &[NoteKey]) -> Muted {
let mut muted = Muted::default();
for nk in nks.iter() {
if let Ok(note) = ndb.get_note_by_key(txn, *nk) {
for tag in note.tags() {
match tag.get(0).and_then(|t| t.variant().str()) {
Some("p") => {
if let Some(id) = tag.get(1).and_then(|f| f.variant().id()) {
muted.pubkeys.insert(*id);
}
}
Some("t") => {
if let Some(str) = tag.get(1).and_then(|f| f.variant().str()) {
muted.hashtags.insert(str.to_string());
}
}
Some("word") => {
if let Some(str) = tag.get(1).and_then(|f| f.variant().str()) {
muted.words.insert(str.to_string());
}
}
Some("e") => {
if let Some(id) = tag.get(1).and_then(|f| f.variant().id()) {
muted.threads.insert(*id);
}
}
Some("alt") => {
// maybe we can ignore these?
}
Some(x) => error!("query_nip51_muted: unexpected tag: {}", x),
None => error!(
"query_nip51_muted: bad tag value: {:?}",
tag.get_unchecked(0).variant()
),
}
}
}
}
muted
}
pub(super) fn poll_for_updates(&mut self, ndb: &Ndb, txn: &Transaction, sub: Subscription) {
let nks = ndb.poll_for_notes(sub, 1);
if nks.is_empty() {
return;
}
let muted = AccountMutedData::harvest_nip51_muted(ndb, txn, &nks);
debug!("updated muted {:?}", muted);
self.muted = Arc::new(muted);
}
}

View File

@@ -0,0 +1,261 @@
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;
#[derive(Clone)]
pub(crate) struct AccountRelayData {
pub filter: Filter,
pub local: BTreeSet<RelaySpec>, // used locally but not advertised
pub advertised: BTreeSet<RelaySpec>, // advertised via NIP-65
}
impl AccountRelayData {
pub fn new(pubkey: &[u8; 32]) -> Self {
// Construct a filter for the user's NIP-65 relay list
let filter = Filter::new()
.authors([pubkey])
.kinds([10002])
.limit(1)
.build();
AccountRelayData {
filter,
local: BTreeSet::new(),
advertised: BTreeSet::new(),
}
}
pub fn query(&mut self, ndb: &Ndb, txn: &Transaction) {
// Query the ndb immediately to see if the user list is already there
let lim = self
.filter
.limit()
.unwrap_or(crate::filter::default_limit()) as i32;
let nks = ndb
.query(txn, &[self.filter.clone()], lim)
.expect("query user relays results")
.iter()
.map(|qr| qr.note_key)
.collect::<Vec<NoteKey>>();
let relays = Self::harvest_nip65_relays(ndb, txn, &nks);
debug!("initial relays {:?}", relays);
self.advertised = relays.into_iter().collect()
}
// standardize the format (ie, trailing slashes) to avoid dups
pub fn canonicalize_url(url: &str) -> String {
match Url::parse(url) {
Ok(parsed_url) => parsed_url.to_string(),
Err(_) => url.to_owned(), // If parsing fails, return the original URL.
}
}
pub(crate) fn harvest_nip65_relays(
ndb: &Ndb,
txn: &Transaction,
nks: &[NoteKey],
) -> Vec<RelaySpec> {
let mut relays = Vec::new();
for nk in nks.iter() {
if let Ok(note) = ndb.get_note_by_key(txn, *nk) {
for tag in note.tags() {
match tag.get(0).and_then(|t| t.variant().str()) {
Some("r") => {
if let Some(url) = tag.get(1).and_then(|f| f.variant().str()) {
let has_read_marker = tag
.get(2)
.is_some_and(|m| m.variant().str() == Some("read"));
let has_write_marker = tag
.get(2)
.is_some_and(|m| m.variant().str() == Some("write"));
relays.push(RelaySpec::new(
Self::canonicalize_url(url),
has_read_marker,
has_write_marker,
));
}
}
Some("alt") => {
// ignore for now
}
Some(x) => {
error!("harvest_nip65_relays: unexpected tag type: {}", x);
}
None => {
error!("harvest_nip65_relays: invalid tag");
}
}
}
}
}
relays
}
pub fn publish_nip65_relays(&self, seckey: &[u8; 32], pool: &mut RelayPool) {
let mut builder = NoteBuilder::new().kind(10002).content("");
for rs in &self.advertised {
builder = builder.start_tag().tag_str("r").tag_str(&rs.url);
if rs.has_read_marker {
builder = builder.tag_str("read");
} else if rs.has_write_marker {
builder = builder.tag_str("write");
}
}
let note = builder.sign(seckey).build().expect("note build");
pool.send(&enostr::ClientMessage::event(&note).expect("note client message"));
}
pub fn poll_for_updates(&mut self, ndb: &Ndb, txn: &Transaction, sub: Subscription) -> bool {
let nks = ndb.poll_for_notes(sub, 1);
if nks.is_empty() {
return false;
}
let relays = AccountRelayData::harvest_nip65_relays(ndb, txn, &nks);
debug!("updated relays {:?}", relays);
self.advertised = relays.into_iter().collect();
true
}
}
pub(crate) struct RelayDefaults {
pub forced_relays: BTreeSet<RelaySpec>,
pub bootstrap_relays: BTreeSet<RelaySpec>,
}
impl RelayDefaults {
pub(crate) fn new(forced_relays: Vec<String>) -> Self {
let forced_relays: BTreeSet<RelaySpec> = forced_relays
.into_iter()
.map(|u| RelaySpec::new(AccountRelayData::canonicalize_url(&u), false, false))
.collect();
let bootstrap_relays = [
"wss://relay.damus.io",
// "wss://pyramid.fiatjaf.com", // Uncomment if needed
"wss://nos.lol",
"wss://nostr.wine",
"wss://purplepag.es",
]
.iter()
.map(|&url| url.to_string())
.map(|u| RelaySpec::new(AccountRelayData::canonicalize_url(&u), false, false))
.collect();
Self {
forced_relays,
bootstrap_relays,
}
}
}
pub(super) fn update_relay_configuration(
pool: &mut RelayPool,
relay_defaults: &RelayDefaults,
pk: &Pubkey,
data: &AccountRelayData,
wakeup: impl Fn() + Send + Sync + Clone + 'static,
) {
debug!(
"updating relay configuration for currently selected {:?}",
pk.hex()
);
// If forced relays are set use them only
let mut desired_relays = relay_defaults.forced_relays.clone();
// Compose the desired relay lists from the selected account
if desired_relays.is_empty() {
desired_relays.extend(data.local.iter().cloned());
desired_relays.extend(data.advertised.iter().cloned());
}
// If no relays are specified at this point use the bootstrap list
if desired_relays.is_empty() {
desired_relays = relay_defaults.bootstrap_relays.clone();
}
debug!("current relays: {:?}", pool.urls());
debug!("desired relays: {:?}", desired_relays);
let pool_specs = pool
.urls()
.iter()
.map(|url| RelaySpec::new(url.clone(), false, false))
.collect();
let add: BTreeSet<RelaySpec> = desired_relays.difference(&pool_specs).cloned().collect();
let mut sub: BTreeSet<RelaySpec> = pool_specs.difference(&desired_relays).cloned().collect();
if !add.is_empty() {
debug!("configuring added relays: {:?}", add);
let _ = pool.add_urls(add.iter().map(|r| r.url.clone()).collect(), wakeup);
}
if !sub.is_empty() {
// certain relays are persistent like the multicast relay,
// although we should probably have a way to explicitly
// disable it
sub.remove(&RelaySpec::new("multicast", false, false));
debug!("removing unwanted relays: {:?}", sub);
pool.remove_urls(&sub.iter().map(|r| r.url.clone()).collect());
}
debug!("current relays: {:?}", pool.urls());
}
pub enum RelayAction {
Add(String),
Remove(String),
}
impl RelayAction {
pub(super) fn get_url(&self) -> &str {
match self {
RelayAction::Add(url) => url,
RelayAction::Remove(url) => url,
}
}
}
pub(super) fn modify_advertised_relays(
kp: &Keypair,
action: RelayAction,
pool: &mut RelayPool,
relay_defaults: &RelayDefaults,
account_data: &mut AccountData,
) {
let relay_url = AccountRelayData::canonicalize_url(action.get_url());
match action {
RelayAction::Add(_) => info!("add advertised relay \"{}\"", relay_url),
RelayAction::Remove(_) => info!("remove advertised relay \"{}\"", relay_url),
}
// let selected = self.cache.selected_mut();
let advertised = &mut account_data.relay.advertised;
if advertised.is_empty() {
// If the selected account has no advertised relays,
// initialize with the bootstrapping set.
advertised.extend(relay_defaults.bootstrap_relays.iter().cloned());
}
match action {
RelayAction::Add(_) => {
advertised.insert(RelaySpec::new(relay_url, false, false));
}
RelayAction::Remove(_) => {
advertised.remove(&RelaySpec::new(relay_url, false, false));
}
}
// If we have the secret key publish the NIP-65 relay list
if let Some(secretkey) = &kp.secret_key {
account_data
.relay
.publish_nip65_relays(&secretkey.to_secret_bytes(), pool);
}
}

364
crates/notedeck/src/app.rs Normal file
View File

@@ -0,0 +1,364 @@
use crate::account::FALLBACK_PUBKEY;
use crate::i18n::Localization;
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, UnknownIds,
};
use egui::Margin;
use egui::ThemePreference;
use egui_winit::clipboard::Clipboard;
use enostr::RelayPool;
use nostrdb::{Config, Ndb, Transaction};
use std::cell::RefCell;
use std::collections::BTreeSet;
use std::path::Path;
use std::rc::Rc;
use tracing::{error, info};
use unic_langid::{LanguageIdentifier, LanguageIdentifierError};
pub enum AppAction {
Note(NoteAction),
ToggleChrome,
}
pub trait App {
fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction>;
}
/// Main notedeck app framework
pub struct Notedeck {
ndb: Ndb,
img_cache: Images,
unknown_ids: UnknownIds,
pool: RelayPool,
note_cache: NoteCache,
accounts: Accounts,
global_wallet: GlobalWallet,
path: DataPath,
args: Args,
settings: SettingsHandler,
app: Option<Rc<RefCell<dyn App>>>,
app_size: AppSizeHandler,
unrecognized_args: BTreeSet<String>,
clipboard: Clipboard,
zaps: Zaps,
frame_history: FrameHistory,
job_pool: JobPool,
i18n: Localization,
}
/// Our chrome, which is basically nothing
fn main_panel(style: &egui::Style) -> egui::CentralPanel {
egui::CentralPanel::default().frame(egui::Frame {
inner_margin: Margin::ZERO,
fill: style.visuals.panel_fill,
..Default::default()
})
}
fn render_notedeck(notedeck: &mut Notedeck, ctx: &egui::Context) {
main_panel(&ctx.style()).show(ctx, |ui| {
// render app
let Some(app) = &notedeck.app else {
return;
};
let app = app.clone();
app.borrow_mut().update(&mut notedeck.app_context(), ui);
// Move the screen up when we have a virtual keyboard
// NOTE: actually, we only want to do this if the keyboard is covering the focused element?
/*
let keyboard_height = crate::platform::virtual_keyboard_height() as f32;
if keyboard_height > 0.0 {
ui.ctx().transform_layer_shapes(
ui.layer_id(),
egui::emath::TSTransform::from_translation(egui::Vec2::new(0.0, -(keyboard_height/2.0))),
);
}
*/
});
}
impl eframe::App for Notedeck {
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
profiling::finish_frame!();
self.frame_history
.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
// handle account updates
self.accounts.update(&mut self.ndb, &mut self.pool, ctx);
self.zaps
.process(&mut self.accounts, &mut self.global_wallet, &self.ndb);
render_notedeck(self, 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.options.contains(NotedeckOptions::RelayDebug) {
if self.pool.debug.is_none() {
self.pool.use_debug();
}
if let Some(debug) = &mut self.pool.debug {
RelayDebugView::window(ctx, debug);
}
}
#[cfg(feature = "puffin")]
puffin_egui::profiler_window(ctx);
}
/// Called by the framework to save state before shutdown.
fn save(&mut self, _storage: &mut dyn eframe::Storage) {
//eframe::set_value(storage, eframe::APP_KEY, self);
}
}
#[cfg(feature = "puffin")]
fn setup_puffin() {
info!("setting up puffin");
puffin::set_scopes_on(true); // tell puffin to collect data
}
impl Notedeck {
pub fn new<P: AsRef<Path>>(ctx: &egui::Context, data_path: P, args: &[String]) -> Self {
#[cfg(feature = "puffin")]
setup_puffin();
// Skip the first argument, which is the program name.
let (parsed_args, unrecognized_args) = Args::parse(&args[1..]);
let data_path = parsed_args
.datapath
.clone()
.unwrap_or(data_path.as_ref().to_str().expect("db path ok").to_string());
let path = DataPath::new(&data_path);
let dbpath_str = parsed_args
.dbpath
.clone()
.unwrap_or_else(|| path.path(DataPathType::Db).to_str().unwrap().to_string());
let _ = std::fs::create_dir_all(&dbpath_str);
let img_cache_dir = path.path(DataPathType::Cache);
let _ = std::fs::create_dir_all(img_cache_dir.clone());
let map_size = if cfg!(target_os = "windows") {
// 16 Gib on windows because it actually creates the file
1024usize * 1024usize * 1024usize * 16usize
} else {
// 1 TiB for everything else since its just virtually mapped
1024usize * 1024usize * 1024usize * 1024usize
};
let settings = SettingsHandler::new(&path).load();
let config = Config::new().set_ingester_threads(2).set_mapsize(map_size);
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(
Directory::new(keys_path),
Directory::new(selected_key_path),
))
} else {
None
};
// AccountManager will setup the pool on first update
let mut pool = RelayPool::new();
{
let ctx = ctx.clone();
if let Err(err) = pool.add_multicast_relay(move || ctx.request_repaint()) {
error!("error setting up multicast relay: {err}");
}
}
let mut unknown_ids = UnknownIds::default();
let mut ndb = Ndb::new(&dbpath_str, &config).expect("ndb");
let txn = Transaction::new(&ndb).expect("txn");
let mut accounts = Accounts::new(
keystore,
parsed_args.relays.clone(),
FALLBACK_PUBKEY(),
&mut ndb,
&txn,
&mut pool,
ctx,
&mut unknown_ids,
);
{
for key in &parsed_args.keys {
info!("adding account: {}", &key.pubkey);
if let Some(resp) = accounts.add_account(key.clone()) {
resp.unk_id_action
.process_action(&mut unknown_ids, &ndb, &txn);
}
}
}
if let Some(first) = parsed_args.keys.first() {
accounts.select_account(&first.pubkey, &mut ndb, &txn, &mut pool, ctx);
}
let img_cache = Images::new(img_cache_dir);
let note_cache = NoteCache::default();
let app_size = AppSizeHandler::new(&path);
// migrate
if let Err(e) = img_cache.migrate_v0() {
error!("error migrating image cache: {e}");
}
let global_wallet = GlobalWallet::new(&path);
let zaps = Zaps::default();
let job_pool = JobPool::default();
// Initialize localization
let mut i18n = Localization::new();
let setting_locale: Result<LanguageIdentifier, LanguageIdentifierError> =
settings.locale().parse();
if setting_locale.is_ok() {
if let Err(err) = i18n.set_locale(setting_locale.unwrap()) {
error!("{err}");
}
}
if let Some(locale) = &parsed_args.locale {
if let Err(err) = i18n.set_locale(locale.to_owned()) {
error!("{err}");
}
}
Self {
ndb,
img_cache,
unknown_ids,
pool,
note_cache,
accounts,
global_wallet,
path: path.clone(),
args: parsed_args,
settings,
app: None,
app_size,
unrecognized_args,
frame_history: FrameHistory::default(),
clipboard: Clipboard::new(None),
zaps,
job_pool,
i18n,
}
}
/// 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
}
pub fn app_context(&mut self) -> AppContext<'_> {
AppContext {
ndb: &mut self.ndb,
img_cache: &mut self.img_cache,
unknown_ids: &mut self.unknown_ids,
pool: &mut self.pool,
note_cache: &mut self.note_cache,
accounts: &mut self.accounts,
global_wallet: &mut self.global_wallet,
path: &self.path,
args: &self.args,
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,
}
}
pub fn set_app<T: App + 'static>(&mut self, app: T) {
self.app = Some(Rc::new(RefCell::new(app)));
}
pub fn args(&self) -> &Args {
&self.args
}
pub fn theme(&self) -> ThemePreference {
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> {
&self.unrecognized_args
}
}

140
crates/notedeck/src/args.rs Normal file
View File

@@ -0,0 +1,140 @@
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 locale: Option<LanguageIdentifier>,
pub keys: Vec<Keypair>,
pub options: NotedeckOptions,
pub dbpath: Option<String>,
pub datapath: Option<String>,
}
impl Args {
// parse arguments, return set of unrecognized args
pub fn parse(args: &[String]) -> (Self, BTreeSet<String>) {
let mut unrecognized_args = BTreeSet::new();
let mut res = Args {
relays: vec![],
keys: vec![],
options: NotedeckOptions::default(),
dbpath: None,
datapath: None,
locale: None,
};
let mut i = 0;
let len = args.len();
while i < len {
let arg = &args[i];
if arg == "--mobile" {
res.options.set(NotedeckOptions::Mobile, true);
} else if arg == "--light" {
res.options.set(NotedeckOptions::LightTheme, true);
} else if arg == "--locale" {
i += 1;
let Some(locale) = args.get(i) else {
panic!("locale argument missing?");
};
let parsed: Result<LanguageIdentifier, LanguageIdentifierError> = locale.parse();
match parsed {
Err(err) => {
panic!("locale failed to parse: {err}");
}
Ok(locale) => {
tracing::info!(
"parsed locale '{locale}' from args, not sure if we have it yet though."
);
res.locale = Some(locale);
}
}
} else if arg == "--dark" {
res.options.set(NotedeckOptions::LightTheme, false);
} else if arg == "--debug" {
res.options.set(NotedeckOptions::Debug, true);
} else if arg == "--testrunner" {
res.options.set(NotedeckOptions::Tests, true);
} else if arg == "--pub" || arg == "--npub" {
i += 1;
let pubstr = if let Some(next_arg) = args.get(i) {
next_arg
} else {
error!("sec argument missing?");
continue;
};
if let Ok(pk) = Pubkey::parse(pubstr) {
res.keys.push(Keypair::only_pubkey(pk));
} else {
error!(
"failed to parse {} argument. Make sure to use hex or npub.",
arg
);
}
} else if arg == "--sec" || arg == "--nsec" {
i += 1;
let secstr = if let Some(next_arg) = args.get(i) {
next_arg
} else {
error!("sec argument missing?");
continue;
};
if let Ok(sec) = SecretKey::parse(secstr) {
res.keys.push(Keypair::from_secret(sec));
} else {
error!(
"failed to parse {} argument. Make sure to use hex or nsec.",
arg
);
}
} else if arg == "--dbpath" {
i += 1;
let path = if let Some(next_arg) = args.get(i) {
next_arg
} else {
error!("dbpath argument missing?");
continue;
};
res.dbpath = Some(path.clone());
} else if arg == "--datapath" {
i += 1;
let path = if let Some(next_arg) = args.get(i) {
next_arg
} else {
error!("datapath argument missing?");
continue;
};
res.datapath = Some(path.clone());
} else if arg == "-r" || arg == "--relay" {
i += 1;
let relay = if let Some(next_arg) = args.get(i) {
next_arg
} else {
error!("relay argument missing?");
continue;
};
res.relays.push(relay.clone());
} else if arg == "--no-keystore" {
res.options.set(NotedeckOptions::UseKeystore, true);
} else if arg == "--relay-debug" {
res.options.set(NotedeckOptions::RelayDebug, true);
} else if arg == "--show-client" {
res.options.set(NotedeckOptions::ShowClient, true);
} else if arg == "--notebook" {
res.options.set(NotedeckOptions::FeatureNotebook, true);
} else {
unrecognized_args.insert(arg.clone());
}
i += 1;
}
(res, unrecognized_args)
}
}

View File

@@ -0,0 +1,24 @@
use crate::{
filter::{self, HybridFilter},
Error,
};
use nostrdb::{Filter, Note};
pub fn contacts_filter(pk: &[u8; 32]) -> Filter {
Filter::new().authors([pk]).kinds([3]).limit(1).build()
}
/// Contact filters have an additional kind0 in the remote filter so it can fetch profiles as well
/// we don't need this in the local filter since we only care about the kind1 results
pub fn hybrid_contacts_filter(
note: &Note,
add_pk: Option<&[u8; 32]>,
with_hashtags: bool,
) -> Result<HybridFilter, Error> {
let local = filter::filter_from_tags(note, add_pk, with_hashtags)?
.into_filter([1], filter::default_limit());
let remote = filter::filter_from_tags(note, add_pk, with_hashtags)?
.into_filter([1, 0], filter::default_remote_limit());
Ok(HybridFilter::split(local, remote))
}

View File

@@ -0,0 +1,29 @@
use crate::{
account::accounts::Accounts, frame_history::FrameHistory, i18n::Localization,
wallet::GlobalWallet, zaps::Zaps, Args, DataPath, Images, JobPool, NoteCache, SettingsHandler,
UnknownIds,
};
use egui_winit::clipboard::Clipboard;
use enostr::RelayPool;
use nostrdb::Ndb;
// TODO: make this interface more sandboxed
pub struct AppContext<'a> {
pub ndb: &'a mut Ndb,
pub img_cache: &'a mut Images,
pub unknown_ids: &'a mut UnknownIds,
pub pool: &'a mut RelayPool,
pub note_cache: &'a mut NoteCache,
pub accounts: &'a mut Accounts,
pub global_wallet: &'a mut GlobalWallet,
pub path: &'a DataPath,
pub args: &'a Args,
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,
}

View File

@@ -0,0 +1,35 @@
use std::time::{Duration, Instant};
/// A simple debouncer that tracks when an action was last performed
/// and determines if enough time has passed to perform it again.
#[derive(Debug)]
pub struct Debouncer {
delay: Duration,
last_action: Instant,
}
impl Debouncer {
/// Creates a new Debouncer with the specified delay
pub fn new(delay: Duration) -> Self {
Self {
delay,
last_action: Instant::now() - delay, // Start ready to act
}
}
/// Sets a new delay value and returns self for method chaining
pub fn with_delay(mut self, delay: Duration) -> Self {
self.delay = delay;
self
}
/// Checks if enough time has passed since the last action
pub fn should_act(&self) -> bool {
self.last_action.elapsed() >= self.delay
}
/// Marks an action as performed, updating the timestamp
pub fn bounce(&mut self) {
self.last_action = Instant::now();
}
}

View File

@@ -0,0 +1,94 @@
use std::io;
/// App related errors
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("image error: {0}")]
Image(#[from] image::error::ImageError),
#[error("io error: {0}")]
Io(#[from] io::Error),
#[error("subscription error: {0}")]
SubscriptionError(SubscriptionError),
#[error("filter error: {0}")]
Filter(FilterError),
#[error("json error: {0}")]
Json(#[from] serde_json::Error),
#[error("io error: {0}")]
Nostrdb(#[from] nostrdb::Error),
#[error("generic error: {0}")]
Generic(String),
#[error("zaps error: {0}")]
Zap(#[from] ZapError),
}
#[derive(Debug, thiserror::Error, Clone)]
pub enum ZapError {
#[error("invalid lud16")]
InvalidLud16(String),
#[error("invalid endpoint response")]
EndpointError(String),
#[error("bech encoding/decoding error")]
Bech(String),
#[error("serialization/deserialization problem")]
Serialization(String),
#[error("nwc error")]
NWC(String),
}
impl From<String> for Error {
fn from(s: String) -> Self {
Error::Generic(s)
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, thiserror::Error)]
pub enum FilterError {
#[error("empty contact list")]
EmptyContactList,
#[error("filter not ready")]
FilterNotReady,
}
#[derive(Debug, Eq, PartialEq, Copy, Clone, thiserror::Error)]
pub enum SubscriptionError {
#[error("no active subscriptions")]
NoActive,
/// When a timeline has an unexpected number
/// of active subscriptions. Should only happen if there
/// is a bug in notedeck
#[error("unexpected subscription count")]
UnexpectedSubscriptionCount(i32),
}
impl Error {
pub fn unexpected_sub_count(c: i32) -> Self {
Error::SubscriptionError(SubscriptionError::UnexpectedSubscriptionCount(c))
}
pub fn no_active_sub() -> Self {
Error::SubscriptionError(SubscriptionError::NoActive)
}
pub fn empty_contact_list() -> Self {
Error::Filter(FilterError::EmptyContactList)
}
}
pub fn show_one_error_message(ui: &mut egui::Ui, message: &str) {
let id = ui.id().with(("error", message));
let res: Option<()> = ui.ctx().data(|d| d.get_temp(id));
if res.is_none() {
ui.ctx().data_mut(|d| d.insert_temp(id, ()));
tracing::error!(message);
}
}

View File

@@ -1,6 +1,5 @@
use crate::error::{Error, FilterError};
use crate::note::NoteRef;
use crate::Result;
use nostrdb::{Filter, FilterBuilder, Note, Subscription};
use std::collections::HashMap;
use tracing::{debug, warn};
@@ -24,7 +23,7 @@ pub struct FilterStates {
}
impl FilterStates {
pub fn get(&mut self, relay: &str) -> &FilterState {
pub fn get_mut(&mut self, relay: &str) -> &FilterState {
// if our initial state is ready, then just use that
if let FilterState::Ready(_) = self.initial_state {
&self.initial_state
@@ -38,17 +37,25 @@ impl FilterStates {
}
}
pub fn get_any_gotremote(&self) -> Option<(&str, Subscription)> {
pub fn get_any_gotremote(&self) -> Option<GotRemoteResult> {
for (k, v) in self.states.iter() {
if let FilterState::GotRemote(sub) = v {
return Some((k, *sub));
if let FilterState::GotRemote(item_type) = v {
return match item_type {
GotRemoteType::Normal(subscription) => Some(GotRemoteResult::Normal {
relay_id: k.to_owned(),
sub_id: *subscription,
}),
GotRemoteType::Contact => Some(GotRemoteResult::Contact {
relay_id: k.to_owned(),
}),
};
}
}
None
}
pub fn get_any_ready(&self) -> Option<&Vec<Filter>> {
pub fn get_any_ready(&self) -> Option<&HybridFilter> {
if let FilterState::Ready(fs) = &self.initial_state {
Some(fs)
} else {
@@ -85,13 +92,35 @@ impl FilterStates {
/// [`FilterState`] tracks this.
#[derive(Debug, Clone)]
pub enum FilterState {
NeedsRemote(Vec<Filter>),
FetchingRemote(UnifiedSubscription),
GotRemote(Subscription),
Ready(Vec<Filter>),
NeedsRemote,
FetchingRemote(FetchingRemoteType),
GotRemote(GotRemoteType),
Ready(HybridFilter),
Broken(FilterError),
}
pub enum GotRemoteResult {
Normal {
relay_id: String,
sub_id: Subscription,
},
Contact {
relay_id: String,
},
}
#[derive(Debug, Clone)]
pub enum FetchingRemoteType {
Normal(UnifiedSubscription),
Contact,
}
#[derive(Debug, Clone)]
pub enum GotRemoteType {
Normal(Subscription),
Contact,
}
impl FilterState {
/// We tried to fetch a filter but we wither got no data or the data
/// was corrupted, preventing us from getting to the Ready state.
@@ -103,6 +132,17 @@ impl FilterState {
/// The filter is ready
pub fn ready(filter: Vec<Filter>) -> Self {
Self::Ready(HybridFilter::unsplit(filter))
}
/// The filter is ready, but we have a different local filter from
/// our remote one
pub fn ready_split(local: Vec<Filter>, remote: Vec<Filter>) -> Self {
Self::Ready(HybridFilter::split(local, remote))
}
/// Our hybrid filter is ready (either split or unsplit)
pub fn ready_hybrid(filter: HybridFilter) -> Self {
Self::Ready(filter)
}
@@ -110,14 +150,14 @@ impl FilterState {
/// for home timelines where we don't have a contact list yet. We
/// need to fetch the contact list before we have the right timeline
/// filter.
pub fn needs_remote(filter: Vec<Filter>) -> Self {
Self::NeedsRemote(filter)
pub fn needs_remote() -> Self {
Self::NeedsRemote
}
/// We got the remote data. Local data should be available to build
/// the filter for the [`FilterState::Ready`] state
pub fn got_remote(local_sub: Subscription) -> Self {
Self::GotRemote(local_sub)
Self::GotRemote(GotRemoteType::Normal(local_sub))
}
/// We have sent off a remote subscription to get data needed for the
@@ -127,7 +167,7 @@ impl FilterState {
local: local_sub,
remote: sub_id,
};
Self::FetchingRemote(unified_sub)
Self::FetchingRemote(FetchingRemoteType::Normal(unified_sub))
}
}
@@ -166,6 +206,49 @@ pub struct FilteredTags {
pub hashtags: Option<FilterBuilder>,
}
/// The local and remote filter are related but slightly different
#[derive(Debug, Clone)]
pub struct SplitFilter {
pub local: Vec<Filter>,
pub remote: Vec<Filter>,
}
/// Either a [`SplitFilter`] or a regular unsplit filter,. Split filters
/// have different remote and local filters but are tracked together.
#[derive(Debug, Clone)]
pub enum HybridFilter {
Split(SplitFilter),
Unsplit(Vec<Filter>),
}
impl HybridFilter {
pub fn unsplit(filter: Vec<Filter>) -> Self {
HybridFilter::Unsplit(filter)
}
pub fn split(local: Vec<Filter>, remote: Vec<Filter>) -> Self {
HybridFilter::Split(SplitFilter { local, remote })
}
pub fn local(&self) -> &[Filter] {
match self {
Self::Split(split) => &split.local,
// local as the same as remote in unsplit
Self::Unsplit(local) => local,
}
}
pub fn remote(&self) -> &[Filter] {
match self {
Self::Split(split) => &split.remote,
// local as the same as remote in unsplit
Self::Unsplit(remote) => remote,
}
}
}
impl FilteredTags {
pub fn into_follow_filter(self) -> Vec<Filter> {
self.into_filter([1], default_limit())
@@ -190,15 +273,74 @@ impl FilteredTags {
}
}
/// Create a "last N notes per pubkey" query.
pub fn last_n_per_pubkey_from_tags(
note: &Note,
kind: u64,
notes_per_pubkey: u64,
) -> Result<Vec<Filter>, Error> {
let mut filters: Vec<Filter> = vec![];
for tag in note.tags() {
// TODO: fix arbitrary MAX_FILTER limit in nostrdb
if filters.len() == 15 {
break;
}
if tag.count() < 2 {
continue;
}
let t = if let Some(t) = tag.get_unchecked(0).variant().str() {
t
} else {
continue;
};
if t == "p" {
let author = if let Some(author) = tag.get_unchecked(1).variant().id() {
author
} else {
continue;
};
let mut filter = Filter::new();
filter.start_authors_field()?;
filter.add_id_element(author)?;
filter.end_field();
filters.push(filter.kinds([kind]).limit(notes_per_pubkey).build());
} else if t == "t" {
let hashtag = if let Some(hashtag) = tag.get_unchecked(1).variant().str() {
hashtag
} else {
continue;
};
let mut filter = Filter::new();
filter.start_tags_field('t')?;
filter.add_str_element(hashtag)?;
filter.end_field();
filters.push(filter.kinds([kind]).limit(notes_per_pubkey).build());
}
}
Ok(filters)
}
/// Create a filter from tags. This can be used to create a filter
/// from a contact list
pub fn filter_from_tags(note: &Note) -> Result<FilteredTags> {
pub fn filter_from_tags(
note: &Note,
add_pubkey: Option<&[u8; 32]>,
with_hashtags: bool,
) -> Result<FilteredTags, Error> {
let mut author_filter = Filter::new();
let mut hashtag_filter = Filter::new();
let mut author_res: Option<FilterBuilder> = None;
let mut hashtag_res: Option<FilterBuilder> = None;
let mut author_count = 0i32;
let mut hashtag_count = 0i32;
let mut has_added_pubkey = false;
let tags = note.tags();
@@ -223,9 +365,16 @@ pub fn filter_from_tags(note: &Note) -> Result<FilteredTags> {
continue;
};
if let Some(pk) = add_pubkey {
if author == pk {
// we don't need to add it afterwards
has_added_pubkey = true;
}
}
author_filter.add_id_element(author)?;
author_count += 1;
} else if t == "t" {
} else if t == "t" && with_hashtags {
let hashtag = if let Some(hashtag) = tag.get_unchecked(1).variant().str() {
hashtag
} else {
@@ -237,6 +386,14 @@ pub fn filter_from_tags(note: &Note) -> Result<FilteredTags> {
}
}
// some additional ad-hoc logic for adding a pubkey
if let Some(pk) = add_pubkey {
if !has_added_pubkey {
author_filter.add_id_element(pk)?;
author_count += 1;
}
}
author_filter.end_field();
hashtag_filter.end_field();
@@ -264,3 +421,11 @@ pub fn filter_from_tags(note: &Note) -> Result<FilteredTags> {
hashtags: hashtag_res,
})
}
pub fn make_filters_since(raw: &[Filter], since: u64) -> Vec<Filter> {
let mut filters = Vec::with_capacity(raw.len());
for builder in raw {
filters.push(Filter::copy_from(builder).since(since).build());
}
filters
}

View File

@@ -1,10 +1,14 @@
use egui::{FontData, FontDefinitions, FontTweak};
use crate::{ui, NotedeckTextStyle};
use egui::FontData;
use egui::FontDefinitions;
use egui::FontTweak;
use std::collections::BTreeMap;
use tracing::debug;
use std::sync::Arc;
pub enum NamedFontFamily {
Medium,
Bold,
Emoji,
}
impl NamedFontFamily {
@@ -12,6 +16,7 @@ impl NamedFontFamily {
match self {
Self::Bold => "bold",
Self::Medium => "medium",
Self::Emoji => "emoji",
}
}
@@ -20,36 +25,77 @@ impl NamedFontFamily {
}
}
pub fn desktop_font_size(text_style: &NotedeckTextStyle) -> f32 {
match text_style {
NotedeckTextStyle::Heading => 24.0,
NotedeckTextStyle::Heading2 => 22.0,
NotedeckTextStyle::Heading3 => 20.0,
NotedeckTextStyle::Heading4 => 14.0,
NotedeckTextStyle::Body => 16.0,
NotedeckTextStyle::Monospace => 13.0,
NotedeckTextStyle::Button => 13.0,
NotedeckTextStyle::Small => 12.0,
NotedeckTextStyle::Tiny => 10.0,
NotedeckTextStyle::NoteBody => 16.0,
}
}
pub fn mobile_font_size(text_style: &NotedeckTextStyle) -> f32 {
// TODO: tweak text sizes for optimal mobile viewing
match text_style {
NotedeckTextStyle::Heading => 24.0,
NotedeckTextStyle::Heading2 => 22.0,
NotedeckTextStyle::Heading3 => 20.0,
NotedeckTextStyle::Heading4 => 14.0,
NotedeckTextStyle::Body => 13.0,
NotedeckTextStyle::Monospace => 13.0,
NotedeckTextStyle::Button => 13.0,
NotedeckTextStyle::Small => 12.0,
NotedeckTextStyle::Tiny => 10.0,
NotedeckTextStyle::NoteBody => 13.0,
}
}
pub fn get_font_size(ctx: &egui::Context, text_style: &NotedeckTextStyle) -> f32 {
if ui::is_narrow(ctx) {
mobile_font_size(text_style)
} else {
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, FontData> = BTreeMap::new();
let mut font_data: BTreeMap<String, Arc<FontData>> = BTreeMap::new();
let mut families = BTreeMap::new();
font_data.insert(
"Onest".to_owned(),
FontData::from_static(include_bytes!(
"../assets/fonts/onest/OnestRegular1602-hint.ttf"
)),
Arc::new(FontData::from_static(include_bytes!(
"../../../assets/fonts/onest/OnestRegular1602-hint.ttf"
))),
);
font_data.insert(
"OnestMedium".to_owned(),
FontData::from_static(include_bytes!(
"../assets/fonts/onest/OnestMedium1602-hint.ttf"
)),
Arc::new(FontData::from_static(include_bytes!(
"../../../assets/fonts/onest/OnestMedium1602-hint.ttf"
))),
);
font_data.insert(
"DejaVuSans".to_owned(),
FontData::from_static(include_bytes!("../assets/fonts/DejaVuSansSansEmoji.ttf")),
Arc::new(FontData::from_static(include_bytes!(
"../../../assets/fonts/DejaVuSansSansEmoji.ttf"
))),
);
font_data.insert(
"OnestBold".to_owned(),
FontData::from_static(include_bytes!(
"../assets/fonts/onest/OnestBold1602-hint.ttf"
)),
Arc::new(FontData::from_static(include_bytes!(
"../../../assets/fonts/onest/OnestBold1602-hint.ttf"
))),
);
/*
@@ -74,36 +120,46 @@ pub fn setup_fonts(ctx: &egui::Context) {
font_data.insert(
"Inconsolata".to_owned(),
FontData::from_static(include_bytes!("../assets/fonts/Inconsolata-Regular.ttf")).tweak(
FontTweak {
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(),
FontData::from_static(include_bytes!("../assets/fonts/NotoSansCJK-Regular.ttc")),
Arc::new(FontData::from_static(include_bytes!(
"../../../assets/fonts/NotoSansCJK-Regular.ttc"
))),
);
font_data.insert(
"NotoSansThai".to_owned(),
FontData::from_static(include_bytes!("../assets/fonts/NotoSansThai-Regular.ttf")),
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(),
FontData::from_static(include_bytes!("../assets/fonts/NotoEmoji-Regular.ttf")).tweak(
FontTweak {
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,
},
}),
),
);
@@ -124,7 +180,9 @@ pub fn setup_fonts(ctx: &egui::Context) {
mono.extend(base_fonts.clone());
let mut bold = vec!["OnestBold".to_owned()];
bold.extend(base_fonts);
bold.extend(base_fonts.clone());
let emoji = vec!["NotoEmoji".to_owned()];
families.insert(egui::FontFamily::Proportional, proportional);
families.insert(egui::FontFamily::Monospace, mono);
@@ -136,8 +194,12 @@ pub fn setup_fonts(ctx: &egui::Context) {
egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()),
bold,
);
families.insert(
egui::FontFamily::Name(NamedFontFamily::Emoji.as_str().into()),
emoji,
);
debug!("fonts: {:?}", families);
tracing::debug!("fonts: {:?}", families);
let defs = FontDefinitions {
font_data,

View File

@@ -0,0 +1,24 @@
use super::IntlKeyBuf;
use unic_langid::LanguageIdentifier;
/// App related errors
#[derive(thiserror::Error, Debug)]
pub enum IntlError {
#[error("message not found: {0}")]
NotFound(IntlKeyBuf),
#[error("message has no value: {0}")]
NoValue(IntlKeyBuf),
#[error("Locale({0}) parse error: {1}")]
LocaleParse(LanguageIdentifier, String),
#[error("locale not available: {0}")]
LocaleNotAvailable(LanguageIdentifier),
#[error("FTL for '{0}' is not available")]
NoFtl(LanguageIdentifier),
#[error("Bundle for '{0}' is not available")]
NoBundle(LanguageIdentifier),
}

View File

@@ -0,0 +1,47 @@
use std::fmt;
/// An owned key used to lookup i18n translations. Mostly used for errors
#[derive(Eq, PartialEq, Clone, Debug)]
pub struct IntlKeyBuf(String);
/// A key used to lookup i18n translations
#[derive(Eq, PartialEq, Clone, Copy, Debug)]
pub struct IntlKey<'a>(&'a str);
impl fmt::Display for IntlKey<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Use `self.number` to refer to each positional data point.
write!(f, "{}", self.0)
}
}
impl fmt::Display for IntlKeyBuf {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Use `self.number` to refer to each positional data point.
write!(f, "{}", &self.0)
}
}
impl IntlKeyBuf {
pub fn new(string: impl Into<String>) -> Self {
IntlKeyBuf(string.into())
}
pub fn borrow<'a>(&'a self) -> IntlKey<'a> {
IntlKey::new(&self.0)
}
}
impl<'a> IntlKey<'a> {
pub fn new(string: &'a str) -> IntlKey<'a> {
IntlKey(string)
}
pub fn to_owned(&self) -> IntlKeyBuf {
IntlKeyBuf::new(self.0)
}
pub fn as_str(&self) -> &'a str {
self.0
}
}

View File

@@ -0,0 +1,907 @@
use super::{IntlError, IntlKey, IntlKeyBuf};
use fluent::{FluentArgs, FluentBundle, FluentResource};
use fluent_langneg::negotiate_languages;
use std::borrow::Cow;
use std::collections::HashMap;
use sys_locale;
use unic_langid::{langid, LanguageIdentifier};
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 PT_BR: LanguageIdentifier = langid!("pt-BR");
const TH: LanguageIdentifier = langid!("th");
const ZH_CN: LanguageIdentifier = langid!("zh-CN");
const ZH_TW: LanguageIdentifier = langid!("zh-TW");
const NUM_FTLS: usize = 10;
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 PT_BR_NATIVE_NAME: &str = "Português (Brasil)";
const TH_NATIVE_NAME: &str = "ภาษาไทย";
const ZH_CN_NATIVE_NAME: &str = "简体中文";
const ZH_TW_NATIVE_NAME: &str = "繁體中文";
struct StaticBundle {
identifier: LanguageIdentifier,
ftl: &'static str,
}
const FTLS: [StaticBundle; NUM_FTLS] = [
StaticBundle {
identifier: EN_US,
ftl: include_str!("../../../../assets/translations/en-US/main.ftl"),
},
StaticBundle {
identifier: EN_XA,
ftl: include_str!("../../../../assets/translations/en-XA/main.ftl"),
},
StaticBundle {
identifier: DE,
ftl: include_str!("../../../../assets/translations/de/main.ftl"),
},
StaticBundle {
identifier: ES_419,
ftl: include_str!("../../../../assets/translations/es-419/main.ftl"),
},
StaticBundle {
identifier: ES_ES,
ftl: include_str!("../../../../assets/translations/es-ES/main.ftl"),
},
StaticBundle {
identifier: FR,
ftl: include_str!("../../../../assets/translations/fr/main.ftl"),
},
StaticBundle {
identifier: PT_BR,
ftl: include_str!("../../../../assets/translations/pt-BR/main.ftl"),
},
StaticBundle {
identifier: TH,
ftl: include_str!("../../../../assets/translations/th/main.ftl"),
},
StaticBundle {
identifier: ZH_CN,
ftl: include_str!("../../../../assets/translations/zh-CN/main.ftl"),
},
StaticBundle {
identifier: ZH_TW,
ftl: include_str!("../../../../assets/translations/zh-TW/main.ftl"),
},
];
type Bundle = FluentBundle<FluentResource>;
/// Manages localization resources and provides localized strings
pub struct Localization {
/// Current locale
current_locale: LanguageIdentifier,
/// Available locales
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>>,
/// Cached normalized keys
normalized_key_cache: HashMap<String, IntlKeyBuf>,
/// Bundles
bundles: HashMap<LanguageIdentifier, Bundle>,
use_isolating: bool,
}
impl Default for Localization {
fn default() -> Self {
// Build available locales list
let available_locales = vec![
EN_US.clone(),
EN_XA.clone(),
DE.clone(),
ES_419.clone(),
ES_ES.clone(),
FR.clone(),
PT_BR.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()),
(PT_BR, PT_BR_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()),
]);
// Detect system locale and find best match
let current_locale = Self::negotiate_system_locale_with_preferences(&available_locales);
// Fallback locale is always EN_US
let fallback_locale = EN_US.clone();
tracing::info!(
"Localization initialized - Selected locale: {}, Fallback: {}",
current_locale,
fallback_locale
);
Self {
current_locale,
available_locales,
fallback_locale,
locale_native_names,
use_isolating: true,
normalized_key_cache: HashMap::new(),
string_cache: HashMap::new(),
bundles: HashMap::new(),
}
}
}
impl Localization {
/// Creates a new Localization with the specified resource directory
pub fn new() -> Self {
Localization::default()
}
/// Disable bidirectional isolation markers. mostly useful for tests
pub fn no_bidi() -> Self {
Localization {
use_isolating: false,
..Localization::default()
}
}
/// Extract just the language and region from locale string (e.g., "fr-FR-u-mu-celsius" -> "fr-FR")
fn extract_language_region(locale_str: &str) -> String {
// Split by '-' and analyze the parts
let parts: Vec<&str> = locale_str.split('-').collect();
if parts.len() >= 2 {
// Check if the second part looks like a region
let second_part = parts[1];
if (second_part.len() >= 2) {
format!("{}-{}", parts[0], parts[1])
} else {
// Second part is not a region, probably an extension (e.g., "u", "t", "x")
// Just return the language part
parts[0].to_string()
}
} else {
// Only one part, return as is
locale_str.to_string()
}
}
/// Negotiate the best locale from all system preferences against available locales
fn negotiate_system_locale_with_preferences(
available_locales: &[LanguageIdentifier],
) -> LanguageIdentifier {
// Get all system preferred locales in descending order
let mut system_locales: Vec<String> = sys_locale::get_locales().collect();
if system_locales.is_empty() {
tracing::info!("No system locales detected, using fallback: en-US");
return EN_US.clone();
}
tracing::info!("System preferred locales: {:?}", system_locales);
// If we only got one locale, it might be that the system only returns the primary locale
// In this case, we can try to add common fallbacks based on the detected locale
if system_locales.len() == 1 {
let primary = &system_locales[0];
// Try to parse the primary locale, handling extensions
let primary_lang = if let Ok(locale) = primary.parse::<LanguageIdentifier>() {
locale.language.as_str().to_string()
} else {
// If parsing fails, try extracting language-region
// let stripped = Self::extract_language_region(primary);
// if let Ok(locale) = stripped.parse::<LanguageIdentifier>() {
// locale.language.as_str().to_string()
// } else {
tracing::info!("Could not parse primary locale: {}", primary);
"unknown".to_string()
// }
};
tracing::info!(
"Only one system locale detected: {} (language: {})",
primary,
primary_lang
);
// Add common fallbacks for the detected language
match primary_lang.as_str() {
"uk" => {
// For Ukrainian, add common fallbacks
system_locales.push("es-ES".to_string());
system_locales.push("en-US".to_string());
tracing::info!("Added fallbacks for Ukrainian: {:?}", system_locales);
}
"es" => {
// For Spanish, add English fallback
system_locales.push("en-US".to_string());
tracing::info!("Added fallback for Spanish: {:?}", system_locales);
}
_ => {
// For other languages, add English fallback
system_locales.push("en-US".to_string());
tracing::info!("Added fallback for {}: {:?}", primary_lang, system_locales);
}
}
}
// Convert system locale strings to LanguageIdentifiers, handling extensions
let mut parsed_system_locales = Vec::new();
for locale_str in system_locales {
// Try to parse the locale string directly first
if let Ok(locale) = locale_str.parse::<LanguageIdentifier>() {
parsed_system_locales.push(locale);
continue;
}
// If parsing fails, try extracting just language-region
// let stripped_locale = Self::extract_language_region(&locale_str);
// if let Ok(locale) = stripped_locale.parse::<LanguageIdentifier>() {
// parsed_system_locales.push(locale);
// continue;
// }
tracing::info!("Failed to parse locale string: {}", locale_str);
}
if parsed_system_locales.is_empty() {
tracing::info!("No valid system locales parsed, using fallback: en-US");
return EN_US.clone();
}
// First try exact matches with fluent_langneg
let fallback = &EN_US;
let negotiated = negotiate_languages(
&parsed_system_locales,
available_locales,
Some(fallback),
fluent_langneg::NegotiationStrategy::Filtering,
);
if let Some(result) = negotiated.first() {
tracing::info!(
"Exact match found: {} from preferences: {:?}",
result,
parsed_system_locales
);
return (*result).clone();
}
// If no exact match, try language-only fallbacks
tracing::info!("No exact matches found, trying language-only fallbacks");
for system_locale in &parsed_system_locales {
let system_lang = system_locale.language.as_str();
// Look for any available locale with the same language
for available_locale in available_locales {
if available_locale.language.as_str() == system_lang {
tracing::debug!(
"Language match found: {} (system: {})",
available_locale,
system_locale
);
return available_locale.clone();
}
}
}
tracing::info!("No language matches found, using fallback: en-US");
EN_US.clone()
}
/// Gets a localized string by its ID
pub fn get_string(&mut self, id: IntlKey<'_>) -> Result<String, IntlError> {
self.get_cached_string(id, None)
}
/// Load a fluent bundle given a language identifier. Only looks in the static
/// ftl files baked into the binary
fn load_bundle(lang: &LanguageIdentifier) -> Result<Bundle, IntlError> {
for ftl in &FTLS {
if &ftl.identifier == lang {
let mut bundle = FluentBundle::new(vec![lang.to_owned()]);
let resource = FluentResource::try_new(ftl.ftl.to_string());
match resource {
Err((resource, errors)) => {
for error in errors {
tracing::error!("load_bundle ({lang}): {error}");
}
tracing::warn!("load_bundle ({}: loading bundle with errors", lang);
if let Err(errs) = bundle.add_resource(resource) {
for err in errs {
tracing::error!("adding resource: {err}");
}
}
}
Ok(resource) => {
tracing::info!("loaded {} bundle OK!", lang);
if let Err(errs) = bundle.add_resource(resource) {
for err in errs {
tracing::error!("adding resource 2: {err}");
}
}
}
}
return Ok(bundle);
}
}
// no static ftl for this LanguageIdentifier
Err(IntlError::NoFtl(lang.to_owned()))
}
fn get_bundle<'a>(&'a self, lang: &LanguageIdentifier) -> &'a Bundle {
self.bundles
.get(lang)
.expect("make sure to call ensure_bundle!")
}
fn has_bundle(&self, lang: &LanguageIdentifier) -> bool {
self.bundles.contains_key(lang)
}
fn try_load_bundle(&mut self, lang: &LanguageIdentifier) -> Result<(), IntlError> {
let mut bundle = Self::load_bundle(lang)?;
if !self.use_isolating {
bundle.set_use_isolating(false);
}
self.bundles.insert(lang.to_owned(), bundle);
Ok(())
}
pub fn normalized_ftl_key(&mut self, key: &str, comment: &str) -> IntlKeyBuf {
match self.get_ftl_key(key) {
Some(intl_key) => intl_key,
None => {
self.insert_ftl_key(key, comment);
self.get_ftl_key(key).unwrap()
}
}
}
fn get_ftl_key(&self, cache_key: &str) -> Option<IntlKeyBuf> {
self.normalized_key_cache.get(cache_key).cloned()
}
fn insert_ftl_key(&mut self, cache_key: &str, comment: &str) {
let mut result = fixup_key(cache_key);
// Ensure the key starts with a letter (Fluent requirement)
if result.is_empty() || !result.chars().next().unwrap().is_ascii_alphabetic() {
result = format!("k_{result}");
}
// If we have a comment, append a hash of it to reduce collisions
let hash_str = format!("_{}", simple_hash(comment));
result.push_str(&hash_str);
tracing::debug!(
"normalize_ftl_key: original='{}', final='{}'",
cache_key,
result
);
self.normalized_key_cache
.insert(cache_key.to_owned(), IntlKeyBuf::new(result));
}
fn get_cached_string_no_args<'key>(
&'key self,
lang: &LanguageIdentifier,
id: IntlKey<'key>,
) -> Result<Cow<'key, str>, IntlError> {
// Try to get from string cache first
if let Some(locale_cache) = self.string_cache.get(lang) {
if let Some(cached_string) = locale_cache.get(id.as_str()) {
/*
tracing::trace!(
"Using cached string result for '{}' in locale: {}",
id,
&lang
);
*/
return Ok(Cow::Borrowed(cached_string));
}
}
Err(IntlError::NotFound(id.to_owned()))
}
fn ensure_bundle(&mut self) -> Result<(), IntlError> {
let locale = self.current_locale.clone();
if !self.has_bundle(&locale) {
match self.try_load_bundle(&locale) {
Err(err) => {
tracing::warn!(
"tried to load bundle {} but failed with '{err}'. using fallback {}",
&locale,
&self.fallback_locale
);
self.try_load_bundle(&locale)
.expect("failed to load fallback bundle!?");
Ok(())
}
Ok(()) => Ok(()),
}
} else {
Ok(())
}
}
fn get_current_bundle(&self) -> &Bundle {
if self.has_bundle(&self.current_locale) {
return self.get_bundle(&self.current_locale);
}
self.get_bundle(&self.fallback_locale)
}
/// Gets cached string result, or formats it and caches the result
pub fn get_cached_string(
&mut self,
id: IntlKey<'_>,
args: Option<&FluentArgs>,
) -> Result<String, IntlError> {
self.ensure_bundle()?;
if args.is_none() {
if let Ok(result) = self.get_cached_string_no_args(&self.current_locale, id) {
return Ok(result.to_string());
}
}
let result = {
let bundle = self.get_current_bundle();
let message = bundle
.get_message(id.as_str())
.ok_or_else(|| IntlError::NotFound(id.to_owned()))?;
let pattern = message
.value()
.ok_or_else(|| IntlError::NoValue(id.to_owned()))?;
let mut errors = Vec::with_capacity(0);
let result = bundle.format_pattern(pattern, args, &mut errors);
if !errors.is_empty() {
tracing::warn!("Localization errors for {}: {:?}", id, &errors);
}
result.to_string()
};
// Only cache simple strings without arguments
// This prevents caching issues when the same message ID is used with different arguments
if args.is_none() {
self.cache_string(self.current_locale.clone(), id, result.as_str());
tracing::debug!(
"Cached string result for '{}' in locale: {}",
id,
&self.current_locale
);
} else {
tracing::trace!("Not caching string '{}' due to arguments", id);
}
Ok(result)
}
pub fn cache_string<'a>(&mut self, locale: LanguageIdentifier, id: IntlKey<'a>, result: &str) {
tracing::debug!("Cached string result for '{}' in locale: {}", id, &locale);
let locale_cache = self.string_cache.entry(locale).or_default();
locale_cache.insert(id.to_owned().to_string(), result.to_owned());
}
/// Sets the current locale
pub fn set_locale(&mut self, locale: LanguageIdentifier) -> Result<(), IntlError> {
tracing::info!("Attempting to set locale to: {}", locale);
tracing::info!("Available locales: {:?}", self.available_locales);
// Validate that the locale is available
if !self.available_locales.contains(&locale) {
tracing::error!(
"Locale {} is not available. Available locales: {:?}",
locale,
self.available_locales
);
return Err(IntlError::LocaleNotAvailable(locale));
}
tracing::info!(
"Switching locale from {} to {}",
&self.current_locale,
&locale
);
self.current_locale = locale;
// Clear caches when locale changes since they are locale-specific
self.string_cache.clear();
tracing::debug!("String cache cleared due to locale change");
Ok(())
}
/// Clears the parsed FluentResource cache (useful for development when FTL files change)
pub fn clear_cache(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
self.bundles.clear();
tracing::debug!("Parsed FluentResource cache cleared");
self.string_cache.clear();
tracing::debug!("String result cache cleared");
Ok(())
}
/// Gets the current locale
pub fn get_current_locale(&self) -> &LanguageIdentifier {
&self.current_locale
}
/// Gets all available locales
pub fn get_available_locales(&self) -> &[LanguageIdentifier] {
&self.available_locales
}
/// Gets the fallback locale
pub fn get_fallback_locale(&self) -> &LanguageIdentifier {
&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;
for locale_cache in self.string_cache.values() {
total_strings += locale_cache.len();
}
Ok(CacheStats {
resource_cache_size: self.bundles.len(),
string_cache_size: total_strings,
cached_locales: self.bundles.keys().cloned().collect(),
})
}
/// Limits the string cache size to prevent memory growth
pub fn limit_string_cache_size(
&mut self,
max_strings_per_locale: usize,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
for locale_cache in self.string_cache.values_mut() {
if locale_cache.len() > max_strings_per_locale {
// Remove oldest entries (simple approach: just clear and let it rebuild)
// In a more sophisticated implementation, you might use an LRU cache
locale_cache.clear();
tracing::debug!("Cleared string cache for locale due to size limit");
}
}
Ok(())
}
}
/// Statistics about cache usage
#[derive(Debug, Clone)]
pub struct CacheStats {
pub resource_cache_size: usize,
pub string_cache_size: usize,
pub cached_locales: Vec<LanguageIdentifier>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_language_region() {
// Test that we extract just language and region from various locale formats
// Test locales with extensions
let unicode_locale = "fr-FR-u-mu-celsius";
let extracted = Localization::extract_language_region(unicode_locale);
assert_eq!(extracted, "fr-FR");
let transformed_locale = "en-US-t-0-abc123";
let extracted = Localization::extract_language_region(transformed_locale);
assert_eq!(extracted, "en-US");
let private_locale = "de-DE-x-phonebk";
let extracted = Localization::extract_language_region(private_locale);
assert_eq!(extracted, "de-DE");
// Test simple locale (no extensions)
let simple_locale = "en-US";
let extracted = Localization::extract_language_region(simple_locale);
assert_eq!(extracted, "en-US");
// Test language-only locale
let lang_only = "en";
let extracted = Localization::extract_language_region(lang_only);
assert_eq!(extracted, "en");
// Test language with extensions (no region)
let lang_with_extensions = "fr-u-mu-celsius";
let extracted = Localization::extract_language_region(lang_with_extensions);
assert_eq!(extracted, "fr");
// Test language with other extension types (no region)
let lang_with_t_ext = "en-t-0-abc123";
let extracted = Localization::extract_language_region(lang_with_t_ext);
assert_eq!(extracted, "en");
let lang_with_x_ext = "de-x-phonebk";
let extracted = Localization::extract_language_region(lang_with_x_ext);
assert_eq!(extracted, "de");
// Test locale with numeric region code
let numeric_region = "es-419-u-mu-celsius";
let extracted = Localization::extract_language_region(numeric_region);
assert_eq!(extracted, "es-419");
// Test locale with 3-letter region code
let three_letter_region = "en-USA-t-0-abc123";
let extracted = Localization::extract_language_region(three_letter_region);
assert_eq!(extracted, "en-USA");
// Test locale with 2-letter region code
let two_letter_region = "fr-FR-u-mu-celsius";
let extracted = Localization::extract_language_region(two_letter_region);
assert_eq!(extracted, "fr-FR");
// Test complex locale with multiple parts
let complex_locale = "zh-CN-u-ca-chinese-x-private";
let extracted = Localization::extract_language_region(complex_locale);
assert_eq!(extracted, "zh-CN");
// Verify that extracted locales can be parsed
let test_cases = ["fr-FR", "en-US", "de-DE", "en", "zh-CN"];
for extracted in test_cases {
if let Ok(locale) = extracted.parse::<LanguageIdentifier>() {
tracing::info!("Successfully parsed extracted locale: {}", locale);
} else {
tracing::error!("Failed to parse extracted locale: {}", extracted);
panic!("Should parse locale after extraction");
}
}
}
//
// TODO(jb55): write tests that work, i broke all these during the refacto
//
/*
use super::*;
#[test]
fn test_locale_management() {
let i18n = Localization::default();
// Test default locale
let current = i18n.get_current_locale();
assert_eq!(current.to_string(), "en-US");
// Test available locales
let available = i18n.get_available_locales();
assert_eq!(available.len(), 2);
assert_eq!(available[0].to_string(), "en-US");
assert_eq!(available[1].to_string(), "en-XA");
}
#[test]
fn test_cache_clearing() {
let mut i18n = Localization::default();
// Load and cache the FTL content
let result1 = i18n.get_string(IntlKeyBuf::new("test_key").borrow());
assert!(result1.is_ok());
// Clear the cache
let clear_result = i18n.clear_cache();
assert!(clear_result.is_ok());
// Should still work after clearing cache (will reload)
let result2 = i18n.get_string(IntlKeyBuf::new("test_key").borrow());
assert!(result2.is_ok());
assert_eq!(result2.unwrap(), "Test Value");
}
#[test]
fn test_context_caching() {
let mut i18n = Localization::default();
// Debug: check what the normalized key should be
let normalized_key = i18n.normalized_ftl_key("test_key", "comment");
println!("Normalized key: '{}'", normalized_key);
// First call should load and cache the FTL content
let result1 = i18n.get_string(normalized_key.borrow());
println!("First result: {:?}", result1);
assert!(result1.is_ok());
assert_eq!(result1.unwrap(), "Test Value");
// Second call should use cached FTL content
let result2 = i18n.get_string(normalized_key.borrow());
assert!(result2.is_ok());
assert_eq!(result2.unwrap(), "Test Value");
// Test cache clearing through context
let clear_result = i18n.clear_cache();
assert!(clear_result.is_ok());
// Should still work after clearing cache
let result3 = i18n.get_string(normalized_key.borrow());
assert!(result3.is_ok());
assert_eq!(result3.unwrap(), "Test Value");
}
#[test]
fn test_ftl_caching() {
let mut i18n = Localization::default();
// First call should load and cache the FTL content
let result1 = i18n.get_string(IntlKeyBuf::new("test_key").borrow());
assert!(result1.is_ok());
assert_eq!(result1.as_ref().unwrap(), "Test Value");
// Second call should use cached FTL content
let result2 = i18n.get_string(IntlKeyBuf::new("test_key").borrow());
assert!(result2.is_ok());
assert_eq!(result2.unwrap(), "Test Value");
// Test another key from the same FTL content
let result3 = i18n.get_string(IntlKeyBuf::new("another_key").borrow());
assert!(result3.is_ok());
assert_eq!(result3.unwrap(), "Another Value");
}
#[test]
fn test_bundle_caching() {
let mut i18n = Localization::default();
// First call should create bundle and cache the resource
let result1 = i18n.get_string(IntlKeyBuf::new("test_key").borrow());
assert!(result1.is_ok());
assert_eq!(result1.unwrap(), "Test Value");
// Second call should use cached resource but create new bundle
let result2 = i18n.get_string(IntlKeyBuf::new("another_key").borrow());
assert!(result2.is_ok());
assert_eq!(result2.unwrap(), "Another Value");
// Check cache stats
let stats = i18n.get_cache_stats().unwrap();
assert_eq!(stats.resource_cache_size, 1);
assert_eq!(stats.string_cache_size, 2); // Both strings should be cached
}
#[test]
fn test_string_caching() {
let mut i18n = Localization::default();
let key = i18n.normalized_ftl_key("test_key", "comment");
// First call should format and cache the string
let result1 = i18n.get_string(key.borrow());
assert!(result1.is_ok());
assert_eq!(result1.unwrap(), "Test Value");
// Second call should use cached string
let result2 = i18n.get_string(key.borrow());
assert!(result2.is_ok());
assert_eq!(result2.unwrap(), "Test Value");
// Check cache stats
let stats = i18n.get_cache_stats().unwrap();
assert_eq!(stats.string_cache_size, 1);
}
#[test]
fn test_string_caching_with_arguments() {
let mut manager = Localization::default();
// First call with arguments should not be cached
let mut args = fluent::FluentArgs::new();
args.set("name", "Alice");
let key = IntlKeyBuf::new("welcome_message");
let result1 = manager
.get_cached_string(key.borrow(), Some(&args))
.unwrap();
assert!(result1.contains("Alice"));
// Check that it's not in the string cache
let stats1 = manager.get_cache_stats().unwrap();
assert_eq!(stats1.string_cache_size, 0);
// Second call with different arguments should work correctly
let mut args2 = fluent::FluentArgs::new();
args2.set("name", "Bob");
let result2 = manager.get_cached_string(key.borrow(), Some(&args2));
assert!(result2.is_ok());
let result2_str = result2.unwrap();
assert!(result2_str.contains("Bob"));
// Check that it's still not in the string cache
let stats2 = manager.get_cache_stats().unwrap();
assert_eq!(stats2.string_cache_size, 0);
// Clear cache to start fresh
manager.clear_cache().unwrap();
let result3 = manager.get_string(key.borrow());
assert!(result3.is_ok());
assert_eq!(result3.unwrap(), "Hello World");
// Check that simple string is cached
let stats3 = manager.get_cache_stats().unwrap();
assert_eq!(stats3.string_cache_size, 1);
}
#[test]
fn test_cache_clearing_on_locale_change() {
let mut i18n = Localization::default();
// Check that caches are populated
let stats1 = i18n.get_cache_stats().unwrap();
assert!(stats1.resource_cache_size > 0);
assert!(stats1.string_cache_size > 0);
// Switch to en-XA
let en_xa: LanguageIdentifier = langid!("en-XA");
i18n.set_locale(en_xa).unwrap();
// Check that string cache is cleared (resource cache remains for both locales)
let stats2 = i18n.get_cache_stats().unwrap();
assert_eq!(stats2.string_cache_size, 0);
}
*/
}
/// Replace each invalid character with exactly one underscore
/// This matches the behavior of the Python extraction script
pub fn fixup_key(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' => out.push(ch),
_ => out.push('_'), // always push
}
}
let trimmed = out.trim_matches('_');
trimmed.to_owned()
}
fn simple_hash(s: &str) -> String {
let digest = md5::compute(s.as_bytes());
// Take the first 2 bytes and convert to 4 hex characters
format!("{:02x}{:02x}", digest[0], digest[1])
}

View File

@@ -0,0 +1,107 @@
//! Internationalization (i18n) module for Notedeck
//!
//! This module provides localization support using fluent and fluent-resmgr.
//! It handles loading translation files, managing locales, and providing
//! localized strings throughout the application.
mod error;
mod key;
pub mod manager;
pub use error::IntlError;
pub use key::{IntlKey, IntlKeyBuf};
pub use manager::CacheStats;
pub use manager::Localization;
/// Re-export commonly used types for convenience
pub use fluent::FluentArgs;
pub use fluent::FluentValue;
pub use unic_langid::LanguageIdentifier;
/// Macro for getting localized strings with format-like syntax
///
/// Syntax: tr!("message", comment)
/// tr!("message with {param}", comment, param="value")
/// tr!("message with {first} and {second}", comment, first="value1", second="value2")
///
/// The first argument is the source message (like format!).
/// The second argument is always the comment to provide context for translators.
/// If `{name}` placeholders are found, there must be corresponding named arguments after the comment.
/// All placeholders must be named and start with a letter (a-zA-Z).
#[macro_export]
macro_rules! tr {
($i18n:expr, $message:expr, $comment:expr) => {
{
let key = $i18n.normalized_ftl_key($message, $comment);
match $i18n.get_string(key.borrow()) {
Ok(r) => r,
Err(_err) => {
$message.to_string()
}
}
}
};
// Case with named parameters: message, comment, param=value, ...
($i18n:expr, $message:expr, $comment:expr, $($param:ident = $value:expr),*) => {
{
let key = $i18n.normalized_ftl_key($message, $comment);
let mut args = $crate::i18n::FluentArgs::new();
$(
args.set(stringify!($param), $value);
)*
match $i18n.get_cached_string(key.borrow(), Some(&args)) {
Ok(r) => r,
Err(_) => {
// Fallback: replace placeholders with values
let mut result = $message.to_string();
$(
result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());
)*
result
}
}
}
};
}
/// Macro for getting localized pluralized strings with count and named arguments
///
/// Syntax: tr_plural!(one, other, comment, count, param1=..., param2=...)
/// - one: Message for the singular ("one") plural rule
/// - other: Message for the "other" plural rule
/// - comment: Context for translators
/// - count: The count value
/// - named arguments: Any additional named parameters for interpolation
#[macro_export]
macro_rules! tr_plural {
// With named parameters
($i18n:expr, $one:expr, $other:expr, $comment:expr, $count:expr, $($param:ident = $value:expr),*) => {{
let norm_key = $i18n.normalized_ftl_key($other, $comment);
let mut args = $crate::i18n::FluentArgs::new();
args.set("count", $count);
$(args.set(stringify!($param), $value);)*
match $i18n.get_cached_string(norm_key.borrow(), Some(&args)) {
Ok(s) => s,
Err(_) => {
// Fallback: use simple pluralization
if $count == 1 {
let mut result = $one.to_string();
$(result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());)*
result = result.replace("{count}", &$count.to_string());
result
} else {
let mut result = $other.to_string();
$(result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());)*
result = result.replace("{count}", &$count.to_string());
result
}
}
}
}};
// Without named parameters
($one:expr, $other:expr, $comment:expr, $count:expr) => {{
$crate::tr_plural!($one, $other, $comment, $count, )
}};
}

View File

@@ -0,0 +1,565 @@
use crate::media::gif::ensure_latest_texture_from_cache;
use crate::media::images::ImageType;
use crate::urls::{UrlCache, UrlMimes};
use crate::ImageMetadata;
use crate::ObfuscationType;
use crate::RenderableMedia;
use crate::Result;
use egui::TextureHandle;
use image::{Delay, Frame};
use poll_promise::Promise;
use egui::ColorImage;
use std::collections::HashMap;
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;
use std::path::PathBuf;
use std::path::{self, Path};
use tracing::warn;
#[derive(Default)]
pub struct TexturesCache {
pub cache: hashbrown::HashMap<String, TextureStateInternal>,
}
impl TexturesCache {
pub fn handle_and_get_or_insert_loadable(
&mut self,
url: &str,
closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>,
) -> LoadableTextureState {
let internal = self.handle_and_get_state_internal(url, true, closure);
internal.into()
}
pub fn handle_and_get_or_insert(
&mut self,
url: &str,
closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>,
) -> TextureState {
let internal = self.handle_and_get_state_internal(url, false, closure);
internal.into()
}
fn handle_and_get_state_internal(
&mut self,
url: &str,
use_loading: bool,
closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>,
) -> &mut TextureStateInternal {
let state = match self.cache.raw_entry_mut().from_key(url) {
hashbrown::hash_map::RawEntryMut::Occupied(entry) => {
let state = entry.into_mut();
handle_occupied(state, use_loading);
state
}
hashbrown::hash_map::RawEntryMut::Vacant(entry) => {
let res = closure();
let (_, state) = entry.insert(url.to_owned(), TextureStateInternal::Pending(res));
state
}
};
state
}
pub fn insert_pending(&mut self, url: &str, promise: Promise<Option<Result<TexturedImage>>>) {
self.cache
.insert(url.to_owned(), TextureStateInternal::Pending(promise));
}
pub fn move_to_loaded(&mut self, url: &str) {
let hashbrown::hash_map::RawEntryMut::Occupied(entry) =
self.cache.raw_entry_mut().from_key(url)
else {
return;
};
entry.replace_entry_with(|_, v| {
let TextureStateInternal::Loading(textured) = v else {
return None;
};
Some(TextureStateInternal::Loaded(textured))
});
}
pub fn get_and_handle(&mut self, url: &str) -> Option<LoadableTextureState> {
self.cache.get_mut(url).map(|state| {
handle_occupied(state, true);
state.into()
})
}
}
fn handle_occupied(state: &mut TextureStateInternal, use_loading: bool) {
let TextureStateInternal::Pending(promise) = state else {
return;
};
let Some(res) = promise.ready_mut() else {
return;
};
let Some(res) = res.take() else {
tracing::error!("Failed to take the promise");
*state =
TextureStateInternal::Error(crate::Error::Generic("Promise already taken".to_owned()));
return;
};
match res {
Ok(textured) => {
*state = if use_loading {
TextureStateInternal::Loading(textured)
} else {
TextureStateInternal::Loaded(textured)
}
}
Err(e) => *state = TextureStateInternal::Error(e),
}
}
pub enum LoadableTextureState<'a> {
Pending,
Error(&'a crate::Error),
Loading {
actual_image_tex: &'a mut TexturedImage,
}, // the texture is in the loading state, for transitioning between the pending and loaded states
Loaded(&'a mut TexturedImage),
}
pub enum TextureState<'a> {
Pending,
Error(&'a crate::Error),
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 {
TextureStateInternal::Pending(_) => TextureState::Pending,
TextureStateInternal::Error(error) => TextureState::Error(error),
TextureStateInternal::Loading(textured_image) => TextureState::Loaded(textured_image),
TextureStateInternal::Loaded(textured_image) => TextureState::Loaded(textured_image),
}
}
}
pub enum TextureStateInternal {
Pending(Promise<Option<Result<TexturedImage>>>),
Error(crate::Error),
Loading(TexturedImage), // the image is in the loading state, for transitioning between blur and image
Loaded(TexturedImage),
}
impl<'a> From<&'a mut TextureStateInternal> for LoadableTextureState<'a> {
fn from(value: &'a mut TextureStateInternal) -> Self {
match value {
TextureStateInternal::Pending(_) => LoadableTextureState::Pending,
TextureStateInternal::Error(error) => LoadableTextureState::Error(error),
TextureStateInternal::Loading(textured_image) => LoadableTextureState::Loading {
actual_image_tex: textured_image,
},
TextureStateInternal::Loaded(textured_image) => {
LoadableTextureState::Loaded(textured_image)
}
}
}
}
pub enum TexturedImage {
Static(TextureHandle),
Animated(Animation),
}
impl TexturedImage {
pub fn get_first_texture(&self) -> &TextureHandle {
match self {
TexturedImage::Static(texture_handle) => texture_handle,
TexturedImage::Animated(animation) => &animation.first_frame.texture,
}
}
}
pub struct Animation {
pub first_frame: TextureFrame,
pub other_frames: Vec<TextureFrame>,
pub receiver: Option<Receiver<TextureFrame>>,
}
impl Animation {
pub fn get_frame(&self, index: usize) -> Option<&TextureFrame> {
if index == 0 {
Some(&self.first_frame)
} else {
self.other_frames.get(index - 1)
}
}
pub fn num_frames(&self) -> usize {
self.other_frames.len() + 1
}
}
pub struct TextureFrame {
pub delay: Duration,
pub texture: TextureHandle,
}
pub struct ImageFrame {
pub delay: Duration,
pub image: ColorImage,
}
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)]
pub enum MediaCacheType {
Image,
Gif,
}
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,
}
}
pub fn rel_dir(cache_type: MediaCacheType) -> &'static str {
match cache_type {
MediaCacheType::Image => "img",
MediaCacheType::Gif => "gif",
}
}
pub fn write(cache_dir: &path::Path, url: &str, data: ColorImage) -> Result<()> {
let file = Self::create_file(cache_dir, url)?;
let encoder = image::codecs::webp::WebPEncoder::new_lossless(file);
encoder.encode(
data.as_raw(),
data.size[0] as u32,
data.size[1] as u32,
image::ColorType::Rgba8.into(),
)?;
Ok(())
}
fn create_file(cache_dir: &path::Path, url: &str) -> Result<File> {
let file_path = cache_dir.join(Self::key(url));
if let Some(p) = file_path.parent() {
create_dir_all(p)?;
}
Ok(File::options()
.write(true)
.create(true)
.truncate(true)
.open(file_path)?)
}
pub fn write_gif(cache_dir: &path::Path, url: &str, data: Vec<ImageFrame>) -> Result<()> {
let file = Self::create_file(cache_dir, url)?;
let mut encoder = image::codecs::gif::GifEncoder::new(file);
for img in data {
let buf = color_image_to_rgba(img.image);
let frame = Frame::from_parts(buf, 0, 0, Delay::from_saturating_duration(img.delay));
if let Err(e) = encoder.encode_frame(frame) {
tracing::error!("problem encoding frame: {e}");
}
}
Ok(())
}
pub fn key(url: &str) -> String {
let k: String = sha2::Sha256::digest(url.as_bytes()).encode_hex();
PathBuf::from(&k[0..2])
.join(&k[2..4])
.join(k)
.to_string_lossy()
.to_string()
}
/// Migrate from base32 encoded url to sha256 url + sub-dir structure
pub fn migrate_v0(&self) -> Result<()> {
for file in std::fs::read_dir(&self.cache_dir)? {
let file = if let Ok(f) = file {
f
} else {
// not sure how this could fail, skip entry
continue;
};
if !file.path().is_file() {
continue;
}
let old_filename = file.file_name().to_string_lossy().to_string();
let old_url = if let Some(u) =
base32::decode(base32::Alphabet::Crockford, &old_filename)
.and_then(|s| String::from_utf8(s).ok())
{
u
} else {
warn!("Invalid base32 filename: {}", &old_filename);
continue;
};
let new_path = self.cache_dir.join(Self::key(&old_url));
if let Some(p) = new_path.parent() {
create_dir_all(p)?;
}
if let Err(e) = std::fs::rename(file.path(), &new_path) {
warn!(
"Failed to migrate file from {} to {}: {:?}",
file.path().display(),
new_path.display(),
e
);
}
}
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 {
let width = color_image.width() as u32;
let height = color_image.height() as u32;
let rgba_pixels: Vec<u8> = color_image
.pixels
.iter()
.flat_map(|color| color.to_array()) // Convert Color32 to `[u8; 4]`
.collect();
image::RgbaImage::from_raw(width, height, rgba_pixels)
.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,
}
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(),
}
}
pub fn migrate_v0(&self) -> Result<()> {
self.static_imgs.migrate_v0()?;
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,
) -> 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)
}
pub fn get_cache(&self, cache_type: MediaCacheType) -> &MediaCache {
match cache_type {
MediaCacheType::Image => &self.static_imgs,
MediaCacheType::Gif => &self.gifs,
}
}
pub fn get_cache_mut(&mut self, cache_type: MediaCacheType) -> &mut MediaCache {
match cache_type {
MediaCacheType::Image => &mut self.static_imgs,
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>;
pub struct GifState {
pub last_frame_rendered: Instant,
pub last_frame_duration: Duration,
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

@@ -0,0 +1,100 @@
use std::{
future::Future,
sync::{
mpsc::{self, Sender},
Arc, Mutex,
},
};
use tokio::sync::oneshot;
type Job = Box<dyn FnOnce() + Send + 'static>;
pub struct JobPool {
tx: Sender<Job>,
}
impl Default for JobPool {
fn default() -> Self {
JobPool::new(2)
}
}
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();
std::thread::spawn(move || loop {
let job = {
let Ok(unlocked) = arc_rx_clone.lock() else {
continue;
};
let Ok(job) = unlocked.recv() else {
continue;
};
job
};
job();
});
}
Self { tx }
}
pub fn schedule<F, T>(&self, job: F) -> impl Future<Output = T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
let (tx_result, rx_result) = oneshot::channel::<T>();
let job = Box::new(move || {
let output = job();
let _ = tx_result.send(output);
});
self.tx
.send(job)
.expect("receiver should not be deallocated");
async move {
rx_result.await.unwrap_or_else(|_| {
panic!("Worker thread or channel dropped before returning the result.")
})
}
}
}
#[cfg(test)]
mod tests {
use crate::job_pool::JobPool;
fn test_fn(a: u32, b: u32) -> u32 {
a + b
}
#[tokio::test]
async fn test() {
let pool = JobPool::default();
// Now each job can return different T
let future_str = pool.schedule(|| -> String { "hello from string job".into() });
let a = 5;
let b = 6;
let future_int = pool.schedule(move || -> u32 { test_fn(a, b) });
println!("(Meanwhile we can do more async work) ...");
let s = future_str.await;
let i = future_int.await;
println!("Got string: {:?}", s);
println!("Got integer: {}", i);
}
}

153
crates/notedeck/src/jobs.rs Normal file
View File

@@ -0,0 +1,153 @@
use crate::JobPool;
use egui::TextureHandle;
use hashbrown::{hash_map::RawEntryMut, HashMap};
use poll_promise::Promise;
#[derive(Default)]
pub struct JobsCache {
jobs: HashMap<JobIdOwned, JobState>,
}
pub enum JobState {
Pending(Promise<Option<Result<Job, JobError>>>),
Error(JobError),
Completed(Job),
}
pub enum JobError {
InvalidParameters,
}
#[derive(Debug)]
pub enum JobParams<'a> {
Blurhash(BlurhashParams<'a>),
}
#[derive(Debug)]
pub enum JobParamsOwned {
Blurhash(BlurhashParamsOwned),
}
impl<'a> From<BlurhashParams<'a>> for BlurhashParamsOwned {
fn from(params: BlurhashParams<'a>) -> Self {
BlurhashParamsOwned {
blurhash: params.blurhash.to_owned(),
url: params.url.to_owned(),
ctx: params.ctx.clone(),
}
}
}
impl<'a> From<JobParams<'a>> for JobParamsOwned {
fn from(params: JobParams<'a>) -> Self {
match params {
JobParams::Blurhash(bp) => JobParamsOwned::Blurhash(bp.into()),
}
}
}
#[derive(Debug)]
pub struct BlurhashParams<'a> {
pub blurhash: &'a str,
pub url: &'a str,
pub ctx: &'a egui::Context,
}
#[derive(Debug)]
pub struct BlurhashParamsOwned {
pub blurhash: String,
pub url: String,
pub ctx: egui::Context,
}
impl JobsCache {
pub fn get_or_insert_with<
'a,
F: FnOnce(Option<JobParamsOwned>) -> Result<Job, JobError> + Send + 'static,
>(
&'a mut self,
job_pool: &mut JobPool,
jobid: &JobId,
params: Option<JobParams>,
run_job: F,
) -> &'a mut JobState {
match self.jobs.raw_entry_mut().from_key(jobid) {
RawEntryMut::Occupied(entry) => 's: {
let mut state = entry.into_mut();
let JobState::Pending(promise) = &mut state else {
break 's state;
};
let Some(res) = promise.ready_mut() else {
break 's state;
};
let Some(res) = res.take() else {
tracing::error!("Failed to take the promise for job: {:?}", jobid);
break 's state;
};
*state = match res {
Ok(j) => JobState::Completed(j),
Err(e) => JobState::Error(e),
};
state
}
RawEntryMut::Vacant(entry) => {
let owned_params = params.map(JobParams::into);
let wrapped: Box<dyn FnOnce() -> Option<Result<Job, JobError>> + Send + 'static> =
Box::new(move || Some(run_job(owned_params)));
let promise = Promise::spawn_async(job_pool.schedule(wrapped));
let (_, state) = entry.insert(jobid.into(), JobState::Pending(promise));
state
}
}
}
pub fn get(&self, jobid: &JobId) -> Option<&JobState> {
self.jobs.get(jobid)
}
}
impl<'a> From<&JobId<'a>> for JobIdOwned {
fn from(jobid: &JobId<'a>) -> Self {
match jobid {
JobId::Blurhash(s) => JobIdOwned::Blurhash(s.to_string()),
}
}
}
impl hashbrown::Equivalent<JobIdOwned> for JobId<'_> {
fn equivalent(&self, key: &JobIdOwned) -> bool {
match (self, key) {
(JobId::Blurhash(a), JobIdOwned::Blurhash(b)) => *a == b.as_str(),
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
enum JobIdOwned {
Blurhash(String), // image URL
}
#[derive(Debug, Hash)]
pub enum JobId<'a> {
Blurhash(&'a str), // image URL
}
pub enum Job {
Blurhash(Option<TextureHandle>),
}
impl std::fmt::Debug for Job {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Job::Blurhash(_) => write!(f, "Blurhash"),
}
}
}

100
crates/notedeck/src/lib.rs Normal file
View File

@@ -0,0 +1,100 @@
pub mod abbrev;
mod account;
mod app;
mod args;
pub mod contacts;
mod context;
pub mod debouncer;
mod error;
pub mod filter;
pub mod fonts;
mod frame_history;
pub mod i18n;
mod imgcache;
mod job_pool;
mod jobs;
pub mod media;
mod muted;
pub mod name;
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;
mod time;
mod timecache;
mod timed_serializer;
pub mod ui;
mod unknowns;
mod urls;
mod user_account;
mod wallet;
mod zaps;
pub use account::accounts::{AccountData, AccountSubs, Accounts};
pub use account::contacts::{ContactState, IsFollowing};
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 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::{
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 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;
pub use relayspec::RelaySpec;
pub use result::Result;
pub use storage::{AccountStorage, DataPath, DataPathType, Directory};
pub use style::NotedeckTextStyle;
pub use theme::ColorTheme;
pub use time::time_ago_since;
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_current_wallet_mut, get_wallet_for, GlobalWallet, Wallet, WalletError,
WalletType, WalletUIState, ZapWallet,
};
pub use zaps::{
get_current_default_msats, AnyZapState, DefaultZapError, DefaultZapMsats, NoteZapTarget,
NoteZapTargetOwned, PendingDefaultZapState, ZapTarget, ZapTargetOwned, ZappingError,
};
// export libs
pub use enostr;
pub use nostrdb;
pub use zaps::Zaps;

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

@@ -0,0 +1,191 @@
use std::collections::HashMap;
use nostrdb::Note;
use crate::jobs::{Job, JobError, JobParamsOwned};
#[derive(Clone)]
pub struct ImageMetadata {
pub blurhash: String,
pub dimensions: Option<PixelDimensions>, // width and height in pixels
}
#[derive(Clone, Debug)]
pub struct PixelDimensions {
pub x: u32,
pub y: u32,
}
impl PixelDimensions {
pub fn to_points(&self, ppp: f32) -> PointDimensions {
PointDimensions {
x: (self.x as f32) / ppp,
y: (self.y as f32) / ppp,
}
}
}
#[derive(Clone, Debug)]
pub struct PointDimensions {
pub x: f32,
pub y: f32,
}
impl PointDimensions {
pub fn to_pixels(self, ui: &egui::Ui) -> PixelDimensions {
PixelDimensions {
x: (self.x * ui.pixels_per_point()).round() as u32,
y: (self.y * ui.pixels_per_point()).round() as u32,
}
}
pub fn to_vec(self) -> egui::Vec2 {
egui::Vec2::new(self.x, self.y)
}
}
impl ImageMetadata {
pub fn scaled_pixel_dimensions(
&self,
ui: &egui::Ui,
available_points: PointDimensions,
) -> PixelDimensions {
let max_pixels = available_points.to_pixels(ui);
let Some(defined_dimensions) = &self.dimensions else {
return max_pixels;
};
if defined_dimensions.x == 0 || defined_dimensions.y == 0 {
tracing::error!("The blur dimensions should not be zero");
return max_pixels;
}
if defined_dimensions.y <= max_pixels.y {
return defined_dimensions.clone();
}
let scale_factor = (max_pixels.y as f32) / (defined_dimensions.y as f32);
let max_width_scaled = scale_factor * (defined_dimensions.x as f32);
PixelDimensions {
x: (max_width_scaled.round() as u32),
y: max_pixels.y,
}
}
}
/// 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
.next()
.and_then(|s| s.str())
.filter(|s| *s == "imeta")
.is_none()
{
continue;
}
let Some((url, blur)) = find_blur(tag_iter) else {
continue;
};
blurs.insert(url.to_string(), blur);
}
}
fn find_blur(tag_iter: nostrdb::TagIter<'_>) -> Option<(String, ImageMetadata)> {
let mut url = None;
let mut blurhash = None;
let mut dims = None;
for tag_elem in tag_iter {
let Some(s) = tag_elem.str() else { continue };
let mut split = s.split_whitespace();
let Some(first) = split.next() else { continue };
let Some(second) = split.next() else { continue };
match first {
"url" => url = Some(second),
"blurhash" => blurhash = Some(second),
"dim" => dims = Some(second),
_ => {}
}
if url.is_some() && blurhash.is_some() && dims.is_some() {
break;
}
}
let url = url?;
let blurhash = blurhash?;
let dimensions = dims.and_then(|d| {
let mut split = d.split('x');
let width = split.next()?.parse::<u32>().ok()?;
let height = split.next()?.parse::<u32>().ok()?;
Some(PixelDimensions {
x: width,
y: height,
})
});
Some((
url.to_string(),
ImageMetadata {
blurhash: blurhash.to_string(),
dimensions,
},
))
}
#[derive(Clone)]
pub enum ObfuscationType {
Blurhash(ImageMetadata),
Default,
}
pub fn compute_blurhash(
params: Option<JobParamsOwned>,
dims: PixelDimensions,
) -> Result<Job, JobError> {
#[allow(irrefutable_let_patterns)]
let Some(JobParamsOwned::Blurhash(params)) = params
else {
return Err(JobError::InvalidParameters);
};
let maybe_handle = match generate_blurhash_texturehandle(
&params.ctx,
&params.blurhash,
&params.url,
dims.x,
dims.y,
) {
Ok(tex) => Some(tex),
Err(e) => {
tracing::error!("failed to render blurhash: {e}");
None
}
};
Ok(Job::Blurhash(maybe_handle))
}
fn generate_blurhash_texturehandle(
ctx: &egui::Context,
blurhash: &str,
url: &str,
width: u32,
height: u32,
) -> Result<egui::TextureHandle, crate::Error> {
let bytes = blurhash::decode(blurhash, width, height, 1.0)
.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,121 @@
use std::{
sync::mpsc::TryRecvError,
time::{Instant, SystemTime},
};
use crate::{GifState, GifStateMap, TextureState, TexturedImage, TexturesCache};
use egui::TextureHandle;
pub fn ensure_latest_texture_from_cache(
ui: &egui::Ui,
url: &str,
gifs: &mut GifStateMap,
textures: &mut TexturesCache,
) -> 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))
}
pub fn ensure_latest_texture(
ui: &egui::Ui,
url: &str,
gifs: &mut GifStateMap,
img: &mut TexturedImage,
) -> 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 now = Instant::now();
let (texture, maybe_new_state, request_next_repaint) = match gifs.get(url) {
Some(prev_state) => {
let should_advance =
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 = SystemTime::now().checked_add(frame.delay);
(
&frame.texture,
Some(GifState {
last_frame_rendered: now,
last_frame_duration: frame.delay,
next_frame_time,
last_frame_index: maybe_new_index,
}),
next_frame_time,
)
}
None => {
let (tex, state) =
match animation.get_frame(prev_state.last_frame_index) {
Some(frame) => (&frame.texture, None),
None => (&animation.first_frame.texture, None),
};
(tex, state, prev_state.next_frame_time)
}
}
} else {
let (tex, state) = match animation.get_frame(prev_state.last_frame_index) {
Some(frame) => (&frame.texture, None),
None => (&animation.first_frame.texture, None),
};
(tex, state, prev_state.next_frame_time)
}
}
None => (
&animation.first_frame.texture,
Some(GifState {
last_frame_rendered: now,
last_frame_duration: animation.first_frame.delay,
next_frame_time: None,
last_frame_index: 0,
}),
None,
),
};
if let Some(new_state) = maybe_new_state {
gifs.insert(url.to_owned(), new_state);
}
if let Some(req) = request_next_repaint {
tracing::trace!("requesting repaint for {url} after {req:?}");
// 24fps for gif is fine
ui.ctx()
.request_repaint_after(std::time::Duration::from_millis(41));
}
texture.clone()
}
}
}

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